Breaking Hardware Dependency with CMock
April 11, 2020
This is the third post in our Test-Driven Development journey. Today, you’ll learn to test your code without hardware.
If you’ve ever said. “I need to run my code on hardware to make sure my code works”… Let me tell you a couple of things.
1) The hardware may not be ready or available whenever you need it.
2) The hardware may have also bugs. So you would deal with a bigger problem.
3) You don’t need the hardware as much as you think. You’ll see it in this post. Keep reading.
The concepts you’ll read here are from the book Test-driven development for embedded C. We highly recommend you read it. You can find great books in our curated booklist.
Understanding Hardware Dependency
What do you need to break hardware dependencies?
Nowadays is common to use libraries to interact with GPIOs (General-purpose input/output) and communication interfaces like I2C, SPI or ADCs. Just to mention some.
Look at the picture to have a better idea:
Let’s say you want to use I2C bus available in your microcontroller.
In this case, the SDA is P1_2 (pin 1, port2) and SCL is P1_4.
You don’t write code that directly interacts with those SDA, SCL and I2C protocol.
That’s the job of the library i2c_adapter.h
.
So basically you’ll interact only with the library. Do not forget.
Libraries Are Software!
In the end, you only have to know what are the inputs the library wants. And the output you expect the library gives to you.
What’s CMock?
CMock is a tool from ThrowTheSwitch.org that can help you mocking or emulating libraries’ behaviors.
In that way, you won’t need the hardware to test your code. If you check the next picture.
When you run your tests you won’t use the original library. Instead you will use the mock version (dashed lines).
CMock comes with Ceedling. If you want to know more about Ceedling and TDD. Read our last post. Test-Driven Development (TDD) with ceedling
CMock Tutorial
This is a follow-along tutorial. ExampleCode
is the base code to start with. You can find the code source here.
The project structure is the following:
.
├── ceedling
├── code
│ ├── config
│ ├── include
│ ├── main.c
│ ├── src
│ └── test
├── project.yml
└── vendor
ceedling
is the building system. You need it to build and run the tests.code
is where your sources, includes and testing files are.project.yml
is ceedling configuration filevendor
is ceelding source files. This folder includesCMock
.
Module Overview
We are going to develop a part of the Si7060 temperature sensor driver, which uses I2C communication protocol.
The library we will use is i2c_adapter.h
from dialog microcontrollers. DA14681 microcontroller will run eventually the temperature driver.
For this post we need to create 5 files. The files and their relations are in the following picture.
Just remember this picture. It’s going to make more sense as we progress through this post.
Temperature Sensor Driver
The driver for the temperature sensor should do the following:
- Initialize the sensor
- Create a OS task
- Send an OS event to do a temperature measurement
- Convert the value of the registers to temperature values
- Read Si7060 registers
We’re going to focus on applying TDD to the last functionality. Because the goal is that you understand how to use CMock.
Reading Sensor Register
The temperature value is given in two registers. DSPSIGM
and DSPSIGL
. Dialog sdk has an I2C adapter that makes it easier to read registers. The adapter behavior is defined in ad_i2c.h
.
In our test file, we add the new test case, and the following headers:cmock.h
, mock_ad_i2c.h
.
cmock.h
is the mocking tool. mock_ad_i2c.h
is the mock version of ad_i2c.h
. The syntax of any mock header is mock_<HEADER_NAME>.h
.
/* ExampleCode/code/test/test_TemperatureDriver.c */
#include "unity.h"
#include "cmock.h"
#include "mock_ad_i2c.h"
#include "TemperatureDriver.h"
Very Important Note: you don’t need the original ad_i2c.h
library to work with CMock. You only need part of it. The interfaces of the functions you use. That’s why…
We need to create our ad_i2c.h
mock header that will replace the original ad_i2c.h
from Dialog sdk.
In other words, the mock header has to give you enough information to make your tests pass.
In our case, we only need 3 functions.
/* ExampleCode/code/test/mocks/ad_i2c.h */
#ifndef _AD_I2C_H
#define _AD_I2C_H
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
typedef uint16_t i2c_device;
i2c_device ad_i2c_open(i2c_device dev);
void ad_i2c_transact(i2c_device dev, const uint8_t *reg, size_t reg_size, uint8_t *res, size_t res_size );
void ad_i2c_close(i2c_device dev);
#endif /* _AD_I2C_H*/
CMock automagically generates stubs and mocks for Unity Tests. What does it mean?
It means that for each function defined in test/mocks/ad_i2c.h
, CMock
will generate interfaces that you could use in your testing.
For example, for i2c_device ad_i2c_open(i2c_device dev)
function prototype.
CMock will generate a bunch of functions like the ones below that you can use in your testing.
void ad_i2c_open_ExpectAndReturn(uint16_t dev, uint16_t toReturn);
void ad_i2c_open_IgnoreAndReturn(uint16_t toReturn);
You can find here all the functions CMock generates.
From Si7060 datasheet we expect our ReadTemperatureFromI2C
function to be like this:
/* ExampleCode/code/src/TemperatureDriver.c */
static const uint8_t SI7060_ID = 0xC0 ;// chipID | revID
static const uint8_t SI7060_DSPSIGM = 0xC1 ;// most significant bits temperature conversion
static const uint8_t SI7060_DSPSIGL = 0xC2 ;// least significant bits temperature conversion
STATIC int16_t ReadTemperatureFromI2C(void)
{
uint8_t _dspsigm, _dspsigl;
int16_t Temperature;
uint8_t _ret;
i2c_device i2c_dev;
i2c_dev = ad_i2c_open(SI7060);
ad_i2c_transact(i2c_dev, &SI7060_DSPSIGM, sizeof(SI7060_DSPSIGM), &_ret, sizeof(_ret) );
_dspsigm = _ret;
ad_i2c_transact(i2c_dev, &SI7060_DSPSIGL, sizeof(SI7060_DSPSIGL), &_ret, sizeof(_ret) );
_dspsigl = _ret;
ad_i2c_close(i2c_dev);
ReadSensorRegisters( &_dspsigm, &_dspsigl);
Temperature = ConvertTemperatureFromRegisters(_dspsigm, _dspsigl);
return Temperature;
}
And it’s header:
/* ExampleCode/include/project/TemperatureDriver.h */
#ifndef _TEMPERATURE_DRIVER_H
#define _TEMPERATURE_DRIVER_H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "def.h"
#include "ad_i2c.h"
STATIC int16_t ConvertTemperatureFromRegisters(uint8_t RegisterMostSignificantByte,
uint8_t RegisterLessSignificantByte);
STATIC int16_t ConcatenateTemperatureRegisters(uint8_t RegisterMostSignificantByte,
uint8_t RegisterLessSignificantByte);
STATIC int16_t ReadTemperatureFromI2C(void);
#endif /* _TEMPERATURE_DRIVER_H*/
Note: STATIC
is a macro defined in def.h
. For testing, functions should be public. But some functions for production need to be private. def.h
help us with that.
/* ExampleCode/code/include/project/def.h */
#ifndef _DEF_H_
#define _DEF_H_
#ifdef TEST
#define STATIC
#else
#define STATIC static
#endif
#endif /* _DEF_H_ */
Here comes the tricky part. How to use mock functions generated by CMock
in our tests?
The idea is that each mock function will simulate calls to external libraries. For example.
/* TemperatureDriver.c */
i2c_dev = ad_i2c_open(SI7060);
will be simulated by
/* test_TemperatureDriver.c */
ad_i2c_open_ExpectAndReturn(SI7060, dev);
Comments in the following code are reminders of the original implementation.
/* test_TemperatureDriver.c */
#include "cmock.h"
#include "mock_ad_i2c.h"
void test_ReadTemperatureFromI2C(void)
{
uint16_t Temperature = 0;
const uint8_t msByte, lsByte;
uint16_t dev = 1;
uint8_t e_m = 0;
uint8_t e_l = 0;
/* i2c_dev = ad_i2c_open(SI7060); */
ad_i2c_open_ExpectAndReturn(SI7060, dev);
/* ad_i2c_transact(i2c_dev, &SI7060_DSPSIGM, sizeof(SI7060_DSPSIGM), &_ret, sizeof(_ret) ); */
ad_i2c_transact_Expect(dev, &msByte, sizeof(msByte), &e_m, sizeof(e_m));
ad_i2c_transact_IgnoreArg_reg();
ad_i2c_transact_IgnoreArg_res();
ad_i2c_transact_ReturnThruPtr_res(&e_m);
/* ad_i2c_transact(i2c_dev, &SI7060_DSPSIGL, sizeof(SI7060_DSPSIGL), &_ret, sizeof(_ret) ); */
ad_i2c_transact_Expect(dev, &msByte, sizeof(msByte), &e_l, sizeof(e_l));
ad_i2c_transact_IgnoreArg_reg();
ad_i2c_transact_IgnoreArg_res();
ad_i2c_transact_ReturnThruPtr_res(&e_l);
/* ad_i2c_close(i2c_dev); */
ad_i2c_close_Expect(dev);
Temperature = ReadTemperatureFromI2C();
TEST_ASSERT_EQUAL_INT16(-47, Temperature);
}
Note: I show first the C implementation, just for educational purposes. As possible try to follow TDD workflow. Write test. Test fails. Write code. Check all tests are OK. Refactoring. And Repeat.
Very Important Note
CMock
is not perfect. That’s why to mock functions like void func(int* a)
, you need to use the following sequence:
int value;
func_Expect(&value);
func_IgnoreArg_a();
func_ReturnThruPtr_a(&value);
We will also to mock another header platform_devices.h
. Because the symbol SI7060
is defined there. Do not forget to add that header to TemperatureDriver.h
and its mock version to test_TemperatureDriver.c
.
/* ExampleCode/code/test/mocks/platform_devices.h */
#ifndef _PLATFORM_DEVICES_H_
#define _PLATFORM_DEVICES_H_
#define SI7060 1
#endif /* _PLATFORM_DEVICES_H_ */
/* ExampleCode/code/include/TemperatureDriver.h */
#include <platform_devices.h>
/* ExampleCode/code/test/test_TemperatureDriver.c */
#include "mock_platform_devices.h"
Let’s build our code ./ceedling test:TemperatureDriver
.
Test 'test_TemperatureDriver.c'
-------------------------------
Generating include list for ad_i2c.h...
Generating include list for platform_devices.h...
Creating mock for ad_i2c...
Creating mock for platform_devices...
WARNING: No function prototypes found!
Generating runner for test_TemperatureDriver.c...
Compiling test_TemperatureDriver_runner.c...
Compiling test_TemperatureDriver.c...
Compiling mock_ad_i2c.c...
Compiling mock_platform_devices.c...
Compiling unity.c...
Compiling cmock.c...
Compiling TemperatureDriver.c...
Linking test_TemperatureDriver.out...
Running test_TemperatureDriver.out...
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 3
PASSED: 3
FAILED: 0
IGNORED: 0
In this post, you worked only with one test. You can find the full tests in the source code.
Summary
Now you know the basic idea behind TDD and have a basic understanding of Unity
and CMock
. This approach is not perfect. But it increases our odds of writing testable code. And most importantly, easy to adapt and understand by computers and humans.
But. If TDD helps you to develop better code in less time. Why don’t use it?
Get one copy Test-driven development for embedded C on Amazon.
Share it!
Comments powered by Talkyard.