Sometimes embedded developers have to use inline assembler instructions to get better control of the processor, or to improve performance. How should we deal with those when we’re doing TDD and testing off the target?
What’s the problem? The embedded asm
statements cause compilation errors if the assembler instructions are not part of the off-target test platform instruction set. Also some of the instructions might not be legal in the test environment. This article shows how to insert a test double for the asm
directives with gcc and CppUTest.
In this example, I’ll assume that whatever the asm
instructions were supposed to do, that they are not needed to run in the test environment. We’ll look at two alternatives: ignoring asm
directives and spying on them.
Here is example production code using the asm
directive:
//snip... do { asm("NOP"); asm("FLOP"); asm("POP"); status = IORead(0); } while ((status & ReadyBit) == 0); |
For some reason NOP, FLOP and POP need to be executed before an I/O read operation. in the unit test environment we don’t want them assembled or executed.
The simplest thing to do is to make asm
go away with the preprocessor. We can force the preprocessor to include a file that makes asm
go away. If you are using the CppUTest makefile system you can add this preprocessor directive:
CPPUTEST_CFLAGS += -include mocks/NullAsm.h |
NullAsm.h would simply be this:
#define asm(x) |
This macro makes asm
and its parameters go away. There might be a gcc command line option to ignore asm
also, but I could not find one.
But maybe you want more than to ignore the asm
directives. Let’s spy on them. AsmSpy
captures the assembly instructions so that the test can check that the right instructions are specified to execute.
Let’s look at the tests for the AsmSpy.
#include "CppUTest/TestHarness.h" TEST_GROUP(AsmSpy) { void setup() { AsmSpy_Create(20); } void teardown() { AsmSpy_Destroy(); } }; TEST(AsmSpy, CaptureAsmInstructions) { AsmSpy("NOP"); AsmSpy("FLOP"); AsmSpy("POP"); STRCMP_EQUAL("NOP;FLOP;POP;", AsmSpy_Debrief()); } |
In setup the spy is created and given a capacity. This spy will remember a string of 20 characters of assembler. In the TEST, you can see that the spy separates each assembly string with a semicolon.
Now we’ve got to sneak the spy into my code. We do this with the preprocessor because asm
is a keyword and we need text replacement. We can force the gcc compiler to include AsmSpy.h like this:
CPPUTEST_CFLAGS = -include mocks/AsmSpy.h |
This directive causes the AsmSpy
to be the first include file of the compilied .c file. We don’t edit in the AsmSpy
because we don’t want any evidence of the spy in the production code. Here is AsmSpy.h.
#ifndef D_AsmSpy_H #define D_AsmSpy_H #define asm AsmSpy void AsmSpy_Create(int size); void AsmSpy_Destroy(void); void AsmSpy(const char *); const char * AsmSpy_Debrief(void); #endif |
Notice #define asm AsmSpy
. This causes asm
directives to be converted to AsmSpy
calls when we build for test. The production code only needs the AsmSpy
prototype, but it does no harm to have the spy’s full interface.
AsmSpy.c looks like this:
#include "AsmSpy.h" #include <string.h> static char * instructions = 0; static int capacity = 0; void AsmSpy_Create(int cap) { capacity = cap; instructions = malloc(capacity+1); instructions[0] = 0; } void AsmSpy_Destroy(void) { free(instructions); } void AsmSpy(const char * i) { if (strlen(i) + strlen(instructions) < capacity) { strcat(instructions, i); strcat(instructions, ";"); } } const char * AsmSpy_Debrief(void) { return instructions; } |
Being that the asm
code is machine dependent, we could also isolate this asm
code in a separate machine dependent function, and then override that function with a linker test double. Then the preprocessor substitution would not be needed. I might go this route if the asm
has to return some value, or I was using Extended Asm.
Pingback: Hiding Non-standard C Keywords for Off-Target Testing « James Grenning’s Blog