Chapter 11: Preprocessor Directives
What's in Chapter 11?
Using #define to create macros
Using #ifdef to implement
conditional compilation
Using #include to load other
software modules
Writing in-line assembly code
C compilers incorporate a preprocessing phase that alters the source code in various ways before passing it on for compiling. Four capabilities are provided by this facility in C. They are:
macro processing
inclusion of text from other files
conditional compiling
in-line assembly language
The preprocessor is controlled by directives which are not part of the C language proper. Each directive begins with a #character and is written on a line by itself. Only the preprocessor sees these directive lines since it deletes them from the code stream after processing them.
Depending on the compiler, the preprocessor may be a separate program or it may be integrated into the compiler itself. C has an integrated preprocessor that operates at the front end of its single pass algorithm.
We use macros for three reasons. 1) To save time we can define a macro for long sequences that we will need to repeat many times. 2) To clarify the meaning of the software we can define a macro giving a symbolic name to a hard-to-understand sequence. The I/O port #define macros are good examples of this reason. 3) To make the software easy to change, we can define a macro such that changing the macro definition, automatically updates the entire software.
#define
Name CharacterString?...
define names which stand for arbitrary strings of text. After such a definition, the preprocessor replaces each occurrence of Name (except in string constants and character constants) in the source text with CharacterString?.... As C implements this facility, the term macro is misleading, since parameterized substitutions are not supported. That is, CharacterString?... does not change from one substitution to another according to parameters provided with Name in the source text.
C accepts macro definitions only at the global level.
The Name part of a macro definition must conform to the standard C naming conventions as described in Chapter 2. CharacterString?... begins with the first printable character following Name and continues through the last printable character of the line or until a comment is reached.
If CharacterString?... is missing, occurrences of Name are simply squeezed out of the text. Name matching is based on the whole name (up to 8 characters); part of a name will not match. Thus the directive
#define
size 10
will change
short
data[size];
into
short
data[10];
but it will have no effect on
short
data[size1];
Replacement is also performed on subsequent #define directives, so that new symbols may be defined in terms of preceding ones.
The most common use of #define
directives is to give meaningful names to constants; i.e., to define so
called manifest constants.
However, we may replace a name with anything at all, a commonly
occurring expression or sequence of statements for instance. TriggerPendSV()
will set bit 20,
triggering a PendSV. SetPA5
(0x20
will set PA5. Wait(500)
will execute the loop
500 times.
#define
TriggerPendSV() { NVIC_INT_CTRL_R = 0x10000000;}
#define SetPA5(x) {
GPIO_PORTA_DATA_R
= (
GPIO_PORTA_DATA_R
&
~0x20) | (x)};
#define
Wait(t)
{int
wait;
for
(wait = 0; wait < (t); wait++) {}
};
#define
CONVERT4BPP(c) ( ((c) << 12) | ((c)
<< 7 ) | ((c) << 1) )
This preprocessing feature lets us designate parts of a program which may or may not be compiled depending on whether or not certain symbols have been defined. In this way it is possible to write into a program optional features which are chosen for inclusion or exclusion by simply adding or removing #define directives at the beginning of the program.
When the preprocessor encounters
#ifdef
Name
it looks to see if the designated name has been defined. If not, it throws away the following source lines until it finds a matching
#else
or
#endif
directive. The #endif directive delimits the section of text controlled by #ifdef, and the #else directive permits us to split conditional text into true and false parts. The first part (#ifdef...#else) is compiled only if the designated name is defined, and the second (#else...#endif) only if it is not defined.
The converse of #ifdef is the
#ifndef
Name
directive. This directive also takes matching #else and #endif directives. In this case, however, if the designated name is not defined, then the first (#ifndef...#else) or only (#ifndef...#endif) section of text is compiled; otherwise, the second (#else...#endif), if present, is compiled.
Nesting of these directives is allowed; and there is no limit on the depth of nesting. It is possible, for instance, to write something like
#ifdef
ABC
... /* ABC */
#ifndef DEF
... /* ABC and not DEF */
#else
... /* ABC and DEF */
#endif
... /* ABC */
#else
... /* not ABC */
#ifdef HIJ
... /* not ABC but HIJ */
#endif
... /* not ABC */
#endif
where the ellipses represent conditionally compiled code, and the comments indicate the conditions under which the various sections of code are compiled.
A good application of
conditional compilation is inserting debugging instrumemts. In this
example the only purpose of writing to PORTC is assist in performance
debugging. Once the system is debugged,we can remove all the debugging
code, simply by deleting the #define
Debug 1
line.
#define
Debug 1
int Sub(int j){ int i;
#ifdef Debug
PORTC |= 0x80; /* PC7 set when
Sub is entered */
#endif
i=j+1;
#ifdef Debug
PORTC &= ~0x80; /* PC7
cleared when Sub is exited */
#endif
return(i);}
void Program(){ int i;
#ifdef Debug
PORTC |=0x40; /* PC6 set when
Program is entered */
#endif
i=Sub(5);
while(1) { PORTB=2; i=Sub(i);}}
void ProgB(){ int i;
i=6;
#ifdef Debug
PORTC &= ~0x40; /* PC6
cleared when Sub is exited */
#endif
}
The preprocessor also recognizes directives to include source code from other files. The two directives
#include
"Filename"
#include
<Filename>
cause a designated file to be read as input to the compiler. The difference between these two directives is where the compiler looks for the file. The <filename> version will search for the file in the standard include directory, while the "filename" version will search for the file in the same directory as the original source file. The preprocessor replaces these directives with the contents of the designated files. When the files are exhausted, normal processing resumes.
Filename follows the normal MS-DOS file specification format, including drive, path, filename, and extension.
In Chapter 10, an example using #include was presented that implemented a feature similar to encapsulated objects of C++, including private and public functions.
Keil uVision uses this syntax to embed assembly code into C programs
__asm void
Delay(unsigned long ulCount)
{
subs r0, #1
bne Delay
bx lr
}
Listing 11-4:
Example of an assembly function
Code composer studio implements assembly functions in a similar manner.
void Delay(unsigned long ulCount){
__asm ( "
subs r0, #1\n"
"
bne Delay\n"
"
bx lr\n");
}
Return to Table of Contents