CHAPTER 15. C and the embedded environment – Designing Embedded Systems with PIC Microcontrollers, 2nd Edition

CHAPTER 15. C and the embedded environment
The C programming language was introduced, in overview, in Chapter 14. This chapter now aims to start applying that skill to writing real embedded C programs. It begins with that most essential of embedded requirements, the manipulation of individual bits, and then moves on to interaction with peripherals. While doing this, many more details of C are introduced, as they come up in example programs.
Examples in this chapter are mostly applied to the Derbot-18 AGV. They can all be simulated, so it does not matter too much whether or not you have the hardware. Most are unashamed reworkings of the Assembler programs which have appeared earlier in the book. By adapting Assembler programs, a comparison can be made between the different programming languages, and the reader who is working through the book is on familiar territory as far as the target hardware is concerned.
By working through this chapter, you should develop a good understanding of:
• How to access and manipulate single bits.
• How to write simple functions and call them.
• How to invoke library functions, including those for control of the microcontroller peripherals.
• How to give some structure to C programs, making appropriate use of functions, and looping and branching constructs.

15.1. The main idea – adapting C to the embedded environment

Now we have adopted a high-level language (HLL), it is as if we are seeking the best of both worlds. We want the benefits of the HLL, but we retain our determination to work close to the hardware – setting up peripherals, setting or clearing individual port bits, and ultimately setting up and responding to multiple interrupts. This tension is resolved in interesting ways, which we now begin to explore.

15.2. Controlling and branching on bit values

Program Example 14.1 illustrated in a very simple way the use of the microcontroller ports. This relied on the port SFRs (Special Function Registers) being declared in the header file, the data direction being set up by writing to the TRIS register, and then moving data to the port by writing a whole byte.
Fundamental to developing programs for embedded systems is, of course, the ability to read and set single bits. Program Example 15.1, which moves the state of the microswitches on the Derbot to the LEDs, demonstrates how this is done in C. Essentially, it rewrites Program Example 7.1. As it must, the program contains a main function. It also contains two user-defined functions, initialise( ) for initialisation and diagnostic( ) for diagnostics. Both can be seen in the program listing. In implementing the diagnostic flashing of the LEDs, the program also makes use of a C18 library function. The way the functions are used will be discussed in Section 15.3.

15.2.1. Controlling individual bits

The bits of each port are defined in the microcontroller header file, using a C structure that is described in Section 17.9. For the purposes of this program, it is enough to recognise that a port bit can be specified by the format PORTxbits.Rxy, where x indicates the port and y the bit in that port. As an example, the diagnostic function lies at the end of the program listing. In it, we see bits 5 and 6 of Port C being set to Logic 1 in the lines:
PORTCbits.RC6 = 1;
PORTCbits.RC5 = 1;
With this simple step we now have the ability to set or clear individual bits in a register, as long as they have been previously declared. As all microcontroller SFRs and their bits are declared in the header file, this represents a great step forward.

15.2.2. The ‘if’ and ‘if–else’ conditional branch structures

The action in this program is built around a conditional ifelse branching structure. This allows a program to contain a choice between two separate paths of action. An example of the structure appears in the main function, as quoted here:
if (PORTBbits.RB4 == 0) PORTCbits.RC6 = 0;
else PORTCbits.RC6 = 1;
This can be interpreted as: if bit 4 of Port B is at Logic 0, then set bit 6 of Port C to 0; otherwise (else) set it to 1. A block of code, rather than just a single line, can also be
Program Example 15.1.
Derbot – moving microswitch states to LEDs
associated with either the if and/or the else, in which case it must be enclosed in curly brackets. For example:
if (PORTBbits.RB4 == 0)
{PORTCbits.RC6 = 0;
PORTCbits.RC0 = 1;
}
else PORTCbits.RC6 = 1;
This would cause two Port C bits to change, if Port B bit 4 was found to be zero.
It is also possible to use the if structure on its own. In this case there is no alternative action if the condition tested is not true. An example is:
if (PORTBbits.RB4 == 0) PORTCbits.RC6 = 0;
if (PORTBbits.RB5 == 0) PORTCbits.RC5 = 0;
In this case, Port C bit 6 is set to 0 if Port B bit 4 is at 0, but no action is taken if Port B bit 4 is at Logic 1. The same can be seen to happen in the line which follows, with bit 5 of each port.
Notice in these examples how the assignment operator ‘=’ and the equal to operator ‘==’ are used. As we have seen, the first is used to assign a value to a variable. The second is used, within the if construct, to test whether a variable is equal to a particular value.

15.2.3. Setting the configuration bits

Settings for the configuration bits are indicated in the program listing. For the time being, these should be set in the MPLAB Configuration Bits window, as seen in Figure 13.17. Don't forget to ‘unclick’ the ‘Configuration bits set in Code’ option. This is not, of course, a very satisfactory procedure and in Chapter 17 we will see a way of embedding the settings in the program. Note that the settings in the window can be returned to their default conditions by right-clicking in the window and clicking Reset to Defaults in the dialogue box.

15.2.4. Simulating and running the example program

It is interesting to simulate this program in the MPLAB simulator. Having created and built the project, set up the simulation with the following steps.
• In MPLAB, select the simulator with Debugger > Select Tool > MPLAB SIM.
• Open a Watch window and select Port B and Port C as variables for display.
• Set up a stimulus controller and simulate inputs for the microswitch inputs, RB5 and RB4, with Toggle as the Action.
• Place breakpoints in the diagnostic( ) function, as shown in Figure 15.1 (a).
Figure 15.1
Simulation settings for Program Example 15.1. (a) Suggested breakpoints in diagnostic function. (b) Stopwatch, after completion of delay function
• Use Debugger > Settings > Osc/Trace, to set the Processor Frequency to 4 MHz.
• Use Debugger > Stopwatch to display the Stopwatch window, as shown in Figure 15.1 (b).
Now reset and run the program to the first breakpoint, which will be the first in Figure 15.1 (a). At this point zero the Stopwatch and then run to the next breakpoint. This is just the line below. To get there, however, the program has to execute the Delay10KTCYx( ) function. The Stopwatch should now be exactly as shown in Figure 15.1 (b). Exactly one second of simulated time has elapsed in execution of the function. This is a satisfying confirmation of the accuracy of this function.
Now set one further breakpoint in the line below the label loop:. Run the program to here and then single-step through the loop. Using the stimulus controller, change the values of the microswitch inputs (Port B bits 4 and 5) and observe how the program loop responds.
If you have the Derbot-18 hardware, download the program in the usual way. The program function is simple; the satisfaction comes from seeing your first C program running in hardware!

15.3. More on functions

Now that we have a program example with several functions, it is useful to pause to explore further the way functions are used. In particular, we need to know how a function is written, how it is called, how data is passed to it and how it is returned.

15.3.1. The function prototype

When main is not the only function in the program, it is necessary to include for every function a function prototype. This is a declaration which informs the compiler of the type of the function's argument(s), if any, and its return type. It is similar to the function header seen earlier and has the same general format. For example, the Delay10KTCYx( ) library function, described below, has the prototype shown in Figure 15.2. It can be seen that one argument is sent, an unsigned character, while no return value is expected.
Figure 15.2
The Delay10KTCYx( ) prototype
Prototypes for library functions appear in the library header files and do not need to be repeated in the user code. In this case, the prototype just discussed can be found in the delays.h header file. Prototypes for the two user-defined functions in the program example can be seen towards the top of the program. This shows that neither expects a return value and neither carries any arguments.

15.3.2. The function definition

The actual code of a function is called the ‘function definition’. It follows the format described in Section 14.2.5 of Chapter 14. The definition for the Delay10KTCYx( ) function is contained within the general software library (Table 14.5) and is merged with the main program at the time of linking.
The definitions for the two user-defined functions can be seen towards the end of the program listing. They are placed here for clarity. They can, however, be placed anywhere in the program listing, as long as they are not inside another function definition. These definitions are easy to follow.
The initialise function sets up the SFRs and initialises the ports to 0. Strictly speaking, initialising variables to 0 should be unnecessary as the ANSI standard requires it anyway. With C18 this is only done if the c018iz.o start-up utility is used (described later, in Section 17.7.1 of Chapter 17). We do not use this in the example programs in this book and variables are hence not initialised to zero as a matter of course. The diagnostic function sets bits 5 and 6 of Port C (the two LED output bits) to 1 and calls the delay function. It then clears the same bits to zero, before calling the same delay again.

15.3.3. Function calls and data passing

A function is called by quoting its name and placing the necessary arguments in the brackets which must follow. Where there are no arguments, the brackets must still be there, but can be left empty. The function calls in Program Example 15.1 are simple and self-explanatory. Notice that the parameter 100D is passed to the delay function.
It is important to be aware of a number of features of function calls which are not evident from this particular example. Importantly, a function call has the type and value of its return type. This is an assertion of some significance! It means that a call can be inserted into an expression; the function is evaluated and its return value acts in its place in the expression. This is done several times in Program Example 16.1 and the programs which follow. An example from there is as follows:
ldr_rt = ReadADC( )&0x03FF; // read it, AND out unwanted bits
Here the function call ReadADC( ) is placed within a statement. It is evaluated first and its return value then takes its place in the statement.
Remember also that any parameters passed to the function are copied to it, with the original value (if a declared variable) retained. It uses these in its internal execution to generate the return value. It does not in itself modify the value of the variable.

15.3.4. Library delay functions and ‘Delay10KTCYx( )’

All the delay functions available in the C18 general software library are shown in Table 14.5. These all have a function prototype of the form seen already for Delay10KTCYx( ), with an unsigned character (i.e. 8-bit word) acting as the multiplier to set the actual delay.
The Delay10KTCYx( ) function used here allows the longest of the delays. It introduces a software delay in multiples of 10 000 instruction cycles, with a maximum of 255 × 104 cycles. Thus, with a clock running at 4 MHz and the variable expressed as 100 (as in this example), the total delay is 10 000 × 100 × 1 μs, i.e. 1 second.
The delays.h header file, required for this function, can be seen included in the early stages of this program.

15.4. More branching and looping

15.4.1. Using the ‘break’ keyword

Now that we have a mechanism in C which gives access to individual register bits, we can make use of this in further bit testing and setting operations.
Returning to the Fibonacci program of Program Example 14.2, let us explore another way of constructing the first loop. The purpose of this loop is to generate the Fibonacci series, within the limits of the 8-bit number used (specified as type unsigned char). In Program Example 14.2 we did this in a rather artificial way, by limiting the number of values calculated to a known ‘safe’ maximum.
Try now replacing the first loop with the program section in Program Example 15.2. This establishes what appears to be a continuous loop, using a while (1) construct. Within the loop, however, is a possible exit strategy, based on the break keyword. The statement that it is part of tests the value of the Carry bit and causes an exit if it is high. If you simulate with this revised version, you will see that an exit from the loop is forced immediately following this statement, once the Carry bit goes high.
Program Example 15.2.
Alternative first loop for the Fibonacci program
Given a way of testing the Carry bit, one might ask – why not make this the while condition? The loop could then be started:
while (STATUSbits.C != 1) //loop while the Carry bit is not 1
{
fibtemp = fib1 + fib2;
The problem here is that the condition is only tested at the end of the loop. By this time the addition has overflowed and incorrect numbers will have been loaded into at least one location in the Fibonacci series. The loop could, of course, be restructured so that the addition was at the bottom of the loop and the test of the Carry bit then took immediate effect.

15.4.2. Using the ‘for’ keyword

The for keyword provides another means of ‘packaging’ conditions for a loop. It has the general format:
for (initialisation; condition; modification) statement, or statements in braces
The three expressions within the for brackets, called here initialisation, condition and modification, are all defined by the programmer. Moving immediately to an example, the first loop in Program Example 14.2 could be rewritten as shown here:
for (counter = 0;counter<12;counter = counter + 1)
{
fibtemp = fib1 + fib2;
//now shuffle numbers held, discarding the oldest
fib0 = fib1; //first move middle number, to overwrite oldest
fib1 = fib2;
fib2 = fibtemp;
}
In the first expression, counter is initialised to 0. This occurs only once, when the loop is entered. The condition tested is whether counter is less than 12, and the modification caused is an increment to the value of counter. This does not occur on the first loop iteration. When the program runs, the loop is repeatedly executed, with counter being incremented each time. When it is incremented to 12, this is immediately detected by the condition expression and no further loop iterations occur.
It is interesting to modify the Fibonacci program to contain this code, and simulate.
Any of the three expressions associated with for can be omitted. If the condition is left out, then there is no test and the loop is continuous. Initialisation and modifications can still, however, apply. A simple way of creating a continuous loop is by entering no expressions at all, giving:
for(;;)
{…
This is a direct alternative to:
while(1)
{…

15.5. Using the timer and pulse width modulation peripherals

We turn now to controlling microcontroller peripherals through the use of library functions. To do this we will use the Derbot ‘blind navigation’ program, first introduced as Program Example 8.4. It is shown in a C version in Program Example 15.3. The program makes use of library functions for Timer 2 and PWM, and writes a number of functions of its own. These tend to replicate the subroutines of the original program.
The program simply causes the Derbot to run forward until it hits an obstacle, detected by a microswitch. It then reverses and turns, turning right if the left microswitch was hit and vice versa for the right microswitch. After this it returns to running forward. These simple moves require the use of the microcontroller PWM facility, which in turn requires the setting of Timer 2.

15.5.1. Using the timer peripherals

There are four 18FXX20 timers and for each of these there are four library functions. These are shown in Table 15.1, where x can be 0, 1, 2 or 3. Full details of their associated arguments are given in Ref. 14.3.
TABLE 15.1 Timer library functions
FunctionAction
OpenTimerx( )Configures Timer x
ReadTimerx( )Reads Timer x
WriteTimerx( )Writes to Timer x
CloseTimerx( )Closes Timer x
This program uses just one timer-related function, OpenTimer2( ). It represents a style of peripheral drive library function that we shall see again. In this, the argument is made up of a bit mask, created by performing a logical AND of a number of settings. These are specified in the library reference [Ref. 14.3] and for this function are reproduced in Table 15.2. In this case three such settings must be chosen, for interrupt enable, prescale and postscale. These can be seen applied in the function call, as quoted here:
OpenTimer2 (TIMER_INT_OFF & T2_PS_1_1 & T2_POST_1_1);
TABLE 15.2 Settings options for ‘OpenTimer2( )’
ValueEffect
Interrupt
TIMER_INT_ONInterrupt enabled
TIMER_INT_OFFInterrupt disabled
Prescaler
T2_PS_1_11:1 prescale
T2_PS_1_41:4 prescale
T2_PS_1_161:16 prescale
Postscaler
T2_POST_1_11:1 postscale
T2_POST_1_21:2 postscale
T2_POST_1_161:16 postscale
This enables the timer, disables its interrupt and sets pre- and postscaler to divide-by-one. It effectively replaces the two Assembler lines shown below, quoted from Program Example 9.2:
movlw B'00000100' ;switch on Timer2, no pre or postscale
movwf t2con
Program Example 15.3.
Derbot ‘blind navigation’ program
In terms of code lines saved, the C version gives little advantage. The benefit lies elsewhere, however. Using library functions like these, the programmer no longer needs to get into the detail of the peripheral structure or of its SFR bits. Once the requirements of the function are understood, then a peripheral can be applied with limited understanding of its internal working.

15.5.2. Using pulse width modulation

The concept of PWM and the use of the peripheral were described in Section 9.5 of Chapter 9. The hardware is built around Timer 2 and can initially be difficult to understand. However, it ends up being easy to use. Table 15.3 shows the PWM library functions that are available for the 18FXX20 microcontroller, where x can take the value 1 or 2.
TABLE 15.3 Pulse width modulation library functions
FunctionAction
OpenPWMx( )Configures period and timebase of PWM x
SetDCPWMx( )Writes a 10-bit duty cycle value to PWM x
ClosePWMx( )Disables PWM x
The OpenPWMx( ) function is used in Program Example 15.3, in the two lines copied here:
OpenPWM1 (0xFF); //Enable PWM1 and set period OpenPWM2 (0xFF); //Enable PWM2 and set period
The function enables the CCP module in PWM mode and loads the PR2 register, seen in Figure 9.11. Of course, the PR2 register is shared between the CCP modules and can only be set to one value. Therefore, it is to be expected that the argument to both function calls is the same. To set the repetition rate, Equation (9.2) is applied. In this case, with no prescale on Timer 2 and a function argument of FFH, a PWM frequency of 3.906 kHz results.
In this program example the SetDCPWMx() function is not used to set and change speed. As only 8-bit resolution is applied, the CCPR1L and CCPR2L registers are written to directly.

15.5.3. The main program loop

The main program loop is formed again with a while (1) construct. The microswitches are tested in turn with an if statement, as shown here:
if (PORTBbits.RB4 == 0) //Test right uswitch rev_left ();
if (PORTBbits.RB5 == 0) //Test left uswitch rev_rt ();
If either microswitch is activated, its associated input value is 0. Then either rev_left or rev_rt is called. It should not be too difficult to follow either of these functions through. The AGV pauses and both motors are then set in reverse for a fixed period. Then one or the other is set forward (while the other continues in reverse) to cause a turn. Program execution then returns to the main loop and the AGV moves forward again. This loop makes use of the motor drive functions, as well as the Delay10KTCYx( ) function.

Summary

This chapter has begun to show how C can, in a practical way, be applied to the embedded environment and the PIC 18 Series microcontroller.
• Individual bits in memory registers can easily be accessed and manipulated.
• There are a variety of branching and looping constructs which allow clearly defined program flow.
• It is easy to identify and use library functions; these greatly simplify interaction with the microcontroller peripherals.
• It is not difficult to write and use functions; a well-structured program will locate distinct tasks in functions, with the main program showing a high number of function calls.