Unit testing with flash (EEPROM)

Do your embedded applications ever save any data to flash memory (aka EEPROM)? This where you typically store non-volatile information that needs to preserved if the device is powered down.

This sort of thing is tough to test in the traditional way -- by loading code onto the target and running it -- because it's hard to set and re-set the data in flash for testing. It can also be harder to inspect the data when it's in flash memory.

And... if you're implementing something more complicated (e.g. a wear-leveling algorithm) it's really hard to test all of the logic paths.

Code that interacts with flash memory like this is a great candidate for host-based unit testing. The strategy here is to isolate this code from the hardware by mocking the flash interface and simulating the flash with RAM. This approach is great because it:

  1. Usually isn't that hard to do
  2. Saves a ton of manual testing
  3. Prevents lots of hard-to-find bugs from escaping into the field

Hmm... what if it's possible to store some data in flash that causes the device initialization to fail, or otherwise makes it useless? Congratulations... your customer now has a shiny, new brick. In this case the flash memory contains application state that isn't resettable -- even rebooting the device to fix it.

We don't want this to happen.

Mocking the flash interface

The goal of this testing is to make it easier to simulate the state of the flash so we can more easily test lots of different situations.

Is pretty straightforward when you have read and write style functions for accessing the flash, maybe implemented in a flash module like this:

In this example the application modules access the flash through the read and write functions in the flash module.

To test this on the host without using any real target flash you can mock the flash module -- providing alternate implementations for read and write that access RAM on the host instead of flash on the target.

This is simple enough to do by creating a mock flash source file to link into our test (instead of the real flash module), but it's even easier to do right inside the test file with CMock and Ceedling.

An example

Suppose we have a config module responsible for loading and storing some non-volatile configuration data in flash. It uses the the flash module to read and write data to flash, looking something like this:

If the flash interface defined in flash.h looks like this:

#include <stdint.h>

void flash_read(uint8_t* dest_buffer, uint8_t* source_address, size_t count);
void flash_write(uint8_t* source_data, uint8_t* target_address, size_t count);

We can create a test file to test the config module by including mock_flash.h. We create an array of bytes in RAM that will serve as the simulated flash and create mocks for the read and write functions that use this RAM. To get our mocks to be used in every test, we register them with CMock StubWithCallback calls in the setUp function.

#include "unity.h"
#include "config.h"
#include "mock_flash.h" // Mock the flash interface.
#include <stdint.h>
#include <string.h>

#define FLASH_SIZE 4096
uint8_t simulated_flash[FLASH_SIZE];

static void mock_read(uint8_t* dest_buffer, uint8_t* source_address,
  size_t count, int num_calls)
{
  memcpy(dest_buffer, &simulated_flash[(int)source_address], count);
}

static void mock_write(uint8_t* source_data, uint8_t* target_address,
  size_t count, int num_calls)
{
  memcpy(&simulated_flash[(int)target_address], source_data, count);
}

void setUp(void)
{
  flash_read_StubWithCallback(mock_read);
  flash_write_StubWithCallback(mock_write);
}

void tearDown(void)
{
}

// Add tests here.

The mock_read copies bytes out of simulated_flash and the mock_write copies them in. This implementation assumes that flash addresses start a 0, but you could easily add an offset to your read and write functions if that's not the case.

The flash size here is a reasonable size of 0x40000 (256 kB). If your flash is really big -- like into the megabytes -- your host might complain when trying to allocate that much memory to run your tests.

Also note that the mock callback functions take the additional int num_calls argument -- this is a CMock convention that allows us to keep track of the number of times that the mock was called. We don't use it here, but the callback signature needs to include it.

We can write a simple test to write and read a single byte from flash like this:

void test_write_read(void)
{
  uint8_t write_data = 0x88;
  uint8_t* flash_address = (uint8_t*)0;
  flash_write(&write_data, flash_address, 1);

  uint8_t read_data = 0;
  flash_read(&read_data, flash_address, 1);
  TEST_ASSERT_EQUAL(write_data, read_data);
}

If we want test config module functions like config_load -- which loads a configuration from flash -- we can do this too:

void test_when_flash_is_erased_then_the_config_load_count_is_reset(void)
{
  // Erase the flash.
  memset(simulated_flash, 0xff, FLASH_SIZE);

  // Load the config.
  config_t config = {0};
  config_load(&config);

  // Verify the load_count is reset to 1.
  TEST_ASSERT_EQUAL_HEX(1, config.load_count);
}

The important thing to note here is that before we call config_load we set up the initial state of the "flash" by writing directly into simulated_flash. In this case we're setting all the bytes to 0xff to simulate a completely erased state, but we could easily write in any data that we want.

And since we have the entire flash memory simulated we can test complicated interactions with the flash -- like a wear-leveling algorithm that reads and writes many records across the entire flash memory.

Some handy error checking

Hey, remember when we created our mock read and write functions a minute ago? We blindly wrote to or read from whatever address we got. This is pretty risky though. Accessing an invalid flash address is just not going to work at best -- and cause our application to crash at worst.

We can add error checking into our mock functions so that our tests will fail on an invalid flash memory access. Here's what the mock_write function looks like with this error handling:

static void mock_read(uint8_t* dest_buffer, uint8_t* source_address,
  size_t count, int num_calls)
{
  if (source_address >= (uint8_t*)FLASH_SIZE)
  {
    TEST_FAIL_MESSAGE("invalid flash memory address");
  }
  memcpy(dest_buffer, &simulated_flash[(int)source_address], count);
}

In this case if the config module ever attempts to write to an invalid flash address during one of our unit tests, then the test will fail immediately.

Sometimes flash can only be written in certain block sizes -- like words or pages. If this is the case, you can test for this sort of thing in your mock read and write functions too.