Saturday, June 30, 2012

Unity TDD with MPLAB C18

I'm lucky enough to be involved in a project at work that requires some microcontroller development. Being a convert to Test Driven Development for enterprise-style code, I was keen to take advantage of a testing framework for my embedded code. I bought James Grenning's book Test-Driven Development for Embedded C to get myself started. The Unity framework seems to be just what I need.

I chose the Microchip PIC family of microcontrollers because they appear to be very popular, flexible, easy to program, there are evaluation boards, and there are so many different variants that it should be possible to find just the right one for any occasion. The MPLAB IDE doesn't come close to the IntelliJ or Eclipse benchmarks, but I'm comfortable doing TDD in a simple text editor (Notepad++), so that's ok.

I downloaded Unity and installed the three source files into my MPLAB project. I wrote a simple test:

    #include "unity.h"

    void setUp(void) { }

    void tearDown(void) { }

    void test_demo()
    {
        TEST_ASSERT_EQUAL_INT(2,3);   
    }

and a simple test runner:

    #include "unity.h"
    void test_demo(void);

    void main(void)
    {
        UnityBegin();
        RUN_TEST(test_demo,1);
        UnityEnd();   
    }

Compiling showed that unity_internals.h was looking for a stdint.h header file, but not finding it. I had to define the constant UNITY_EXCLUDE_STDINT_H in my build. To do that, I used Project > Build Options > Project, and on the MPLAB C18 tab, clicked "Add..." to add it as a preprocessor macro.

The next problem was the lack of a putchar() function. I added this function to my test runner:
int putchar(int c)
{
    putc((char)c,stdout);
}

I selected the MPLAB debugger (Debugger > Select Tool > MPLAB Sim), and enabled the UART output tab (Debugger > Settings > Uart 1 IO > Enable, and choose Window for output). When I clicked Run, my SIM Uart1 window filled up with trash. I decided to check putc:

     void main(void)
    {
         putc('a',stdout);  // try this one
         while(1);
         UnityBegin();
         RUN_TEST(test_demo,1);
         UnityEnd();   
     }

Yes, when that runs, the SIM Uart1 window shows an 'a'. Each time I press reset, I get an extra 'a'. Try puts:

    void main(void)
    {
        putc('a',stdout);
        puts("abcde"); // try this one
        while(1);
        UnityBegin();
        RUN_TEST(test_demo,1);
        UnityEnd();   
    }

Yes, that works too. I looked into UnityBegin() (not much there) and then UnityEnd(). That starts with a UnityPrint. Let's try that one.

    void main(void)
    {
        putc('a',stdout);
        puts("abcde");
        UnityPrint("Does this print?");
        while(1);
        UnityBegin();
        RUN_TEST(test_demo,1);
        UnityEnd();   
    }

Looks like there's a problem with UnityPrint(). After some searching, I discovered (in the C18 C Compiler Getting Started manual) that the C18 compiler puts string constants in the code section, so they are const rom char*, rather than just const char *.

After a lot of experimentation, I decided that I needed to have two versions of the UnityPrint() function: one with a const rom char* parameter, and the original one with const char *. The two versions only differ in the signature and the first line:

    void UnityPrint(const char* string)
    {
        const char* pch = string;

    void UnityPrintRom(const rom char* string)
    {
        const rom char* pch = string;
   
When I changed my code to use UnityPrintRom, it worked. By the way, I don't really understand why it should help to have that apparently unused char c. It seems to be necessary to convince the compiler to use the right addressing.

Now I had to make Unity use UnityPrintRom at the appropriate times. I did this by two global search and replaces in unity.c. The first was to change all occurrences of UnityPrint(" to UnityPrintRom(" (there were 8 of these) and the second was to change UnityPrint(UnityStr to UnityPrintRom(UnityStr (there were 50 of these). The last two to change were UnityPrintRom(file) and UnityPrintRom(Unity.CurrentTestName). And I added a couple of prototypes into unity.h:

    void UnityPrintRom(const rom char* string);
    int putchar(int c);

Now, it works.

    testDemo.c:8:test_demo:FAIL: Expected 2 Was 3
    -----------------------
    1 Tests 1 Failures 0 Ignored
    FAIL

And when I fix up the assertion, I get:

    testDemo.c:1:test_demo:PASS
    -----------------------
    1 Tests 0 Failures 0 Ignored
    OK

The last step was to surround these changes with some #ifdef UNITY_MPLAB directives. The resulting code is now in a fork of the original repository at https://github.com/johnyesberg/Unity.