Editor’s Note: In Part 3, excerpted from their book Fast and effective embedded systems design: Applying the ARM mbed , authors Tim Wilmshurstand Rob Toulson take the techniques for function-based program structuring used on the mbed IDE and apply them to a variety of complex functions typical of some high end ARM designs.
Large complex embedded projects in C/C++ benefit from being split into a number of different files, usually so that a number of engineers can take responsibility for different parts of the code.
This approach also improves readability and maintenance. For example, the code for
a processor in a vending machine might have one C/C++ file for the control of the actuators delivering the items and a different file for controlling the user input and liquid crystal display.
It does not make sense to combine these two code features in the same source file as they each relate to different peripheral hardware. Furthermore, if a new batch of vending machines were to be built with an updated keypad and display, only that piece of the code would need to be modified. All the other source files could be carried over without change.
Modular coding uses header files to join multiple files together. In general, a main C/C++ file (main.c or main.cpp) is used to contain the high-level code, but all functions and global variables (variables that are available to all functions) are defined in feature-specific C files. It is good practice therefore for each C/C++ feature file to have an associated header file (with a.h extension). Header files typically include declarations only, for example compiler directives, variable declarations and function prototypes.
A number of header files also exist from within C/C++, which can be used for more advanced data manipulation or arithmetic. Many of the built-in C/C++ header files are already linked to through the mbed.h header, so we do not need to worry too much about those here.
Overview of the C/C++ Program Compilation Process
To understand further the design approach to modular coding, it helps to understand the way programs are preprocessed, compiled and linked to create a binary execution file for the microprocessor. A simplified version of this process is shown in Figure 6.7 and described in detail below.
In summary, first a preprocessor looks at a particular source file and implements any preprocessor directives and associated header files. The compiler then generates an object file for the particular source code. In doing so, the compiler ensures that the source files do not contain any syntax errors – note that a program can have no syntax errors, but still be quite useless. The compiler then generates correct object code for each file and ensures that the object and library files are formatted correctly for the linker.
Object files and built-in library files are then linked together to generate an executable binary (.bin) file, which can be downloaded to the microprocessor. The linker also manages the allocation of memory for the microprocessor application and it ensures that all object files are located into memory and linked to each other correctly. In undertaking the task, the linker may uncover programming faults associated with memory allocation and capacity.
The C/C++ Preprocessor and Preprocessor Directives
The C/C++ preprocessor modifies code before the program is compiled. Preprocessor directives are denoted with a ‘#’ symbol and, may also be called ‘compiler directives’ as the preprocessor is essentially a subprocess of the compiler.
The #include (usually referred to as ‘hash-include’) directive is commonly used to tell the preprocessor to include any code or statements contained within an external header file. Indeed, we have seen this ubiquitous #include statement in every complete program example so far in this book, as this is used to connect our programs with the core mbed libraries.
Figure 6.8 below shows three header files to explain how the #include statement works. It is important to note that #include essentially just acts as a copy-and-paste feature. If we compile afile.h and bfile.h where both files also #include cfile.h, we will have two copies of the contents of cfile.h (hence variable pears will be defined twice). The compiler will therefore highlight an error, as multiple declarations of the same variables or function prototypes are not allowed.
We can also use #define statements (usually referred to as ‘hash-define’), which allow us to use meaningful names for specific numerical values that never change. Here are some examples:
#define SAMPLEFREQUENCY 44100
#define PI 3.141592
#define MAX_SIZE 255
The preprocessor replaces #define with the actual value associated with that name, so using
#define statements does not actually increase the memory load on the processor.
The #ifndef Directive
We have just seen that multiple declarations of a variable or function prototype are not allowed. However, it is important for header files to include all the variables and features that are required for the linker to build the project successfully. It is therefore necessary to ensure that all variables and function prototypes are preprocessed on just one occasion.
When using header files it is therefore good practice to use a conditional statement to define variables and prototypes only if they have not previously been defined. For this the #ifndef directive, which means ‘if not defined’, can be used. In order to use the #ifndef directive effectively, a conditional statement based on the existence of a #define value is required. If the #define value has not previously been defined then that value and all of the header file’s variables and prototypes are defined.
If the #define value has previously been declared then the header file’s contents are not implemented by the preprocessor (as they must certainly have already been implemented). This ensures that all header file declarations are added only once to the project. The example code shown in Program Example 6.4 represents a template header file structure using the #ifndef condition, avoiding the error highlighted in Figure 6.8 earlier in this article.
Using mbed Objects Globally
All mbed objects must be definedin an ‘owner’ source file. However, we may wish to use those objectsfrom within other source files in the project, i.e. ‘globally’. This canbe done by also defining the mbed object in the associated owner’sheader file. When an mbed object is defined for global use, the externspecifier should be used. For example, a bespoke file called my_functions.cpp may define and use a DigitalOut object called ‘RedLed ’ as follows:
If any other source files need to manipulate RedLed , the object must also be declared in the my_functions.h header file using the extern specifier, as follows:
extern DigitalOut RedLed;
Notethat the specific mbed pins do not need to be redefined in the headerfile, as these will have already been specified in the objectdeclaration in my_functions.cpp .
Modular Program Example
Amodular program example can now be built from the non-modular codegiven in Program Example 6.3 earlier. Here, the functional features areseparated to different source and header files. Therefore, akeyboard-controlled seven-segment display project with multiple sourcefiles is created as follows:
- main.cpp – contains the main program function
- HostIO.cpp – contains functions and objects for host terminal control
- SegDisplay.cpp – contains functions and objects for seven-segment display output. The following associated header files are also required:
Note that, by convention, the main.cpp file does not need a header file. The program file structure in the mbed compiler should be similar to that shown in Figure 6.9 . Note that modular files can be created by right-clicking on the project name and selecting ‘New File’.
The main.cpp file holds the same main function code as before, but with #include directives to the new header files. Program Example 6.5 below details the source code for main.cpp .
The SegInit( ) and SegConvert( ) functions are to be ‘owned’ by the SegDisplay.cpp source file, as are the BusOut objects named ‘Seg1 ’ and ‘Seg2 ’. The resulting SegDisplay.cpp file is shown in Program Example 6.6 .
Note that SegDisplay.cpp file has an #include directive to the SegDisplay.h header file. This is given in Program Example 6.7 .
Program Example 6.7: Source code for SegDisplay.h
The DisplaySet and GetKeyInput functions are to be ‘owned’ by the HostIO.cpp source file, as is the serial USB interface object named ‘pc ’. The HostIO.cpp file should therefore be as shown in Program Example 6.8 below.
The HostIO header file, HostIO.h , is shown in Program Example 6.9 .
Createthe modular seven-segment display project given by Program Examples6.5-6.9. You will need to create a new project in the mbed compiler andadd the required modular files by right clicking on the project andselecting ‘New File’. Hence, create a file structure which replicatesFigure 6.9.
You should now be able to compile and run your modular program, using the circuit in Figure 6.6 shown in Part 2.
Tim Wilmshurst ,head of Electronics at the University of Derby, led the ElectronicsDevelopment Group in the Engineering Department of Cambridge Universityfor a number of years, before moving to Derby. His design career hasspanned much of the history of microcontrollers and embedded systems.
Rob Toulson is Research Fellow at Anglia Ruskin University in Cambridge. Aftercompleting his PhD, Rob spent a number of years in industry, where heworked on digital signal processing and control systems engineeringprojects, predominantly in audio and automotive ﬁelds. He then moved toan academic career, where his main focus is now in developingcollaborative research between the technical and creative industries.
This article is excerpted from Fast and effective embedded systems design: Applying the ARM mbed byRob Toulsonand Tim Wilmshurst, used with permission from Newnes, adivision of Elsevier. Copyright 2012. All rights reserved. For moreinformation on this title and other similar books, visit www.newnespress.com .