STM32/ARM Cortex-M3 HOWTO: Development under Ubuntu (Debian)

Unity

The last example showed how a basic test suit can be created quite easily, but if we do it that way it does not really scale. So let's have a look at a test framework called Unity.

Before I start I would like to mention that the Unity homepage is a little bit cryptic on how to use it, but there is a nice book that I recommend that you read that is called Test Driven Development for Embedded C by James W. Grenning. Please note that they sell this book as eBook as well as the classic paper book, and both the pdf and ePub works on my iRiver Story HD (my e-Ink eBook reader). This book goes thru TDD on embedded devices from the ground up.

When we continue, please remember that half the magic is done in the Makefile and the rest is done by the framework. So this page is as much about make as it is about Unity.

Makefile

The first problem we had before was that the main Makefile had to know about all the different test:s, and therefore must grow with each added test and would therefor become quite unreadable quite fast.

One solution to this problem is simply to make sure that each test dir contains the information needed to build the stuff that is in that dir. More or less split the main Makefile into many different small ones, and have the main Makefile include the other when he needs them.

Let's say that we would like to build test01. So we include a makefile in the test01 dir that we call make.mk. One thing is that we define this name our self, but it is a good idea to use some fileextention like .mk so we know that it is a makefile. We could call this test.mk but in this case I just call it make.mk.

TEST := test01
include $(TEST)/make.mk

Then we must make sure that we have some variables that the sub makefile can manipulate so the main makefile gets the information. More or less the main Makefile has a couple of global variables, that everybody can play with. In this case I will have a list with objects that the linker needs in a variable called OBJ. We also need to tell the system where the headerfiles can be found, so we can modify CFLAGS.

So let's have a look at the linker part in the main Makefile, that depends on all the different objects in OBJ.

OBJ = main.o  
main.elf: $(LINKFILE) $(OBJ) 
    @ echo "..linking"
    $(LD) $(LFLAGS) -o $@ -Xlinker -Map=main.map $(OBJ)

If we then have a look at the sub makefile that we have call test01/make.mk, we can see that it has two parts. The first part modifies the global flags with information about this test dir, and the second part tells us how to build the files that we find in this dir.

#Add this dir
CFLAGS  += -Itest01/

#Include this obj
OBJ += test_main.o

test_main.o: test01/test_main.c 
    @ echo ".compiling"
    $(CC) $(CFLAGS) -o $@ $<

The second big problem is that it would be nice to be able to build for pc or for the stm32. More or less what target we build for.

A easy way is to create 2 new makefile in the project root, right next to the main Makefile, that we call make_pc.mk and make_target.mk. In make_pc.mk we put things we need when we build for pc, and in make_target.mk we put things we need to build for the stm32. Then we include the one we need.

Let's begin with how to build for stm32. At the top we always put variables like CC and LD that tells us what gcc to use, and for stm32 that is arm-none-eabi-gcc. But there is also some target specific files that we only need on the target, things like the startup and linker script. It could look a little bit like this.

CC      = arm-none-eabi-gcc
LD      = arm-none-eabi-gcc 
AR      = arm-none-eabi-ar
AS      = arm-none-eabi-as
CP      = arm-none-eabi-objcopy
OD      = arm-none-eabi-objdump

MCUFLAGS = -mcpu=cortex-m3 -mthumb 
DEBUGFLAGS = -O0 -g

CFLAGS  = -Wall -Wextra -I./ -c -fno-common $(DEBUGFLAGS) $(MCUFLAGS) -mfix-cortex-m3-ldrd
AFLAGS  = -ahls $(MCUFLAGS) 
LINKFILE = src/stm32.ld
LFLAGS  = -T$(LINKFILE) -nostartfiles $(MCUFLAGS) -mfix-cortex-m3-ldrd

CPFLAGS = -Obinary
ODFLAGS = -S

OBJ += nvic.o 
nvic.o: src/nvic.c src/main.c 
   @ echo ".compiling"
   $(CC) $(CFLAGS) -o $@ $<
   $(OD) $(ODFLAGS) $@ > nvic.lst

For the pc we can remove most things and we only need a small subset.

CC      = gcc
LD      = gcc 
AR      = ar
AS      = as
CP      = objcopy
OD      = objdump

MCUFLAGS = 
DEBUGFLAGS = -O0 -g

CFLAGS  = -Wall -Wextra -I./ -c $(DEBUGFLAGS) $(MCUFLAGS) -DDEBUG_QUIT 
#-DUNITY_OUTPUT_PRINT_RING
AFLAGS  = -ahls $(MCUFLAGS) 
LFLAGS  = $(MCUFLAGS) 

CPFLAGS = -Obinary
ODFLAGS = -S

Then we include the file we need with something like this.

TARGET = target
#TARGET = pc
include make_$(TARGET).mk

And we use it from the command line like this.

$> make TARGET=pc TEST=test01
... build pc version ...

$> make TARGET=target TEST=test01
... build for stm32 ...

But please not that if the variable has value that we don't modify it acts like a default selection, and since we have target and test01 as default we only need to type make if we want to build the stm32 version of test01.

Filetree

In order to see the structure at the point we need to have a look at the current filetree so we can see where the different files lives and where new ones will be placed.

In the project root we find the main Makefile and the included makefile that tells us how to build to stm32 or for the pc.

./Makefile
./make_pc.mk
./make_target.mk

Then we put main.c in a dir called src, if you like you could split this dir in two and put the headerfiles in a dir called inc. But I don't see any point of doing so in this case. In the main src dir I also put the linker information, but since it is the main code I let the main Makefile have the information on how to build this code.

./src/main.c
./src/main.h

./src/nvic.c
./src/nvic.h
./src/syscalls.c

./src/STM32F103RB.memory
./src/stm32.ld

Then there is the two test dirs that has the it's own makefile that knows how to build to code. In this case to simplify the include the makefile has the same name in both cases.

./test01/make.mk
./test01/test_main.c
./test01/test_main.h

./test02/make.mk
./test02/test_main.c
./test02/test_main.h

And to make things happen we have a dir with external stuff, and I call it vendor in this case but another common name for this dir is external. One magic thing is that sometimes this code is handled in a separate repository and called with technologies like git submodule (but in this example I don't use it).

# The Unity framework is put in a dir called vendor
./vendor/unity/make.mk
./vendor/unity/unity.c
./vendor/unity/unity_fixture.c
./vendor/unity/unity_fixture.h
./vendor/unity/unity_fixture_internals.h
./vendor/unity/unity_fixture_malloc_overrides.h
./vendor/unity/unity.h
./vendor/unity/unity_internals.h
# Then the printbuffer so we don't have to use a uart/usb
./print_ring/make.mk
./print_ring/print_ring.c
./print_ring/print_ring.h

Move to unity

Now we have a little bit more flexible structure for the build, then let's see how the test can become a little bit more flexible as well.

The heart of the test is a number of macros used to compare different things, and then act on the result. If we would like to compare a int we could use the TEST_ASSERT_EQUAL_INT macro.

TEST_ASSERT_NOT_EQUAL(0, -1);
TEST_ASSERT_EQUAL_INT(1, 1);

Let's update the old simple integer test to use Unity instead.

First we have the old code.

int start_test()
{
    int test = 1;

    int a = 0;
    int b = 0;
    int c = 0;

    a = 10;
    b = 10;

    c = a * b;
    if (c != 100) {
        return test;
    }

    test++;

    c = a / b;
    if (c != 1) {
        return test;
    }
    return 0;
}

And then the updated unity example.

TEST(GroupName, BasicInt)
{
    int a = 0;
    int b = 0;

    a = 10;
    b = 10;

    TEST_ASSERT_EQUAL_INT( (a*b), 100);
    TEST_ASSERT_EQUAL_INT( (a/b),   1);
}

As you can see all the if:s is now in the test macros, and the code is smaller and more readable. And please remember that this is just in the most basic example, if we start to use it for some real TDD the difference would be even bigger.

Funtech test

Just as before the real code can be found in the github project, FunTechCortexMX_test. But I will go over the code right here and describe what is going on.

The test code with Unity

And we can start with main, and here we call UnityMain that starts the actual tests. As one of the arguments to UnityMain we tell him to call runAllTests.

Filename: src/main.c

#include "main.h"
#include "test_main.h"
#include "unity_fixture.h"
#include "print_ring.h"

#ifdef DEBUG_QUIT
#include "stdio.h"
#include "stdlib.h"
#endif

void test_failed() {

#ifdef DEBUG_QUIT
    printf("Test failed\n");
    exit(1);
#endif

    while(1);
}

void test_success() {

#ifdef DEBUG_QUIT
    printf("Test ok\n");
    exit(0);
#endif

    while(1);
}


int main(int argc, char * argv[])
{
    reset_buffer();
    return UnityMain(argc, argv, runAllTests);

    while(1);
}

Filename: src/main.h

#ifndef  __MAIN_H
#define  __MAIN_H


int main(int argc, char * argv[]);

void test_success();
void test_failed();

#endif  // __MAIN_H 

Then we can wait a little bit with the steps inside Unity and have a look at test01 where I placed the function runAllTests.

Filename: test01/test_main.c

//test harness include
#include "unity_fixture.h"

#include "test_main.h"

TEST_GROUP(GroupName);

//Define file scope data accessible to test group members prior to TEST_SETUP.
TEST_SETUP(GroupName)
{
    //initialization steps are executed before each TEST
}
TEST_TEAR_DOWN(GroupName)
{
    //clean up steps are executed after each TEST
}

/**
 * Test that normal ints work on the system.
 */
TEST(GroupName, BasicInt)
{
    int a = 0;
    int b = 0;

    a = 10;
    b = 10;

    TEST_ASSERT_EQUAL_INT( (a*b), 100);
    TEST_ASSERT_EQUAL_INT( (a/b),   1);
}

//Each group has a TEST_GROUP_RUNNER
TEST_GROUP_RUNNER(GroupName)
{
    //Each TEST has a corresponding RUN_TEST_CASE
    RUN_TEST_CASE(GroupName, BasicInt);
}

void runAllTests()
{
    RUN_TEST_GROUP(GroupName);
}

And here is when we see the big difference, since we have no clear flow in this file. It is easy to get the flow since Unity puts a setup function before and a teardown function after each test, so we can have a common init for all the tests. That kind of gives us this flow in the test file.

Fatal error: Uncaught Error: Undefined constant "RUN_TEST_GROUP" in /customers/9/b/5/fun-tech.se/httpd.www/stm32/TestSuite/unity.php:406 Stack trace: #0 {main} thrown in /customers/9/b/5/fun-tech.se/httpd.www/stm32/TestSuite/unity.php on line 406