EmbeddedRelated.com
Blogs
The 2024 Embedded Online Conference

Best Firmware Architecture Attributes

Tayyar GUZELJune 4, 20166 comments

Architecture of a firmware (FW) in a way defines the life-cycle of your product. Often companies start with a simple-version of a product as a response to the time-to-market caveat of the business, make some cash out of the product with a simple feature set. It takes only less than 2-3 years to reach a point where the company needs to develop multiple products derived from the same code base and multiple teams need to develop various other new features simultaneously. This could be efficiently achieved only if you started with a good FW architecture. If not, at some point, your company will need a substantial amount of effort to either re-factor your code or even you need to re-architect and re-write the code from scratch. 

What are then the best architecture attributes?

Modularity

This article is available in PDF format for easy printing
Modularity is important. Instead of a firmware application that has several thousands of lines of code with no boundaries in terms of responsibilities, one should have a set of modules that are responsible for executing different parts of the application. That way, similar products can be derived from the same code base with addition of new modules or modification of existing modules that would do the differentiation. In any case, most of the modules will be reused to develop a new product. 

                             Figure 1 - An example to a modular firmware consisting of different modulesIn

In Figure-1, arrows show dependencies. Module C depends on Module D at compile time and both Module A and Module B depend on Module C. Dependency can be a huge problem as the code base grows and it may cause unnecessary refactoring efforts. This issue will be handled later.


Low
coupling

What is coupling or dependency exactly then?

Coupling or dependency for a module is defined as being dependent on another module (meaning module A is reading from or writing to a variable in module C in the example above) to compile or successfully run during run-time. Therefore, there's a compile-time and run-time dependency. What we are talking about here is compile-time dependency. In this case, the application code is not even compiled without existence of the modules that the other modules depend on.

                                Figure -2 An illustration of dependency

In Figure-2, Module B depends on Module A to compile because the variable "color" used in Module B is actually defined in Module A.

Imagine you had a firmware consisted of 6 modules and there were compile-time dependencies from almost each module to the others. That would be the worst case you would be in because even if you need to change a single thing in one module, you may end up making changes in another five modules. For instance, a change in Module C may trigger a change in Module A, B, D and E in the example below. 
 
            

Figure-3 "Spaghetti Diagram" where there are dependencies all over the place

If there low coupling concern is not addressed in a firmware architecture from the beginning, dependencies among the modules could turn into the one illustrated in Figure-3. This kind of dependency diagram could be called "spaghetti diagram". Fixing issues in such dependency could be very cumbersome and module reuse per product with minimal changes could not be possible. That means redundant development cost to the company. 

Another issue could be "circular dependency". Figure-3 illustrates the circular dependency which is another bad practice. If one wants to use a module in another project, he/she must also bring the other module that is not needed along with the one needed. Therefore, it costs extra Flash memory. Circular dependencies should not be allowed between modules.                                             

Figure - 4 Circular dependency

Therefore, it is good to follow the best practices

  • Dependencies among modules should be as minimum as possible to maintain the amount of change at a minimum when a small change is needed in a feature. 
  • Each module should have single responsibility.
  • From a design perspective, low coupling can be maintained by defining dependency rules.
  • Quality should be measured by design and code review. 
  • There should not be circular dependencies between modules.

You could use Doxygen in your build system to see the dependencies in your FW. Doxygen allows to visualize the relations between the various modules by means of including dependency graphs, inheritance diagrams, and collaboration diagrams, which are all generated automatically.

Encapsulation

Each module should have access restrictions. By defining setter and getter functions, one can restrict the read/write access to variables in a module. Therefore, when you change the name of the variable in one module, the other module does not  


Encapsulation is one of the most important properties of Object Oriented Programming paradigm and actually means data hiding. However, the concept can be applied to conventional C programming as well. 

In this case, for instance, Module B should not know about the internals of Module A even though it uses data from Module A. How could this be possible? If we go back to our example on dependency. 

  Figure -5 Module B knows about Module A

How do we hide variable "color" from Module B and still let Module B use the value stored in variable "color"?

We need to use "setter" and "getter" functions to achieve that. If we use a setter API to set the value stored in "color" variable via a function called "setColor", then, we don't really need to know what the actual variable name is. Hence, when the name of that variable is changed in Module A for some reason, Module B would not be impacted by that. 

Figure -6 Module B uses the setter function but does not know the name of the variable "color" to set its value.

Setter API is implemented as the following.

void SetColor(int var){
        color = var;
}

If in the future, let's say the name of the variable "color" is changed to "backgroundcolor", only the implementation of SetColor should be changed and Module B does not need to change. Therefore, impact of change in Module A to other firmware modules is minimized.


Same thing applies to retrieving  variable content from other firmware modules. One needs to implement getter APIs to provide encapsulation. If Module A implements the following getter API,

int GetColor(void){
return color;
}

Then, there is no need for Module B to know what the actual name of the "color" variable is. Module B directly uses GetColor() API.

It is not the only reason why we need to hide data. The other important reason to hide data is to protect other modules from accessing any data in other modules arbitrarily. They should only access the data provided by public setter/getter APIs rather than directly accessing global variables of other modules. Therefore, the design of Module A dictates which variables should be exposed to the other modules. 

Extensibility

Architecture should support adding a new feature to the FW. Firmware architecture should make sure separation of concerns is achieved in a way that adding a feature to the application should be seamless and easy thing to do. Modularity, Encapsulation and Low Coupling help achieve do that. If features are implemented as firmware modules, then it is easy to add a feature to the firmware.

How are encapsulation and low coupling related to Extensibility?

If there is low coupling and encapsulation is achieved, then impact of adding a feature to the firmware would be minimized as well. The new module would depend on less number of other modules and there will not be major changes in many other modules.

Scalability and Portability

Architecture should allow the FW to be scalable to various HW sizes (sensor size, etc) and easy migration to another hardware.

To provide that, there should be a hardware abstraction layer (HAL) consisting of hardware (HW) specific firmware modules to isolate hardware initialization and control from other modules. Therefore, each module does not directly access HW registers. They access HW registers through APIs of the HAL layer. This minimizes the impact of HW changes to the rest of the firmware modules since if a HW register changes, only the HAL is updated.  

                                     

Figure -7 An example of a hardware abstraction layer (HAL) as a module

Following example shows a wrong dependency. 

 

Figure -8 An example to wrong dependency

Dependency from an upper layer should be down to HAL not beyond in order to access any hardware. If this concern is always addressed in implementation, it would be easy to swtch to another hardware or scale hardware easily by only making changes in the HAL.

Data flow

There are well known data flow design patterns in software architectures and one of them could be used in firmware architectures as well depending on system requirements. Most commonly used two patterns are called "Pipes and Filters" and "Blackboards". In "Pipes and Filters" pattern, the data flows from one module to another. In the following example, some data is acquired from hardware by the Acquisition module and pushed to Processing module to get it processed, then the processed data is pushed to Presentation module to show it to a user via Leds, LCD or something of that sort or transfer it to another processor using the available communication peripherals (I2C, SPI, USB, etc).  Firmware systems realizing this architecture should at least have one source and one sink and one or more processing stages should exist.

In this context, Pipe is an abstraction of a data transfer mechanism which could be simply passing a pointer to data in a function argument whereas Filter is an abstraction for a processing stage. In the following example, source could be the Data Acquisition module where the data is read from an ADC of some type and sink could be considered as the Presentation module where it sends the data to a HW to be displayed or conveys it to another processor through a protocol.

Figure -9 An illustration of Pipes and Filters data flow 

In "blackboards" design pattern, data is always written to a shared memory area and used from there. Only the pointers to the shared area are passed to the modules but the data resides in shared memory. Blackboards pattern can be thought of as wires connecting modules to each other since only the pointers are passed. A canonical form of Blackboards pattern is illustrated below.

 Figure -10 Blackboards pattern

As you can see from Figure-10, Data Acquisition module reads the data from hardware and writes it to the shared memory. Once that is done, the controller passes the pointers to Processing module to read/write from/to the Blackboards and it does the same thing for Presentation module. Therefore, there is a need for a Controller in Blackboards pattern. Note that Controller gets to decide which API will be called from which module and in which order. Therefore, not only data flow but also execution flow is controlled by the Controller in this case. 

Fundamental idea behind Blackboards pattern is that several Expert modules contribute to solving a problem by processing the data in the shared memory. This decouples the FW modules from each other and allows adding/removing modules to the project easily. The Controller entity coordinates execution of the Expert modules based on high-level decisions.

Re-usability

Architecture should allow or promote module re-usability to keep amount of changes from one product to another.

Communications

A good architecture defines which modules own data, which modules have access, what is life-cycle of data and what is the format of data.

Fault tolerance (robustness)

Scope of faults and desired actions should be clearly defined.

Testability and Verifiability

Modules should be testable and verifiable. That means, the external (customer) requirements of a project should be de-composed into pieces (internal firmware functional requirements) that can be implemented as features of the modules and the modules then should be tested to verify correctness of functionality or features to see if the external requirements are met.  

Unit test driven firmware development provides the tool for verification  of functionality as long as the requirements are well decomposed and distributed to modules. Once that is achieved, then, integration testing should be carried out to verify performance as well.  

Nightly regression (continuous integration) testing using build system tools such as (Jenkins, Bamboo, etc), build system sanity checks (to see if the interface definitions break the rules), static code analysis tools, and unit test automation should be part of the firmware development cycle to early catch the issues.

Maintainability

Developers should follow a coding convention, care about their API namings, do proper amount of commenting and try write readable code rather than writing obfuscated codes with fancy looking function pointers and such unless really necessary.

Documentation

Architecture should be easily documented with event or timing diagrams, flowcharts, state machines, class diagrams etc. 



The 2024 Embedded Online Conference
[ - ]
Comment by jms_nhJune 7, 2016
Good topic and some good points, but there are a couple of odd things. You never really explain why circular dependencies are bad (in languages like C the source code may not compile, depending on how it's implemented). The bullet point list at the end covers unrelated topics, and I disagree with several of them: "Try to minimize the use of function pointers", "Try not to use compiler optimizations and do optimize your code as you write", "No global variable unless really needed" (why? see https://www.embeddedrelated.com/showarticle/691.php), "Do not nest structures more than 1 level if not really needed". And the remaining guidelines aren't very clear what they mean and why we should follow
them.
"The event driven systems are comparatively simpler." ... simpler than what?
[ - ]
Comment by Fernando43June 4, 2016
Figures 1, 3 and 8 missing.
[ - ]
Comment by tayyarguzelJune 5, 2016
Should be visible now. Thanks.
[ - ]
Comment by Rick LyonsOctober 11, 2016

Hi,

  The figures still seem to be missing. I used the FireFox browser as well as the Internet Explorer browser. Neither browser displayed the missing figures as of October 11, 2016.

[ - ]
Comment by sachingoleJune 8, 2016
Good article, I think if each concept given with reference code snippet or real world example will help to understand better.

Now a days if you google for embedded FW principles you will get ton of papers, but nobody explain with example.

[ - ]
Comment by BitterActivitySeptember 10, 2019

Very nice article. Maintaining clean and and good firmware architecture has been a constant struggle in my life and i think that is going to be there for me, but this article is a great help for me, and i happened to read at right time when i am entangled in a huge chunk of spaghetti code.

To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Please login (on the right) if you already have an account on this platform.

Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: