Mocking Embedded Hardware Interfaces with Ceedling and CMock

Hey -- this article is pretty long and technical. Get everything in here (and more) in my downloadable how-to guide.

Updated (September 9, 2017): Updated this post use ceedling commands instead of rake commands based on recent changes to Ceedling. See this post for more details.

Get the source code for this example on GitHub.

How can you unit test your embedded software? What about your hardware dependencies?

The secret is mocking.

We can mock the interfaces to our hardware so that we don't need the actual hardware to test. This allows us to run our tests more quickly and before the hardware might even be available.

The Plan

If we're developing the software for an embedded microcontroller, we're probably going to be using the microcontroller-provided hardware modules for things like SPI, I2C, timers, etc.

Application software interfaces to microcontroller hardware.

Application software interfaces to microcontroller hardware.

For each of these hardware interfaces, we want to have a corresponding software module containing the microcontroller hardware dependencies (i.e. hardware register accesses).

Each hardware interface has a corresponding software module.

Each hardware interface has a corresponding software module.

We can then mock each of these hardware interfaces, eliminating our hardware dependencies but still allowing us to unit test our application. Instead of compiling these tests for the embedded microcontroller, we compile them for and run them on our host PC.

Mocks for software modules allow us to isolate the application software from the hardware.

Mocks for software modules allow us to isolate the application software from the hardware.

To help you create your mocks you want to use a mocking framework. The mocking framework included with Ceedling is CMock. It allows you to create mocks of individual software modules from their header files. Ceedling improves the experience by automatically using CMock to generating the mocks that we need.

A Test Driven Example

Note that this example assumes that we already have an existing Ceedling project. See my other article for help creating one.

Imagine that we want to talk to an external I2C temperature sensor.

Microcontroller connected to external temperature sensor via I2C.

Microcontroller connected to external temperature sensor via I2C.

Create the Temperature Sensor Module

Let's create a module that will be our temperature sensor driver.

$ ceedling module:create[tempSensor]
Generating 'tempSensor'...
mkdir -p ./test/.
mkdir -p ./src/.
File ./test/./test_tempSensor.c created
File ./src/./tempSensor.c created
File ./src/./tempSensor.h created

Write Our First Test

What is the first thing I want to be able to do with this sensor? I'd like to be able to read the current temperature value.

Cool. So I take a look at the datasheet for my fictional temperature sensor and I can see that it has a bunch of 16-bit registers -- each with 8-bit addresses -- one of which is the temperature register.

The scaling of the values is such that a register value of 0 is -100.0°C and a register value of 0x3FF is +104.6°C. This makes each bit equivalent to 0.2°C.

The senor temperature register values and their corresponding temperatures.

The senor temperature register values and their corresponding temperatures.

Now lets add our first test to test_tempSensor.c. I want to know that when I read a temperature register value of 0x3FF that the temperature calcualted is 104.6.

void test_whenTempRegisterReadsMaxValue_thenTheTempIsTheMaxValue(void)
{
    uint8_t tempRegisterAddress = 0x03;
    float expectedTemperature = 104.6f;
    float tolerance = 0.1f;

    //When
    i2c_readRegister_ExpectAndReturn(tempRegisterAddress, 0x3ff);

    //Then
    float actualTemperature = tempSensor_getTemperature();
    TEST_ASSERT_FLOAT_WITHIN(tolerance, expectedTemperature, 
        actualTemperature);
}

First we set up some variables to hold our expected values. Then in the "when" clause, we need to simulate (or mock) the I2C module returning a value of 0x3ff on a read of the temperature address.

For the moment, we pretend that there is another i2c module (it doesn't actually exist yet) which handles the I2C communication with the temperature sensor. This is where our hardware dependent code will eventually go.

So, the i2c_readReadgister_ExpectAndReturn function is actually a mock function used to simulate a call to a function called i2c_readRegister in the i2c module. We'll come back to this in a moment.

The "then" clause is where we test that the tempSensor module actually returns the correct temperature when we call tempSensor_getTemperature. This function doesn't exist yet either.

Create the Function Under Test

Lets create the tempSensor_getTemperature function with a dummy implementation:

tempSensor.h:

# ifndef tempSensor_H
# define tempSensor_H

float tempSensor_getTemperature(void);

# endif // tempSensor_H

tempSensor.c:

# include "tempSensor.h"

float tempSensor_getTemperature(void)
{ 
    return 0.0f;
}

Mock the I2C Interface

If we try and run the test now, the compiler will complain that it doesn't know about the i2c_readReadgister_ExpectAndReturn mock function. This is because the i2c_readRegister function doesn't exist and we haven't yet told Ceedling to mock it.

We don't actually need to implement this function however. It's enough to declare the function prototype in a header file and tell Ceedling to mock it with CMock.

Create the header file, i2c.h:

# ifndef i2c_H
# define i2c_H

# include <stdint.h>

uint16_t i2c_readRegister(uint8_t registerAddress);

# endif // i2c_H

The way we tell Ceedling to mock this module is to add this line to test_tempSensor.c:

# include "mock_i2c.h"

This tells Ceedling: You know the i2c.h header you see over there? Well... use CMock to generate the implementation and compile it in for us, okay?

When CMock gets a hold of the header file it looks at all the functions defined there and generates several mock functions for each... including the i2c_readRegister_ExpectAndReturn function we used in the test. This mock function appends an additional argument to the original i2c_readRegister function, which is the value we want the function to return to the calling function.

For more details on all the mock functions available with CMock, see the CMock documentation.

Implement the Function Under Test

Now we can implement the logic for our tempSensor_getTemperature function. Our new tempSensor.c is:

# include "tempSensor.h"
# include "i2c.h"
# include <stdint.h>

float tempSensor_getTemperature(void)
{
    uint16_t rawValue = i2c_readRegister(0x03);

    return -100.0f + (0.2f * (float)rawValue);
}

If we run our test, it should pass now.

Adding Another Test

We'll next want to add more tests for other possible return values from i2c_readRegister. This is easily done by changing the return value provided to the mock function.

For example, to test that the minimum temperature value is read correctly:

void test_whenTempRegisterReadsMinValue_thenTheTempIsTheMinValue(void)
{
    uint8_t tempRegisterAddress = 0x03;
    float expectedTemperature = -100.0f;
    float tolerance = 0.1f;

    //When
    i2c_readRegister_ExpectAndReturn(tempRegisterAddress, 0x0);

    //Then
    float actualTemperature = tempSensor_getTemperature();
    TEST_ASSERT_FLOAT_WITHIN(tolerance, expectedTemperature,
        actualTemperature);
}

Now we have a driver for an external hardware device that we can test without any of the hardware. We can continue to develop the driver -- adding more tests and features -- by building and testing on our host PC.

By putting all of the microcontroller-dependent I2C operations into their own module, we easily mocked them with Ceedling and CMock. In fact, we didn't even have to implement this module yet -- we just had to define its interface in the header file.

Using our mocks, we created unit tests that verify the behavior of our temperature sensor driver. As the rest of our application is developed, we can easily run these unit tests at any time to make sure the driver will still work correctly.

Get the source code for this example on GitHub.

 
E-book

Save this for later

There's a ton of stuff in this article. If you want to save it to refer to later -- get everything in this here (and more) in my downloadable "how to" guide.

Sign up to get it here.