I've described before how I've been testing Firmware code written in C by compiling it into Ruby Extensions and using ruby's excellent unit testing framework.
Up till now I've been testing the 'middle tier' algorithmic code but how to go about testing the low level stuff, that which pokes the hardware?
The firmware I am developing is for the ST ARM750 microcontroller and I am using ST's own standard library which is very useful driver code for all the peripherals embedded on the chip. It is a very useful resource as the peripherals on this chip are very complex and the user manual is written in hardware engineer gobbledygook that you have to study like a legal document, looking for the small print (e.g. running it at 60MHz it kept crashing until I found the small print that advised me to enable 'burst mode').
Scanning through the source to this library I saw some #ifdef DEBUG statements that implied that it could be compiled into an emulation library. The code was written very cleanly so it took me very little time to write something in (of course) ruby to scan the header files and generate 5250 lines of C code to emulate this library. Here is an example of one of the generated library functions:
1
2
3
4 void TIM_ClearFlag(TIM_TypeDef* TIMx, u16 TIM_FLAG)
5 {
6 VALUE nRet;
7 VALUE oArgs = rb_ary_new();
8 VALUE strFuncName = rb_str_new2( "TIM_ClearFlag");
9 ID nId = rb_intern( "Function");
10
11 rb_ary_push( oArgs, PassStructureArg( (void *)TIMx, sizeof( TIM_TypeDef)));
12 rb_ary_push( oArgs, LONG2NUM( TIM_FLAG));
13
14 nRet = rb_funcall( g_oCallBack, nId, 2, strFuncName, oArgs);
15
16
17 }
TIM_ClearFlag is the name of the function in the ST standard library. The emulation code takes the name of the emulated function and the two arguments to the function, converts them into ruby objects and passes them to a method called 'Function' in a ruby object. In the testing case, this object is something that will make sure the functions are being called in the correct sequence and with the correct parameters and will return the appropriate return value. Structure arguments are passed as binary objects which the ruby can decipher from the structure definitions in the ST library header files (also parsed using ruby).
Note that my system has a few limitations that are acceptable because of the simplicity of the ST library and the way it was coded:
-
The parser I wrote just about handles the coding style of this library
-
The calls to ruby do not support the case where the library may poke the values within a structure passed to it by pointer.
-
It doesn't handle passing structures by value.
Now I have my low level testing library I will be able to do my testing using something I call signature analysis (I'm not sure there is a better software engineering term). I write the code, test it on the real hardware, then I can record the sequence of function calls made including the values of function arguments. Later on, in leiu of real hardware to test it on I can run the test code and compare it against the previously recorded signatures. Et voila, the reassurance that I haven't broken anything and I can sleep at night.
UPDATE:
My test library now includes a YamlFaker that can be used in two modes:
-
it will record a sequence of function calls
-
it will ensure a sequence of function calls matches a previous recording
The yaml script looks like this:
#
# This file is maintained by the YamlFaker module DO NOT EDIT
#
---
- test_CommandPWMSet Demand -200:
- :Comment: |-
Looking for demand to be correct according to the requested range and the
scaling factors. Also ensure that for the fifth channel, the period is
large for small demand as the output of this channel is inverted
- TIM_SetPulse:
args:
- TIMx: TIM0
- TIM_Channel: "0x2"
- Pulse: "0x0"
- TIM_SetPulse:
args:
- TIMx: TIM1
- TIM_Channel: "0x2"
- Pulse: "0x0"
- TIM_SetPulse:
args:
- TIMx: TIM2
- TIM_Channel: "0x2"
- Pulse: "0x0"
- PWM_SetPulse:
args:
- PWM_Channel: "0x2"
- Pulse: "0x0"
- PWM_SetPulse:
args:
- PWM_Channel: "0x4"
- Pulse: "0x12c"
- test_CommandPWMSet Demand -100:
- TIM_SetPulse:
args:
- TIMx: TIM0
- TIM_Channel: "0x2"
- Pulse: "0x0"
Most of the entries in the script describe the expected sequence of function calls, complete with arguments. I can ensure that the demand being sent to my PWM channels is what I expect. These recorded scripts could be quite useful, it is a complete log of the interaction with the hardware. If I ever had to change the code that poked the hardware I can compare the script recorded from the new code against my old script and make sure that any changes are what I would expect. This way I can be far more certain that my changes are correct before they hit real hardware.