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:

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.

Macro Processing

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 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

will change

into

 

but it will have no effect on

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) )

Listing 11.1: Example of #define

 

Conditional Compiling

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

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

or

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

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

Listing 11.2: Examples on conditional compilation

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
}

Listing 11.3: Conditional compilation can help in removing all debugging code


 

Including Other Source Files

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.

 

Inline assembly

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");
}

Listing 11-5:  Example of an assembly function.
 

 

Return to Table of Contents