Lab zero (Digital Systems)

Copyright © 2024 J. M. Spivey
Jump to navigation Jump to search

The purpose of this lab exercise is to get started with using the micro:bit and the software toolchain that supports building programs for it. The actions needed to prepare each program are spelled out in a Makefile, which can be interpreted by the unix program make to build the program automatically. You can do this from the unix command line, and upload the resulting binary program by copying it with a shell command to a virtual disk drive that represent's the micro:bit's memory. You can get started with programming the micro:bit by building and running a simple program that lets you connect to the micro:bit over a serial interface, then echoes the characters that you type. Like all the programs we will work with, this one depends on no machine-specific library code, so all the details of how the machine is programmed are explicit.

To carry out the instructions, you will need:

  • A BBC micro:bit, which we will supply.
  • A USB cable, with a full-size 'Type A' plug on one end and a 'Micro B' plug on the other. This we ask you to supply, because you probably have several spare ones already – they are the kind often used with mobile phones. The cable doesn't need to be very long, but it does need both power and data wires in it. Some cables (often the ones supplied for charging bike lights, in my experience) have power wires only, and they are useless for our purposes. Our micro:bits will be powered over USB, and will also communicate with the host computer over USB for downloading programs, for sending and receiving charaters on the serial port, and for connection with debugging software running on the host.

If you already have a preferred editor, especially one that is able to build the program being edited by invoking make, then you can use it to edit the provided programs and make micro:bit programs of your own. I developed the programs using Emacs myself, which can invoke make in an editor window, parse any resulting error messages, and show the lines of source code where the errors occurred. Other editors can do the same, but if you find yourself reading the error messages from the C compiler and counting lines in the source file, then you are using precious brain cells to do something a machine can do better.

If you don't already have an editor you like, then I suggest using Geany for this course. Geany is a simple, open-source editor and IDE that comes with most Linux distributions and is installed by default on the Raspberry Pi. I've prepared a version of Geany that can understand the syntax of ARM assembly language, and set up project files for each lab that contain the commands needed to build the programs (using make behind the scenes), to upload them to the micro:bit, and if needed to start the GDB debugger. Things on the lab machines will be set up so that you just need to double-click on one of these project files in the file manager to open the project in the Geany editor.

If you are using the lab machines, then the toolchain should already be installed for you; otherwise, there's a page with installation instructions for Linux systems.

Getting the sources

You should begin by making a copy of the Mercurial repository containing the lab materials.

$ hg clone https://spivey.oriel.ox.ac.uk/hg/digisys

The repository contains various, independent subdirectories for multiple labs, and the materials for this lab are in the lab0-echo subdirectory. You can browse the repository contents by opening https://spivey.oriel.ox.ac.uk/hg/digisys in a web browser.

We won't be making much use in this course of the power of Mercurial to track changes across multiple versions and multiple files and directories, but in future courses like Compilers you will be required to make modifications, check them in to a version control system, and submit a report on your changes. I'm recommending Mercurial for version control because it's easier to learn than Git and just as powerful – I use it for all my own work. If you want to know more about it, there's a nice tutoral online written by Joel Spolsky.

It seems necessary in today's world to believe one or other of the propositions, "Git is better than Mercurial," or "Mercurial is better than Git." If you are already a Git afficionado, then you can use instead the command,

$ git clone https://spivey.oriel.ox.ac.uk/git/digisys.git

Having cloned the repository, if you want to use Geany then you will need to configure it for editing ARM assembly language, and you will need to generate Geany project files for each lab. (These project files are machine-specific and not suitable for checking in to version control.) To do this, change to the digisys directory and invoke the shell script setup/genproj:

$ cd digisys
$ setup/install

This permanently installs settings under $HOME/.local/share and $HOME/.config/geany, and creates files lab0-echo/lab0.geany, etc. in the directory for each lab. The command needs to be run only once, not once for each session.

The source code you need for this lab exercise is in the subdirectory lab0-echo of the course materials. The following files are provided.

Makefile Build script
echo.c Program source
startup.c Startup code
hardware.h Header file with layout of I/O registers
nRF51822.ld Linker script
lab0.geany Geany project file
debug Shell script for starting debugger

Remarkably for an embedded program, all the code is in a high-level language, and there is no assembly-language code. That this is possible is a nice feature of the Cortex-M platform.

Compiling the program

Once you have obtained a copy of these files, all you should need to do to build the program from the command line is to change to the lab0-echo subdirectory and give the command make:

$ cd lab0-echo
$ make

The individual commands shown below will be executed automatically. I will spell them out so that you know what they do, but after you have built a few programs, you will no doubt be content to let them whizz by without paying much attention – unless the build grinds to a halt with an error message, that is.

The first command to be executed is this:

arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O -g -Wall -ffreestanding -c echo.c -o echo.o 

It uses a C compiler, arm-none-eabi-gcc, to translate the source file echo.c into object code in the file echo.o. This compiler is a cross compiler, running on an Intel machine, but generating code for an embedded ARM chip: the none in its name indicates that the code will run with no underlying operating system. The flags given on the command line determine details of the translation process.

  • -mcpu=cortex-m0 -mthumb: generate code using the Thumb instruction set supported by the ARM model present on the micro:bit.
  • -O: optimise the object code a little.
  • -g: include debugging information in the output.
  • -Wall: warn about all dubious C constructs found in the program.
  • -ffreestanding: this program is self-contained, so don't make some common assumptions about its environment.
  • -c: compile the C code into binary machine language, but don't put together an executable image.
  • -o echo.o: put the binary code in the file echo.o.

Next, make also compiles the file startup.c in a similar way.

arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O -g  -Wall -ffreestanding -c startup.c -o startup.o

The file startup.c contains the very first code that runs when the microcontroller starts. It is written in C, but it uses several non-portable constructions, and few of the usual assumptions about the behaviour of C programs apply.

With both of the source files translated into object code, it is now time to link them together, forming a file echo.elf that contains the complete, binary form of the program. This is done by invoking the C compiler again, but this time providing the two file echo.o and startup.o as inputs.

arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O -g -Wall -ffreestanding  -T nRF51822.ld -nostdlib \
    echo.o startup.o -lgcc -o echo.elf -Wl,-Map,echo.map 

Again, the detailed behaviour of this command is determined by the long sequence of flags. The new ones are as follows.

  • -T nRF51822.ld: use the linker script in the file nRF51822.ld. This script describes the layout of the on-chip memory of the micro:bit: 128K of flash memory at address 0, and 16K of RAM at address 0x20000000. It also says how to lay out the various segments of the program: the executable code 2.text and string constants .rodata in flash, and the initialised data .data and uninitialised globals .bss@ in RAM.
  • -nostdlib: the usual startup code and libraries for a C program are omitted, because we are supplying our own.
  • -lgcc: the C compiler's own library is searched for functions (such as out-of-line code for integer division) that the program needs.
  • -Wl,-Map,echo.map: a map of the layout of storage is written to the file echo.map
  • -o echo.elf: the output goes into a file echo.elf that has the same format as the .o files prepared earlier, but now contains a complete program.

Many of these flags can be used unchanged in building other programs in the course, and it is good to know why they are there. Faced with the problem of fitting an application into a tiny amount of memory, embedded programmers become intensely interested in storage layouts and linker scripts.

We are nearing the end of the process. The next command just prints out the size of the resulting program.

arm-none-eabi-size echo.elf
  text	   data	    bss	    dec	    hex	filename
  1268	      0	     84	   1352	    548	echo.elf

Here we see that the program has 1268 bytes of code, no initialised storage for data, and 84 bytes of uninitialised space (actually, it is initialised to zero) for global variables. In the echo program, this consists almost entirely of an 80-byte buffer for a line of keyboard input.

The final stage prepares the binary object code in another format, ready to be downloaded to the micro:bit board.

arm-none-eabi-objcopy -O ihex echo.elf echo.hex

The file echo.elf is a binary file, containing the object code and a lot of debugging information, whereas echo.hex is actually a text file, containing just the object code encoded as long hexadecimal strings, a format that the loading mechanism on the micro:bit understands.

Running the program

If you plug in a micro:bit, it will appear as a USB drive on your computer, and you can copy the file echo.hex to it, either by dragging and dropping from the file manager with the mouse, or by using a shell command:

$ cp echo.hex /run/media/mike/MICROBIT/

(with mike replaced your own username, typically in the format u19xyz). The yellow LED on the micro:bit will flash briefly, then the program will start to run. Note that the USB drive appears to have a couple of files on it – one a text file giving the version number of the board and its embedded software, another an HTML file with a link to the micro:bit website. Any files you drag and drop there do not appear as files on the drive, however: they are instantly consumed by the flash loader and not stored as files.

The echo program reads and writes the serial interface of the micro:bit, which appears as a terminal device /dev/ttyACM0 on the Linux machine. To connect with this device, it's convenient to use a program called minicom on the Linux machine. Start a shell window, then type the command,[1]

$ minicom -D /dev/ttyACM0 -b 9600

After connecting, you should press the reset button on the micro:bit to start the program again. You should see the message Hello micro:world, followed by > as a prompt. Type characters at the prompt: they will be echoed, and you can use the backspace key to make corrections. When you press Return, the line you typed will be repeated, and then a new prompt appears.

Using Geany

The instructions above tell you how to compile, upload and run a micro:bit program from the command line, but all the same actions can be performed from the Geany editor. To open the program as project within Geany, use the file manager to look for the file lab0-echo/lab0.geany and double-click on it. This should launch Geany with the file echo.c initially open, and the Build menu filled with appropriate actions for the project. Specifically, when we choose Build>Make in a moment to compile the program, Geany will use the Makefile provided, and therefore invoke the cross-compiler arm-none-eabi-gcc rather than the native C compiler that is called just plain gcc. (Don't try opening individual files of C code with Geany rather than opening the project, or the setup will be wrong.)

To build and run the program from within Geany:

  • Choose Build>Make to compile the program. Geany will invoke make, and the same steps will happen as were listed earlier. The commands and any error messages will appear in a separate pane at the bottom of Geany's window, and afterwards Geany will analyse the error messages and highlight corresponding lines in the source file.
  • Choose Build>Upload to upload the program to a plugged-in micro:bit.
  • Choose Build>Minicom to launch a new window running minicom to talk to the micro:bit. You can leave this window open as long as you like, or close it when you have finished interacting with the running program.

Whether using Geany or your own choice of editor, you can now go on to try some assembly language programming in Lab one, or you can try connecting to the micro:bit with the symbolic debugger GDB as described in the next section, and come back to using GDB later.

Using a debugger

The USB interface between the host computer and the micro:bit serves three purposes: it enables us to upload programs to the board, it lets us interact with a program running on the board over the micro:bit's serial port, and thirdly it allows a debugger running on the host to monitor and control the execution of the program on the micro:bit.

Here's how to run the program under control of a debugger, so as to execute it step by step. You will use two terminal windows for this experiment – one to connect to the micro:bit using minicom as before, a second one to run the GNU debugger gdb. Instructions are given here to start a debugging session from the command line, but the same effect can be achieved by choosing Build>Minicom and Build>Debug from within Geany. However you start the debugger, you may like to enlarge its window to show more lines, particularly if you want to use the multi-panel interface described later.

1. Plug in the microbit, and open a terminal window to run minicom, as before. Check that you can type characters and have them echoed by the program.

2. Open another terminal window, change to the lab0-echo directory, and run the shell script ./debug echo.elf. This script first starts an adapter program pyocd that can talk to the micro:bit over the USB link, then connects to the adapter with the interactive debugger GDB. You should see some messages along the following lines:

$ ./debug echo.elf
...
0x00000150 in serial_getc () at echo.c:28
28     while (! UART_RXDRDY) { }
(gdb) 

The debugger has stopped the program wherever it was: as you might expect, the program is sitting in a tight loop, waiting for a character to be typed on the keyboard. As we'll learn much later, UART_RXDRDY is a register in the serial interface whose value indicates whether a character has been received, and line 28 is a loop that tests the register repeatedly until a character arrives.

Sadly, the debugger doesn't have a sophisticated graphical user interface, and we will interact with it using text commands. At this point, you can either continue with the program from where it is, or you can use the command

(gbd) monitor reset

to start it again from the very beginning. After doing so, you can use

(gdb) advance init

to run the startup code and skip to the start of the main program init.

When the program is stopped, you can use

(gdb) cont 

to continue running it, and press Ctrl-C to stop it again. Alternatively you can use commands like

(gdb) step 

and

(gdb) next 

to run the program line by line: one steps into function calls, and the other steps over them.

Although GDB does not have a GUI, it can produce a display of what is happening in the program by using a multi-panel "Text User Interface". Before activating it, you may like to stretch the terminal window vertically so it occupies most of the height of your display. By using the command

(gdb) layout split

you can enter a mode where the display is split into several panes, with one showing the C source and another showing the disassembled object code. The command

(gdb) layout regs 

then switches to show the registers and object code. It then becomes interesting to give the command

(gdb) stepi 

in place of step and execute the program one instruction at a time, watching the register contents as you do so.

GDB provides many other commands that enable you to set breakpoints where execution will stop, to display the contents of variables, decode the subroutine stack, and even change values and alter the program's flow of control.

When you have finished, you can give the command

(gdb) quit

to leave GDB; this also shuts down the adapter program.

Starting the debugger manually

If an attempt to start the debugger doesn't work, using the debug script either from the command line or from Build>Debug menu item in Geany, then there are several things to try.

  • Before trying other things, make sure you have an up-to-date version of the lab software, including the debug script. To update, give the commands
$ hg pull
$ hg up
These will pull down any changes from the Mercurial server, then update your working copy. (If you are using Git instead of Mercurial, you have already accepted responsibility for helping yourself here.)
  • If you have made previous attempts to use the debugger, then defunct instances of the debugging adapter process pyocd may be hanging around and blocking access to the micro:bit board. Get rid of them by giving the shell command
$ killall pyocd
If there are no pyocd processes hanging around, then this command does no harm, so you may as well try it in every case. You might then like to try again with starting the debugger using the debug script. Previous versions of the debug script were likely to leave such processes around if the window was closed abruptly rather than exiting the debugger with the quit command; this should be fixed in the latest version.
  • If the debug script still doesn't work, then use killall pyocd again and follow the instructions below for starting both the debugger and the pyocd adapter separately by hand.

To start things manually, you will need three shell windows. In one, run minicom so you can see that the micro:bit is printing on its serial port. In another window we will run the adapter program pyocd that interfaces the debugger and the micro:bit over USB, and in the third window we will run the interactive debugger GDB itself.

In the second window, give the shell commands

$ killall pyocd
$ pyocd gdbserver -t nrf51

The first of these kills defunct instances of pyocd, and the second starts a fresh one, fulfilling the rôle of a server for debugger access, and expecting to find a board with an NRF51 series chip on it. The pyocd program should produce a slew of messages, ending with one to the effect that it is now listening on port 3333.

In the third window, change to the directory containing the program, then start GDB. On the lab machines, the relevant version of GDB os called gdb-arm; on other machines, try gdb-multiarch or arm-none-eabi-gdb instead.

$ cd digisys/lab0-echo
$ gdb-arm echo.elf

We tell GDB that we want to debug the program echo.elf, naming the binary file containing debugging information, rather than the downloadable file echo.hex. GDB will read the debugging information and show its prompt.

(gdb) target remote :3333

At this point, we tell GDB that it will be communicating with the debug adapter over a socket; the syntax :3333 refers to network port 3333 on this machine (because the portion in front of the colon is empty). When you give this command, you should see responses both from GDB and in the window where pyocd is running, then the GDB prompt should show where the program is stopped, and away we go. HAppy debugging!



  1. If you are lucky, this setup will agree with the default, and you can type just minicom.