tklab - courses
[This page is CSS2 enabled. Your browser might not fully support it]
[http://www.ee.oulu.fi/research/tklab/courses/521419A/]
Introduction to C programming
This document gives basic information and resources for further studies about
programming embedded systems with the C language.
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.
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");
break;
}
case 1: {
printf("Hi again!\n");
break;
}
default: {
printf("Not you again!?\n");
break;
}
}
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.
if-statement
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.
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.
If the compiler tells you that errors were produced during the compilation,
follow these steps before screaming for help, please:
- Read the error message
- Understand what it says
- Find out what you need to do to fix the problem
- Try to fix the problem by yourself
- 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.
The C standard (ANSI) defines a whole bunch of primitive types and their sizes.
In the following table you can see some of them:
| Type | Typical Usage | Description |
| char | char 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. |
| int | int count = 12765; | An integer variable whose size in
bits is equal to or higher than char's size. Usually 16 bits or more. |
| long | long big_number = 0xF00FBA55; | A long variable is equal
or larger in size than an int variable. Usually 32 bits wide. |
| float | float 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. |
| double | double 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;
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.
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!
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.
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.
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:
^.
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.
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.
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.
[http://www.ee.oulu.fi/research/tklab/courses/521419A/]
[This page is CSS2 enabled. Your browser might not fully support it]
|