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
- …
- raspi/
- lib/
- raspi/
- raspi.a
- generic.a
- host/
- stubs.a
- generic.a
- test.a
- raspi/
- 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!
Pingback: Building Raspberry Pi code for unit tests | Raspberry Alpha Omega