27 July 2023 by Phillip Johnston • Last updated 15 August 2023
This was originally a lesson in the Designing Embedded Software for Change course. Version 2 resulted in a refactoring of the module. The original lesson is preserved in the Field Atlas for those who want to reference it.
Processor dependencies are the most common form of tight coupling we find in embedded software, and it is also the most problematic.
Coupling to the processor manifests in these ways:
- Using processor vendor SDK APIs or processor-specific headers and types throughout the project
- Making direct register accesses throughout the project
- Using libraries and frameworks that are target architecture- or vendor SDK-specific
- Making assumptions about endianness
- Making assumptions about data alignment
- Making assumptions about word size
- Use of special machine instructions
Processor coupling starts innocently enough. Processor vendors have invested heavily in SDKs to encourage developers to pick their chips by dangling the carrot of “rapid prototyping and development.” Embedded teams want to get started with development as quickly as possible, and vendor SDKs allow them to do just that.
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. The rapid, early progress feels good, especially since the team has saved a significant amount of time by leveraging the vendor’s development efforts.
There are many reasons processors change. The longer your product is maintained, the more likely you are to run into one of these scenarios:
- The team is required to migrate to another processor or vendor. Coupling to a specific vendor’s SDK will trigger a rewrite of most of the system.
- Migration can happen for many reasons: insufficient supply to meet production requirements, processor EOL, reducing power consumption, increasing processing power, requiring additional peripherals, accessing new features, or supporting multiple platforms/customers/environments.
- The processor vendor updates their SDK, making subtle changes that break the existing software, causing the team to make adjustments throughout the program when updating SDK versions (or, in an even worse outcome, causing the team to avoid updates altogether).
- The processor vendor changes their entire framework, making it impossible to update SDK without rewriting the software.
- An example of this can be seen with Nordic’s deprecation of the nRF5 SDK, which is no longer actively maintained. Users must migrate to the newer nRF Connect SDK, which is based on Zephyr.
In other cases, software becomes dependent upon the underlying processor architecture. Your embedded software may work on an ARM system, but not be readily portable to PIC, AVR, or x86_64. It can also subtly manifest in the types used for interfaces and data structures: developers who are working on 32-bit processors write 32-bit code, which causes problems or inefficiencies when porting the software to a 64-bit processor (or, perhaps less commonly nowadays, porting to an 8-bit processor).
A subtler form of processor coupling can occur even when using abstraction layers. Teams can create abstractions which depend on a specific feature, an operating model particular to a single vendor, or a processor architecture-specific interaction.
You can also couple your software to the processor using architecture-specific libraries and abstractions, which we’ll cover when we discuss library and framework dependencies.
Directly calling a processor vendor’s SDK or HAL in your application is an anti-pattern. De-coupling firmware from dependencies on the underlying processor is one of the most important activities when designing your embedded systems for change.
References
-
Tight Coupling Makes Change Difficult
-
Library and framework dependencies make embedded software difficult to change
-
Musings on the Tight Coupling Between Firmware and Hardware
-
Prototyping and Design for Change: Lightweight Architectural Strategies
-
Building Software for Portability by Greg Blackham (1988)
Architecture issues arise from the instruction set available on the specific processor on which the application will run. Each processor has certain idiosyncrasies that you must consider when you write portable code. [PJ: Such issue might include endianness, address sizes, word sizes]
-
Portability by Design by Michael Ross
- Notes on things that change across processors:
- Alignment of data.
- Special machine instructions, such as transcendentals.
- Size of various fundamental data types.
- Endianness
- Floating-point format. [PJ: perhaps not as much anymore – we’ve standardized pretty well on IEEE 754]
Your code should contain safety checks to prevent you from relying too much on a particular word size or floating-point representation.
Alignment of data is a problem mainly when your application depends on transmitting binary data across platforms, or uses different language processors on the same platform. Aside from the obvious endianness difficulties, there are variations in field alignment. This often necessitates representing the binary data in textual form or having a data broker perform the necessary transformations between the two architectures. Consider the “bad” C code in Example 1(a), which was intentionally written to demonstrate alignment problems. The alignment of the various structure fields will vary from one compiler to another and from one architecture to another. Imagine trying to write this to a binary file and read it back on another platform! Even with the same language processor, this structure will vary in size, due to memory “holes”; for example, on SPARC sizeof(mystruct)=24 bytes, on 80386 sizeof(mystruct)=16 bytes, and on HP-PARISC sizeof(mystruct)=24 bytes.