C preprocessor usage example

We are going to look at how the C preprocessor is used in the textbook examples to configure the clock of the PIC24 processor.

The common processor setup

Almost all of the textbook examples start with the following include to get the needed definitions and compiled functions.

#include "pic24_all.h"

The textbook’s include files are located in the lib/include directory. The pic24_all.h include file contains the standard #include guard to prevent double inclusion.

#ifndef _PIC24_ALL_H_
#define _PIC24_ALL_H_

The pic24_all.h file also #includes several more .h that provide specialized fucntionality. One of these, pic24_chip.h, provides definitions that vary according to the processor.

#include "pic24_chip.h"

Processor specific definitions

The first action of pic24_chip.h is to include the appropriate definition files provided with the Microchip compiler.

#if defined(__PIC24H__)
#include "p24Hxxxx.h"

The way in which __PIC24H__ is defined isn’t straightforward. In our case, the MPLAB X IDE generates a Makefile which controls the build process. The Makefile specifies that the C compiler is invoked with a command line argument similar to -mcpu=24HJ64GP502. The C compiler will then generate a pre-defined macro __PIC24H__. C and C++ compiler generates a lot of pre-defined macro. Programmers just need to look up what they are.

The include file p24Hxxxx.h is part of the Microchip compiler distribution, not the textbook examples. You’ll find this file within the support/PIC24H/h subdirectory of the Microchip distribution. This include file contains unusual C definitions, such as the following, which are used to manipulate specical function registers.

extern volatile unsigned int  PORTB __attribute__((__sfr__));
typedef struct tagPORTBBITS {
  unsigned RB0:1;
  unsigned RB1:1;
  unsigned RB2:1;
  unsigned RB3:1;
  unsigned RB4:1;
  unsigned RB5:1;
  unsigned RB6:1;
  unsigned RB7:1;
  unsigned RB8:1;
  unsigned RB9:1;
  unsigned RB10:1;
  unsigned RB11:1;
  unsigned RB12:1;
  unsigned RB13:1;
  unsigned RB14:1;
  unsigned RB15:1;
} PORTBBITS;
extern volatile PORTBBITS PORTBbits __attribute__((__sfr__));
/* PORTB */
#define _RB0 PORTBbits.RB0
#define _RB1 PORTBbits.RB1
#define _RB2 PORTBbits.RB2
#define _RB3 PORTBbits.RB3
#define _RB4 PORTBbits.RB4
#define _RB5 PORTBbits.RB5
#define _RB6 PORTBbits.RB6
#define _RB7 PORTBbits.RB7
#define _RB8 PORTBbits.RB8
#define _RB9 PORTBbits.RB9
#define _RB10 PORTBbits.RB10
#define _RB11 PORTBbits.RB11
#define _RB12 PORTBbits.RB12
#define _RB13 PORTBbits.RB13
#define _RB14 PORTBbits.RB14
#define _RB15 PORTBbits.RB15

These C definitions do not specify the actual address of the special function register PORTB. That is found in linker files within the directory support/PIC24H/gld subdirectory containing lines similar to the following:

 PORTB        = 0x2CA;
_PORTB        = 0x2CA;
_PORTBbits    = 0x2CA;

Clock configuration

Eventually p24Hxxxx.h “returns” to pic24_chip.h which “returns” pic24_all.h. There we will find an include of another configuration file.

// Include user-configurable options
#include "pic24_libconfig.h"

It is within pic24_libconfig.h that we find a macro definition for the default of the clock for a PIC24H processor. (Doxygen is a tool, similar to Javadoc, for generaing documention from a program.)

#elif defined(__PIC24H__) || defined(__DOXYGEN__)
#define CLOCK_CONFIG FRCPLL_FCY40MHz

We soon drop back to pic24_all.h which contains the include for the file for actually defining the clock configuration options.

#include "pic24_clockfreq.h"

C macro follies

Within pic24_clockfreq.h is some of the most complicated C macrory (invented word) you will even encounter. First is the definition of possible clock frequencies.

#define FRCPLL_FCY40MHz                 5,            FNOSC_FRCPLL, 40000000L, POSCMD_NONE,       -1,  (PIC24H_DEFINED || dsPIC33F_DEFINED),  configClockFRCPLL_FCY40MHz,                498

This is followed by a “non-standard #if” to “to make sure the CLOCK_CONFIG choice selected exists and is valid”.

#if (CLOCK_CONFIG != 498)

This test takes advantage of the C compiler’s ability to do a little expression evalution. Remember that CLOCK_CONFIG is FRCPLL_FCY40MHz and FRCPLL_FCY40MHz is a sequence of eight expressions separated by commas. The last expression is the number 498.

When the macro substiutions are made the last expression of this sequence will be 498 != 498. Because the comma operator of C, just like the comma operator of Java, returns its last value; (CLOCK_CONFIG != 498) evalutes to zero. Wow!

If you continue through the file, you will encounter several definitions designed to produce a value for FCY.

#define GET_FCY(params)                   _GET_FCY(params)
#define _GET_FCY(ndx, oscSel, Fcy, posCmdSel, poscFreq, isSupported, configClockFunc, magic)                   Fcy
#define FCY                     GET_FCY(CLOCK_CONFIG)

There is also a series of definitions to define a default code configuration function

#define GET_IS_SUPPORTED(params)          _GET_IS_SUPPORTED(params)
#define GET_CONFIG_DEFAULT_CLOCK(params)  _GET_CONFIG_DEFAULT_CLOCK(params)
#define _GET_IS_SUPPORTED(ndx, oscSel, Fcy, posCmdSel, poscFreq, isSupported, configClockFunc, magic)          isSupported
#define _GET_CONFIG_DEFAULT_CLOCK(ndx, oscSel, Fcy, posCmdSel, poscFreq, isSupported, configClockFunc, magic)  configClockFunc
#define CONFIG_DEFAULT_CLOCK()  GET_CONFIG_DEFAULT_CLOCK(CLOCK_CONFIG)()
#if GET_IS_SUPPORTED(FRCPLL_FCY40MHz)
void configClockFRCPLL_FCY40MHz(void);
#endif
static inline void configClock() {
  CONFIG_DEFAULT_CLOCK();
}

It’s the last definition that makes a call to configClock() result in a call of configClockFRCPLL_FCY40MHz().

Controlling compilation

If you take a look at pic24_clockfreq.c, contained in the lib/common directory, you’ll see the configClockFRCPLL_FCY40MHz() is compiled only when the FRCPLL_FCY40MHz clock is supported for your chosen processor. This is explained in great detail in the textbook.