One of the goals for embedded systems developers is to create firmware in a programming environment that supports and enhances low bill-of-materials cost, software reliability, and fast development time. The best way to achieve such a programming environment is to use a unified firmware architecture that acts as the skeleton during product development and supports “firmware modularization.”
A design architecture that incorporates the right mix of firmware modularization, testability, and compatibility can then be applied to any firmware development project to maximize code reusability, speed up firmware debugging, and increase firmware portability.
Modular programming breaks down program functions into firmware modules, each of which performs a single function and contains all the source code and variables needed to complete that function (Figure 1 shows an example).
Click on image to enlarge.
Modularity helps to coordinate the work of many people on a team, manage interdependency between various parts of a project, and enable designers to assemble complex systems in a reliable way. Specifically, it helps designer achieve and manage complexity. As applications grow in size and functionality, modularity is necessary to separate them into individual pieces (whether as “components,” “modules,” or “plugins”). Each such separated piece then becomes one element of the modular architecture. As such, each piece can be isolated and accessed using a well-defined interface. In addition, modular programming improves firmware readability while simplifying firmware debugging, testing, and maintenance.
Modular programming is a key component of realizing code reusability. All the source code and variables needed to accomplish the specific function are integrated into a single independent source file and can be easily reused in other projects. Modular programming also speeds firmware development. Over time, developers can create a library of firmware modules, with every module's function and interface clearly defined. To create a new system, the firmware engineer only has to build up the firmware system with these modules in a manner similar to how engineers build up the hardware system with individual components.
Firmware modules principles
The basic concept of modular programming in firmware development is to create firmware modules . Conceptually, modules represent separation of concerns (SoC) . In computer science, SoC is the process of breaking a computer program into distinct features that rarely overlap in functionality. A concern is any piece of interest, or function, of a program and is synonymous with features or behaviors. Progress toward SoC is traditionally achieved through modularity and encapsulation.
Firmware modules can be categorized into several types. For example:
- Code related to each user module is implemented as an individual firmware module. For example, Um_Adc.c is the firmware module for an ADC user module and Um_Timer.c is the firmware module for a Timer user module.
- Code for specific pure software algorithms is implemented as an individual firmware module. For example, Alg_SoftFilter.c is the firmware module to perform software filters such as a median filter, mean filter, or weighted mean filter.
- Code for a specific application is implemented as an individual firmware module. For example, App_BatteryCharger is the firmware module for a battery charger application.
- Code for a specific tool is implemented as an individual firmware module. For example, Tool_Printf.c is the firmware module to implement printing features.
There are general principles to keep in mind when implementing firmware modules:
- All module-related functions should be integrated into a single source file.
- Have a header file that declares all the resources (MACROs/constants/ variables/functions) of the FW module.
- Include self-test code section within the source file to implement all the self-test functions of a FW module.
- The interfaces of FW module should be well-designed and defined.
- Since firmware is dependent on hardware, hardware dependencies need to be mentioned clearly in the source file header.
- Often, FW modules are available for other team members to use in other projects. To manage changes, an owner should maintain the module. The source file header should contain “owner” and “version” information.
- Firmware is dependent on the compiler to some extent. A “code tested with” item should be in the source file header to specify compiler or IDE-related information.
Firmware modularization does not come without cost. Modularization may introduce extra layers of function calls due to encapsulation as well as increase overall code size. However, the improved portability and reusability could be an acceptable tradeoff. To optimize code size, pay more attention to related technologies and accumulate more experience instead of throwing away FW modularization. In regards to code execution speed, encapsulation offers minimal overhead: with a CPU clock running at 24 MHz, one call depth consumes less than 1 Âµs, which can be ignored except for timing critical cases. For timing-critical cases, if necessary, the timing-critical code could be encapsulated into a special FW module to minimize overhead.
For detailed information and specific examples, refer to the source code associated with this article, CyTemplate_ FwModule.c and CyTemplate_FwModule.h, which are FW module template files for Cypress PSoC firmware design and can be easily applied to other MCUs.
As firmware is developed, temporary test functions and flows may become necessary to implement directly within subroutines or functions, particularly when debugging system-level problems or complex designs with many function dependencies. There are several drawbacks to this method. First, the test code modifies the original source code, potentially introducing new bugs. Secondly, such test code is difficult to preserve for later use. Because the same problem or bug may recur, if test code is not preserved, developers will have to write the same test code repeatedly.
Alternatively, developers can build self-test into their firmware architecture. Such an evolved firmware architecture is shown in Figure 2 .
Click on image to enlarge.
Compared with Figure 1 , this evolved firmware architecture provides a new execution path when running test code. It also specifically defines how test code is added and used. Individual self-test code related to the module is placed inside the FW module itself. Such module-based self-test code is used to debug the individual FW module.
Likewise, code used to test build-up functions of FW modules is called system-based self-test code . This code is placed in its own individual source file SelfTest.c. If a bug is triggered at the system level, it means that all the FW modules work fine individually but manifest a bug when combined together. Then the system-based self-test code helps to figure it out.
As shown in Figure 2 , using conditional compilation preprocessor directives enables source code to be compiled into different versions for both RELEASE and SELF-TEST . When VERSION equals RELEASE , all the self-test code is not compiled into the final hex file. Therefore, neither code size nor code speed is affected by self-test code.
For detailed information and examples, refer to the SelfTest.c implementation used in CyProject_Test.c and CyProject_Test.h., downloadable at www.embedded.com/code.new/.
Often, it's difficult for the existing firmware code of a specific project to be reused in other projects. Designers can spend a great deal of time trying to apply existing code to a new project. Sometimes, efforts to reuse code can exceed the time required to simply rewrite new code. However, by considering firmware compatibility during firmware development and creating a unified interface, code reusability can be improved significantly.
Firmware compatibility is affected by the following factors:
- Hardware platform: The target hardware platform may have different versions (such as different pin assignments) for different development stages or different customers. It's quite normal to support different hardware platforms with the same firmware
- MCU: MCUs in the same series or family are similar and compatible with each other to some extent. It's common to need to migrate firmware between MCUs in the same series
- MCU configuration: Some MCUs are highly configurable. For these processors, firmware should be updated to correspond with different MCU configurations
Integrated development environment
The firmware discussed in this article is the user-defined firmware that is only part of the whole firmware system. Almost all MCUs provide their own IDE which is responsible for managing the other components of the firmware architecture. For example, Cypress PSoC IDE (PSoC Designer) provides boot.asm, PSoCConfig.asm, and low-level drivers for the user modules. These automatically generated source files are the dependencies of user-defined firmware.
Firmware development stage
Firmware development may be divided into debug, self-test, and release stages. Different stages require different firmware behaviors.
It's common that the same firmware code compiled with different compilers generates different results. Compilers may differ in compiler features that lead to different compiling results, including code size and code execution speed.
Given all of a system's firmware dependencies, firmware development can be simplified with a unified interface that allows developers to manipulate firmware dependencies and increase firmware compatibility as much as possible. Such an evolved firmware architecture is shown in the Figure 3 .
Click on image to enlarge.
Compared with Figure 2 , this evolved firmware architecture introduces a new individual header file Version.h, which defines all firmware dependencies as MACROs. Version.h acts on all the source code (as the dotted lines show) to control firmware versions through C's conditional compilation directives (#if , #else , #endif , #ifdef , #ifndef , and so on).
To switch firmware development stages requires only two simple steps. First, define the firmware development stage MACROs in Version.h, shown in Listing 1 . Second, apply the firmware development stage MACROs in source code Listing 2 .
Click on image to enlarge.
Click on image to enlarge.
Thus, firmware development stage switching is easily controlled using VERSION_FW_STAGE .
Designing with a flexible firmware architecture provides developers with many advantages. The ability to integrate all reusable code into an expandable and sharable firmware module library facilitates code reusability. The architecture also supports self-testability to accelerate code debugging and testing as well as provides a unified interface to manipulate firmware compatibility. Modularization also supports customization of code while increasing readability, maintainability, and expandability.
While the concept of firmware modularization is one that can be applied to any microcontroller architecture or design, a firmware reference example description, along with the accompanying source files based on the use of the Cypress PSoC is available for download from www.embedded.com/code.new/.
Vincent Cai is an application engineer with Cypress Semiconductor and is responsible for the application of the PowerPSoC. He has a master's in electrical engineering from Xi'Dian University and has spent more than a decade in firmware development. Contact him at CaiWG.NiosII@gmail.com. His Chinese website is NiosII.cublog.cn.