Software Development Magazine - Project Management, Programming, Software Testing |
Scrum Expert - Articles, tools, videos, news and other resources on Agile, Scrum and Kanban |
Mocking the Embedded World - Appendix
Michael Karlesky, Greg Williams, William Bereza, Matt Fletcher
Atomic Object, http://atomicobject.com
Sample Project
To demonstrate the ideas in this article, we created a full sample project for an Atmel ARM7-based development system (AT91SAM7X). Thisexample system samples a thermistor, calculates a weighted average temperature,outputs the value through an RS232 serial port, and handles error conditions.The project includes three MCH triads: analog-to-digital conversion (ADC), timermanagement, and serial data output. The full sample project including supporttools is available at http://atomicobject.com/pages/Embedded+Software.In the near future, much improved versions of our custom tools with documentation will be made freely available.
For discussion here, we will walk through development of the ADC triad from beginning to end. Along the way we will also demonstrate the useof the tools we developed to complement the principles espoused earlier.
Summary of Steps
- Create system tests for the temperature conversion feature to be developed.
- Generate a skeleton Model, Conductor, Hardware, and accompanying test files.
- Add tests and production code for initialization to the Executor, ADC triad, and helper functions.
- Add tests and production code to the Executor, ADC triad, and helper functions for runtime temperature capture and conversion. Start with the Conductor first.
- Verify that all unit tests and system tests pass.
Create Temperature Conversion System Tests
Using Systir, the miniLAB driver, and a serial port driver, our system test provides several input voltages to the development system(simulating a thermistor in a voltage divider circuit) and expects temperaturesin degrees Celsius to be read as ASCII strings from the development PC’sserial port. The system test is running externally to the development board itself.
proves "voltage read at temperature sensor input is translated and reported in degrees C"
# Verifies the 'stable' range of the sensor (10C-45C).
Instability is due to the flattening
# of the temperature sensor voltage to temperature response
curve at the
extremes.set_temperature_in_degrees_C(10.0)
verify_reported_temperature_in_degrees_C_is_about(10.0)
set_temperature_in_degrees_C(25.0)
verify_reported_temperature_in_degrees_C_is_about(25.0)
set_temperature_in_degrees_C(35.0)
verify_reported_temperature_in_degrees_C_is_about(35.0)
set_temperature_in_degrees_C(40.0)
verify_reported_temperature_in_degrees_C_is_about(40.0)
Fig. 3. Systir system test for verification of temperature conversion. Driver code handling timing, test voltage calculation, and communications is not shown.
Generate Skeleton MCH Triad and Unit Test Files
We create skeleton versions of the source file, header file, and test file for each member of an ADC MCH triad. The use of the generation script and an Empty ADC Conductor are shown in the following figures. For each component in the system (whether a member of a triad or a helper), this same process is repeated to create skeleton files.
> ruby auto/generate_src_and_test.rb AdcConductor
> ruby auto/generate_src_and_test.rb AdcModel
> ruby auto/generate_src_and_test.rb AdcHardware
Fig. 4. Generating a skeleton MCH triad and tests with the module generation Ruby script.
#ifndef _ADCCONDUCTOR_H
#define _ADCCONDUCTOR_H
#include "Types.h"
#endif // _ADCCONDUCTOR_H
Fig. 5. AdcConductor skeleton header.
#include "AdcConductor.h"
Fig. 6. AdcConductor skeleton source file.
#include "unity_verbose.h"
#include "CMock.h"
#include "AdcConductor.h"
static void setUp(void)
{
}
static void tearDown(void)
{
}
static void testNeedToImplement(void)
{
TEST_FAIL("Implement me!");
}
//[[$argent require 'generate_unity.rb';
inject_mocks("AdcConductor");$]]
//[[$end$]]
//[[$argent require 'generate_unity.rb'; generate_unity();$]]
//[[$end$]]
Fig. 7. TestAdcConductor skeleton unit test file. Note inclusion of Argent blocks. Before compilation, a Rake task will execute Argent, and these blocks will be populated with C code to use the Unity test framework and CMock generated mock functions (mocks are generated from header files by another build task prior to Argent execution).
Add Tests and Production Code for Initialization
The Executor initializes the system by calling initialization functions in each triad’s Conductor. The Conductors delegate necessary initialization calls. The ADC Hardware will use a helper to configure the physical analog-to-digital hardware. For brevity, the Executor’s tests and implementation are not shown here.
static void testInitShouldCallHardwareInit(void)
{
AdcHardware_Init_Expect();
AdcConductor_Init();
}
Fig. 8. Initialization integration test within the Conductor’s test file. The "_Expect" function is automatically generated by CMock from the interface specified in the Hardware header file.
void AdcConductor_Init(void)
{
AdcHardware_Init();
}
Fig. 9. Conductor’s initialization function that satisfies its unit test.
static void
testInitShouldDelegateToConfiguratorAndTemperatureSensor(void)
{
Adc_Reset_Expect();
Adc_ConfigureMode_Expect();
Adc_EnableTemperatureChannel_Expect();
Adc_StartTemperatureSensorConversion_Expect();
AdcHardware_Init();
}
Fig. 10. Initialization integration test within the TestAdcHardware’s test file. Calls are made to an AdcHardwareConfigurator helper and an AdcTemperatureSensor helper. The "_Expect" and "_Return" functions are automatically generated by CMock from the interfaces specified in header files.
void AdcHardware_Init(void)
{
Adc_Reset();
Adc_ConfigureMode();
Adc_EnableTemperatureChannel();
Adc_StartTemperatureSensorConversion();
}
Fig. 11. AdcHardware’s initialization function that satisfies its integration test.
static void
testResetShouldResetTheAdcConverterPeripheral(void)
{
AT91C_BASE_ADC->ADC_CR = 0;
Adc_Reset();
TEST_ASSERT_EQUAL(AT91C_ADC_SWRST,
AT91C_BASE_ADC->ADC_CR);
}
static void
testConfigureModeShouldSetAdcModeRegisterAppropriately(void)
{
uint32 prescaler = (MASTER_CLOCK / (2 * 2000000)) - 1; //
5MHz ADC clock
AT91C_BASE_ADC->ADC_MR = 0;
Adc_ConfigureMode();
TEST_ASSERT_EQUAL(prescaler, (AT91C_BASE_ADC->ADC_MR &
AT91C_ADC_PRESCAL) >> 8);
}
static void
testEnableTemperatureChannelShouldEnableTheAppropriateAdcInput(void)
{
AT91C_BASE_ADC->ADC_CHER = 0;
Adc_EnableTemperatureChannel();
TEST_ASSERT_EQUAL(0x1 << 4,
AT91C_BASE_ADC->ADC_CHER);
}
Fig. 12. AdcHardwareConfigurator’s initialization unit tests.
void Adc_Reset(void)
{
AT91C_BASE_ADC->ADC_CR = AT91C_ADC_SWRST;
}
void Adc_ConfigureMode(void)
{
AT91C_BASE_ADC->ADC_MR = (((uint32)11) << 8) |
(((uint32)4) << 16);
}
void Adc_EnableTemperatureChannel(void)
{
AT91C_BASE_ADC->ADC_CHER = 0x10;
}
Fig. 13. AdcHardwareConfigurator’s initialization functions that satisfy its unit tests.
static void
testShouldStartTemperatureSensorConversionWhenTriggered(void)
{
AT91C_BASE_ADC->ADC_CR = 0;
Adc_StartTemperatureSensorConversion();
TEST_ASSERT_EQUAL(AT91C_ADC_START,
AT91C_BASE_ADC->ADC_CR);
}
Fig. 14. AdcTemperatureSensors’s start conversion unit test.
void Adc_StartTemperatureSensorConversion(void)
{
AT91C_BASE_ADC->ADC_CR = AT91C_ADC_START;
}
Fig. 15. AdcTemperatureSensors’ start conversion function that satisfies its unit test.
Add Tests and Production Code for Temperature Conversion
The models of all the triads are tied together for inter-triad communication. The timing Model repeatedly updates a task scheduler helper with time increments. The ADC Model returns to the Conductor a boolean value processed by the task scheduler helper to control how often an analog-to-digital conversion occurs.
The ADC Conductor is repeatedly serviced by the Executor once initialization is complete (the other triad Conductors are serviced in an identical fashion). Each time the ADC Conductor is serviced, it checks the state of the ADC Model to determine whether an analog-to-digital conversion should occur. When a conversion is ready to occur, the ADC Conductor then instructs the ADC Hardware to initiate an analog-to-digital conversion. Upon completion of a conversion, the Conductor provides the raw value in millivolts to the ADC Model for temperature conversion. The timing and serial communication triads will cooperate to average and periodically output a properly formatted temperature string. Here, we walk through the tests and implementation of the ADC Conductor’s interaction with the ADC Hardware and ADC Model down through to the hardware read of the analog-to-digital channel.
static void testRunShouldNotDoAnythingIfItIsNotTime(void)
{
AdcModel_DoGetSample_Return(FALSE);
AdcConductor_Run();
}
static void
testRunShouldNotPassAdcResultToModelIfSampleIsNotComplete(void)
{
AdcModel_DoGetSample_Return(TRUE);
AdcHardware_GetSampleComplete_Return(FALSE);
AdcConductor_Run();
}
static void
testRunShouldGetLatestSampleFromAdcAndPassItToModelAndStartNewConversionWhenItIsTime(void)
{
AdcModel_DoGetSample_Return(TRUE);
AdcHardware_GetSampleComplete_Return(TRUE);
AdcHardware_GetSample_Return(293U);
AdcModel_ProcessInput_Expect(293U);
AdcHardware_StartConversion_Expect();
AdcConductor_Run();
}
Fig. 16. TestAdcConductor integration tests for interactions with AdcHardware and AdcMcodel. The "_Expect" and "_Return" functions are automatically generated by CMock from the interfaces specified in header files.
void AdcConductor_Run(void)
{
if (AdcModel_DoGetSample() &&
AdcHardware_GetSampleComplete())
{
AdcModel_ProcessInput(AdcHardware_GetSample());
AdcHardware_StartConversion();
}
}
Fig. 17. AdcConductor’s run function (called by the Executor) that satisfies the Conductor unit tests.
static void
testGetSampleCompleteShouldReturn_FALSE_WhenTemperatureSensorSampleReadyReturns_FALSE(void)
{
Adc_TemperatureSensorSampleReady_Return(FALSE);
TEST_ASSERT(!AdcHardware_GetSampleComplete());
}
static void
testGetSampleCompleteShouldReturn_TRUE_WhenTemperatureSensorSampleReadyReturns_TRUE(void)
{
Adc_TemperatureSensorSampleReady_Return(TRUE);
TEST_ASSERT(AdcHardware_GetSampleComplete());
}
static void
testGetSampleShouldDelegateToAdcTemperatureSensor(void)
{
uint16 sample;
Adc_ReadTemperatureSensor_Return(847);
sample = AdcHardware_GetSample();
TEST_ASSERT_EQUAL(847, sample);
}
Fig. 18. Integration tests for AdcHardware. Note that the "_Expect" and "_Return" functions are automatically generated by CMock from the interfaces specified in header files.
void AdcHardware_StartConversion(void)
{
Adc_StartTemperatureSensorConversion();
}
bool AdcHardware_GetSampleComplete(void)
{
return Adc_TemperatureSensorSampleReady();
}
uint16 AdcHardware_GetSample(void)
{
return Adc_ReadTemperatureSensor();
}
Fig. 19. AdcConductor’s functions satisfying the Conductor integration tests. Note that AdcHardware calls helper functions in AdcTemperatureSensor
static void
testShouldStartTemperatureSensorConversionWhenTriggered(void)
{
AT91C_BASE_ADC->ADC_CR = 0;
Adc_StartTemperatureSensorConversion();
TEST_ASSERT_EQUAL(AT91C_ADC_START,
AT91C_BASE_ADC->ADC_CR);
}
static void
testTemperatureSensorSampleReadyShouldReturnChannelConversionCompletionStatus(void)
{
AT91C_BASE_ADC->ADC_SR = 0;
TEST_ASSERT_EQUAL(FALSE, Adc_TemperatureSensorSampleReady());
AT91C_BASE_ADC->ADC_SR = ~AT91C_ADC_EOC4;
TEST_ASSERT_EQUAL(FALSE, Adc_TemperatureSensorSampleReady());
AT91C_BASE_ADC->ADC_SR = AT91C_ADC_EOC4;
TEST_ASSERT_EQUAL(TRUE, Adc_TemperatureSensorSampleReady());
AT91C_BASE_ADC->ADC_SR = 0xffffffff;
TEST_ASSERT_EQUAL(TRUE, Adc_TemperatureSensorSampleReady());
}
static void
testReadTemperatureSensorShouldFetchAndTranslateLatestReadingToMillivolts(void)
{
uint16 result;
// ADC bit weight at 10-bit resolution with 3.0V reference =
2.9296875 mV/LSB
AT91C_BASE_ADC->ADC_CDR4 = 138;
result = Adc_ReadTemperatureSensor();
TEST_ASSERT_EQUAL(404, result);
AT91C_BASE_ADC->ADC_CDR4 = 854;
result = Adc_ReadTemperatureSensor();
TEST_ASSERT_EQUAL(2502, result);
}
Fig. 20. Unit tests for AdcTemperatureSensor helper.
void Adc_StartTemperatureSensorConversion(void)
{
AT91C_BASE_ADC->ADC_CR = AT91C_ADC_START;
}
bool Adc_TemperatureSensorSampleReady(void)
{
return ((AT91C_BASE_ADC->ADC_SR & AT91C_ADC_EOC4) ==
AT91C_ADC_EOC4);
}
uint16 Adc_ReadTemperatureSensor(void)
{
uint32 picovolts =
ConvertAdcCountsToPicovolts(AT91C_BASE_ADC->ADC_CDR4);
return ConvertPicovoltsToMillivolts(picovolts);
}
static inline uint32 ConvertAdcCountsToPicovolts(uint32 counts)
{
// ADC bit weight at 10-bit resolution with 3.0V reference = 2.9296875 mV/LSB
uint32 picovoltsPerAdcCount = 2929688;
return counts * picovoltsPerAdcCount; // Shift decimal to preserve accuracy in fixed-point
}
static inline uint16 ConvertPicovoltsToMillivolts(uint32 picovolts)
{
const uint32 halfMillivoltInPicovolts = 500000;
const uint32 picovoltsPerMillivolt = 1000000;
// Add 0.5 mV to result so that truncation yields properly rounded result
picovolts += halfMillivoltInPicovolts;
return (uint16)(picovolts / picovoltsPerMillivolt); // Divide to convert to millivolts
}
Fig. 21. AdcTemperatureSensor helper functions satisfying the AdcTemperatureSensor unit tests.
Verify that all unit tests and system tests pass.
The process of adding tests and production code to satisfy a system requirement, of course, requires an iterative approach. Design decisionsand writing test and production code require multiple passes.
The code samples presented in the preceding section represent finished product. As the sample project was developed, tests and code wererefactored and run numerous times. With the automation provided by our unit andsystem test frameworks, CMock, and the build tools, this process was nearly painless.
> rake clean
> rake test:units
> rake test:system
Fig. 22. Running rake tasks to build and test the system. The definitions of the rake tasks and the calls to CMock & Argent, dependencygeneration, compiling, and linking are defined within the Rakefile and not shown.
Page 3 Back to the archive list
Click here to view the complete list of archived articles
This article was originally published in the Summer 2007 issue of Methods & Tools
Methods & Tools Testmatick.com Software Testing Magazine The Scrum Expert |