Open source project that uses CppUTest framework to test firmware for embedded systems. The project covers some firmware examples that are usually hard to test, including:
- Simple API unit test
- Memory Leaks test
- API unit test with ISR (interrupt service routines) dependencies
- API unit test with hardware dependencies
- Test/Run main firmware
Usually, an embedded system communicates with many inner devices, such as accelerometers, GPS modules, CAN controllers, RFID readers or Touchscreen+LCD modules. Now, with the IoT trend, they may also communicate with remote servers and/or nearby devices like smartphones, TVs, smartwatches or even your fridge and microwave! Thus, testing software for embedded systems can sometimes be very painful.
The goal is to show how to remove these hardware/devices dependencies and test/run code on a host machine using CppUTest framework.
-
You will need to install the following tools:
sudo apt install git gcc g++ libtool autoconf
-
Download this repository
git clone https://github.com/pelco/firmware_testing.git
-
Run setup.sh script (will create tools folder, get CppUTest and LCOV)
cd firmware_testing ./setup.sh
-
Go inside the code folder to get started
cd code
tree -d
Output:
.
├── src -> Firmware source code
│ ├── hw -> Hardware/Target specific code
│ └── include -> Header files
└── tests -> Test cases
└── mocks -> Mocks
-
Build "firmware" (Source)
make
The command will build the main firmware located at src/main.c and create src/run_this_firmware binary.
./src/run_this_firmware
Output:
Hello World from Firmware I2C write value 0x1 to reg 0xa at slave 0x30 I2C read reg 0x30 from slave 0xb I2C write value 0x1 to reg 0xa at slave 0x30
-
Run test cases
The next command will build CppUTest and run all test cases in tests/t_*.cpp files.
make test
The command will also build and run the test binary file located tests/test_firmware. Output:
... OK (12 tests, 12 ran, 20 checks, 0 ignored, 0 filtered out, 1 ms)
-
Run coverage report
This command will run all test cases and generate a firmware coverage report.
make coverage
A coverage folder should have been created and you can access the result by open coverageTest.html/index.html with your browser:
In src/math.c file has a simple calculator API that implements addition and subtraction operations.
math.c:
uint8_t calculator(char op, uint8_t val1, uint8_t val2)
{
uint8_t result = 0;
switch (op) {
case '+':
result = (uint8_t)(val1 + val2);
break;
case '-':
result = (uint8_t)(val1 - val2);
break;
default:
break;
}
return result;
}
The test cases are implemented in tests/t_math.cpp and cover:
- All switch cases;
- Return overflow;
In src/other.c file is implemented a function that is leaking memory.
other.c:
uint8_t mem_leak_function(void)
{
uint8_t *ptr;
ptr = malloc(1);
(void)(ptr);
// free(ptr);
return 1;
}
By default memory leak detection is turned off. To enable memory leak detection change CPPUTEST_USE_MEM_LEAK_DETECTION=N
to CPPUTEST_USE_MEM_LEAK_DETECTION=Y
in MakefileCppUTest.mk file.
Then build and run test cases again.
The test cases are implemented in tests/t_other.cpp.
In src/other.c is also implemented a function (wait_for_ISR_func()) that depends on a ISR() (interrupt service routines) to happen in order for the firmware function continue it's execution.
other.c:
volatile uint8_t brick_code = 1;
void ISR(void)
{
brick_code = 0;
}
void wait_for_ISR_func(void)
{
while (brick_code) {
//printf(" Stuck forever here\n");
};
printf(" -> Out of the loop");
}
The test cases are implemented in tests/t_other.cpp and show how to handle these cases.
In src/main.c is implemented the main firmware code.
main.c:
uint32_t main(void)
{
printf(" Hello World from Firmware\n");
uint8_t reg = 0;
init_device(); /* Configure i2c device */
reg = i2c_read(I2C_SLAVE_ADDRESS, I2C_REG2);
if (reg == DEVICE_READY) {
i2c_read(I2C_SLAVE_ADDRESS, I2C_REG3);
} else {
init_device(); /* Reconfigure device if it's not ready */
}
return 0;
}
This main firmware code has some hardware dependencies (i2c) that are usually painful to get it running on a host machine. The test cases show how to get this main function running and how to handle this dependencies.
These hardware dependencies are mocked in tests/mocks/i2c_mock.cpp and the main test cases are implemented in tests/t_main.cpp.