C Language

back to main tutorial page

The first version of this course assumed that students knew at least a bit of the C computer language. It soon became clear, however, that many or even most students said "we don't know any C".

Fortunately it is possible to make a lot of progress with the AVR without knowing C in great detail. There are some standard things we want to do, and once you see how each is done, you will quickly become an expert.

Program flow

In C a program generally executes from top to bottom, and left to right, just as you are reading this document. The AVR effectively "reads" your program to decide what to do. We speak of "program flow" meaning the series of statements the AVR will execute.

When you are reading a book, you normally do not jump over paragraphs, nor do you read them several times. However in most programs we must make the AVR skip statements or else do them many times. So there are conditional statements and also various loops.

The concept of program flow is very fundamental to understanding both C and how the AVR processes the statements. You can imagine a finger pointing to a particular line in a program, the current active statement. The AVR is reading that statement and is obeying what it says. If the statement says "go back to the beginning", then that is what the AVR will do.

The AVR does not actually read and process C language. Instead it processes machine language which is a compiled form of your C program. A given C language statement might be compiled into one or several machine language statements, depending on the type of statement. Fortunately, if you program in C, the compiler will make the machine language for you and you never have to see it (which is good because it is very hard for us humans to follow). Instead we can just concentrate on how the AVR's behaviour corresponds to the C language we can write and understand.

Syntax

The C pre-processor

Think about one of those military style jackets with lots of pockets and straps. When you first put it on, the use of all the features may not be clear. But if you take your new jacket on a real journey like a camping trip, you may find uses for them and soon they will be indispensible. The C language is like such a jacket. When you first see it you will probably wonder what all the fiddly bits are for. But after you have used it for a while, you will need the features that are provided and you will praise the wisdom of the designers of the language. So bear with me while I explain something that you don't need yet: the preprocessor.

You don't need it yet because when you first start writing programs they will tend to be rather short and straightforward. But later on, as you tackle more ambitious programming tasks, your programs will grow in length and usually there will be many source files. Organisational problems begin to occur. Meanwhile we are nearly always trying to write code that will be lean and mean i.e. it will not require much flash memory to store, and it will execute swiftly.

The C preprocessor is a program that processes your C source code before the compiler starts to work. It is a slightly odd beast which has different syntax conventions from C itself.

For example, suppose that we wish to define a constant, a number that won't be changed while the program is running. We usually write

#define kWiseMenCount 3

Note carefully that the line starts with # (we say "the hash character"), and that there is NO EQUALS SIGN between "kWiseMenCount" and the number 3. The preprocessor understand this to mean:

"OK, if I see the string "kWiseMenCount" anywhere below this point, I'll replace it with the number 3"

When I say "below this point" I mean "in this file or in files that include this file". That will become clear later on I hope. For now, just remember that one of the functions of the preprocessor is to do a search-and-replace where one text string is replaced with another, before compiling. Why would we want to do that? For speed we would like numbers like 3 to be compiled directly into the code where they are needed. But if you actually write 3 in your code, there are cases where this can be extremely unclear. But if you write kWiseMenCount in your program, everyone who reads the program will understand what this number really means - its significance instead of just its value. And because the preprocessor replaces kWiseMenCount with 3, there is no speed penalty in doing so. Everyone wins. And there is another important reason. Suppose that later on, when your program is a huge spaghetti monster with 100 source files, you want to change the number of wise men. If this number is used often in your souce code, you may have hundreds of "3"s scattered around. So if you want to change them to, say. 4, you have to go and find every last one of them and change it. That sounds like hard work and also a recipe for human error to me. So the right way to do it is to define the number of wise men right at the top of your code, in some high level header file (.h file) and then #include that file where you need access to the number. If at any time you change your mind about the number of wise men, you then have only to change the value once, in the top header, and all the references in the rest of the code will automatically be correct.

I hope you can see there are some principles coming out already and we haven't even got to the C syntax proper. This is as good a place as any to spell them out

  • No matter how careful you think you are, you make mistakes.
  • No matter how good you think your memory is, your code will confuse you when you see it again next year. And other people who have to read it, your boss and your co-workers for example, have to try to understand what you did without the benefit of your memories.
  • The C language has good organisational features such as #define and comments to help make your code more readable, and easier to modify. Use them!
What about that #include that I slipped in there? Well, in C we usually only write two kinds of files:

my_code.c might be the name of a file containing C language for execution;

my_code.h is then a good name for a file containing the definitions (#define statements) pertinent to the .c file. We say those names like this: "my underscore code dot c",

You may be wondering why there is a distinction between .c and .h files. Why not just put all the definitions and code in one file and be done with it? Well, when you first start writing C you can do that if you want to - just write a .c file and put everything in it. Fine. But remember my comment above about leaving organisational clues for yourself and other readers. One way to do that, a good way, is to have a separate .c file for each of the main functions of your code. There are lots of advantages to this. The header file becomes what computer science people call a "formal interface" to the block of code in the C file. What that means is that you can pretty much understand what the C code does, or is meant to do, by looking at the definitions in the .h file. The header file becomes like an index or contents page for the .c file. So if you look in the .h file and see that there are some prototypes for procedures called

MotorOn(int speed);

MotorOff(void);

You know straight away that the code in the C file probably has the ability to do those things. The header file doesn't - just as the table of contents in a book doesn't tell you the details of the paragraphs in the text.

So, back to #include. This is an instruction to the preprocessor to copy all the text from one file and insert it in another file. For example it is usual at the top of a .c file to have some lines like this:

#include "my_code.h"

#include <string.h>

The first #include above is replace, by the preprocessor, with the entire contents of the file my_code.h, which should normally exist in the same directory as the file my_code.c (it's possible to bend this rule but let's not do that yet!)

The second #include line above tells the preprocessor to insert (include) the entire contents of a system header called string.h. A system header means a set of declarations and definitions for common system functions. The file <string.h> is a header with definitions for manipulating strings of characters and also some basic memory copying functions. Note that the name of a system header has < ... > around it instead of quotes. Also note there is no semicolon at the end of these #include lines.

So we now have the #define statement, to set up a text search-replace process, and the #include statement, to bring another file in. The preprocessor also provides something called conditional compiling. Briefly, this provides a way to control which bits of your code will actually be compiled. Here's an example:

#define kExtraWM 1
int j = kWiseMenCount;
#if kExtraWM
// this code will be compiled because it is inside the #if statement and kExtraWM is non-zero i.e. true
j++; // increase j by one
#else
// this code will not be compiled. it is not even "seen" by the compiler because it is
// stripped out by the preprocessor
j--;
#endif

This is a silly example because we probably wouldn't use a conditional-compile block like this. But the message here is that these #if.. #else.. #endif statements are processed before the compiler even sees the code. We can control which bits of the code are "in" and which are "on hold, waiting in the file, maybe because I want to use them later". It's a bit like a C comment really except that we can control whether it is "commented out" or not. This is a feature you will need later on but probably not right now.

Enough about the preprocessor. From here on it is all genuine C.

Comments and "commenting something out"

A comment is some non-program text which you can, no, must insert near every statement to explain what is going on. Comments are probably stripped out by the preprocessor and thus are not seen by the compiler. Therefore you can write anything you want in them.

Traditional C had only one kind of comment which is still called "A C comment". It starts with /* and ends with */. We pronounce those "slash star" and "star slash". And it is OK to have many lines (carriage returns) between those marks. This kind of comment is still available these days and is used mainly for

  1. Long comments e.g. a big explanation at the top of a .c or .h file
  2. Commenting code out so that it won't be compiled but is still in the file in case you need it later.

The latter is very handy!

Modern C environments usually allow another kind of comment which was invented as part of the C++ language. These comments are called "C plus plus comments" and they can only be one line long. The comment starts with // and continues to the end of the line. We pronounce that "slash slash" or "start C++ comment". Here are some examples:

int i, count; // this is a good place to explain what these variables are for

/* Roses are red, violets are purple,

you're as sweet as maple surple

-- Ogden Nash */

The poem could be called a multi-line comment.

In modern IDEs (Integrated Development Enviroments, such as ICCAVR, CrossStudio and AVRStudio) the text editor is usually quite sophisticated and can colour the code (pretty printing). In ICCAVR, for example, comments show up in green text and the code is black. So you always know which bits of the program are comments.

In my code I use the C++ type of comment a lot and reserve the C comment for "commenting out". The C comment works fine even if there are C++ comments inside the C comment. Anyway, comments are very important, write lots of them everywhere, in detail, as you write your code. You will understand, and thank me, later on.

The C language is case sensitive. If you define a variable

int my_integer;

then you must always refer to it exactly as you defined it: my_Integer would not work because it has a capital I.

A statement in C is either a single line OR a group of code lines between curly brackets { .... }. Let's explore this using a conditional (if-then) statement.

Suppose we want to turn on an LED but ONLY if there is a logic HIGH at a certain input pin. Let's assume the input pin is PORTC, pin 6 and the LED drive pin is PORTD, pin 4.

// All pins on the AVR are inputs unless we
// configure them for output.
// So we need to ensure that our LED drive pin
// is set up for output.
DDRD |= BIT(4); // set up LED output pin.
if (PINC & BIT(6))
PORTD |= BIT(4); // turn on the LED

Note that we use lots of comments. A comment starts with // and continues until the end of the line. Also note the use of indentation to show the statement under the "if ()..." statement is controlled by the if statement. Programs are hard to understand at the best of times - so make good use of the format to help show what the program does.

Suppose we want to do several things in response to the high voltage on PC6. In this case we can use curly brackets to group a bunch of program lines:

int i = 0; // declare a variable and initialise it to 0
if (PINC & BIT(6))
{ // start a compound statement
PORTD |= BIT(4); // turn on the LED
i++; // increase the value in the variable i
} // end compound statement

In that example we defined a variable, initialised it and also did an arithmetic operation on it. Arithmetic is explained in a later part.

Single line program statements must end with a semicolon. There is no need to use a semicolon at the end of a compound statement.

if statement

You have seen the conditional statement "if () ...". The full structure is

if (condition)

{

// do something if condition is TRUE

}

else

{

// do something different if condition is FALSE

}

OK, so what do we mean by TRUE and FALSE? In C, 0 means FALSE and anything else - anything else at all - means TRUE. Any integer, or expression which turns into an integer value, can appear in the (condition) of the if statement.

Some examples:

if (PINC) i++; // if ANY pin of port C is high, increment i
if (PINC & BIT(6)) i++; // this time we check only pin 6 by using a bit mask
if (0) i++; // 0 means false so i will never be incremented here
if (1) i++; // 1 means true so i will always be incremented
if (23) i++; // any integer that is not zero means TRUE
if (j == 23) i++; // if j happens to be exactly 23, then and only then
// we will increment i
if (a < b) i++; // is the value of a strictly less than b?

Stacking if-else1-else2....

Sometimes it is useful to do a series of tests in a specific order. This can be done by a longer form of the if-else statement.

if (j < 10)
i++;
else if (j < 50)
i += 2;
else // any case not trapped above
i = 0;

Depending on the value of j when the statement starts, we will either add 1 to i, add 2 to i, or set i to 0. There are no other options.

Assignment vs. comparison syntax

This may seem a trivial point but it may trip you up if you don't know it. In C we can set the value of a variable like this:

i = 37;

Note how similar that is to the "test" syntax (i == 37). C will allow you to use the assignment syntax inside an if statement, like this:

if (i = 37) j++;

But what will that do? You wanted it to be a TEST where i is COMPARED to 37. But in fact the compiler understands this to mean "set the value of i to 37, then treat 37 as a boolean". Of course 37 is non-zero, meaning TRUE, so j will ALWAYS be incremented. You might well have a problem finding this error. So, always use double == in if statements until you are a real expert. Then you might start to use the assignment = for a specific purpose.

Some programmers always do tests like this:

if (37 == i) j++;

This has the advantage that if you accidentally use the single =...

if (37 = i) j++;

the compiler will tell you about the error because it is not possible to "set" the value of 37 - it is not negotiable! However it is OK to TEST (37 == i).

Since this is a course on microprocessors, it is good to think about what C language statments mean to the hardware. In the case of an assignment, it means that data is moved in memory from one place to another. "i = 37" means "copy the bit pattern for the number 37 into the memory location for variable i. In the case of a comparison, the bit patterns at two memory locations are examined for differences, but neither bit pattern is changed.

while loop

Sometimes we want to execute a statement, or group of statements, many times. One way to do that is to use a while loop.

while (condition)
{
// do something
}

Let's use a concrete example:

int i = 3;
while (i)
{
i--; // reduce the value of i by 1
}

Imagine the program flow.

  • The AVR sets i to 3. Then it sees "while (3) ...". 3 is non-zero so is treated as a boolean TRUE. So the AVR knows it must execute the lines of code between the curly brackets {...}. These are taken a line at a time, as usual. In this case there is only one line: i is reduced by one.
  • Then the AVR executes the while () line again. i is now 2 and is still TRUE. So the {...} part is done again. i goes down to 1.
  • The AVR executes the while () line again. i is 1 and so is TRUE. This time, inside the {...} part, i becomes 0.
  • The AVR executes the while () part again but this time i is 0 and so is FALSE. The {...} part is thus NOT executed - it is skipped.

That's not so bad, is it?

Variable types, value ranges and value wrapping

The C language defines some standard variable types. The ones you will use most in AVR programming are

type

size in bytes

numeric range

char

1

0..255

signed char

1

-128..127

int

2

-32768..32767

unsigned int

2

0..65536

It is also possible to use the float and long int types - but this is just an introduction, remember.

The key points with the variables in the table are

  • there are two main sizes: one byte and two bytes. If you have a number that you expect to stay in the range for a one byte variable, there is no point using a bigger variable - it would just waste precious AVR memory.
  • each data type comes in a signed and an unsigned version. They occupy the same space in AVR memory but are interpereted differently by the compiler. Many program errors can be traced to a problem where the programmer used a signed variable when an unsigned one should have been used, or vice-versa.

To illustrate the difference between signed and unsigned, consider these statements:

char i = 0xFF; // set i to 255
signed char j = 0xFF; // set j to -1 !!!!!!

Why -1? Because the smart people who invented the C language had to make a decision about how a negative integer should be stored. They decided to store the negative sign in the highest bit of the bit pattern. The meaning of 0xFF - which has the highest bit set - is thus very different depending on whether we say it is a signed or unsigned variable.

You may still be wondering how 0xFF can mean -1. The truth is that the encoding of the low 7 bits is also different for negative numbers. But this is a detail I would rather not distract you with just now.

Another point about my table above: char is unsigned by default BUT int is signed by default. I do not know why, this was a decision of the compiler authors. If you are nervous about signed vs. unsigned, you could always qualify your types e.g.

unsigned char q;
signed int alpha;

It is not strictly necessary but might save you some frustration.

OK, now consider the following.

char i = 255;
i++;

After that, what is the value of i? The AVR has a strict limit of one byte of RAM allocated to store the value of i. It cannot store the value 256 in a single byte - that would require at least an int (2 byte) storage area. So what will happen?

The AVR will "wrap" i around to the minimum possible value! i will become ZERO.

That may at first seem shocking but you will get used to it. In fact, when you are confident, you will even start to use this behaviour to advantage. For now, just be aware of it. For example:

char i = 3;
while (i > 0)
{
i -= 2; // reduce i by 2
}

The programmer might have wanted the {...} part to execute just twice: once with i == 3, and again with i == 1. After that, presumably, i will be less than zero so the loop will end. Sorry - not so! Because the loop variable i is unsigned, it will jump from 1 to ... what? It cannot store a negative value - so instead it will become a large positive value, 255. Then the loop will continue to reduce it... but the same thing will happen again. The AVR will be trapped in this loop FOREVER. When this happens to you - and I am sorry to say that it will, once or twice, as you are learning - the AVR may seem to become unresponsive. There is more to this than meets the eye - because the AVR has effectively TWO "simultaneous" programs running - but again, I don't want to confuse the issue at this stage. For now, just be aware that a program can get caught in an endless loop.

Math

The C language has a full set of math operators. Generally they will do just what you expect. However there are some cases where caution is needed.

Operator

Example

Notes

Addition

a = b + c;

Should generally do the expected thing BUT beware of the overflow of variables mentioned above.

Subtraction

a = b - c;

Again, beware of overflow. Also watch out for unsigned variable types. The result of the subtraction, if negative, can only be stored in a signed variable type. If you try to store it in an unsigned variable, the result will probably not be what you think.

Multiplication

a = b * c;

Watch out for overflow in variable a.

Division

a = b / c;

In the C language, if b and c are both integers, the result is an integer. Therefore 1/3 evaluates to ZERO. So what about 2/3? Sorry, also evaluates to zero. 3/3 evaluates to 1.

I have probably wasted weeks of my life searching for unexplained errors in software which ultimately came down to forgetting the statement above. If you are working with integers, just remember that the division operator works like "divide then truncate".

Remainder or modulus

a = b % c;

This means "set a to the remainder when b is divided by c". So 1 % 3 is 1; 4 % 3 is also 1.

continue and break

The C language provides some useful statements to give more control over how a loop statement is processed. Suppose you want to leave a loop early, before the condition says so. This can be done with the break statement:

int i = 37;
while (1) // note deliberate endless loop
{
i -= 2; // reduce i by 2
if (i < 0) break; // leave loop

}

I use this structure all the time in my programs.

Sometimes it is also helpful to jump to the next time around the loop. This can be done with continue:

int i = 37;
while (1) // note deliberate endless loop
{ 
i -= 2; // reduce i by 2
if (i > 10) continue; // go back to top of loop
            
// do something
if (i == 0) break;

}

Again, I have found this to be a very useful structure. The C language provides another loop type called "do... while" but I don't even use it because the structure above provides all the function and saves me remembering the extra syntax. Each programmer has to find his or her own style, and it is good if it works and you can make progress.

for loop

This statement may at first seem intimidating, but give it time, you will be using it like a professional. Suppose that we want to execute some statement an exact number of times. For example, suppose we want to flash an LED 3 times. We can do it with a while loop:

char i = 3;
while (i)
{
PORTD |= BIT(4); // turn LED on
DelayMs(10); // delay a short time
PORTD &= ~BIT(4); // turn LED off
DelayMs(20);
i--; // reduce loop counter (don't forget to do this!)
}

BUT this is such a common situation that C has provided another way to do it.

char i; // no need to initialise it this time
for (i = 0; i < 3; i++)
{
PORTD |= BIT(4); // turn LED on
DelayMs(10); // delay a short time
PORTD &= ~BIT(4); // turn LED off
DelayMs(20);
// note that we DO NOT change i here
}
         

The body of the {...} is very similar except that we DON'T modify i. The for statement does that for us. The structure is

for (initialisation; test; increment)
{
// do something
}

The initialisation part is only done once, when the for statement is first executed. Usually we set the start value of our counting variable (in this case called i).

The test part is done each time around the loop, BEFORE executing the {...} part. It is possible to have a case where the test can NEVER evaluate to a boolean TRUE, in which case the {...} part won't be executed at all.

The increment part is done AFTER each time around the loop. This is only done if the {...} is excecuted. You can use this part to increase i, decrease it, or do something else to it. The important thing is that at some point your test should evaluate to FALSE so that the loop will stop going around.

Note that the value of i after my example above will be 3. You might like to follow the code around the loop and confirm this.

functions

I used a function in the previous example: DelayMs(). In my code examples for this course, DelayMs() is a procedure declared in the file delay.h, and the code itself is in the file delay.c. Procedures are "called" by using the name in your code. Many procedures have "arguments", meaning you can put some numbers in the () part to tell the procedure what you want. In the case of DelayMs(t), we pass the number to tell the procedure we want to delay for that many milliseconds. Note, by the way, that DelayMs() is very inaccurate and should only be used for rough delays such as flashing lamps for humans to see.

If you have books about programming you might see the words "function", "procedure" or even "method". For our purposes, though, all of these are really the same thing. They just mean "a block of code which can be executed".

The syntax of a function - a simple one with no arguments - is

return_type function_name()
{
 // code goes here
}

Note that the function has a pair of curly braces { } surrounding the code in the function, just like the previous use for grouping a bunch of statements into a compound statement.

Also note that the function can return a result, and that the type of the result is the very first thing in the function declaration. Here are some examples:

char GiveMeALetter()
{
 return 'w'; // return the character 'w' as the
 // result of the function
}
         
void DontGiveMeAnything()
{
 // void is a special return type that means
 // "nothing returned"
}

In the main program you could call these functions:

int main() // note that even the main program is a function
{
 char a;
 a = GiveMeALetter();
 // the variable a now contains value 'w'
 DontGiveMeAnything();
 GiveMeALetter(); // functions can be called
 // any number of times.
}

Did you notice that the second time I called "GiveMeALetter()" I did not use the same syntax? I did not say a = GiveMeALetter(). So what will the compiler do? The function will return a letter, so where does the letter go? In fact this is OK in C. It does not cause an error. It only means that the result of the function is thrown away.

 

Global variables

Variables can be declared at the top of a program, and this makes them available to all the functions and to the main program. All functions can read the values of the global variables and can also change the values. Here is an example of a program with global variables, a function, and a main program. In fact, this could be a complete program file:

int a, b; // declare these as global variables
char my_string[10];
         
// define a function
void TurnOnLED()
{
// turn on an LED but only if a condition is met
 if (a == 3) PORTD |= BIT(3);
}
         
// main program
int main()
{
 DDRC |= BIT(3); // set direction of PORTC, BIT(3) to
 // output
 a = 3; // set value of a global variable
 // b has not been initialised so it contains a random value!
 TurnOnLED();
}
         

Do you think the LED will turn on?


					

Pointers (and arrays)

I can clearly remember being confused when pointers were first explained to me. However after a while the concept did sink in and I now appreciate what they can do. So please persist in trying to understand them.

You now know that a variable is stored on the AVR in part of the AVR memory or RAM. The RAM looks a bit like a CD rack:

 

Figure 1 RAM compared to a CD rack.

A variable with size one byte fits into just one slot in the "rack". We can uniquely identify that byte (or CD) by saying, "please give me the third one from the top".

A variable with some other size, for example an int (2 bytes) can also be identified by where it starts. It is then understood that the "other" byte is the next in numerical order.

We can also imagine several variables all next to each other in memory, like this:

 

Figure 2 Variables arranged next to each other in memory.

There are several ways to get access to variables which are arranged like that. The first way is to take note of the index (address) of the first in the list. A variable that stores the index (address) of some memory location is called a pointer. In C we might write

char *p = 23; // actually this won't work as shown,
// but that is a fine point I would prefer to ignore
// for now

This is different from anything you have seen so far because of the new use of the character *. This character is used to tell the C language that p is not going to store a character, it is going to store the ADDRESS of a character. We say that it will POINT at the character.

The other way to get access to variables which are in a list is to treat them as an array.

In the C language pointers and arrays are really the same thing. This makes sense when you realise that if you have a pointer to one element of an array, you can find all the others by doing some simple addition or subtraction. In Australia, house numbers are even on one side of the street and odd on the other side. Therefore if you know the house number for one house (in other words, if you have a pointer to a certain house), you can calculate a pointer to any other house. Note, however, that if you want only a house on the same side of the street, you would add 2 for the next house number.

 

Figure 3 Australian house numbering system and "pointer arithmetic".

Let's set up an array of integers:

int my_list[3]; // this reserves space for three integers in memory.
int *p; // this is a pointer but we have not yet
// initialised it so it contains a random value.
my_list[0] = 23; mylist[1] = 67; mylist[2] = -30;
// store data in the array
// note that the first array index is always ZERO
// and the maximum legal array index is thus 2.
p = mylist; // pointers and arrays are actually the
// same thing.

With those declarations, here are some true statements:

*p will have value 23. The * used this way means "whatever p is pointing to right now". It is currently set to point to the memory location that stores my_list[0].

p[1] will have value 67. It is OK to use pointers like arrays, or arays like pointers.

A very common array type is an array of characters. Programmers call this a string, not sure why. There are many uses for strings and many library functions for working with them. However this is only an introduction to C - for more detail please read a proper textbook like Kernighan and Ritchie's "A book on C".

Local variables and parameters to a function

Let's define a function that can calculate the sum of two numbers. This is not a very useful function but at least it is easy to understand what it does:

int my_sum(int a, int b)
{
 char my_local; // example of a local variable
 my_local = a + b; // use the local variable to store
 // an intermediate result
 return my_local; // value is sent back to caller
}

To use this function, we must write (perhaps in the main program):

int result;
result = my_sum(23, -78);
// result should now contain the expected value, -55.

Note that we passed information to the function in the ( ) part after the function name. The function receives this information and can use it to assist with calculating the result. The function my_sum has access to several kinds of information:

  1. Global variables;
  2. Local variables such as my_local. These local variables contain random values when the function first begins to execute - be careful. Local variables are used as storage space by the function, for temporary or intermediate calculations. Information stored in local variables is lost when the function finishes.
  3. Parameters such as a and b - which look just like local variables and can be used as local variables BUT which have guaranteed initial values. The initial values are copies of the values sent by the caller. The caller is the line of code in the main program "result = my_sum(23, -78);". We say it is the caller because it has asked (called upon) the function my_sum to do something.

Note that parameters are copies of information sent by the caller. Because they are copies, and not the actual variables, you can change the values of the parameters in your function and this will have no effect on the caller. For example

int my_sum(int a, int b)
{
 a++; // change OUR LOCAL COPY ONLY
 
 return a + b; // value is sent back to caller.
// note that the result will be off by one!
}
         
int main()
{
 int result;
 int aa = 4;
 result = my_sum(aa, 6);
// result should now contain.... what?
// I think it will be 11 but please check that you agree.
// note that aa still has value 4, not 5. my_sum only
// changed the copy we gave to it. Our original variable
// was not touched.
}

You may be wondering why we bother with this business of passing parameters, when global variables are available. As you go on you will write longer and longer programs, and there will come a time when you can no longer remember what all your global variables do. Parameters help us to organise program code in a way that is easy to remember and easy to recycle. Consider this function:

int my_strlen(char *str)
{
 int result = 0;
 while (*str)
 {
str++; // advance string pointer
result++; // count how many non-zero bytes were found
 }
 return result;
}

This is my version of one of the usual string library functions (strlen) which can tell you the length of a string (character array) which has a zero terminator.

A good toolbox contains many general-purpose tools. For example a pair of scissors can cut paper, carboard, thin plastic sheet, aluminium foil and even other metals if the sheet is very thin. A good function is like a general tool that can be applied to a range of situations. We give the function input (the parameters), it does something and may also produce output. It is modular: we can re-use the function code very easily. We can copy the function and paste it into some other program. Software libraries - like the one for working with strings - are collections of the most generally useful functions, developed by smart people over many years.

However for now, as you start with C, don't worry too much about modularity and other desirable goals. Just get something working, and learn to control your AVR.

 

Error messages

You will probably see many error messages when you compile your first programs. These can be very frustrating when you start out, because they use unfamiliar language. If you can get help from an experienced programmer this will save you a lot of time. However if you are on your own and you have errors you cannot resolve, perhaps your problem is one of these:

  1. Make sure that each program statement ends in a semicolon ( ; ). If you leave off a semicolon on a line, the compiler will probably give an error message for the next line.
  2. Semicolons are also used in the for statement. However commas are used when passing parameters to functions.
  3. Beware of the difference between = and ==. Do not use the single = in an if-then statement until you really know what you are doing.
  4. Beware of the size and type of your variables. Could you have unexpected overflow of the value in a variable - setting it to zero or to a negative value? Could an unsigned value attempt to store a "negative" bit pattern - and be set to a large positive amount?
  5. Beware of the unsigned data types if you use a for loop that counts down. It is possible to accidentally make an endless loop in this case. See the example above under "for loop".
  6. Remember that a pointer can point to any part of the memory. If you then write information to the memory location, are you changing "your" part of the memory or are you changing some other part? This is a very common source of error. Effectively a pointer that is not properly set can cause a change to any variable anywhere in your program. This can lead to very frustrating situations where a program does not work, does strange things, or only works some of the time.
  7. Remember that local and global variables contain random values until you put some definite value into them.
  8. All variables you use in your program have to be declared somewhere. You can't just use "i" for example, without also telling the compiler what i is.
  9. A local variable can have the same name as a global variable. Your compiler may not even give you a warning when you do this (probably by accident). Just be careful not to declare any variable more than once. If you declare it at the top of your program, do NOT declare it again later on. I always use the letter g as the first character of all global variable names e.g. "gMyInteger". This way I can easily remember which variables are local and which are global. Develop good habits and they will save you a lot of time.
  10. The #define statement is very useful but beware - it does NOT usually have an = sign in it. We say #define thing 3, NOT #define thing = 3. If you put an = sign there, the compiler will get very confused and then so will you.

Debug output - see what is going on

The most powerful tool I know of to find errors in programs is to produce some debug output. This means that we insert some code into our program that will make a display for us.

If you have a spare I/O pin on your AVR, connect an LED to it, set the direction of the pin to output, then use it as a debug signal. Change your program so that when it reaches a critical point, the LED is turned on. This will at least tell you if a long program has reached a certain point. If you happen to have an oscilloscope, you can use it to monitor voltages on such debug output pins and get even more information.

If possible it is a good idea to set up RS232 communication as explained in the RS232 tutorial. At any point in your program you can then print text information to the terminal. This can be incredibly helpful because you can use it to examine the value of variables. For example:

#include "uart.h" // don't forget to include uartintr.c in your project
         
int DoSomething(int arg)
{
 // send output to the terminal so we know where the
 // program is and what the value of the arg parameter is.
 putstr("\r\nDoSomething(");
 putInt(arg); // hexadecimal string output e.g. 0FA5
 putch(')'); // close bracket, looks better that way
 return 0;
}
         
int main()
{
 int i, j;
 InitUART(UBRR115200); // assuming your crystal speed
 // supports this baud rate
 for (i = 0; i < 0x30; i++)
 {
  j = DoSomething(i);
 } // end for loop in i
 putstr("\r\nDone");
}

If the UART is working properly and your terminal is connected, when the program runs on the AVR you will see:

DoSomething(0000)
DoSomething(0001)
DoSomething(0002)
.... many more like that ...
DoSomething(002A)
Done

This example may not be very exciting but at least it shows that you can use the terminal output any way you wish to track program flow. You can print words that identify what part of the program is executing, and you can view the value of any variable.

Note that I used plenty of return and newline characters in my output strings. The sequence "\r\n" tells the terminal program to move the cursor back to the start of the line and also one line down. This is usually what you want when displaying a history of program execution. If you use "\r" by itself, the new text will overwrite whatever was already there. Sometimes that is quite useful e.g. for a live display of a variable.


Exercises

Set up the breadboard as shown for the program tutorial. Modify the code hello.c to achieve the following:
  1. Change the rate of flashing to a 200 millisecond flash every second. Check this with an oscilloscope.
  2. Put 4 or more LEDs on PORTC and use them to show the binary numbers 0, 1, 2... in an endless loop.
  3. Now make your LEDs work like Christmas lights (make a pulse of light travel along the chain in a loop).
  4. Now make some LEDs work on PORTD.

(Stuck? click here for some hints)

back to main tutorial page


Dr Nathan Scott · nscott@mech.uwa.edu.au