So you are pretty much into Test-Driven Development (TDD)? You love to practice it on your development machine but think it might be quite a hassle when it comes to writing code for small microcontrollers?

In this blog article we are going to take a journey to Embedded TDD using the MCP2515 Controller Area Network (CAN) controller as an example. We will begin to write a device driver for that particular controller without even having any working hardware at hand....

Of course, eventually in another part of this series we will compile our code for the target platform, upload it and see if it really works.

What we need

For the first part of this series we will stick to our host development system so there's no additional hardware necessary. This is what you need to follow along:

  • A working C++11 toolchain (let's say gcc)
  • GNU make
  • A text editor or C++ IDE
  • MCP2515 Datasheet

Don't worry about setting up the tools just yet. You will find a link to a make-based project at the end of the article.

Abstraction of the SPI interface

The communication between CAN controller and microcontroller (uC) is based on SPI where the uC acts as the bus master. So let's create a C++ class that represents the logical point-to-point connection between these two devices.

class SpiDriver {
public:
	virtual ~SpiDriver() {}
	virtual void select() = 0;
	virtual void deselect() = 0;
	virtual uint8_t read() = 0;
	virtual void write(uint8_t value) = 0;
};

This is an abstract base class that defines how to access a particular SPI connection. Later, we may provide both a test implementation we utilize during unit testing and a real one that does the actual work on the target platform.

Desired behavior: defined by the data sheet

In chapter 12 of the MCP2515 data sheet, the manufacturer defines a set of SPI instructions the chip supports. The first one, the RESET instruction, shall be taken as an example for now. According to the document, the effect of this instruction is equivalent to pulling the reset pin of the controller.

We want a class that represents the core functionality of the MCP and provides a method - among others - called reset(), corresponding to that particular core instruction of the device. Calling reset() should yield an SPI write of one byte, 0xC0. The Chip Select line (low-active, nCS) should be low during that write and high otherwise.

Test harness preparation

To get started, let's prepare the SpiDriver implementation we are going to use for unit testing.

There are several possibilities on how to do that, though. We may create a simple spy, whose state is evaluated after exercising the code under test. Another approach is to use a mock: before exercising the code under test, the mock is configured to listen for specific method calls that follow. Afterwards, the test framework checks if all these expectations are met. That way not the state of the SpiDriver is evaluated but the actions that are executed on it. More about the difference between mocks and spies can be found in this article by Martin Fowler: Mocks Aren't Stubs.

For this particular scenario the mock might be a good fit, because the order and exact number of SPI actions executed is important to the behavior we'd like to achieve. Especially the synchronization between select() and write() is crucial.

This is the mock implementation of SpiDriver in GoogleMock syntax:

class MockSpiDriver : public SpiDriver {
public:
	MOCK_METHOD0(select, void());
	MOCK_METHOD0(deselect, void());
	MOCK_METHOD0(read, uint8_t());
	MOCK_METHOD1(write, void(uint8_t));
};

Additionally, we create a test fixture where we will put all of our tests:

class Mcp2515CoreTest : public Test {
};

Implementation

Ok, let's dive into the nano cycle 1 of TDD by adding the first failing test:

TEST_F(Mcp2515CoreTest, construction) {
	Mcp2515Core mcp;
}

We can fix this by creating an empty class definition:

class Mcp2515Core {
};

Add a dependency to SpiDriver:

TEST_F(Mcp2515CoreTest, construction) {
	MockSpiDriver spiDriver;
	Mcp2515Core mcp{spiDriver};
}

Add this to the code under test:

class Mcp2515Core {
public:
	Mcp2515Core(SpiDriver& spiDriver) {
	}
};

Enforce the reset() method:

TEST_F(Mcp2515CoreTest, reset) {
	MockSpiDriver spiDriver;
	Mcp2515Core mcp{spiDriver};
	mcp.reset();
}

Implement it:

class Mcp2515Core {
public:
	Mcp2515Core(SpiDriver& spiDriver) {
	}

	void reset() {
	}
};

Let's see if the nCS line is pulled low and then high:

TEST_F(Mcp2515CoreTest, reset) {
	MockSpiDriver spiDriver;
	Mcp2515Core mcp{spiDriver};

	InSequence inSequence;
	EXPECT_CALL(spiDriver, select());
	EXPECT_CALL(spiDriver, deselect());

	mcp.reset();
}

Implement this one:

class Mcp2515Core {
public:
	Mcp2515Core(SpiDriver& spiDriver) :
		spiDriver(spiDriver) {
	}

	void reset() {
		spiDriver.select();
		spiDriver.deselect();
	}

private:
	SpiDriver& spiDriver;
};

Now ask for the actual data byte:

TEST_F(Mcp2515CoreTest, reset) {
	MockSpiDriver spiDriver;
	Mcp2515Core mcp{spiDriver};

	InSequence inSequence;
	EXPECT_CALL(spiDriver, select());
	EXPECT_CALL(spiDriver, write(0xC0));
	EXPECT_CALL(spiDriver, deselect());

	mcp.reset();
}

Transmit it:

class Mcp2515Core {
public:
	Mcp2515Core(SpiDriver& spiDriver) :
		spiDriver(spiDriver) {
	}

	void reset() {
		spiDriver.select();
		spiDriver.write(0xC0);
		spiDriver.deselect();
	}

private:
	SpiDriver& spiDriver;
};

That's it for our first core instruction.

To be fair, the reset instruction is by far the simplest one to be found in the specification. Eventually we'll end up with an Mcp2515Core class that implements all the core instructions according to the data sheet definitions. It will represent the descriptions, tables and figures in chapter 12.

Conclusion

The class hierarchy created so far is summarized in the following diagram. It shows that our production class Mcp2515Core depends only on the interface to SPI we established at the beginning.

What else did we achieve?

No hardware needed so far. This is great, because debugging on real hardware takes time. And not having hardware means not to be coupled too much to it.

Specification written in executable form, i.e. data sheet translated into test code.

Where to go from here

We utilized C++ polymorphism to decouple SPI hardware interaction from the actual device driver behavior. Sadly, this doesn't come at no cost. Each and every call to reset() will have a runtime overhead for calling that virtual method via the base class reference.
An evaluation of that cost needs to be done, also with having in mind other core instructions like read and write. They are going to be needed far more often than reset during typical operation.

How about an actual SPI driver by the way? Real hardware is always interesting. So as one of the next steps, another SpiDriver implementation is to be created and compiled for the target board. We can then plug it into an Mcp2515Core instance and inspect actual traffic on the SPI lines.

Please let me know what you think about this first article on my blog via twitter @ronalterde!

Sample code

You can get the code at github.com/ronalterde/mcp-tdd-part1. Feel free to add the implementation of the various instructions I have not provided yet.

References

Robert C. Martin: The Cycles of TDD

Martin Fowler: Mocks Aren't Stubs