Preprocessor¶
Learning objectives: The student can use preprocessor directives in C language programs and understands their benefits in hardware-oriented programming.
So far, in the C code introduced during the course, we have already used some mysterious commands starting with
#
. These are the preprocessor commands (directives) that control the C language compilation process. As part of the process, external libraries can be included in the program, as well as introducing custom constants and macros.Generally, preprocessors have their own programming language, which doesn't have to be tied to the language being compiled.
Including Libraries¶
Header files are declaration files for library functions, containing, for example, their prototypes. We'll explore these more when discussing libraries, but let's take a look at the two different forms of the
#include
preprocessor directive.#include <headerfile.h>
#include "headerfile.h"
There are a couple of syntax details here. If
< >
brackets are used, the compiler looks for the header file in the compiler environment's own libraries, such as the C standard library or the embedded device's programming environment libraries. If " "
quotes are used, the header file is searched for in the same (project) directory structure where our own program code is located. The idea is that user libraries or externally imported libraries are accessed using quotation marks.In itself, the order in which header files are introduced in a code module doesn't matter for compilation. If you look deeper into the contents of the header files, you'll notice that they often pull in other header libraries into the program. More on this in a moment...
For example, the prototypes for the familiar printing functions from the standard library like
printf
and sprintf
are found in the header file stdio.h
:#include <stdio.h>
Constants¶
The preprocessor can also introduce constants, which are not the same as (constant) variables in C. The value of a constant can be anything, as long as it adheres to C's syntax.
Constants are handy for naming frequently occurring values, such as numerical constants set from outside the program. For example, the constant M_PI from the math library
math.h
provides a standardized approximation of pi, which should be used... because... well, it's a standard, ensuring that all functionality requiring the value of pi works in the same way.Now, note that these preprocessor constants exist only in the precompiled code. During the compilation process, the preprocessor replaces the constant in the code with its value, literally one-to-one. This means that a preprocessor constant does not occupy a memory location and, therefore, cannot be used like a variable.
A preprocessor constant is introduced in the program using the
#define
directive. The syntax is as follows:#define PI 4.0 // Constants are generally written in uppercase letters.
float circumference = 2 * PI * radius;
This code would be precompiled as follows:
float circumference = 2 * 4.0 * radius;
As noted, the constant's value is inserted as is in the code.
Of course, there are exceptions in the compiler's behavior; a constant cannot be placed inside quotation marks, nor can it be part of a word.
#define TRUE 1
...
printf("TRUE"); // this will not be interpreted as a constant, try it!
int x = TRUETRUE; // this will not be interpreted as one or two constants
A New enum Variable Type¶
There is another way to introduce constant values, using the
enum
variable type. Now, constants introduced with enum
are set incrementally, starting from the initialization value of the first constant. If no initialization value is given, it starts from zero.enum retro_computer { PET=1, VIC20, C64, C128, AMIGA };
// Introducing a variable of this type
retro_computer home_computer = C64; // Now home_computer = 3
Naturally, we can create boolean values in our program using
enum
. Now, the constant FALSE
should be 0, and TRUE
should be 1, meaning it is not equal to zero.enum boolean { FALSE=0, TRUE };
...
boolean is_true = FALSE;
Note! Often, different development environments have their own library (header file) where the constants
TRUE
and FALSE
are already predefined. It's better to use these rather than creating your own constants. This avoids the possibility of our own constant definitions interfering with the functioning of other libraries.Macros¶
A constant can also be used to replace frequently used code, which is then called a macro. Preprocessor macros can also take arguments.
#define FOREVER for(;;)
#define INC(A) A++;
In the code, the preprocessor replaces the macros just like constants.
In the latter
INC
constant, we pass the preprocessor variable A
as a parameter to the macro, which increments its value by one. Now the programmer has to ensure that the macro is used correctly in the C code, considering variable types, etc. This can easily lead to bugs in the code if, for example, we don't enclose the macro in parentheses, which can cause the execution order to be different from what was intended. Or, if we define a semicolon ;
at the end of the macro definition, we won't be able to use the INC
macro as part of a for statement.#define INC(A) A++;
for (x=0; x < 100; INC(x)) {}
// the macro would be replaced exactly with its value, leading to
// an extra semicolon after the assignment statement x++.
for (x=0; x < 100; x++;) {}
If we omit the semicolon from the constant definition, it can be used elsewhere in the code, if we do not forget adding the
;
when appropiate.We have this other example:
#define SQUARE(X) X * X // problematic, lacks parentheses
int result = SQUARE(4 + 1); // expands to 4 + 1 * 4 + 1 -> results in 9, not 25
that can be correcting using:
#define SQUARE(X) ((X) * (X)) // properly wrapped in parentheses
int result = SQUARE(4 + 1); // now expands correctly to ((4 + 1) * (4 + 1)) -> 25
Conditional Compilation¶
We can also define a constant without a value. Similarly, such a constant only exists during the compilation process.
This feature is typically used to control the compiler's behavior so that we can specify which (device) libraries or other code are selectively included in the program. Often, portable (i.e., working in multiple environments) code uses OS-specific constants to help with compilation. For example, the compiler environment constant
_WIN32
indicates that we want to compile our code for a Windows machine. Compiler environments usually automatically set these constants when you tell them which device you're working on. So there's no need to worry about them in this course.#define _WIN32
In different operating systems, we may have different libraries for using peripherals, even though the program logic works the same. Often, such a constant is provided to the compiler as a command-line parameter before it starts compiling the code. An example of platform constants that could be included in the compilation process via the compiler (gcc) command-line parameter
gcc -D_WIN32 -DVERSION=190
.#ifdef __unix__ // Test if the constant __unix__ is set
#include <unix.h>
#elif defined _WIN32 // Test if the constant _WIN32 is set
#include <windows.h>
#endif
#if VERSION < 190
#error versions below 1.9.0 not supported
#endif
Thus, the preprocessor also implements a familiar
if-else if-else
control structure. This conditional statement must end with the #endif
directive. We also notice the preprocessor directive #error
.Preprocessor conditions have a very important use case in bringing the functionality of libraries into a program. Conditional preprocessing ensures that the header file's code is compiled only once. Based on previous knowledge, when we want to use a library in several different code modules, we add the corresponding
#include
directive to each module. However, this would mean that the header file's code (e.g., prototypes) would be introduced multiple times in the program, which C compilers don't like.We work around this problem with a preprocessor conditional statement by checking for the existence of a constant at the beginning of the header file.
#ifndef _HDR // Check if the constant _HDR is set
#define _HDR // Set the constant _HDR
...
// Declarations in the header file
...
#endif
Here, we instruct the compiler by setting the constant
_HDR
during the first compilation, whose existence prevents the same code from being recompiled if the header file is reused in another module.Although the header guards using
#ifndef
have been popular in embedded systems, nowadays most compilers accept #pragma once
, which is a simpler and less error-prone alternative. The #pragma once
directive substitutes the previous examples. Example¶
In embedded systems, conditional compilation is typically used to define device-specific I/O, so that the same code works on different devices/microcontrollers. Compiler environments use constants with the same names, whose values depend on the device.
Below, we create a constant to check if button 1 is pressed, regardless of which MCU we have (either ATmega128 or ATtiny13). We note that on different MCUs, the push button is connected to different I/O ports, for which the device libraries define their own constants
PINA
, PAO
, PINB
, and PB2
.#ifdef __AVR_ATmega128__
#define BUTTON1_DOWN (!(PINA & (1 << PA0)))
#else ifdef __AVR_ATtiny13__
#define BUTTON1_DOWN (!(PINB & (1 << PB2)))
#endif
..
// later in the code
if (BUTTON1_DOWN == VALUE) { ...
...
}
More on these constants related to embedded device I/O can be found in the hardware-related section of the course.
Conclusion¶
There is still much more to preprocessor directives and languages that we haven't covered here. In this course, there is no need to define your own macros, but the hardware-related libraries we use for programming our device have plenty of predefined macros. Well, those macros have been tested by thousands of coders over time.
Give feedback on this content
Comments about this material