CHAPTER 17. More C and the wider C environment – Designing Embedded Systems with PIC Microcontrollers, 2nd Edition

CHAPTER 17. More C and the wider C environment
We have now reached a stage at which we should have some level of confidence in writing simple C programs for the PIC microcontroller. There still remain gaps in our knowledge, however. One of these is the use of interrupts. An exploration of this takes us to the very boundary of how C is used and actually makes us step outside the language altogether. Furthermore, as our programs become bigger, it is useful to know about the wider environment in which we are programming, for example those other files which we link in or include. We have used these so far with little knowledge of how they work.
The aims of this chapter are therefore twofold. One is to develop knowledge of language aspects which enable closer working with the hardware. This includes use of Assembler inserts and interrupts. The other is to expand our knowledge of the wider context within which C operates. To allow this to happen, we will need to develop our knowledge of certain other aspects of C itself.
On completion of this chapter you should have developed a good understanding of:
• The use of Assembler inserts.
• The use of interrupts.
• Further aspects of data definition and storage, and how memory usage can be controlled.
• The files usually linked in to an application, including header and start-up files.
• The Linker and Linker Script.
As in other chapters, examples are used as widely as possible.

17.1. The main idea – more C and the wider C environment

While this chapter is meant to develop your expertise further in C, the one thing it will probably not do much is develop your skill in writing C code itself. Instead, we meet C at its limits, confronting the need to work very close to the hardware through interrupts or out-of-the-ordinary memory maps. We will also look at those files outside the source code, as well as the Linker, which pulls them all together.
To help relate these different elements to each other, a number of examples will be taken from the file which starts all the C program examples in this book, the c018i.c start-up file. This contains a number of interesting programming features. Through studying these, we will at the same time learn something about this important program component. It is not simple code, however, and you don't need to understand it all. It is suggested that you print out the source version of it and have it to hand as you work through the chapter. It is a comparatively short piece of code – a print-out will require about two and a half pages. To find it, simulate any C file in this book with MPLAB and it automatically pops up on the screen as the simulation starts. Otherwise, find it in the mcc18\src\traditional\startup folder of the C18 installation.

17.2. Assembler inserts

Despite the usefulness of C in the embedded environment, there remain times when it is still better to use Assembler. These times include:
• When certain instructions cause very processor-specific actions, for which C simply has no equivalent – in the PIC world these include the instructions SLEEP or CLRWDT.
• Where the timing requirements of program execution are very specific and the programmer needs to have direct control over how a certain program section is written.
• Where a section of program must execute very fast and the programmer wishes to write it in the most efficient way possible.
It is therefore useful to be able to switch to Assembler programming, when necessary, within a C program. This is called ‘in-line assembler’. It presents an opportunity distinct from writing a whole program section in Assembler and linking it into the main program through the build process, as illustrated in Figure 14.2.
The MPLAB C18 compiler allows Assembler inserts to be placed in a C program. These can range from a single instruction to a whole block. One might expect the C compiler to refer blocks of in-line assembler code back to MPASM, the regular MPLAB Assembler. However, it doesn't. The C18 compiler has its own internal assembler, which is applied to these sections of in-line code.
The C18 in-line assembler differs from the regular MPLAB Assembler in a number of significant ways. The major differences are:
• The Assembler section must be contained between the identifiers _asm and _endasm.
• Assembler directives may not be used.
• Comments must be in C or C++ format.
• No operand defaults are applied; operands must be fully specified.
• Full text versions of Table Read/Write instructions (as seen at the end of Table A5.1) must be used.
• The default radix is decimal.
• Literals are specified using C radix notation.
• Labels must end with a colon.
An example of in-line assembler code is given in Program Example 17.1. While at first glance it looks like a regular piece of code, there are in fact no fewer than six of the characteristics of in-line assembler coding applied. All are labelled and relate directly to the list above. The example is taken from the start-up file c018i.c.
Program Example 17.1.
A fragment of the start-up code c018i.c
In-line assembler should be used with some care, especially if the block of Assembler is of any length. As we know, Assembler imposes less discipline than C. It is easy to step outside this discipline, even unknowingly, while writing a section in Assembler, and in so doing corrupt the structure of the host program. As a beginner it is unwise to write inserts that impact on variables or functions that are declared in the C file. If a big block of Assembler is to be written, it is better to write it as a separate file, assemble it with the MPASM Assembler and link it into the main program. This will help to ensure that order is maintained in memory mapping, usage of variables and calling of functions.

17.3. Controlling memory allocation

One of the benefits of working with a high-level language should be that the programmer does not need to worry about the memory map or how memory is allocated. The C compiler will, if we want it to, look after almost all of this. Somewhere, of course, the compiler needs to be given information about the memory of the computer for which it is compiling. This is hidden in the Linker Script, which is described in Section 17.10.
There are situations in the embedded environment, however, when we want to take back control of memory allocation. These include those very hardware-specific activities, like dealing with interrupts or configuration bits, as well as the broader issues of optimising use of the memory map.
The techniques that are available in the C18 compiler for control of memory allocation are varied. Some are complex and should only be used by experienced programmers. A few need to be recognised by all. These are now explored.

17.3.1. Memory allocation pragmas

We have already (in Section 14.2.9) come across the concept of the preprocessor directive. A special type of directive is the #pragma. This allows C to be customised by a specific compiler – every time #pragma is used, the statement which follows it is specific for that compiler.
The C18 compiler has four pragmas to control memory allocation. These change the ‘section’ (i.e. a specifically identified memory block) into which the compiler puts data. They are shown in Table 17.1. The full format of each allows for a number of different options, given in full in Ref. 14.2. These can be quite complex, so we only go into limited detail here.
TABLE 17.1 Pragmas for memory allocation
# pragma code …Locates program code in program memory
# pragma romdata …Locates data in program memory
# pragma udata …Locates uninitialised user variables in data memory
# pragma idata …Locates initialised user variables in data memory
The #pragma met most often is the first in the table, which has the general format of:
This acts in a way like the org directive in Assembler, specifying – if it is needed – where program code should be placed in memory. Both the terms in brackets are optional. It is one of these pragmas which starts every C18 program we write, coming at the beginning of the c018i.c start-up file. The opening lines are shown here:
#pragma code _entry_scn=0x00
void _entry (void)
_asm goto _startup _endasm
#pragma code _startup_scn
_startup (void)
Here the specified format for #pragma code is applied with a section name of _entry_scn and the address of 0x00. A few lines down the pragma is used again, with a section name of _startup_scn, although this time an address is not specified. This allows the Linker to set the address. Both _entry_scn and _startup_scn are names reserved by C18, the former to locate the reset vector and the latter to hold the start-up code.

17.3.2. Setting the Configuration Words

With the large number of configuration bits in the PIC 18 Series microcontrollers, it is attractive to set them within the program. Version 3.0 of the C18 compiler allows Configuration Words to be set simply, using the #pragma config directive. The actual settings to be used are processor-specific. The options, even for one microcontroller, are surprisingly extensive. Therefore they are not reproduced here, but can be found for all 18 Series PIC microcontrollers in Ref. 17.1, or from the Help facility in MPLAB.
Program Example 17.2 illustrates settings appropriate for the Derbot-18. It is useful to look back at Table 13.4 to see the configuration options. One #pragma config line is used per Configuration Word, with the format of the directive drawn from Ref. 14.2. Any configuration bits not defined are left at their default values, shown in Figure 13.17.
Program Example 17.2.
Setting configuration bits for the Derbot
Once a configuration bit is set with a pragma directive, it overrides any setting in the MPLAB IDE Configuration Bits window. During a project build, the compiler sets the bits according to their setting in the program. It is interesting to check this in practice by setting some configuration bits ‘wrong’ in the Configuration Bits window and then building the program. Note, however, that if a bit is not specified in the source code, but is set in a particular way in the Configuration Bits window, then the build process does not force the bits back to their default values. As described in Section 7.11.3, click Configure > Settings > Program Loading in MPLAB and check the ‘Clear configuration bits upon loading the program’ box. This ensures that on program download the window setting is cleared, and that it is only the configuration bits defined in the program which are downloaded.

17.4. Interrupts

Interrupts present a number of challenges in the C environment. When working with interrupts we are working very close to the hardware, yet a high-level language tends to distance us from it. A number of distinct and important actions must be taken in order to allow the 18 Series interrupts to work successfully. The interrupt must be enabled and allocated to the desired priority. The ISR must be located in program memory at the right start address (noting that there are two interrupt vectors in the 18 Series structure) and context saving must be managed. Check Figure 12.7 and the accompanying description for a reminder of these points if needed.

17.4.1. The Interrupt Service Routine

Interrupt Service Routines in C are similar to C functions, except of course they are called by occurrence of an interrupt and terminate with a return from interrupt instruction. They can have local variables (i.e. variables declared within the ISR) and access global ones. Global variables which are accessed by an ISR should, however, be designated volatile. This indicates that the variable value can be changed outside normal program operation. As the ISR can be called anywhere, it is not allowed to transfer any parameters or return values.

17.4.2. Locating and identifying the Interrupt Service Routine

The C18 compiler uses several pragmas, first to locate the start of the ISR at the reset vector and then to distinguish the ISR from a regular function.
Like the reset vector, the C18 compiler does not automatically start the ISR at the high or low interrupt vector in program memory. This requires the use of the #pragma code, already described. This is used to locate the start of the ISR correctly.
The ISR is identified in the program through use of a pragma. Two are available for ISR definition:
#pragma interrupt function_name (save = save_list). This pragma declares function_name to be a high-priority ISR. The Fast Register Stack (Section 13.6.3) is used to save the minimum context – the STATUS, WREG and BSR registers. The interrupt is ended with a fast return from interrupt.
#pragma interruptlow function_name (save = save_list). This pragma declares function_name to be a low-priority ISR. The software stack is used to save the minimum context. This slows response to the interrupt. The interrupt is ended with a normal return from interrupt.
Further context saving, beyond the minimum, can be achieved in either interrupt type by specifying register(s) to be saved in the optional save section of the pragma.

17.5. Example with interrupt on overflow – flashing LEDs on the Derbot

Program Example 17.3 uses the 18F2420 Timer 0 interrupt on overflow to flash the LEDs on the Derbot. This seemingly simple application allows us to take some useful further steps in C programming.
It can be seen that the main function of this program is very short – once initialisation is complete all significant action is contained within the ISR. All the main function does is to initialise Port C and set its value to zero, initialise Timer 0 (see below), set the Global Interrupt Enable and finally enter an endless while loop, waiting for interrupts to occur.

17.5.1. Using Timer 0

We have already met the library functions available for the 18 Series timers in Section 13.14.1. Timer 0 itself is described in Section 13.14.1, with its various modes of operation evident from Figure 13.20. The timer's header file timers.h must be included in any program that will use it, as we see here.
This program uses the function OpenTimer0 in the initialisation section of main, as shown below:
OpenTimer0 (TIMER_INT_ON & T0_SOURCE_INT & T0_16BIT & T0_PS_1_4);
Timer set-up data is specified in the argument of this function. Information on the full range of options for this can be found in Ref. 14.3. This implementation enables the timer interrupt on overflow, sets the clock source to be the internal oscillator, selects 16-bit operation (as opposed to 8-bit) and sets the prescaler to be divide-by-four. Once this is set, the timer free runs and generates a series of interrupts to which the microcontroller can respond. With these settings, the counter requires 65536 cycles to count through its range and each input cycle has a duration of 4 μs. The interval between interrupts is therefore 65536 × 4 μs, or 262.144 ms.
Program Example 17.3.
‘Flashing LEDs’ program, using Timer 0 interrupts

17.5.2. Using interrupts, and the Interrupt Service Routine action

This program example uses a single interrupt, from Timer 0. Prioritisation has not been enabled, so by default the high-priority vector is used. As mentioned, the interrupt vectors need to be specified in the program listing, making use of the #pragma code option. To set the high-priority vector (Figure 13.6), the following is used:
#pragma code high_vector = 0x08
This specifies that the code section which follows is to be placed in code memory starting at memory location 08H. It has been named high_vector by the programmer. This is not a reserved name and another could be chosen. Alternatively, for the low-priority vector:
#pragma code low_vector=0x18
specifies that code which follows is to be located at code memory location 18H. It has been named low_vector by the programmer.
With the vector correctly placed, there follows in the program a single line of in-line assembler code, forcing a jump to the main body of the ISR:
_asm GOTO timer0_isr _endasm //jump to ISR
To ‘undo’ the action of the earlier pragmas, and allow the compiler to control code location again, the pragma:
#pragma code
is applied. This returns the code location to the default.
The ISR, timer0_isr, appears as a function prototype near the start of the program. The action of the ISR is simple. The value of counter is first incremented. It is then shifted left five times and assigned to Port C. The effect of this is to transfer the least significant two bits of counter to the LEDs, which are located at bits 5 and 6 of Port C. The Timer 0 interrupt flag is then cleared. The compiler automatically ends the ISR with a retfie (return from interrupt) instruction.
This simple interrupt example uses just a single interrupt and does not enable the 18 Series interrupt prioritisation. The use of two, prioritised, interrupts is illustrated in Program Example 19.6.

17.5.3. Simulating the flashing LEDs program

It is interesting to simulate Program Example 17.3, whether or not it is downloaded to hardware. Create a project around the program using the source code on the book's companion website. Build it and simulate with the MPLAB simulator. Open a Watch window and display PCL, counter, PORTC, INTCON and TMR0, as seen in Figure 17.1 (a). Step quickly through the early stages of the program, noting how the OpenTimer0 function appears. Once main is entered and the user initialisation is complete, the program enters the continuous while loop, waiting for the interrupt.
Figure 17.1
Suggested settings for the ‘flashing LEDs’ simulation. (a) Watch window. (b) Breakpoint location
Now force an interrupt by setting high the Timer 0 interrupt flag (bit 2 of the INTCON register) in the Watch window. With further single-stepping, program execution jumps to the ISR, via the assembler insert at the high-priority interrupt vector. The actions of the ISR can be observed by continued single-stepping.
Let us now examine the action of the timer interrupt. First, place a breakpoint at the very start of the interrupt routine, as shown in Figure 17.1 (b). If run, the program will now stop whenever it reaches this point, i.e. every time the timer interrupt occurs. From the toolbar, select Debugger > Settings > Osc/Trace and set the oscillator frequency to 4 MHz. Open the Stopwatch window, as seen in Figure 17.2, from the Debugger pull-down menu.
Figure 17.2
Stopwatch for the ‘flashing LEDs’ simulation
From wherever you are in the program, run to the breakpoint. Zero the Stopwatch and run again. After a moment, program execution should again stop at the breakpoint. The Stopwatch time displayed should be 262.144 ms, exactly as shown in Figure 17.2. This confirms the calculated time between overflows. At the same time, the Watch window values should appear similar to Figure 17.1 (a). Timer 0 has just overflowed and is beginning to step up from zero again; the lower byte of the Program Counter (PCL) has been set to the high-priority reset vector, 08H, and the timer interrupt flag, bit 2 of INTCON, is set.
If you have the Derbot-18 hardware, then you can use this program to provide a pleasing display of flashing LEDs.

17.6. Storage classes and their application

The rest of this chapter aims to look at the wider environment within which a C source file is placed. Notable among these are three files used by just about every program: the microcontroller header file, the start-up file and the Linker Script. To understand these, it is necessary to explore a few more aspects of C. We start that process here, with storage classes.

17.6.1. Storage classes

As we have begun to see, the C language controls the use of data carefully, in terms of how it is declared and how it can be used. This is necessary partly because C programs can be complex things: made up of different files and functions, written at different times by different people and saved in different stages of compilation. Functions and data may, for example, be declared in one file, but need to be accessible to others.
A characteristic of data used in C is therefore its ‘storage class’. This defines its status within and across blocks of code, functions and files. Table A6.3 shows the four C keywords which are used in connection with this.
The C18 compiler uses only three of these, auto, static and extern. These are somewhat peculiar terms and don't seem to make much sense, even when we realise that auto is short for automatic and extern for external. It is useful to know that the use of the word ‘automatic’ was borrowed from other computer languages, and implies a variable which comes into existence for a particular purpose within a certain function but does not exist at other times. Static, on the other hand, implies a variable which has some form of continuous existence. External implies a variable which has continuous existence and that can be accessed by any function.
Let us return to the storage class. It determines three things: the ‘scope’, ‘duration’ and ‘linkage’ of the data. We will examine each of these in turn. Table 17.2 summarises the points made, for all possible combinations of applications. Some of these are, of course, more widely used than others and we only make use of a selection in the example programs of this book.
TABLE 17.2 Storage classes: effect of specifier and position
Storage class specifierDeclared outside all functionsDeclared inside a function
noneFile scope, Static duration, External linkageBlock scope, Automatic duration, No linkage
autoBlock scope, Automatic duration, No linkage
staticFile scope, Static duration, Internal linkageBlock scope, Static duration, No linkage
externFile scope, Static duration, External linkageBlock scope, Static duration, External linkage

17.6.2. Scope

The scope of a variable determines the part of the program in which it can be used. Two possible scopes are:
Block scope. The variable can only be used in the block of code within which it is declared, starting with the point of declaration. Variables of this type are called ‘local’ variables. The same name for local variables can be used in different blocks of the program and they will be unrelated.
File scope. The variable can only be used in the file within which it is declared, starting with the point of declaration. The variable must be declared outside all blocks.

17.6.3. Duration

The duration of a variable can be one of two possibilities:
Automatic storage duration. An automatic variable is declared within a block of code and is recreated every time program execution enters that block. When the block ends, the variable ceases to exist and the memory occupied by it is freed. The C keyword auto defines this storage duration. As it is the default duration when a variable is defined within a block, the keyword is not often used.
Static storage duration. A static variable exists throughout program execution and is identified by the keyword static. Static variables may still be local to a block, but retain their existence outside that block. Variables declared outside all blocks, whether or not the keyword is explicitly used, are interpreted as static.

17.6.4. Linkage

Both within files, and in the case that several files are used, it is important to consider how variable names can be recognised as referring to the same variable. There are thus three possible types of ‘linkage’:
External linkage. If a variable or function is externally linked, it is recognised throughout the program, wherever it is declared. The name is recognised by the Linker. A variable has external linkage if it is declared with the storage class specifier extern, or if it is declared outside all functions, with no storage class specified.
Internal linkage. A variable has internal linkage if it is declared static, outside all functions. The variable remains internal to the translation unit but is recognised throughout it. The Linker has no ‘knowledge’ of it.
No linkage. This is the state of all other variables, for example those with automatic duration.

17.6.5. Working with 18 Series memory

A further complication – or opportunity – arises with the specification of storage in the C18 compiler. Due to its Harvard memory structure and flexible use of memory, the PIC microcontroller presents a challenge to how C treats memory allocation. The C18 compiler therefore introduces the storage qualifiers far and near. These act as keywords and are effectively C18-specific extensions to C. They indicate the microcontroller memory size or the way memory should be used. Each can be applied to two more keyword extensions, rom and ram. These are used if the type of memory must be specified in the declaration of a variable or constant. When data is declared without storage qualification, the default is ram and far. The action of all of these is summarised in Table A6.8, along with a brief description.
Two memory models can also be specified, ‘small’ and ‘large’. The properties of each of these an summarised in Table A6.9. They are selected by command line options, with small being the default. The only difference is in pointer size needed. Only the small model, which is the default, is needed for programs of introductory or medium length.

17.6.6. Storage class examples

The first of two code fragments from the c018i.c file is copied below, taken from towards the beginning. In it we see variables being declared. Here the word extern is explicitly being used, indicating variables which have external linkage. All three variables also appear in the 18f2420.h header file. The use of the qualifier near indicates that they are to be placed in Access RAM. The data types, some of which we have not met before, can be checked by reference to Table A6.4.
extern volatile near unsigned long short TBLPTR;
extern near unsigned FSR0;
extern near char FPFLAGS;
The second example, below, shows the start of the _do_cinit() function, with comments removed. Four variables are declared at the head of the function. Each is static, so it will have continuous existence. Being declared within a block, i.e. the function, they will be local to that function.
void _do_cinit (void) {
static short long prom;
static unsigned short curr_byte;
static unsigned short curr_entry;
static short long data_ptr; …
The implication of the way these variables are declared is explored further when we come to simulate the c018i.c file, in Section 17.7.3.

17.7. Start-up code: c018i.c

We come now at last to viewing the c018i.c file in its entirety. It comes as a surprise when simulating a first C program that the simulator doesn't go straight to main. Surely this is what all the textbooks tell us, and shouldn't main just start at the reset vector? There are, however, things which need to be set up for the C program to run correctly, even before it starts. These include anything needed for correct operation of the C program, for example software stacks for transfer of data, and the initialisation of all variables and constants. This may be due to a requirement of C itself, or because values have been initialised in the program itself.
Initialisation routines that perform these functions are included with every compiler and may be nearly invisible to the programmer. If we depend on something, however, it is worth developing at least some sort of acquaintance with it.

17.7.1. The C18 start-up files

The C18 compiler provides three start-up program files, at varying levels of complexity. These are available as pre-compiled object files, which are linked to the main program at the build stage. They are normally linked in to the user application with the Linker Script. A source file version is also available. The start-up routine is the program element that is placed at the reset vector, so it is the very first thing that the CPU executes. It initialises the software stack and initialises all data which has a defined starting value. It then jumps to the user's main function.
The programs illustrated so far have all made use of the c018i.c file. When you simulate any of the example C programs with MPSIM, this is what you see if you start to single-step through the program. It is intended for programs with the processor operating in non-extended mode. The c018i_e.c version is for extended-mode operation.
A simpler version of the start-up file is c018.c. This simply sets up the software stack and jumps to main, without any data memory initialisation. A more complex version is c018iz.c. This does the same as c018i.c, but also sets all uninitialised variables to zero, as required in strict ANSI C. These two versions are for non-extended-mode operation. Their extended-mode equivalents are c018_e.c and c018iz_e.c.

17.7.2. The c018i.c structure

The opening of the c018i.c program section has already been quoted in Section 17.3.1.
Its first action is to initialise FSR1 and FSR2, which are used for the software stack (and hence not available to the programmer). It then calls a function named _do_cinit. This initialises variables in RAM, if there are any that need it. At the end of this function, the main function is called, in the lines shown below.
// Call the user's main routine
main ();
goto loop;
It is interesting to see from this that if main ever executes a return, then it is immediately called again.

17.7.3. Simulating c018i.c

You will have passed through c018i.c many times if you are simulating the programs in this book. Let us now, however, step through it with a little more interest as to what is going on. We can also use it to check up on the storage class of certain variables that we have already seen in examples.
Open the project you made for Program Example 14.1 and enable the MPLAB simulator. Open the Watch window and select the variables shown in Figure 17.3. Reset the simulator. It is interesting to observe that the variables declared in the source code appear to be valid, as does TBLPTR, declared towards the beginning of the program. The other variables in the Watch window, those declared inside the _do_cinit function, are specified as being ‘out of scope’, as seen in the figure. This is in accordance with the description of Section 17.6.2. Single-step through the program until the _do_cinit function is entered. Notice how these three variables suddenly come into scope and take on a (zero) value. If you continue to single-step through the program, you will see that execution returns from the function at an early stage. This simple program requires no initialisation. The main function is then called.
Figure 17.3
Watch window for c018i.c execution, Program Example 14.1
Now, in the source code, move the declaration of counter, in the line
unsigned char counter; //specify counter as unsigned character
to just inside the main function. Build the program again and simulate. Notice now that, at the beginning of program execution, counter is ‘out of scope’. By declaring it within a function, it has lost its external linkage, as shown in Table 17.2. If you single-step through the program, you will find it becomes in scope once main is entered.
Now open the project of Program Example 16.2, which has plenty of lists to initialise. Set up a Watch window with the variables shown in Figure 17.4. The lowest three variables will again initially be out of scope.
Figure 17.4
Watch window for c018i.c execution, Program Example 16.2
Single-step through the program, into the _do_cinit function. As before, the lower three variables take on numerical values. Continue single-stepping and notice that program execution enters a major loop within the function. Ultimately, you will see the character strings being populated as data is moved over into data memory from program memory. The figure shows the ‘Apple’ string partially completed.
The character strings we have been looking at are only placed in data memory because that is the default location, as Section 17.6.5 has indicated. Let us explore using the rom extension keyword to put (or in fact leave) one of them in program memory, which is much more sensible. In the declaration of the ‘Apple’ string in the source file, insert the word rom as follows:
rom char item1[ ] = "Apple";
Rebuild the program and reset the simulator. Notice now (from the addresses used) that the ‘Apple’ string has been placed into program memory, as seen in Figure 17.5. It is immediately available and no initialisation of memory is needed for it. The “P” symbol in the Watch window indicates the specified location is in Program Memory.
Figure 17.5
Watch window for Program Example 16.2– use of rom keyword

17.8. Structures, unions and bit-fields

In the section that follows this, we will be looking at microcontroller header files. A large part of these apply certain data types which we have yet to meet. These are therefore now introduced.
‘Structures’ and ‘unions’ are both sets of related variables, defined through the C keywords struct and union. In a way they are like arrays, but in both cases they can be of data elements of different types.
Structure elements, called ‘members’, are arranged sequentially, with the members occupying successive locations in memory. A structure is declared by invoking the struct keyword, followed by an optional name (called the structure ‘tag’), followed by a list of the structure members, each of these itself forming a declaration. For example:
struct resistor {int val; char pow; char tol;};
declares a structure with tag resistor, which holds the value (val), power rating (pow) and tolerance (tol) of a resistor.
Structure elements are identified by specifying the name of the variable and the name of the member, separated by a full stop (period). Therefore, resistor.val identifies the first member of the example structure above.
Like a structure, a union can hold different types of data. Unlike the structure, union elements all begin at the same address. Hence the union can represent only one of its members at any one time, and the size of the union is the size of the largest element. It is up to the programmer to track which type is currently stored! Unions are declared in a format similar to that of structures.
Unions, structures and arrays can occur within each other. We will see an example of this in the following section.
In embedded systems we are very interested in identifying and accessing individual bits. The bit-field capability of C assists with just that, where a bit-field is a set of adjacent bits within a single word. Bit-fields can only be declared as members of a structure or union. The format for declaring the bit-field is:
type [name]:width
Here ‘type’ is either a signed or unsigned integer, ‘width’ is the number of bits and ‘name’ is an optional name.

17.9. Processor-specific header files

The processor-specific header files are very important in embedded C. They include definitions for all the Special Function Registers (SFRs) and their bits, as well as some useful extras, for example extra features for working with Assembler. It is instructive to look further at one of the processor header files. We will do this with the 18f2420.h file. You can find it in the mcc18\h folder of the C18 software.

17.9.1. Special Function Register definitions

An excerpt of the 18F2420 header file is shown as Program Example 17.4. In this we see Port B and its bits being declared. The first line of the excerpt uses no less than four of the C keywords to define the PORTB type. Use of unsigned char specifies it as single byte, while volatile indicates that it can be changed outside program control; extern indicates that the variable can be accessed outside this file. Finally, the use of near indicates that it is placed within Access RAM.
By looking carefully at this program example it is possible to see that the declaration of port bits is arranged as a union named PORTBbits, containing three structures (two are shown here). Like Port B the union is specified as extern volatile near. It can be seen that the first structure within the union is a list of the conventional names of the port bits, each declared as a single-bit bit-field.
The second and third structures list the alternative uses of the port bits, each again being a bit-field. As both these structures belong to a union, they effectively occupy the same memory space and can be used as alternatives to each other.
Program Example 17.4.
Port B declaration – part of the 18F2420.h file
We are now at last in a position to understand the format we have used for several chapters to identify port and other SFR bits. When we write, for example
PORTBbits.RB7 = 1;
we now know that this invokes the member RB7 (a bit-field) of a structure, which in turn is a member of the union PORTBbits.

17.9.2. Assembler utilities in the header file

Program Example 17.5 shows the #define preprocessor directive being used to define certain 18 Series Assembler instructions. By doing this they can be used in a C program, without even invoking the usual in-line assembler procedure. To the casual observer their use will appear as functions. We find this technique applied again in Chapter 19, with the Salvo real-time operating system.
Program Example 17.5.
Part of the 18F2420.h file – Assembler utilities

17.10. Taking things further – the MPLAB Linker and the .map file

Figure 14.2 shows the central part that the Linker plays in any build process. For straightforward applications it is not necessary to understand how the Linker works. An approximate understanding of the Linker is, however, useful even in simple applications to appreciate how a program is put together, particularly in terms of finding and understanding those ‘hidden’ files which are linked in. For more advanced applications the programmer may want to modify or rewrite the Linker file provided. This section introduces the MPLAB Linker, MPLINK. Reference information on this can be found in Ref. 17.2.

17.10.1. What the Linker does

As the build process of Figure 14.2 shows, the Linker takes object files as its input and combines these to create executable code, which can be downloaded to the microcontroller. It also provides background information on how it has allocated memory, which can be used for debug purposes. The object files may be application code, generated first as C or Assembler. Alternatively, they may be general-purpose library files. In either case, the code they contain is largely ‘relocatable’. This means that addresses in memory, whether data or program, have not yet been assigned. It is the function of the Linker to locate all the object files into memory and ensure that they link across to each other correctly. It can also control allocation of the software stack. It is guided in all this by the Linker Script, which contains essential information about the memory map of the microcontroller that is to be used. In undertaking its task, the Linker may well uncover programming faults, for example addresses which clash, or inadequate information.

17.10.2. The Linker Script

The Linker Script is a text file made up of a series of Linker directives which tell the Linker where the available memory is and how it should be used. Thus, they reflect exactly the memory resources and memory map of the target microcontroller. Standard Linker Scripts are provided in MPLAB for all available PIC 18 Series microcontrollers. In a standard C18 installation they can be found in the mcc\lkr folder. The script for the 18F2420, the 18f2420.lkr file, is used in every C program example in this book. It is reproduced as Program Example 17.6. An alternative but very similar version, not specific to a C18 implementation, can also be found in any MPLAB installation.
Program Example 17.6.
Linker Script for the 18F2420
Let us now explore this example Linker Script. Our goal at this stage is to appreciate what it is saying, not to write a new file. Therefore, we will not worry about exact formats.
• Linker comments. All comments are preceded by //. All text following this on a line is ignored by the Linker. As may be expected, the comments seen here provide title and version information.
• Directive LIBPATH. This provides an optional search path for files to be included. It is not used in this example.
• Directive FILES. This directive specifies object files for linking. Three files are specified here:
c018i.o. This is the object code version of the start-up file, already described in this chapter.
clib.lib. This contains the standard C library supported by the C18 compiler.
p18f2420.lib. This file contains processor-specific information and effectively works alongside the processor-specific header file.
• Directive CODEPAGE. This directive is used to allocate program memory. It is used no less than five times in this example, with the primary purpose of conveying the microcontroller memory maps. The main block of memory, to which the Linker can allocate program code, is located from address 00H to 03FFFH. This accords with the memory map of Figure 13.6. Blocks for configuration data and device identification are also reserved, corresponding to the locations shown in Table 13.4. Further space is reserved for EEPROM and identification.
• Directive ACCESSBANK. This directive, used twice in this example, allocates access data memory. The first time it is used, the RAM located in access memory is labelled accessram and is correctly located in the address range 0 to 7FH. In the second, the SFR memory block is identified, located in the memory map and labelled accesssfr. This memory block is specified as being protected, which stops the Linker allocating it for general-purpose usage. The absolute memory allocations made elsewhere for the SFRs are thus preserved.
• Directive DATABANK. This directive is similar to ACCESSBANK and uses the same format. It is used to specify banked RAM. Its implementation in this example can be seen to follow exactly the data memory map seen in Figure 13.4. Each block is available to be used by the Linker, so none is protected.
• Directive SECTION. This directive allows a name identified in the source code with a #pragma directive to be linked across to a block of memory identified in the Linker Script. In this case the connection is being made for configuration memory, so that data generated by use of #pragma config (as illustrated in Program Example 17.2) is placed in the right location.
• Directive STACK SIZE. This directive allows the software stack location and size to be specified. In this example it can be seen that a stack of size 100H is specified, located in RAM Bank 2.

17.10.3. The .map file

The result of the Linker's action is that all code is mapped correctly into the different categories of memory. How can this be checked? The answer lies in the .map file, an optional file which we can ask the compiler to generate. This file shows all memory allocation. For a given project, it can be generated by clicking Project > Build Options > Project > MPLINK Linker > Generate map File.
Following a successful project build, the .map file can be found along with other output files, with name The .map file is not a pretty sight for the casual observer, as it contains all the address locations of all symbols used, as well as the memory mapping derived from the Linker Script. However, it can be useful as a diagnostic tool if one is having trouble working out how a variable has been treated, or what has happened to a memory location or block of memory. Another useful feature of the .map file is that it shows the proportion of memory used. This, of course, becomes very important as programs grow.
Fragments of the .map file for Program Example 14.1 are shown in Program Example 17.7. It shows where some of the main program sections are placed and goes on to indicate that program memory usage is a modest one per cent!
Program Example 17.7.
Fragments of .map file for Program Example 14.1


• It may still be necessary from time to time to step outside the strict confines of C to make use of Assembler.
• It is not difficult to use interrupts in C, but an understanding is needed of how the interrupt vectors are defined and how the service routine is constructed.
• To work with larger programs, it is useful to develop further knowledge of different data types and storage classes.
• The development of a program in C involves far more than simply writing source code. A wide selection of other files can (and effectively must) be used. It is useful to have some understanding of what these are, how they work and how they relate to each other.
• The Linker brings the various contributing files together. Knowledge of the Linker at an appreciation level is useful for simple programs. A detailed knowledge becomes important when writing major pieces of software.
At the end of these four chapters on C, we have reached an introductory but useful understanding of the C language, as applied to embedded systems. This should allow you to go on to write increasingly complex C programs, as indeed is done in Chapter 19. While the basics of C have been introduced, there are plenty of features of C which haven't. A deeper and wider knowledge of C can be gained by more programming experience, studying good example programs and reading from the various specialist C books that are available.
17.1. PIC18 Configuration Settings Addendum (2005). Microchip Technology Inc., Document no. DS51537C.
17.2. MPASM Assembler, MPLINK Object Linker, MPLIB Object Librarian User's Guide. (2009) Microchip Technology Inc ; Document no. DS33014K;