Oulun yliopisto - Etusivulle University of Oulu in English


Electrical and Information Engineering

University of Oulu > Faculty of Technology > Electrical and Information Engineering

tklab - courses 

[This page is CSS2 enabled. Your browser might not fully support it]

Introduction to C programming

$RCSfile: c_intro.html,v $ $Revision: 1.25 $ $Date: 2004/02/18 11:41:45 $


This document gives basic information and resources for further studies about programming embedded systems with the C language.

Table of Contents


As you will be required to implement a few bits of software with C language to pass this course, it is recommended that you begin to familiarize yourself with it as soon as possible. Remember that programming is very much similar to mathematics: you can memorize all you want, but you still need to practice your skills on a regular basis.

Prerequisites to this course include Introduction to programming and Computer Engineering I, so you should be familiar with Java and Assembly. Think of C as something in between those two languages: you don't have the cozy environment of Java and its automatic memory handling or clean object encapsulation. On the other hand, most probably you need not worry about the order of bytes on your stack before calling routines - which might be a lot of fun, but not very interesting to us right now.

In case you are already familiar with C and wish to seek additional infromation about programming languages and computer systems in general, we recommend warmly that you read the excellent book Structure and Interpretation of Computer Programs by Abelson, Sussman and Sussman. This book leads you to the inner workings of computer programs and languages. In fact, it is recommended reading for anyone interested in this subject. The C Reference Manual by Dennis Ritchie is a good document when you know how to program but don't yet know much about C. To accompany that, Brian Kernighan has written A tutorial about Programming in C. These texts are pretty old, but they are still valid. That says something about C and its ability to adapt.

We have provided you with a couple of web links to sites about C programming and embedded systems in general. You don't have to read them all, just find a suitable format of information and begin studying.

Your First C Program

Consider the example code:

#include <stdio.h>

int limit = 5;

int main(void) {
	/* this
	 * is a 
	 * multi-line 
	 * comment
	int i;
	for(i = 0; i<limit; i++) {
		switch(i) {
			case 0: {
				printf("Hi there!\n"); 
			case 1: {
				printf("Hi again!\n");
			default: {
				printf("Not you again!?\n");
		if(i > 1) {
			printf("Oh come on!!\n");
	return 1;


You should be able to comprehend what this particular piece of C does, but here goes anyway:

In C, you include a header file that contains information about a library you intend to use. In this case, you need stdio.h, because the printf-function is defined there. In Java you would import packages according to your needs. The reason behind this procedure will be covered later: all you need to know is that you need to include header files.

The next line introduces a variable called limit, which is of type int and it is accessible from all function blocks within this particular file or other files: it has a global scope.

The program has a main function that takes no parameters (hence the keyword void) and returns an integer value. Inside the function block (delimited by braces) resides the actual program code. It begins with a comment block that has multiple lines. The C standard we use and most ancient compilers support does not allow one-liner comments beginning with double-slashes, such as this:

	// this is acceptable in C99 and C++ but not here

All variables that you intend to use only within a block (ie. within the same set of braces) must be declared at the beginning of that block. This is different from Java, where you can introduce new variables practically anywhere. An integer i is declared here and it is used within a for structure by initializing it to value zero. During each loop (of the for structure), the value of i is compared to limit and if the result of comparison is true, the execution of the for block may begin. The value of i is increased after each loop.

Inside the for block are two structures: a switch-case-structure and an if-statement. Syntax and logic for these two are mostly the same as in Java:

switch - case

Switch takes one primitive parameter and executes the case block that has an equal condition value as the parameter. You must issue the break-statement inside each case block, because the list of case-conditions is walked through and all matching entries are executed. The default-case block is executed every time, unless a break statement has been executed before it.


The if-statement in C works much like in Java, but the expression inside the parentheses must return zero if the expression's truth value is false and a positive integer (namely 1) if it is true.

Compiling and Linking

The next step we need to take is to compile the source code into an object file. This is a hugely complicated matter, but lucky for us, we have programs that can do it for us.

To make things even easier for us, most compilers have graphical user interfaces or at least ones that have visual aids, so that you don't have to worry which compiler executable to use or what command-line parameters it takes.

Turbo C 2.01 screenshot

If the compiler tells you that errors were produced during the compilation, follow these steps before screaming for help, please:

  1. Read the error message
  2. Understand what it says
  3. Find out what you need to do to fix the problem
  4. Try to fix the problem by yourself
  5. If the error persists, ask for help

This procedure is not recommended just to annoy you, but to emphasize the fact that you really need to solve problems on your own and not always expect answers from someone else. So please do not feel intimidated to ask for help, when you really need it. That's what the course assistants are for, anyway. :)

The C compiler you will be using during the lab sessions is Borland C 3.1, but we will try to make sure that at least the examples work with Borland Turbo C 2.01, which is available for free from the Borland Antique Software site.

Once the compilation goes through without errors (and preferably without any warnings ;), you can link your code to produce an executable. What really happens during linking, is that the linker goes through all your compiled object files and finds out any references to each other and outside libraries. The header files that you included earlier defined symbolic names for all external references, so that the linker knows what to look for in the library files. These references are updated into temporary object files and then they are assembled together to form an executable binary file that can dynamically load external libraries and use their functions. The alternative is to copy all the referenced external functions and data from the library files to the binary file, forming a static executable. For embedded systems this is usually the case, because of the overhead in dynamic library handling. By default Turbo C 2.01 and Borland C 3.1 (in 16bit realmode) create static binaries.

Linking is once again eased by the visual interface of Turbo C (or Borland C or Whazzama Gizmo C). If you feel like using another editor than the one provided by the interface, you may wish to compile and link from the command-line. Both Turbo C and Borland C provide information about this subject.

Defining and Accessing Various Data Structures


The C standard (ANSI) defines a whole bunch of primitive types and their sizes. In the following table you can see some of them:

TypeTypical UsageDescription
charchar foo = 'a';The most primitive unit of data. Length can be anything from 8 bits upwards, but in a PC a char (byte) is 8 bits wide.
intint count = 12765;An integer variable whose size in bits is equal to or higher than char's size. Usually 16 bits or more.
longlong big_number = 0xF00FBA55;A long variable is equal or larger in size than an int variable. Usually 32 bits wide.
floatfloat c = 0.42;A float variable contains a floating-point value. The size is unspecified, but in most cases something between 20 and 48 bits.
doubledouble d = 0.42;Whereas a double is by definition double the size of a float variable in bitwidth.

Note that all of these variables can contain negative values, unless explicitly specified with the keyword unsigned, like this: unsigned int foo;.

You may want to check the variable sizes on your system before deciding which types you will use in your software. An example program is provided for this purpose. Modify it to suit your needs, if necessary.


A pointer is a value that denotes an address in the program memory. You can for example declare a variable:

	int v = 30;

.. and then you can fetch the address to this variable, store that into a pointer variable and use it to access the original variable v:

	int *vp;
	unsigned int addr = &v;
	vp = addr;
	printf("%d", *vp);

The magic happens around two special characters, & and *. The former forces the compiler to use the variable's address when used immediately in front of a variable. Note that when an & is used in between two variables, it is interpreted as the bitwise and operator. To confuse you even more, the asterisk character has multiple semantics depending on the context it is used in: when a variable is introduced as a pointer, e.g. int *vp;, asterisk means that the variable in question is a pointer to an integer value. Then again, if you use it in an assignment context, for example like this: int a = *vp;, it means that you wish to assign the value from where vp points and not the pointer value itself. Most likely you have seen the asterisk been used as a normal multiplication operator. :)

See file pointers.c for examples of using pointers.


Arrays in C are a very powerful way to access data, because they are contiguous memory sequences and they allow random access to their contents. What's more, array variables are actually pointers to the beginning of the contiguous memory block. There are two ways to access an array depicted in file arrays.c:

Indexed reference with brackets: a[cnt] = 'a' + cnt;

This is the common way to access an array, and it is by far the most intuitive way. The variable cnt simply denotes the index of an array element in array a.

Pointer arithmetics: *(a + cnt) = 'b' + cnt;

Because the arrays in C are always contiguous, it is possible to access their elements by incrementing the array pointer by the index value and referring to the element.

You can define an array with a constant size using the bracket notation: int a[15];. Some C compilers are also happy with the following notation: int *a = {1, 2, 3, 0};. Strings in C are char arrays and practically all compilers can deal with the following type of declaration: char *name = "Copernicus";, which creates a char array of length 11, last element being a NULL character. ALL STRINGS IN C MUST END IN A NULL CHARACTER!

You can use multi-dimensional arrays in C. Arrays are indexed in row-major style and you have to be careful about how your program walks through the indexes.

int a[20][4];

That code block defines an 20-row array of four-number arrays. Using pointer arithmetic, you can access row 3, column 2 like this:

int row = 3;
int col = 2;
int val = *(a + ((row * 4) + col));

In some programs the command line is processed by looking at the parameters defined in the main function. First parameter contains the number of arguments that have been passed to the program and as we take two values from the command line, it should be equal or higher than two. The second parameter seems a bit complicated, but it really isn't. As you should already know, one can access data arrays in C in many ways. The double asterisk ('**') tells you and the compiler that the variable in question must be a pointer to another pointer. Simple, huh? :) So, when it is later referred to with brackets or double brackets, the compiler knows that it is legal to do so.


You may recall the object oriented approach to programming. Forget data concealing, protection, member functions and inheritance and you end up with C structures. :)

No, seriously, C structures are a nifty way to avoid redundance in data structures. Consider the example in file structs.c. The source introduces two structures:

Plain structure definition:

struct cat {
	int age;
	char *name;
} mycat;

This is the way to introduce a single structure reference, in this case it is called mycat. You can reference this structure via the mycat-variable from anywhare within this source file.

Structure type definition:

typedef struct {
	int age;
	char *kind;
} alldogs;

In this case alldogs refers to a type definition, which can be used to declare variables later on. It works just like a normal variable declaration:

alldogs mydogs[3];

To access the variables within structures, there are two ways, depending on the type of variable which is used to refer to the structure data:

Immediate reference:

alldogs mydogs;
mydogs.age = 15;
mydogs.kind = "keeshond";

The simplest way to use structures is to reference them directly and you have access to the member variables with the '.'-operator. You can create arrays of structure variables, but using the pointer arithmetic method to access array elements should be avoided. There is a substantial amount of hidden magic behind memory allocation and data alignment, and the best way to avoid trouble is to not do anything exotic with structure arrays.

Pointer to a structure:

alldogs *mydogs;
/* allocate memory for the mydogs-structure before doing the following! Use
 * malloc()! 
mydogs->age = 15;
mydogs->kind = "rottweiler";

The mydogs-variable is a pointer and we have to use the "arrow" style notation to reference the structure members. You could also use this kind of style: (* mydogs).age = 15;

Common Binary Data Operations with C

Now that you know how to compile and link your programs, let's dive into the magic world of bitwise operations. At first these might seem really confusing, but you will notice that you have in fact seen all of them during Computer Engineering I.

Shift Operations

Download file shft.c and examine it. It might seem a bit more cryptic than the first example, but don't be frightened. What you see is the usual stdio.h which is included at the beginning and then a function called getbinarystring. Skip it over and move on to the main function, which introduces two consecutive loops right after a block that handles command line processing.

The binary shift operator in C is a double-less-than or a double-greater-than character. Less-than means shifting to the left and greater-than the opposite. In effect, binary shifting is actually multiplying or dividing by powers of two. So, if you wish to divide a value by four, you shift the bits to the right by two:

  value = value >> 2;

... Or if you want to multiply by 8:

  value = value << 3;

Of course, you need to know that overflows are not prohibited and you might send your MSB:s to bit heaven!

Bitwise AND

You can use two notations to perform bitwise and-operations:

result = op1 & op2;

.. or ..

result &= op;

which is equal to

result = result & op;

Experiment with the code in file and.c.

Bitwise OR

The notation of OR is similar to AND in C, but the operator character is a pipe character:

result = op1 | op2;

Again, there is a code snippet in file or.c that should clarify the usage of OR operator.

Bitwise XOR

Have a look at file xor.c and see that the XOR operation is nothing special, except for the operator character in C, a caret: ^.

Using Hardware Resources with C

As you already know (you really should!), the system architecture that we are interested during this course, is the PC architecture. Embedded systems are not really all that different from your average PC: you usually have a programmable processor (CPU, microcontroller) and a few peripheral devices (interrupt controllers, bus controllers) connected to it. There might be pre-written software available for various purposes (eg. PC BIOS, device firmware) or not (simple microcontrollers). You might have an operating system and its services or not. We will focus on one particular system: PC that has a BIOS and the services of DOS available.


DOS is a peculiar operating system. It has abstraction layers, but it doesn't enforce their use. It provides no multi-tasking, but it is possible to write programs that run in the background (TSR). These and many other reasons make it quite near ideal for learning how to program hardware-dependant software.

Performing Raw I/O with C

The two following functions from dos.h are the easiest way to access external registers:


Using outportb is really easy:

outportb(0x21, (0x40 | original_mask)); /* set IMR bit 6 up */


When you wish to read values:

unsigned char mask = inportb(0x21); /* read IMR */


You should be already familiar with the concept of hardware interrupts, but here goes: In the PC architecture, peripheral devices that connect to the CPU via a bus or dedicated hardware, can interrupt the CPU. Usually there's an interrupt controller chip between the peripherals and the CPU, so that interrupts can be managed without hardware reconfiguration. This means that the interrupt controller, which in a PC is most likely a variant of the Intel 8259 chip, can be programmed with software.

The i8259 provides a mask register for masking out unwanted interrupt lines (for example while software wishes to ignore a device for a while). This register is located at address 0x21 in a typical PC. The following code snippet commands the i8259 to mask out interrupt (IRQ) 4 and pass IRQ 5:

unsigned char IRQ4 = 0x4;
unsigned char IRQ5 = 0x5;
unsigned char mask = inportb(0x21);
mask = mask & IRQ4;
mask = mask & ~IRQ5;
outportb(0x21, mask);

Did you notice how the mask register works? If the bit associated with an interrupt line is zero (low), then the interrupts are passed through. When one (high), the interrupt is prevented from accessing the CPU.

Setting Interrupt Vectors

Interrupt Service Routines

Bus Protocols, Timing

Common Problems

Problems with installing Turbo C 2.01

The zip package you downloaded contains install disks inside separate directories. In directory Disk1 you will find install.exe which will guide you through the install process. In case the install fails for some reason (eg. complains about a missing file), try to move the files and subdirectories from directories Disk2 and Disk3 to directory Disk1 and run install.exe again. Borland license terms prohibit us from redistributing a bullet-proof version of the install system.

Turbo C complains about erroneus keywords: 'asm' or '_asm'

Turbo C does not support inline assembly, but get yourself familiar with int86() from dos.h and the REGS structure if you need to control CPU registers or call BIOS routines from your code.

My fancy timer and interrupts don't work!

Check if you are running Windows XP and boot to Windows 98 if yes. If you are running and compiling your code under Windows 98, check which version of the Borland compiler GUI you are running. The win32 interface (ie. bcw.exe) does not produce proper 16bit realmode code, so you need to switch to the dos ide (bc.exe). If the problem persists, try booting to DOS mode instead of using the Windows' dos box. If nothing works and you think your code should work, ask the assistants during consultation hours or drop an email. :)


And always remember your trustworthy friend, Google Search.

[This page is CSS2 enabled. Your browser might not fully support it]