OK04: A tale of debugging

Working through the Baking Pi tutorial, I ended up having some trouble getting OK04 working.

OK04 is a "bare-metal operating system" in that it is the only program running on the RPi; there's no Linux or anything else running. It's extremely basic, and just flashes the activity / OK LED. It's the fourth in a sequence of such operating systems. The first turns on the LED, the second blinks it using a fixed delay, the third introduces no functional changes but explains how to employ functions in assembly, and the fourth uses the GPIO timer for more accurate delays. As part of debugging why the code wouldn't work, I rewrote most of the code to clean it up. This turned out to be useful as the code became clearer and easier to work with.

The previous three projects had been built and ran; the LED was blinking. Introducing the GPIO timer code broke it: the LED would turn on, but wouldn't turn off.

At first I scratched my head; I ran through my code and it certainly looked like it was supposed to be doing the right thing. The one useful piece of information was that the ACT LED turned on. This means the OS at least made it through the "main screen turn on" stage.

Here's the main run loop from main.s:

loop$:
        /* Turn on ACT. */
        mov     r0,     #16
        mov     r1,     #0
        bl      SetGPIO

        /* Wait 100ms. */
        ldr     r0,     =100000
        bl      Wait

        /* Turn off ACT. */
        mov     r0,     #16
        mov     r1,     #1
        bl      SetGPIO

        /* Wait 300ms. */
        ldr     r0,     =300000
        bl      Wait

        /* Rinse and repeat. */
        b       loop$

The SetGPIO function handles setting a GPIO pin high or low, so the first place I wanted to look was there. In fact, this is exactly where the bug was. It helps to trace back starting at the known good place.

The problem was this initial bit of the SetGPIO function:

.globl SetGPIO
SetGPIO:
        cmp     r0,     #53
        movhi   pc,     lr

        mov     r2,     r0
        bl      GPIOAddress
        lsr     r3,     r2,     #5
        lsl     r3,     #2
        add     r0,     r3

The experienced reader will probably spot the bug. The ARM ABI requires the following of us:

  1. Registers 0-3 are used as inputs to the function.
  2. Registers 4-12 must have their values preserved.
  3. The function's output goes in r0.
  4. The lr register contains the address of the instruction after the function call.
  5. The stack pointer must be returned to its original value when the function exits.

The fix is to add push {lr} above the first mov instruction:

.globl SetGPIO
SetGPIO:
        cmp     r0,     #53
        movhi   pc,     lr
        push    {lr}

        mov     r2,     r0
        bl      GPIOAddress
        lsr     r3,     r2,     #5
        lsl     r3,     #2
        add     r0,     r3

The link register, or the address of the next instruction after the branch, wasn't being pushed onto the stack. This means that garbage values were being loaded into memory and the code was derailing off into the void.

Always double-check that you're following the ABI.

Bonus: my final solution ended up producing a kernel that was 12 bytes smaller.

Here's the final code:


Tags: , , ,