C Library design for embedded applications: tips and hints
Why a library
Write reusable code is one of the most powerful and time-saving thing that an embedded programmer should learn. Compose well-arranged libraries makes things easiest, especially when we want to add old code in a new project. Citing one of the most talented programmers I have ever met, “Well done code should be like a LEGO: must fit perfectly and without any effort”. So a good library should be quite abstract, with a simple hierarchy and should provide some well documented APIs.
Even if this is a generic embedded article, we will provide some concrete example.
When we want to include new header or source files to a project we need to edit makefile because compiler requires the list of source file to compile and list of folder in which look for headers. When we create a new library a good idea is create a file (typically .mk or .mak) that contains these lists. This file and their new entries are included in makefile, this way our file act like a piece of makefile.
This way if we add new headers or source files to our library we don’t need to re-edit main makefile. This method is also used in ChibiOS/HAL to connect higher level layer with hardware dependent low-level.
In what follows as example we will consider a simple library containing 2 header and 1 source file (Figure 1). File user.mk should contain two include directories and a source file.
There is a statement for the lib relative path, one for source files (USERSRC) and another for included directories (USERINC). Note that we need to include userlib folder since we need for userconf.h.
That file follows makefile syntax that could be found in GNU make documentation. Always remember that to continue a statement on the next line you need for a straight(\) and that TAB spaces are denied.
USERLIB = ./userlib # List of all the Userlib files USERSRC = $(USERLIB)/src/lcd.c # Required include directories USERINC = $(USERLIB) \ $(USERLIB)/include
In makefile we have to include user.mk, add USERSRC to CSCR and add USERLIB to INCDIR. As example consider a ChibiOS makefile:
# Imported source files and paths # Other files (optional). include ./userlib/user.mk # Define linker script file here LDSCRIPT= $(STARTUPLD)/STM32F401xE.ld # C sources that can be compiled in ARM or THUMB mode depending on the global # setting. CSRC = $(STARTUPSRC) \ $(KERNSRC) \ $(PORTSRC) \ $(OSALSRC) \ $(HALSRC) \ $(PLATFORMSRC) \ $(BOARDSRC) \ $(TESTSRC) \ $(CHIBIOS)/os/hal/lib/streams/memstreams.c \ $(CHIBIOS)/os/hal/lib/streams/chprintf.c \ $(USERSRC) \ main.c ... INCDIR = $(STARTUPINC) $(KERNINC) $(PORTINC) $(OSALINC) \ $(HALINC) $(PLATFORMINC) $(BOARDINC) $(TESTINC) \ $(USERINC) $(CHIBIOS)/os/hal/lib/streams \ $(CHIBIOS)/os/various ./userlib/
Hierarchy Should be kept as simplest it is possible, avoiding to create much nested folders facilitating its consultation. Creating a new library first step is a simple hierarchy, edit makefile and make sure your project compile without errors.
When library becomes large it’s a good idea collect configuration defines in one or more configuration files. Furthermore if library is organized as modules and not every one is required, disable some of them could be important to keep software efficient and streamlined. Considering the example we could have this userconf.h
#ifndef _USERCONF_H_ #define _USERCONF_H_ /** * @brief Enables the LCD subsystem. */ #if !defined(USERLIB_USE_LCD) || defined(__DOXYGEN__) #define USERLIB_USE_LCD TRUE #endif /*===========================================================================*/ /* LCD driver related settings. */ /*===========================================================================*/ /** * @brief Enables backlight APIs. * @note Enabling this option LCD requires a PWM driver * */ #if !defined(LCD_USE_BACKLIGHT) || defined(__DOXYGEN__) #define LCD_USE_BACKLIGHT TRUE #endif #endif /* _USERCONF_H_ */
#ifndef _LCD_H_ #define _LCD_H_ #include "hal.h" #include "userconf.h" #if USERLIB_USE_LCD || defined(__DOXYGEN__) ... //Here some header stuffs ... #endif /* USERLIB_USE_LCD */ #endif /* _LCD_H_ */ /** @} */
and this lcd.c
#include "lcd.h" #if USERLIB_USE_LCD || defined(__DOXYGEN__) ... // here some code ... #endif /* USERLIB_USE_LCD */
This way defining USERLIB_USE_LCD as FALSE all the lcd module is disabled.
Everything has its place
It is clear that the design for each kind of application could be structured in different ways and often this depends on code author, however there are some generic rules to follow.
Prototypes in header files, function body in the source files
If we need to call a function from other points of our software then its prototype should be in a header file that we could include. On the other hand, function body should be developed in a source file that typically has the same name as the header. This allow to keep header slim and easy to scan for the programmer.
Masks, defines and typedefs in the header, variables in the source files
Bit masks, defines and typedefs externally needed should be in a header file as well as function prototypes. Variable externally needed instead should be declared in .c adding an external declaration in related header file for inclusion.
Include header not sources
Only header should be included, furthermore if hierarchy is well-organised every header should include “children” header and re-inclusion should be avoided. As result, main.c will include a single header for each independent library making things much more elegant.
Documentation on the body function
Documentation is the most important thing in a code library and is the task that programmers love less. It is important write documentation on APIs even for a programmer that “brush up” its code after a long while. Add documentation on prototypes makes header too much messed up, therefore is a good idea put documentation in form of comment on the body function.
Same style for everything
Decide a common indentation for a whole project makes code reading much easier. Imagine to read three parts of the same story from three different books written using different indentation and different literary style:it is quite annoying. Someone said that code is like poetry, so decide your style and be ordered
Keep It Simple Stupid. This is the most important rule to follow. Don’t make things complicated if not required, don’t write ten lines of code if only two are needed. First self-question should be: is that libraries actually needed?