Unit Testing with AVR Studio and AVR-GCC

Overview

When developing embedded C applications for 8-bit AVR microcontrollers, I found the lack of a unit test framework troublesome. Especially as unit testing now forms a standard part of our C-sharp and Java development processes.

There are a number of C and C++ unit testing frameworks available but nothing geared up for running in the target environment of an 8-bit AVR.

The “framework” produced here is a lightweight starting point that can be enhanced to provide output in any form useful to the user. It is currently used in the AVR Simulator as part of the AVR Studio 5 environment, but there’s no reason why it couldn’t be run on the target device with test result output streamed over a USART connection.

Init levels

Adding the tests in JUnit etc allows you to scan the classes and run them all automatically. We can’t quite do the same in C. However, what we can do is to make use of the .init levels and have a function for each test automatically called on system start-up. These functions will then automatically add each test to the test suite.

AVR Memory Sections

Simulator

By running the tests in the AVR simulator, we can use the Watch tools to view the test results by putting breakpoints after each test is run, or after all tests have run. Monitor the m_Test_activeTest for the current test data, and m_Test_result for the final results.

The Code

test.h

#ifndef TEST_H
#define TEST_H

#include <inttypes.h>

// typedefs
typedef enum TestResult
{
	NOT_RUN = 0,
	SUCCESS,
	FAILURE
} TestResult;

typedef struct Test_TestHolder
{
	char * name;
	void (*testFunction)(void);
	char * file;

	int line;
	TestResult testResult;

	struct Test_TestHolder* next;
} Test_TestHolder;

// Initialise the test framework

void Test_add(Test_TestHolder* test);

void Test_assertTrueLog(uint8_t condition, uint16_t lineNumber);

void Test_assertEqualLog(uint16_t expected, uint16_t actual, 
    uint16_t lineNumber);

void Test_runall(void);

#if __TEST__

void Test_init(void) __attribute__ ((naked)) 
    __attribute__ ((section (".init7")));

#define Test_run() {Test_runall(); for(;;); }

#define Test_test(MODULE, NAME)
	void MODULE##_test_##NAME(void);
	void MODULE##_appendtest_##NAME(void) 
            __attribute__ ((naked))   
            __attribute__ ((section (".init8")));
	Test_TestHolder m_##MODULE_test_##NAME = { #NAME,
            MODULE##_test_##NAME, __FILE__, 0, NOT_RUN };
	void MODULE##_appendtest_##NAME(void) { 
            Test_add( &m_##MODULE_test_##NAME ); }; 
	void MODULE##_test_##NAME(void)

#else

void Test_init(void);

#define Test_run()

#define Test_test(MODULE, NAME)
	void MODULE##_test_##NAME(void);
	void MODULE##_test_##NAME(void)

#endif

#define Test_assertTrue(condition)
    Test_assertTrueLog((condition), __LINE__);
    if (!(condition)) {
        return;
	}		

#define Test_assertEquals(expected, actual)
    Test_assertEqualLog((expected), (actual), __LINE__);
    if ((expected) != (actual)) {
        return;
    }

#endif /* TEST_H */

test.c

#include "test.h"

#include <stdlib.h>
#include <string.h>

Test_TestHolder* m_Test_head;

Test_TestHolder* m_Test_activeTest;

typedef struct Test_ResultType
{
	uint16_t totalTests;
	uint16_t successCount;
	uint16_t failureCount;
} Test_ResultType;

Test_ResultType m_Test_result;

// Initialise the test framework
void Test_init(void)
{
	m_Test_head = NULL;
	m_Test_activeTest = NULL;

	m_Test_result.totalTests = 0;
	m_Test_result.successCount = 0;
	m_Test_result.failureCount = 0;
}

void Test_add(Test_TestHolder* test)
{
	// Put to front of chain
	test->next = m_Test_head;
	m_Test_head = test;
}

void Test_assertTrueLog(uint8_t condition, uint16_t lineNumber)
{
	// We have the active test
	if( !(condition) )
	{
		m_Test_activeTest->testResult = FAILURE;
		m_Test_activeTest->line = lineNumber;	
	}
}

void Test_assertEqualLog(uint16_t expected, uint16_t actual, 
    uint16_t lineNumber)
{
	if( expected != actual )
	{
		m_Test_activeTest->testResult = FAILURE;
		m_Test_activeTest->line = lineNumber;	
	}
}

// Run through all the tests
void Test_runall(void)
{
	// Reset counts
	m_Test_result.totalTests = 0;
	m_Test_result.successCount = 0;
	m_Test_result.failureCount = 0;

	// Reset status of all
	m_Test_activeTest = m_Test_head;
	while( m_Test_activeTest != NULL )
	{
		m_Test_result.totalTests++;

		m_Test_activeTest->testResult = NOT_RUN;
		m_Test_activeTest->line = 0;

		// next in the chain
		m_Test_activeTest = m_Test_activeTest->next;
	}

	// Now execute the tests
	m_Test_activeTest = m_Test_head;
	while( m_Test_activeTest != NULL )
	{
		m_Test_activeTest->testFunction();

		if( m_Test_activeTest->testResult == NOT_RUN )
		{
			m_Test_activeTest->testResult = SUCCESS;
			m_Test_result.successCount++;
		}
		else
		{
			m_Test_result.failureCount++;
		}

		asm("nop");

		// next in the chain
		m_Test_activeTest = m_Test_activeTest->next;
	}

	// Get the results
	asm("nop");
}

// Examples
/*
Test_test(Test, testWillFail)
{
	Test_assertEquals(1, 0);
}

Test_test(Test, testWillPass)
{
	Test_assertTrue(1);
}
*/

main.c

#include "test.h"

int main(void)
{	
	Test_run();        // System code
}

2 thoughts on “Unit Testing with AVR Studio and AVR-GCC

  1. Will I need to set up a different compiler to compile the unit tests? Is it possible to compile the unit tests with a different compiler toolchain to the default avr-gcc installed or will I have to compile the unit tests elsewhere? Is there an easier way of doing this?

    • Hi, I use the same compiler but run it in the simulator (so you can see the output) rather than on the device itself. The unit tests are compiled as part of the main program when you enable them. There may be better ways of doing this with the IDE these days, I haven’t worked with AVR-GCC for a couple of years now.

Comments are closed.