Unit testing bare-metal Raspberry Pi code

A few days ago I took a few small steps toward creating a project structure and build process which would allow me to build and test the majority of my bare-metal Raspberry Pi code on my development (Windows or Linux) machine, with only the very hardware-specific bits needing to be built and tested on the Raspberry Pi itself.

After the few small steps, I kind of ran aground, unsure about what to do next, and unwilling to break a working process. This evening I obviously had a bit more enthusiasm, and set to work on re-organising the code and hammering at folders and makefiles. Eventually I have now got a build working really sweetly.

The project is structured as follows:

  • Makefile
  • src/
    • raspi/
      • gpio.c
      • startup.s
      • raspi/
        • gpio.o
        • startup.o
    • stub/
      • stub_gpio.c
      • stub_memory.c
      • host/
    • generic/
      • morse.c
      • ledborg.c
      • raspi/
        • morse.o
        • ledborg.o
      • host/
        • morse.o
        • ledborg.o
    • test/
    • gpio.h
    • ledborg.h
  • lib/
    • raspi/
      • raspi.a
      • generic.a
    • host/
      • stubs.a
      • generic.a
      • test.a
  • bin

The idea is that the project can be built in two modes “host” and “target”. To build host mode, type make host, to build target mode type make target.

Target mode builds two submodules using the ARM compiler: generic and raspi. generic is the platform independent-code, in this case that’s things such as the Morse code translation and the LEDBorg functions. raspi is the hardware-specific code including basic memory and GPIO access, the assembler code used to boot into C, and so on.

Host mode builds three submodules using the host compiler: generic, stubs, and test. generic is the same code as above, just built with a different compiler. stubs is local, diagnostic versions of the API functions in raspi. test is code to run tests on the generic code using the stubs.

As much as possible, the build process is the same for all the code. Some variables are set in the top-level makefile indicating which compiler and compile options to use, then separate makefiles are invoked for the various submodules. To keep object and executable files separate, and to avoid any chance of accidentally building with the wrong versions, all object files are built into subdirectories named for the platform (“host” or “raspi”) and each module is built into a library, which is placed in a similarly named subdirectory of “lib”.

When all the libraries have been built, target mode builds a Raspberry Pi boot image from lib/raspi/raspi.a and lib/raspi/generic.a, but host mode builds a local executable from lib/host/stubs.a, lib/host/generic.a and lib/host/test.a. Running this executable runs the “main” within the test module, which in turn runs any tests.

For now, the stubs just print out which low level functions have been called, and what their parameters were, and there are no unit tests as such. I plan to improve these to make them more suitable for automated unit tests later.

The top-level makefile looks like:

export FLAVOURS = host raspi
export MODULES = generic raspi stubs test

default: all

CFLAGS_GLOBAL = -O0 -g -std=gnu99 -Werror 
CFLAGS_TARGET = -D__$(PLATFORM)__ -DRASPBERRY_PI -fno-builtin -mcpu=arm1176jzf-s
CFLAGS_HOST = 

LDFLAGS_GLOBAL = --error-unresolved-symbols 
LDFLAGS_TARGET = -static -nostdlib 
LDFLAGS_HOST = 

all: host target

target: export ARCH = /drives/c/devtools/yagarto-20121222/bin/arm-none-eabi-
target: export PLATFORM = raspi
target: export CFLAGS = $(CFLAGS_GLOBAL) $(CFLAGS_TARGET) 
target: export LDFLAGS = $(LDFLAGS_GLOBAL) $(LSFLAGS_TARGET) 
target: export ASFLAGS = -mcpu=arm1176jzf-s -g

host: export ARCH = # /drives/c/devtools/MinGW/bin/
host: export PLATFORM = host
host: export CFLAGS = $(CFLAGS_GLOBAL) $(CFLAGS_HOST) 
host: export LDFLAGS = $(LDFLAGS_GLOBAL) $(LSFLAGS_HOST) 

host: FORCE
	cd src/stubs; $(MAKE)
	cd src/generic; $(MAKE)
	cd src/test; $(MAKE)
	${ARCH}gcc -o bin/test ${LDFLAGS} lib/host/test.a lib/host/generic.a lib/host/stubs.a
	bin/test

target: FORCE
	cd src/raspi; $(MAKE)
	cd src/generic; $(MAKE)
	${ARCH}ld ${LDFLAGS} lib/raspi/generic.a lib/raspi/raspi.a lib/raspi/generic.a lib/raspi/raspi.a -Map bin/kernel.map -o bin/kernel.elf -T raspi.ld
	${ARCH}objcopy -O binary bin/kernel.elf bin/kernel.img
	
clean: FORCE
	rm -f bin/kernel.*
	rm -f $(foreach flav,$(FLAVOURS),lib/$(flav)/*.a)
	rm -f $(foreach mod,$(MODULES),$(foreach flav,$(FLAVOURS),src/$(mod)/$(flav)/*.o))
	rm -f $(foreach mod,$(MODULES),src/$(mod)/*.o)
	
FORCE:

Each submodule of the src directory has its own makefile, with very little in it. For example:

MODULE=generic

include ../make.inc

This just defines the name to be used for the generated lib, and hands off to common stuff. The included file is not particularly complicated, but there’s no point having several duplicates cluttering up the project:

make.inc

default: ../../lib/$(PLATFORM)/$(MODULE).a

CFILES=$(wildcard *.c)
SFILES=$(wildcard *.s)

../../lib/$(PLATFORM)/$(MODULE).a: $(addprefix $(PLATFORM)/,$(subst .c,.o,$(CFILES))) $(addprefix $(PLATFORM)/,$(subst .s,.o,$(SFILES)) )
	$(ARCH)ar cr $@ $^

$(PLATFORM)/%.o: %.c
	$(ARCH)gcc -I. -I.. $(CFLAGS) -c -o $@ $<

$(PLATFORM)/%.o: %.s
	$(ARCH)as $(ASFLAGS) -o $@ $<

clean: FORCE
	rm -f $(foreach dir,$(FLAVOURS),$(FLAVOUR)/*.o)
	
FORCE:

Even though I don't have any proper unit tests yet, it is kind of fun to run the same code on the real Raspberry Pi hardware, and on the development box with fake hardware. As an example, the following "test" code:

#include 

#include "morse.h"
#include "ledborg.h"

int main() {
  puts("tests go here!");
  morse_set_switch(&ledborg_set_all);
  morse_string("host");
}

gives the following output:

bin/test
tests go here!
raspi_set_gpio_level: pin=17 level=0
raspi_set_gpio_level: pin=21 level=0
raspi_set_gpio_level: pin=22 level=0
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=1
raspi_set_gpio_level: pin=21 level=1
raspi_set_gpio_level: pin=22 level=1
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=0
raspi_set_gpio_level: pin=21 level=0
raspi_set_gpio_level: pin=22 level=0
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=1
raspi_set_gpio_level: pin=21 level=1
raspi_set_gpio_level: pin=22 level=1
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=0
raspi_set_gpio_level: pin=21 level=0
raspi_set_gpio_level: pin=22 level=0
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=1
raspi_set_gpio_level: pin=21 level=1
raspi_set_gpio_level: pin=22 level=1
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=0
raspi_set_gpio_level: pin=21 level=0
raspi_set_gpio_level: pin=22 level=0
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=1
raspi_set_gpio_level: pin=21 level=1
raspi_set_gpio_level: pin=22 level=1
raspi_timer_wait: usec=150000
raspi_timer_wait: usec=450000
raspi_set_gpio_level: pin=17 level=0
raspi_set_gpio_level: pin=21 level=0
raspi_set_gpio_level: pin=22 level=0
raspi_timer_wait: usec=450000
raspi_set_gpio_level: pin=17 level=1
raspi_set_gpio_level: pin=21 level=1
raspi_set_gpio_level: pin=22 level=1
raspi_timer_wait: usec=150000
raspi_set_gpio_level: pin=17 level=0
raspi_set_gpio_level: pin=21 level=0
raspi_set_gpio_level: pin=22 level=0
raspi_timer_wait: usec=450000
raspi_set_gpio_level: pin=17 level=1
raspi_set_gpio_level: pin=21 level=1
...

You can clearly see that the Morse code translation is working, the GPIO pins are being switched on and off, and the timer is being called to wait.

As usual, the code is available on github for you to fork, borrow and play with.

I think that's pretty cool!

One Comment

  1. Pingback: Building Raspberry Pi code for unit tests | Raspberry Alpha Omega

Leave a Reply

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