Who says you can’t test drive a device driver?

I keep hearing that you can’t write unit tests for device drivers. I don’t believe that’s true. To disprove this claim, I thought I would find a device driver and write some unit tests for it. This blog posting shows what device driver unit tests look line.


I found a flash memory driver at the
ST site.

I grabbed the driver for the AN2414, and took a look at it. It seems pretty simple. Its made of two files: 2414.h and 2414.c

After a few minutes of looking at the source code I discovered that the driver’s designer did a nice job by making all access to the flash device go through two functions: FlashRead() and FlashWrite(). I like that, just having two access points to intercept data to/from the hardware. The driver compiled without a problem using gcc.

Objective of unit testing a device driver
1) I want to test a device driver for an embedded system on the development system, using a hardware fake. Allowing driver development and testing before hardware is ready.

2) I want to have some unit tests that I can run on the target hardware to make sure the flash is working properly.

If possible I’d like to use the same tests in the host and the target where possible.

The First Test
The first test will test the driver’s ability to report if the flash memory supports the Common Flash Interface (CFI). If this test passes in target hardware, its fairly clear that the hardware is responding. Random chance would not yield the necessary results as you will see. When a 0x98 is written to flash address 0x55 then the driver will make sure that at locations 0x10, 0x11, and 0x12 contain ‘Q’, ‘R’, and ‘Y’ respectively. Its unlikely this would happen by coincidence.

The ST 2414 driver implements the CFI query in this function:

ReturnType FlashReadCfi( uword uwCfiFunc, 
                         uCPUBusType *ucpCfiValue ) {
    ReturnType rRetVal = Flash_Success;
    udword udCfiAddr;
 
    /* Step 1: Send the Read CFI Instruction */
    FlashWrite( ANY_ADDR, CMD(0x0098) );
 
    /* Step 2: Check that the CFI interface is operable */
    if ((FlashRead( 0x00000010 )  != CMD(0x0051)) ||
        (FlashRead( 0x00000011 )  != CMD(0x0052)) ||
        (FlashRead( 0x00000012 )  != CMD(0x0059)) )
      rRetVal = Flash_CfiFailed;
    else {
      /* Step 3: Read the required CFI Info */
      udCfiAddr = (udword)uwCfiFunc;
      *ucpCfiValue = FlashRead( udCfiAddr & 0x000000FF );
      } /* Endif */
 
    /* Step 4: Return to Read Array mode */
    FlashReset();
 
    return rRetVal;
} /* EndFunction FlashReadCfi */

A little refactoring for testability
Because there is only one source file, there is a small testability problem. FlashRead() and FlashWrite() interact with the device and the tests run on the development system can’t have that. We need to intercept calls to FlashRead() and FlashWrite().

To make the driver testable I’ll extract the FlashRead() and FlashWrite() functions into their own C file. Then I’ll make a fake version of them for test purposes. I’ll link the fake version for development system based tests and the real version for testing with the real hardware.

The first test case looks like this. Its not 100% complete, but it’s a start and it checks our current understanding
of the device. Specifically it checks that after a 0x98 is written to 0x55, location 0x10 contains “QRY” and when that is true FlashReadCfi() returns Flash_Success. Oh yeah, FlashReadCfi() is accessed through Flash(ReadCfi, &p) interface.

TEST(Flash, CheckCfiCommand)
{
    ParameterType p;
    ReturnType result;
    Reset_FlashRead_and_FlashWrite();
 
    result = Flash(ReadCfi, &p);
 
    LONGS_EQUAL(Flash_Success, result);
}

The second fake versions of FlashRead() and FlashWrite() look like this. I skipped showing you the first fake version because it had only enough code to make the compiler and linker happy.

static unsigned char flashQueryStructure[0x40] = {0};
 
void Reset_FlashRead_and_FlashWrite() {
    memset(flashQueryStructure, 0, sizeof(flashQueryStructure));
}
 
uCPUBusType FlashRead( udword offset ) {
    return flashQueryStructure[offset];
}
 
void FlashWrite( udword offset, uCPUBusType value ) {
    if (offset == 0x55 && value == 0x98) {
        flashQueryStructure[0x10] = 'Q';
        flashQueryStructure[0x11] = 'R';
        flashQueryStructure[0x12] = 'Y';
    }
}

The test fails. What the heck! As it turns out the driver is not compliant with my understanding of CFI. I guess I could be wrong, but the three CFI specs I looked at seemed pretty clear. The ST 2414 is happy to enter CFI command mode when a 0x98 written to location 0x00. I change the fake to expect what the driver is giving and now tests pass.

Our first test is not very thorough and the fake is setup to only deal with the CFI Query command. There are 25 other commands, so we need a better fake. After looking over FlashReadCfi() and evolving the test I came up with a more sophisticated fake mechanism. Here’s the test:

TEST_GROUP(Flash)
{
    void setup()
    {
        Reset_FlashRead_and_FlashWrite();
    }
    void teardown()
    {
    }
};
 
TEST(Flash, CheckCfiCommand)
{
    ParameterType p;
    ReturnType result;
 
    p.ReadCfi.uwCfiFunc = 0x24;
 
    Expect_FlashWrite(0x0, 0x98); 
    Expect_FlashRead(0x10, 'Q'); 
    Expect_FlashRead(0x11, 'R'); 
    Expect_FlashRead(0x12, 'Y'); 
    Expect_FlashRead(p.ReadCfi.uwCfiFunc, 0xAA); 
    Expect_FlashWrite(0x0, 0xff); 
 
    result = Flash(ReadCfi, &p);
 
    LONGS_EQUAL(Flash_Success, result);
    LONGS_EQUAL(0xAA, p.ReadCfi.ucCfiValue);
    Check_FlashWrite_Expectations();
}

The Expect calls are telling the fake versions of FlashRead() and FlashWrite() what parameters to expect and in what order. In the first Expect_FlashWrite, the fake is being told that the first flash operation is a write to location 0x0 with value 0x98. In the first Expect_FlashRead, the fake is being told that the second flash operation should be a read from location 0x10 and that it should return a ‘Q’ in that case. If any of the expectations are not met, or not in the order the test dictates, the test will fail.

The call to Check_FlashWrite_Expectations() makes sure that there are no extra expectations left after running the Flash(ReadCfi, &p) command.

The fakes look like this:

#include <string.h>
#include "c2414.h"  
#include "CppUTest/TestHarness_c.h"  
 
#define FLASH_READ 1
#define FLASH_WRITE 2
 
typedef struct RwExpectation {
    int kind;
    udword addr;
    uCPUBusType value;
} RwExpectation;
 
static RwExpectation rwExpectations[20];
static int setExpectationCount;
static int getExpectationCount;
 
void Reset_FlashRead_and_FlashWrite() {
    memset(rwExpectations, 0, sizeof(rwExpectations));
    setExpectationCount = 0;
    getExpectationCount = 0;
}
 
void Expect_FlashWrite(udword addr, uCPUBusType value) {
    rwExpectations[setExpectationCount].kind = FLASH_WRITE;
    rwExpectations[setExpectationCount].addr = addr;
    rwExpectations[setExpectationCount].value = value;
    setExpectationCount++;
}
 
void Expect_FlashRead(udword addr, uCPUBusType value) {
    rwExpectations[setExpectationCount].kind = FLASH_READ;
    rwExpectations[setExpectationCount].addr = addr;
    rwExpectations[setExpectationCount].value = value;
    setExpectationCount++;
}
 
void checkExpectationCount() {
    if (getExpectationCount >= setExpectationCount) {
        FAIL_TEXT_C("Not enough expectations set");
    }
}
 
uCPUBusType FlashRead( udword addr ) {
    uCPUBusType result;
 
    checkExpectationCount();
    if (FLASH_READ != rwExpectations[getExpectationCount].kind)
        FAIL_TEXT_C("READ Expected");
 
    CHECK_EQUAL_C_INT(addr, rwExpectations[getExpectationCount].addr);
    result = rwExpectations[getExpectationCount].value;
    getExpectationCount++;
    return result;
} 
 
void FlashWrite( udword addr, uCPUBusType value ) {
 
    checkExpectationCount();
    if (FLASH_WRITE != rwExpectations[getExpectationCount].kind)
        FAIL_TEXT_C("Write Expected");
 
    CHECK_EQUAL_C_INT(addr, rwExpectations[getExpectationCount].addr);
    CHECK_EQUAL_C_INT(value, rwExpectations[getExpectationCount].value);
    getExpectationCount++;
}
 
void Check_FlashWrite_Expectations() {
    CHECK_EQUAL_C_INT(getExpectationCount, setExpectationCount);
}

The Expect calls are setting up an array of expected flash reads and writes. The fake FlashRead() and FlashWrite() functions are checking that the right addresses are being accessed.

Its a lot of code for testing a small functions, but with this fake in placewe are ready to add other test cases.

The title of this article is about test driving a driver, but we’re adding tests to an existing driver. You can see that this is a testable driver with very few modifications.

What makes me think the driver could have been test driven? Because the TEST(Flash, CheckCfiCommand) could have been written and tested without hardware right from the data book on the 2414.

I had a second objective: to run these tests on the actual hardware. If we made the Expect calls do nothing on the target (the hardware will fill the role of the fake) and we knew what value to really expect in this following assert we could run this test on the target.

  LONGS_EQUAL(0xAA, p.ReadCfi.ucCfiValue);</code>

I’ll add a few more tests for this driver later. Maybe you’d like to write one and post it.

3 thoughts on “Who says you can’t test drive a device driver?

  1. I confess, until I read this post, you would not have caught me dead trying to test-drive low-level stuff. But James gives me courage to take a couple of forays *WAY* outside my comfort zone. James, maybe you and I could try a little low-level driver TDD over beers at the next conference we are both at?

    Like I always say, Learning will Happen.

    Keep the great posts coming, dude! This is a horrendously under-served agile niched!

  2. Pingback: James Grenning’s Blog » Blog Archive » Why Test Driven Development for Embedded?

Leave a Reply

Your email address will not be published. Required fields are marked *

Be gone spammers * Time limit is exhausted. Please reload the CAPTCHA.