Embedded TDD: Breaking the Hardware Dependency

Posted by Austin Glaser on Apr 4, 2017 8:01:00 AM
Find me on:

In a previous blog post I wrote about some of the challenges facing the use of test driven development (TDD) in an embedded environment. It's important to be aware of these obstacles, but also important to keep in mind that all of them are solvable. This post deals with perhaps the most prominent problem (or at least the one I found most daunting originally): hardware dependency.


The Issue

For the most part, embedded development takes place at a very low level. While many applications will include a real-time operating system (RTOS), even those will often interact directly with hardware registers. Those that don’t will generally instead access custom libraries which have no direct analog on the development system.


A Solution


According to a quick google search, polymorphism is "the condition of occurring in several forms." In the computer science (thanks wikipedia!), it refers to "the provision of a single interface to entities of different types."  This is often exploited in high-level languages to allow testing of code with dependencies. By some mechanism, the depended-on code can be replaced, allowing a module to be tested in a more isolated manner. This is often useful when the dependency is something either very complex or unpredictable/uncontrollable, like a web API.

Link-time Polymorphism

In C, there are fewer features allowing this kind of abstraction. However, with a clean application architecture, there are ways to open up the same kinds of 'seams' in embedded code as in higher-level applications.

The main way of abstracting multiple implementations for a single interface, then, is to do it when the application is linked (hence "link-time polymorphism"). The same functions will be defined in multiple `.c` files and compiled. When the binary is linked, however, only one object will be selected. For the test binary, an object file with a fake implementation can be selected. This implementation might perform simplistic, hardcoded, or test-controllable actions, depending on the needs of the test.

Managing these different builds can quickly become a headache depending on your project's build system. Tools exist which to help with this situation -- I'll talk in more detail about these in a future post, but the one I most commonly use is called Ceedling.

Controlling Memory Access

When working with embedded systems, it's very common to see constructs similar
to the following:

#define PWR_ADDRESS        ((uint32_t) 0x40008020)
#define PWR                             (*((volatile uint32_t *) PWR_ADDRESS))

or maybe:

typedef struct {
     volatile uint32_t CONFIG;
     volatile uint32_t STATUS;
     volatile uint32_t OUT;
} SomeModule_t;

#define SM1_ADDRESS        ((uint32_t) 0x4008000)
#define SM2_ADDRESS       ((uint32_t) 0x4008020)

#define SM1                             ((SomeModule_t *) SM1_ADDRESS)
#define SM2                            ((SomeModule_t *) SM2_ADDRESS)

In either case, the code is declaring pointers into arbitrary memory, which on the target system contains peripheral registers. On the host system, however, dereferencing these pointers will result in undefined behavior -- if you're lucky, an immediate segmentation fault will alert you to a problem. However, we need to be able to test code which directly accesses registers as much as any other code.

I'll show here a couple possible solutions, one for either of the cases above.

For the first case, code using the register will most likely look similar tothe

void turn_on_foo(void)
     PWR |= PWR_FOO;

In this case, PWR is treated simply as a global value. This means that we
somehow have to override the register definition. One possible solution could look like the following:

#define PWR_ADDRESS         ((uint32_t) 0x40008020)

#if   !defined(TEST)
#      define PWR                             (*((volatile uint32_t *) PWR_ADDRESS))
static volatile uint32_t PWR;

Note, however, that this would be one of many similar register definitions in a header file likely provided by the chip manufacturer. Modifying the version used to compile release code has the potential for frustrating headaches and bugs, so I recommend making a copy that is only included when tests are built.

The second situation is to be preferred -- in that case, code that accesses registers will look more like this:

void some_module_init(SomeModule_t * sm)

void some_module_write(SomeModule_t * sm, uint32_t val)
     while (sm->STATUS | SM_STATUS_BUSY);
     sm->OUT = val;

This provides the opportunity to quickly and transparently substitute a dummy copy of the SomeModule_t register map:

SomeModule_t sm_dummy;

some_module_write(&sm_dummy, 42);

While the function calls will now be performing more-or-less nonsensical memory accesses, these accesses can be verified to match an expected pattern.

What does this gain us?

Making these kinds of substitutions requires the developer to make many assumptions about the functioning of their system. It's not a good way to diagnose obscure concurrency bugs or experiment with the right way to interact with a hardware peripheral. In general, unit testing in any language doesn't seek to solve these problems.

Instead, we're developing what effectively is an executable specification for our code. We have absolute control over the 'hardware,' which allows us to test the code's behavior in obscure error cases which might be difficult to induce on the actual system. And, finally, we force ourselves to write code which can be tested, which often leads to designs with looser coupling between system elements and more logical separation between appropriate layers of abstraction.

In future posts, I'll talk a little more about tools specifically for managing C unit tests. These tools provide, among other things, tools for streamlining some of the tasks I discussed above. Additionally, I'll provide some insight into what I've found to be useful (and testable!) system architectures for embedded projects.

Interested in learning more about Boulder Engineering Studio? Let's chat!

Schedule A Consult

Topics: Testing, Embedded, Software