Embedded software is often tightly coupled to the underlying hardware and RTOS. There is a real cost associated with this tight coupling, especially in today’s increasingly agile world with its increasingly volatile electronics market: teams cannot rapidly respond to changes. Instead, changes tend to trigger large-scale rewrites or contribute to the degradation of the code base.
I’ve been musing about the sources of coupling between firmware and the underlying platform. As an industry, we must focus on creating abstractions in these areas to reduce the cost of change.
Let’s start the discussion with a story.
Table of Contents:
- The Hardware Startup Phone Call
- Coupling Between Firmware and Hardware
- Why Should You Care?
- Further Reading
The Hardware Startup Phone Call
In our consulting business, we are frequently contacted by companies that need help porting their firmware from one platform to another. These companies are often on tight schedules with a looming development build, production run, or customer release. Their stories follow a pattern:
- We built our first version of software on platform X using the vendor SDK and vendor-recommended RTOS
- We need to switch to platform Y because:
- X is reaching end of life
- We cannot buy X in sufficient quantities because Big Company bought the remaining stock
- Y is cheaper
- Y’s processor provides better functionality / power profile / peripherals / GPIO availability
- Y’s components are better for our application’s use case
- Platform Y is based on a different processor vendor (i.e. SDK) and/or RTOS
- Our engineer is not familiar with Platform Y’s processor/components/SDK/RTOS
- The icing on the cake: We need to have our software working on Platform Y within 30-60 days
After hearing the details of the project, we typically ask the following questions.
- Did you create abstractions to keep your code isolated from the vendor SDK and RTOS?
- Do you have a set of unit/integration tests that we can run to make sure the software is working correctly after the port?
- How can we tell whether or not the application software is working correctly after the port?
The first two are typically greeted with the same answer, which is something along the lines of “no, we’re a startup and we’ve been focused on moving as quickly as possible.” The last question is usually answered by “we’ll just try it out ourselves and make sure everything works.”
Given these standard answers, there’s practically no chance we can help the company and meet their deadlines. If there are large differences in SDKs and RTOS interfaces, the software has to be rewritten (potentially from scratch) using the old code base as a reference.
We also know that if we take on the project, we are in for a risky business arrangement. How can we be sure that our porting effort was successful? How can we defend ourselves from the client’s claim that we introduced issues without having a testable code base to compare against?
This scenario arises from a single strategic failure: failure to decouple the firmware application from the underlying RTOS, vendor SDK, and hardware. And as an industry we are continually repeating this strategic failure in the name of “agility” and “time to market”.
Even though these companies thought they were “moving quickly”, in the end they failed to do so. The consequences of this common blunder are extreme: schedule delays, lost work, reduced morale, and increased expenditures. It even might be the decision that causes your business to fail.
Coupling Between Firmware and Hardware
Software industry leaders have been writing about the dangers of tight coupling since the 1960s, so I’m not going to rehash coupling in detail. If you’re unfamiliar with the concept, here is some introductory reading:
In Why Coupling is Always Bad, Vidar Hokstad brings up consequences of tight coupling, two of which are relevant here:
- Changing requirements that affect the suitability of some component will potentially require wide ranging changes in order to accommodate a more suitable replacement component.
- More thought needs to go into choices at the beginning of the lifetime of a software system in order to attempt to predict the long term requirements of the system because changes are more expensive.
We see these two points play out in the scenario above.
If your software is tightly coupled to the underlying platform, changing a single component of the system – such as the processor – can cause your company to effectively start over with firmware development.
The need to swap hardware components late in the program (and the resulting need to make large changes to the software) is a failure to perform the up-front, long-term thinking required in order for tightly coupled systems to succeed. Otherwise, the correct components would have been selected during the first design iteration, rendering the porting process unnecessary. Of course, this type of goal is irrational: instead, we enter into development knowing that our systems will change, so we need to design our systems to make them easy to change.
Let’s review on a quote from Quality Code is Loosely Coupled:
Loose coupling is about making external calls indirectly through abstractions such as abstract classes or interfaces. This allows the code to run without having to have the real dependency present, making it more testable and more modular.
Decoupling our firmware from the underlying hardware is As Simple As That ™.
Up front planning and design is usually minimized to keep a company “agile”. You may argue that taking the time to design and implement abstractions for your platform introduces an unnecessary schedule delay. However, without abstractions that easily enable us to swap out components, our platform inevitably becomes tied to the initial hardware selection. How does that time savings stack up against the delays caused by the need to rewrite large chunks of software due to changes made later in the development cycle?
We all want to be “agile”, and abstractions help us achieve agility. What could be more agile than having the ability to swap out hardware or software components without needing to rewrite large portions of your system as a result? You can try more designs at a faster pace when you don’t need to rewrite the majority of your software to support a new piece of hardware.
Your abstractions don’t need to be perfect. They don’t need to be reusable on other systems, just yours. But if you want to move quickly, they need to exist.
We can start by producing abstractions that minimize the four sources of tight coupling in our embedded systems:
Processor Dependencies
Processor dependencies are the most common form of coupling and arise from two major sources:
- Using processor vendor SDKs or processor-specific headers, types, and registers throughout the program
- Using libraries which are coupled to a target architecture
In the most common cases, teams will develop software using a vendor’s SDK without an intermediary abstraction layer. Processor-level function calls are commonly intermixed with application logic and driver code, ensuring that the software becomes tightly coupled to a specific processor. When the team is required to migrate to another processor or vendor, the coupling to a specific vendor’s SDK often triggers a rewrite of the majority of the system. At this point, many teams realize the need for abstraction layers and begin to implement them.
In other cases, software becomes dependent upon the underlying architecture. Your embedded software may work on an ARM system, but not be readily portable to PIC, MIPS, AVR, or x86 machine. This is common when utilizing libraries such as CMSIS, which provides an abstraction layer specifically for ARM Cortex-M processors.
A subtler form of architecture coupling can occur even when abstraction layers are used. Teams can create abstractions which depend on a specific feature, an operating model particular to a single vendor, or an architecture-specific interaction. This form of coupling is less costly, as the changes are at least isolated to specific areas. Interfaces may need to be updated and additional files may need to change, but at least we don’t need to rewrite everything.
De-coupling firmware from the underlying processor is one of the most important activities when designing our embedded systems for change and reuse.
Platform Dependencies
Embedded software is often written specifically for an underlying hardware platform (i.e., your custom PCB). Rather than abstracting platform-specific functionality, embedded software often interacts directly with the hardware components that are installed in the system.
Without being aware of it, we develop our software based on assumptions we make for our underlying hardware. We write our code to work with four sensors, and then in the second version we only need two sensors. Or we start with one sensor, but realize later that it’s better if we have four sensors. On top of that, development hardware is limited, and you likely need to support both version one and version two of the product with a single firmware image.
Consider another common case, where our software supports multiple versions of a PCB. Whenever a new PCB revision is released, the software logic must be updated to support the changes. Supporting multiple revisions often leads to #ifdefs and conditional logic statements scattered throughout the codebase. What happens when you move to a different platform, with different revision numbers? Wouldn’t it be easier if the differences for each board revision were contained in a single location rather than scattered throughout the code base?
When these changes come, how much of your code needs to be updated? Do you need to add #ifdefstatements everywhere? Do your developers cringe and protest because of the required effort? Or do they smile and nod because it will only take them 15 minutes?
To decouple our software from the underlying hardware details, we can abstract our platform/hardware functionality behind an standard interface (commonly called the Board Support Package). What features is the hardware platform actually providing to the software layer? What might need to change in the future, and how can we isolate the rest of the system from those changes? We can use these questions to determine what abstractions our BSP needs to supply.
Multiple platforms & boards can be created that provide same set of functionality and responsibilities in different ways. If our software is built upon a platform abstraction, we can move between supported platforms with greater ease.
Component Dependencies
Component Dependencies are a specialization of the platform dependency, where software relies on the presence of a specific hardware component instance.
In embedded systems, software is often written to use specific driver implementations rather than generalized interfaces. This means that instead of using a generalized accelerometer interface, software typically works directly with a BMA280 driver or LIS3DH driver. Whenever the component changes, all code interacting with the driver must be updated to use the new part. Similar to the board revision case, we will probably find that #ifdefs or conditionals are added to select the proper driver for the proper board revision.
Higher-level software can be decoupled from component dependencies by working with generic interfaces rather than specific drivers. If you use generic interfaces, underlying components can be swapped out without the higher-level software being aware of the change. Whenever parts need to be changed, your change will be isolated to the driver the declaration (ideally found within your platform abstraction).
RTOS Dependencies
An RTOS’s APIs are commonly used directly by embedded software and scattered throughout the code base. When a processor change occurs, the team may find that the RTOS they were previously using is not supported on the new processor.
Migrating from one RTOS to another requires a painful porting process, as there are rarely straightforward mappings between the functionality and usage of two different RTOSes.
Providing an RTOS abstraction allows platforms to use any RTOS that they choose without coupling their application software to the RTOS implementation. Abstracting the RTOS APIs also allows for running code on your development machine, since you can provide a pthread implementation for the RTOS abstraction.
Why Should You Care?
It’s a fair question. Tight coupling in firmware has been the status quo for a long time. You might even be of the opinion that it must remain that way due to resource constraints. After all, vendor SDKs are readily available. You can start developing your platform immediately. The rapid early progress feels good. Hey, perhaps you even picked all the right parts the first time, and the reduced time-to-market will actually happen for your team.
If not, you will likely find yourself repeating this tried-and-true cycle and calling us for help.
It’s not all doom and gloom, however. You can design your embedded software so it can be easily changed. There are great benefits from reducing coupling and introducing abstractions.
- We can rapidly prototype hardware without triggering software rewrites
- We can take better advantage of unit tests, which are often skipped on embedded projects due to hardware dependencies
- We can implement the abstractions on our development machines, granting us the ability to write and test software on their PC before porting it to the embedded system
- We can reuse subsystems, drivers, and embedded system applications on across an entire product line
Check out the Further Reading section below for explorations of all of these topics.
Happy hacking! (and get to those abstractions!)
Designing Embedded Software for Change
Are you tired of every hardware or requirements change turning into a large rewrite? Our course teaches you how to design your software to support change. This course explores design principles, strategies, design patterns, and real-world software projects that use the techniques.
Further Reading
- Designing Embedded Software for Change course
- Prototyping and Design for Change: Lightweight Architectural Strategies
- Leveraging Our Build Systems to Support Portability
- Practical Decoupling Techniques Applied to a C-based Radio Driver
- Virtual Devices in Embedded Systems Software
- Real-World Portable Driver Examples
- Managing Complexity with the Mediator and Facade Patterns
- Paper: A Procedure for Designing Abstract Interfaces for Device Interface Modules
- Information Hiding
- Paper: Designing Software for Ease of Extension and Contraction
- Paper: On the Criteria to Be Used in Decomposing Systems into Modules

This is a great post, and hit me with multiple examples I have encountered in my own work as well. Thanks for the musings!