These best practices could help you design memorable and extendable interfaces and ease development efforts for embedded software.

Jacob Beningo

November 11, 2022

6 Min Read
embedded software EPFMMF.jpg
Image courtesy of David Castillo Dominici / Alamy Stock Photo

An everyday activity for an embedded software developer is designing an interface for the software component they are working on. An interface describes the interactions that can be performed with the component, its behaviors, and its inputs and outputs. Unfortunately, many poorly thought-through interfaces out in the wild make it more difficult for developers to use them effectively. This post will explore five tips for designing software interfaces that all embedded software developers should follow.

Tip #1 – Use common interface language

When designing an interface, try to follow industry-standard terminology and techniques to make your interface memorable. Even though most IDEs today have some intelligence for code completion, I find that it doesn’t always work. Being forced to look up the interface can interrupt development when a developer is “in the flow.”

For example, if you are designing a digital input/output interface, you might consider defining your interface function and method names like:

Dio_Init

Dio_Write

Dio_Read

Dio_DeInit

While I’ve always liked having an init function, having a deinit always felt awkward. I think a better interface that more closely follows object-oriented paradigms would be:

Dio_Create

Dio_Write

Dio_Read

Dio_Destroy

We would want to avoid names for interfacing with hardware that are unclear.

Tip #2 – Limit the size of the interface

I have found that any interface I design seems to become unwieldy if it grows beyond 10–12 functions. The human mind can only store 7–12 pieces of information in short-term “RAM.” If more than that is required, it becomes more difficult to remember. Limiting the size of an interface to 10-12 is usually more than enough for what a component needs to do.

If the interface grows beyond 10–12 functions, it’s a sign that the component is trying to do too much! Developers, at that point, should carefully explore whether the component should be broken up into multiple components with a more well-defined purpose. Doing this can improve the code structure, portability, and reusability of the component. Developers will also, more likely than not, be able to remember the interface better!

Tip #3 – Use TDD to specify the interface

Test-Driven Development is an excellent tool for developers who want to write testable code and avoid “debug later” coding practices. However, TDD does have a further use in that we can use it to create tests that verify how the interface behaves as expected.

Early in the interface design process, developers will often create a list of functions or methods they want to include. Then, they’ll take an initial guess at what the inputs and outputs of those functions will be. Once the initial list is created, developers should make a list of tests for their component that will be used to prove that their component works.

With their initial test list in hand, developers can start to build out unit tests that require the developer to build the interface. As they work through each test, they’ll discover that their initial interface design will evolve to meet the system’s needs. When the tests are completed, two things will occur. First, a list of tests will describe how to interact with the component and the interface. Second, a component will be production ready.

Tip #4 - Make the interface extendable

While designing an interface, developers should follow SOLID principles. SOLID includes the open-closed principle, which states, “Software entities should be open for extension but closed for modification.” The idea here is that once we define our interface, we don’t want to modify it, but we want to be able to extend it if we need to add new functionality.

When interacting with hardware using C, such as with a digital input/output peripheral, we can build an interface that allows us to add features to the driver by extension. For example, I might include the following two functions in my interface:

Dio_RegisterRead

Dio_RegisterWrite

These two functions would provide low-level access to the hardware peripheral registers. Developers could build any number of custom modules and interfaces based on the application at hand that extends the simple digital input/output interface provided by the driver. In this case, careful planning allows us to easily extend the interface without modifying our low-level driver.

Tip #5 – Abstract the interface when necessary

Nearly 80% of embedded software developers use C to develop their embedded systems. At first glance, this can make it difficult for developers to reuse their interfaces without first creating interface templates that are copied and pasted. Using C seems not to offer abstractions as we have in C++. In C++, we can create a class with virtual methods to define our abstract interface. We want to abstract our interfaces when we need to change the underlying code. For example, if I want my digital input/output driver to work for two different microcontrollers.

In C, we can use a little trick to get similar behavior in our interfaces. The trick is to not define our interface as a list of prototype functions in a header but to define our interface as a list of function pointers in a structure like the below:

typedef struct

{

   bool (*Init)( const DioConfig_t * const Config);

   bool (*Read)(const DioObj_t * const, DioData_t * const DioData);

   bool (*Write)(const DioObj_t * const, DioData_t * const DioData);

} Dio_t;

 

When we define our structure variable, we can assign the function that will be called as follows:

 

const Dio_t Dio =

{

   Dio_Init,

   Dio_Read,

   Dio_Write

};

 

I can easily change what low-level driver I am accessing by changing the structure initialization. For example, for an STM32 microcontroller, I might use the following:

 

const Dio_t Dio =

{

   STM32_Dio_Init,

   STM32_Dio_Read,

   STM32_Dio_Write

};

 

For an MSP430, I might use something like:

 

const Dio_t Dio =

{

   MSP430_Dio_Init,

   MSP430_Dio_Read,

   MSP430_Dio_Write

};

The application code, at this point, doesn’t care what the underlying Dio function call is! If I want to initialize Dio, I simply call:

Dio.Init(Config);

What’s cool about that is that it looks a lot like I’m making a call to a method in a class, but I’m using C! I also have a nice abstraction that decouples my application code from the underlying hardware calls. That’s a win-win!

Conclusions

Developers create new interfaces all the time when they implement new modules. The trick is to do so that the interfaces are natural, memorable, and easily extendable. There is often a temptation to create the interface and move on; However, the interface is perhaps the essential element of a component. Once the interface is set, making changes can lead to massive headaches and efforts to change them.

This post explored a few essential tips to help you design your embedded software interfaces. Following these minimal best practices will help you design memorable and extendable interfaces and keep developers from being tripped up during development.

About the Author(s)

Jacob Beningo

Jacob Beningo is an embedded software consultant who currently works with clients in more than a dozen countries to dramatically transform their businesses by improving product quality, cost and time to market. He has published more than 300 articles on embedded software development techniques, has published several books, is a sought-after speaker and technical trainer and holds three degrees which include a Masters of Engineering from the University of Michigan.

Sign up for the Design News Daily newsletter.

You May Also Like