The most difficult step in unit testing embedded software can be getting your code to compile on your host system. Once you can get the code to compile on a host machine it is easy to simply follow standard practices to test any C code. This article will show you how to make your embedded software portable.
The major challenges to portability of embedded are:
- Target architecture and word size
- Non-standard compilers
- Hardware specifics
The key to overcoming these obstacles is abstraction. Typically we start with source code like this:
And we’d like to end with something like this:
You can achieve this by leaning heavily on typedefs and your pre-processor…
1. Abstract your data types.
It doesn’t matter what your target architecture is, the most important thing is to hash define your data types. The easiest way to do this is to create a project Types.h where you define all your types. It might look something like this depending on your target:
// TargetTypes.h typedef uint24 uint24_t; typedef int24 int24_t; typedef ulong48 uint48_t; typedef long48 int48_t; typedef fix fix24_t; typedef lfix fix48_t; typedef acc fix56_t; typedef char char24_t; // HostTypes.h typedef int int24_t; typedef unsigned uint24_t; typedef long long48_t; typedef unsigned long long ulong48_t; typedef unsigned long long uint48_t; typedef float fix; typedef double lfix; // ...etc
This makes it possible to compile your code for both your host and your target architectures, just use a different Types.h depending on the platform; as a bonus it also makes it easier to port the code to other systems in the future. Once your code is portable it is much easier to test.
2. Abstract your compiler
Compilers for embedded systems rarely follow the ANSI standard, typically adding keywords to define data storage specifics of interrupt routines. For example, the Keil 8051 compiler has keywords “code” and “data” to specify memory locations; you can imagine how bad it is when you try to hash define that out! If you compiler has some esoteric language extensions it is pretty safe just to hash define them out, but in my experience you really must be disciplined in the code to use keywords that you are sure will not clash.
// TagetTypes.h continued... #define CODEMEM code #define INTERRUPTMEM interrupt #define XDATAMEM xdata #define DATAMEM data // HostTypes.h // on the host make these directives disappear from the source #define CODEMEM #define INTERRUPTMEM #define XDATAMEM #define DATAMEM #endif
Now we can use these much safer constructs in the source code, safe in the knowledge that we won’t step on anyone’s toes.
3. Abstract your hardware
// Registers.h #ifdef TARGET #define IOMEM_BASE 0x2FF #else int32_t fake_registers[IO_MEM_RANGE]; uint32_t IOMEM_BASE = (int32_t)&fake_registers; #endif
So there it is
The first step in unit testing embedded software is making it compile on a host machine. This can be difficult but if you follow these simple tips you can save yourself a lot of pain. Remember, the earlier in the project you do this the easier it is!