The Preprocessor
Even if you want to program “bare metal” you can rely on some things others have programmed for you. This is done by including certain existing functionality into the software of the compiler you are using. A typical C program on an MCU (here an AVR-type) can start like this:
#include <avr/io.h> #include <inttypes.h> #include <stdio.h> #define CPUCLK 16
Some files containing specific data for a certain type of MCU are included to your code. They come with the toolchain or environment you have installed with the compiler. If you use a certain “integrated” function like strlen() (measuring the length of a “\0”-terminated string) for example you have to include the specific “.h”-file (string.h in this case) in advance.
Also constants can be defined here. The #define directive tells the preprocessor to replace specific text (e.g. a certain word) with another text in all subsequent source code. This can be coupled to a certain condition like:
#ifndef CPUCLK #define CPUCLK 16 #endif
If a value for CPUCLK (the MUC’s clock rate) is unknown to the compiler, then it will be defined as 16 (MHz). Sometimes definitions are made in a header file you are about to include and you have to reset them to another value. This is good location for an “ifdef” statement:
#ifdef CPUCLK #undef CPUCLK #endif #define CPUCLK 16
You can define lots of things. Here are some self-explaining examples:
#define LCD_PORT PORTD #define CTRL_PORT PORTA #define E PD7 #define PI 3.1415927 #define SYSCLK 16000000UL
(Hint: 16000000UL is defined as an unsigned long data type. See next step!)
With the preprocessor directives you define the basic outline of the functions and constants you are about use if they are not programmed by your own.
Variables
Variables are important to store any kind of volatile data during run-time of your program.
Declarations
Before you can use a variable in your program, you have to declare it. This is extremely useful for both, the compiler and you. The compiler can reserve memory space and you are not that much prone to get problems with typing errors. For Example when using the variable “value1” in your program and you later use “valeu1” a language like BASIC for example (where declaration only is mandatory if you set “OPTION EXPLICIT”) will not show an error but the code won’t work because now there are 2 different variables in your code in different places.
A C program consists of functions which will be explained in detail later. To make it short here: Functions are separate blocks of code that serve a specific purpose and form an encapsulated structure. This structure is coded between the “{” and the “}” parenthesis at the beginning and the ending of the function’s code. “{” and the “}” are for a distinguishable block of code, in this case the whole function.
In C declarations can be global (then they are declared outside any function, mostly located in the code’s head section) or they can be local, thus they are only valid inside the specific function. Some examples:
Global variable:
#include <stdio.h> int x; int main(void) { }
Local variable, only valid in main(). For understanding functions see next chapter…
#include <stdio.h> int main(void) { int x; }
If you have the choice to declare a variable as local or global, it is recommend to set it as local. Stack memory will be made free after the function has been completed thus memory issues are not so likely to occur compared to setting most of the variables as global.
There are different types of variables. In contrast to other languages we have only numerical values. Therefore in C the handling of alphanumerical variables (“Hello, this is a text!”) is a little bit different from other programming languages. I have written a short intro that can be found here. Texts are coded as arrays of characters where the type of char is used for preferably:
char c0a; //8 bits from -128 to 128, single character only char c0b[100]; //8 bits from -128 to 128, 99 character + terminating "\0" unsigned char c1a; //8 bits from 0 to 255 unsigned char c1b[16]; //8 bits from 0 to 255, 15 characters + termination "\"
Numerical values can be stored in the char type as well (with limitations to values in the 8-bit range). If you need larger numbers there are types like int and long and even long long.
int x0; //16 bits holding data from -32768 to +32767; unsigned int x1; //16 bits holding data from 0 to 65635; long x2; //32 bits -2,147,483,648 to 2,147,483,647 unsigned long x3; //32 bits 0 to 4294967296
The difference between signed and unsigned is that unsigned can only contain positive values (except from 0). When no negative value in a given variable can occur, it is a good idea to use the unsigned subtype.
Definitions
When assigning a value to a variable that you have previously declared, it is mandatory to use the variable in a mathematically correct manner:
x0 = 12; x1 = 3 * x0; x2 = (x0 * x0 + 12) / 10;
The well-known rules for working with mathematical equations (“brackets before multiplication/division before addition/substraction”) are applied here by the compiler when converting the formula into the code for your MCU.
It is also allowed to do declaration and definition in one step and setting an initial value this way:
int x0 = 15; int x1 = CPUCLK / 3; int x2 = x1 * 3;
When using another variable to assign a value to directly on declaration it must have been defined before!
Functions
Functions are code sequences for a specific purpose that can be called repeatedly during the run of the program whenever a certain functionality (represented by a specific function) is required.
A typical “C”-program consists of at least one function (called main()) or several hundreds or even thousands of functions where main() only is the starting point and later the functions are called out of main() or other functions subsequently in a logical way. Programming non-OOP above all is calling functions by a main program or from another function.
Declarations
It is a common and good practice to declare functions by the first section of a program;
#include <avr/io.h> #include <inttypes.h> #include <stdio.h> #define CPUCLK 16 int main(void); void wait_ms(int); // Cheap & dirty delay void wait_ms(int ms) { int t1, t2; for(t1 = 0; t1 < ms; t1++) { for(t2 = 0; t2 < 137 * CPUCLK; t2++) { asm volatile ("nop" ::); } } } int main() { while(1) { wait(100); //Useless! :-) } }
The two lines in red hand over information to the compiler that there are two functions in the application: wait_ms() and main(). This declaration can be left out but under certain circumstances the compiler will state an error. This will occur when a calling function is placed above the function called. If the code of main() should be placed above wait_ms() (a simple delay function) the function wait_ms() will be called before it has been defined on compiling.
Today in “modern” C only the types of variables are in parenthesis not the names.
Mathematical functions
Many mathematical functions have been included into the C compiler system by its creators, like the sine-function (sin()) which requires including the “math.h” file. But most of the functions are created by the programmer, like the square() function in this example:
unsigned long square(int x) { long sq; sq = x * x; return sq; }
“Return” hands over the value calculated by the function to the calling process. This can be used as another variable. The type of the function should be the same like the value calculated and returned.
A very short version of this function would be:
unsigned long square(int x) { long sq = x * x; return sq; }
And even shorter would be:
unsigned long square(int x) { return x * x; }
Calling (i. e. using) this function would look like that:
int main(void) { long s = square(7); }
The variable s now contains the value of 49 as soon as the assigned has been completed and the functions square() has been called.
Functions tailored to the point
Usually you will write a lot of functions for a program running on an MCU. They are specialized code and can be designed for nearly every purpose. If you need to do a certain routine more than once, then write a function for it. You hand over one or more values (called parameters) and you get back one value from the return-statement. Sometimes it is not necessary to get back a value, then we have a void function:
void led(int status) { if(status) { PORTD &= ~(1 << 1); //PD1 low } else { PORTD |= (1 << 1); //PD1 hi } }
This function switches an LED on or off, depending on a parameter (status) handed over to the function. As you can see there is no returned value. So, this is called a “void” function. For a full explanation of this code snippet please refer to the section “Bit manipulation” later on in this text.
Comments and names – Make your code understandable
There are two things you can do to make code understandable to you (for later revision, sometimes months after coding) and others:
- Well chosen names for variables and functions
- Comments
The first aspect in this context is the sensible naming of variables and functions. For example “function1()” is not a very clever name, “display_init()” is. The same is true for
long x1 = 0;
in contrast to
long frequency = 0;
You can us any letter (A..Z, a..z), any number (0..9) and the underscore (_) or the dash (-) for names of variables and functions. But the first character must not be a number! And “A” is not the same as “a”. C is case sensitive.
Another important thing about in source code are comments. This is text for describing the programmer’s intentions for a respective part of code. The compiler will ignore it because comments’ only intention is to explain to the reader what the code is expected to do. The comments therefore are superfluous to the compiler, the program runs just as well without comments. But if you re-read the code later without comments you usually can write it once again. Thus if you want to understand your own C code after a few weeks or months, you should use comments to describe what the task of the respective code is, or you will find it difficult to understand your own thoughts again.
Comments must be specially marked so that the preprocessor can remove them from the code before the compiler translates the code. A single-line comment begins with a double slash:
//This is a short comment
Multi line comments are included in the “/*” (start) and “*/” (end) characters:
/* This is a longer comment showing the purpose of this function */
Generally spoken we should always prefer, self-explaining code over comments. Comments are often useful but we should not exaggerate.