Unit Test How? Registers

This is part of an ongoing series of articles about using Unity to Unit Test C applications, often for Embedded Applications. If you're looking for other tips, be sure to check out the others here.

Today's topic is registers. This is THE most common question people have when they start thinking about unit testing C code. It's the reason it's better to test in a simulator or using native code than it is to test on the target hardware (Not familiar with this discussion? Catch up here).

We begin this tale with good news and bad news.


First, if we're using a simulator, we have really good news: We're probably done! Simulators almost always treat registers as just a big pool of RAM. That's perfect! We can write initial conditions or testable values in our tests. Our source will then read and write to them normally. Finally, our tests can again read from them to verify our source did what was expected. It doesn't matter if the target considers those registers as read-only, write-only, or whatever... we can simulate whatever we want.


Second, even if you are using a native application, transforming your register set into something testable isn't hard. But...


It's tedious. Most modern microcontrollers have complex register sets... and we need to do a little footwork to prepare for testing any of the ones that your application may require!

Our goal? Our goal is to "remap" all those registers regular heap-declared variables so that we can read/write them as desired. The address 0x10000000 might be the GPIO control register on our target hardware, but it's unlikely to be directly accessible under Linux / Windows / Mac OS / whatever.

The trick is to rebuild the micro's header file, transforming it into something testable. It's straightforward... we want to use the define TEST to know if we're creating a RAM version of those names, or if they will continue to be mapped to the register set. The details really only vary in how our supplier built their micro's header file in the first place. Let's look at some examples... enough to give us an idea of how to approach other variants.


One common paradigm (if not THE most common paradigm) is to treat registers as some form of integer (usually unsigned) and access them through dereferencing a pointer to a specific address. It looks something like this:

There are many slight variations on this theme, which might change the size of the int, use macros to make it easier to understand, or other such tricks. The point is that we're casting a memory location to a pointer, which we then dereference and treat as some sort of integer.

#define PORTA (*(volatile uint32_t*)(0x40001000))

These are easy to create testable alternatives, because it's already telling us what type it wants to be. We just need to pay attention. We then create an alternate standin that we can use during a test:

#ifndef TEST
#define PORTA (*(volatile uint32_t*)(0x40001000))
EXTERN volatile uint32_t PORTA; 

We should keep in mind that the address isn't important when running the test. As long as all accesses happen through a consistent interface like this, the tests and the releases will both be happy with our definition of PORTA and everything should work well.

Occasionally, we come across registers that overlap. In these situations, we can use defines and casts to mimic the same behavior.

Hello, World!

#ifndef TEST
#define SPI_WORD (*(volatile uint16_t*)(0x80000000))
#define SPI_HI (*(volatile uint8_t*)(0x80000001))
#define SPI_LO (*(volatile uint8_t*)(0x80000000))
EXTERN volatile uint16_t SPI_WORD;
#define SPI_HI (*(volatile uint8_t*)(void*)(&SPI_WORD+1))
#define SPI_LO (*(volatile uint8_t*)(void*)(&SPI_WORD))

The (void*) is useful to keep your compiler from complaining about the pointer casting.


Another common paradigm found in header files is the use of custom structs. Often a struct is created for a single peripheral on the microcontroller. Then, one or more memory addresses are cast to be "instances" of that struct. It's effectively just a fancier version of the INT POINTER case, and the solution is the same:

typedef struct _GPIO_PORT_T { 
  volatile unsigned int DIR; 
  volatile unsigned int PULLUP; 
  volatile unsigned int INPUT; 
  volatile unsigned int SET_OUTPUT; 
  volatile unsigned int SET_INPUT; 
#ifndef TEST
#define PORTA (*(GPIO_PORT_T*)(0x40001000))
#define PORTB (*(GPIO_PORT_T*)(0x40002000))
#define PORTC (*(GPIO_PORT_T*)(0x40003000))

As you can see, it quickly becomes very similar to our first example.


Sometimes the developers of compilers for embedded applications give you "helpful" extensions to the C language. It's tempting to use these conventions because they provide shortcuts to otherwise tedious tasks... but often they are just keeping your code from being as portable as it could be.

One extension that seems to show up from time to time is the use of the ampersand to lock a variable to a location. It effectively does exactly what our two options above do, but with a neater notation, something like this:

volatile uint32_t PORTA @ 0x400000000;

That's kinda nice to look at, right? It is, but it's not standard C. If you try to compile your tests using a native compiler, it is likely to complain about these extensions.

Depending on your compiler, you may be able to get around this just by properly using your #ifdefs, and the compiler will ignore all your release-only code. You can then use one of the tricks above.

Otherwise, we need to get a little fancier: We need to create a second header file.


We've shown all the examples above using our old friend #ifdef and the TEST define. That's often a good way to go, but maybe we don't want to use that trick? Micro definition files are often already huge, why make them bigger? Maybe we're not liking the way that this has required a lot of changes to our "release code" (even if it's all in opposite #ifdefs).

Surely there is another way!

There is. We make a second header file. We name it EXACTLY the same thing, but put it in a different folder in our source tree (possibly directly in our test folder, so that it's clear it goes with our tests). We fill it with the testable versions of all our registers. Then, we tell our build system to use that header for tests, and the other header for releases. Done. A little organization goes a long way!


Did you notice that all the testable examples above have an all-capital EXTERN in front of them? Did you figure out why?

The thing about micro register files is that they tend to get included a lot. Our test likely will include it, as will the release file we are testing. It's possible that other files getting compiled into the mix will also be including this file.

This is harmless when it's full of macros that are just dereferencing pointers... but now that we are filling it with actual instances of variables, we're going to start getting multiple declarations of things. That's going to be a problem when we link!

Because of this, we employ this little trick. At the top of our register file, we include this:

#ifdef TEST
#ifndef EXTERN
#define EXTERN extern

So, if we're building a test and we haven't defined EXTERN yet, we set it to extern. So far, that's all files, right? So now we just need to have ONE of our files in each test define EXTERN to be nothing, so that it will declare an actual instance of these variables.

The best candidate is the Test file, since there is only one Test file per build. You can do it the obvious way.

#include "unity.h"
#define EXTERN
#include "testable_micro_registers.h"
#include "file_to_test.h"

Or, a little more tidy, you can create a very small header file which looks like this:

#define EXTERN
#include "micro_registers.h"

Then, you can include that INSTEAD of the normal micro register header in your tests only, like so:

#include "unity.h"
#include "testable_micro_registers.h"
#include "file_to_test.h"


Well, there it is. The options are fairly straightforward... but clearly there is some legwork to getting a new microcontroller in a testable state the first time you use it. I've found it to be well worth it, but this procedure is conclusive proof for many that a simulator is the simpler approach. But we'll discuss the tricks related to that another day!

Happy Coding!