Tue
Feb 10

Running bare-metal code on a ci20: part 2

This is a follow-up to part 1, from yesterday.

By this point you have a serial connection to the board, and you have a toolchain. Let’s write some code!

Step 3: Code

What to code? We could always output something to the UART, but why do that when the board has a nice, large, glowing LED on it? The hardware page has a short recipe to make the led appear purple by rapidly switching it between red and blue. Let’s do that.

Ideally we’d like to write as much as possible in C, but C needs a stack, so the first thing we do is write a very short assembly language program which sets up a stack pointer and jumps to C. Create a file named start.S and add this to it:

#include "mipsregs.h"

/* make it accessible outside */
.globl _start
/* Tell binutils it's a function */
.ent _start
.text

_start:
	/* Set up a stack */
	li sp, 0xa9000000 

	/* And jump to C */
	la t0, entrypoint
	jr t0
	nop

.end _start
	  
All this file does, apart from play nice with binutils, is set up a stack pointer and jump to a symbol named “entrypoint”, which is where we’re going to write our C. Where did the stack pointer address come from? I basically made it up. :) Looking at the MIPS memory map, we know we have an uncached area, called kseg1, starting at 0xa0000000 and ending at 0xc0000000. On this board the first 256MB of that are directly mapped to RAM. We’ll use this area to store code, data, and the stack.

What about “mipsregs.h”? GCC doesn’t natively understand the symbolic register names “sp” and “t0”, so I wrote a small header which defines them for me. The important parts are these two lines:
#define t0 $8  /* temporary values */
#define sp $29 /* stack pointer */
	  

… but the full file is included in my Github repository. See below.

We now have some code which jumps to a function named “entrypoint”, so the next step is to write “entrypoint”. Create a file named main.c, and add these lines:

#define GPIO_F_SET 0xb0010544
#define GPIO_F_CLEAR 0xb0010548

#define GPIO_F_LED_PIN (1 << 15)

static inline void write_l(unsigned int addr, unsigned int val)
{
    volatile unsigned int *ptr = (unsigned int *)(addr);

    *ptr = val;
}

static void delay()
{
    volatile int i;

    for(i=0; i<1000; i++)
        ;
}

void entrypoint(void)
{
    /* Do the purple LED thing */
    while(1) {
        write_l(GPIO_F_CLEAR, GPIO_F_LED_PIN); /* Turn LED blue */
        delay();
        write_l(GPIO_F_SET, GPIO_F_LED_PIN); /* Turn LED red */
        delay();
    }

}
		


As described in the ci20 hardware page, the LED is accessible via a general-purpose IO port — GPIO — called GPIO F (the board also has GPIO ports A to E, and each port is 32 bits wide). Bit 15 of that port controls the LED. If it’s set to 0, the LED is blue, and if it’s set to 1, the LED is red.

The jz4780 makes it very easy to set and clear bits in the GPIO ports. Each port has a “set” address and a “clear” address. Any 1s which you write to the “set” address cause the corresponding bit of the GPIO port to be set, and, conversely, and 1s which you write to the “clear” address cause the corresponding bit to be cleared. In both cases, 0s are ignored. So you can just set or clear the bits you need without worrying about reading from the port first to avoid changing values you’re not interested in.

You can see that the GPIO ports lie in the uncached KSEG1 region as well. In fact, the jz4780 processor reserves the upper 256MB of KSEG1 (0xb0000000-0xbfffffff) for memory-mapped devices.

The final thing we need to do is to instruct the linker to put everything at an address inside KSEG1. Create a linker script named linker.lds and add the following:

OUTPUT_ARCH(mips)

ENTRY(_start)

SECTIONS
{
    /* Our base address */
    . = 0xa8000000;

    /* Code */
    .text : {
        *(.text)
    }

    /* Static data */
    .rodata : {
        *(.rodata)
        *(.rodata.*)
    }
    /* non-static data */
        .data : {
        *(.data*)
    }
}
Linker scripts are often referred to as voodoo, but this one is pretty simple. We tell the linker to start writing “text” (program code) at 0xa8000000, and that the first thing in it must be _start. Data comes after the code.

Step 4: Compiling

Now to build everything. Let’s use Make. Create a file named Makefile:

AS=mipsel-unknown-elf-as -mips32
CC=mipsel-unknown-elf-gcc
LD=mipsel-unknown-elf-ld
OBJCOPY=mipsel-unknown-elf-objcopy
CFLAGS=-Os

OBJS=start.o main.o

hello.bin: hello.elf
	$(OBJCOPY) -O binary $< $@

hello.elf: $(OBJS)
	$(LD) -T linker.lds -o $@ $+

%.o: %.[Sc]
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm -f *.o *.elf *.bin

This Makefile does a few interesting things.

  • It uses our custom-built toolchain, in which all the tools are named mipsel-unknown-elf-something
  • It uses GCC to compile everything, even the assembly language files. This is standard practise and it’s because GCC runs the C preprocessor over the file, if it ends in S, so we can use those symbolic register names.
  • It turns the output of the linker into a raw binary file using objcopy.

That last step is required because the linker will produce an “elf” file, which is a structured file in the Executable and Linkable format, the standard format for executable files on Linux (and many other Unix-like systems, but not Macs). However, we’re going to use uboot to load the file, and uboot expects the file to be in a raw “memory image” format, which it can just copy directly into RAM and run. We keep the .elf file around, though, because it’s useful for debugging.

You should now build everything:

$ make

If you get this far, you’re ready to boot your new “kernel”.

Step 5: Booting!

First, we have to set up a TFTP server. On a Mac, you can use the built-in TFTP server by running these commands from a terminal:

$ sudo launchctl load -F /System/Library/LaunchDaemons/tftp.plist
$ sudo launchctl start com.apple.tftpd

This will serve files from /private/tftpboot. Copy your new code there:

$ sudo cp hello.bin /private/tftpboot/

Now connect an Ethernet cable to your ci20, get your serial terminal ready, and reset the board. When you see "Hit any key to stop autoboot”, which happens within 5 seconds of booting, press a key. You should be greeted by a uboot prompt:

ci20#

This is uboot, which is quite a featureful bootloader — type “help” to get an idea of what it can do. For now, we’re going to configure it to load our file via tftpboot. Set the server IP (the IP address of your TFTP server) and the board’s IP address. My server is at 192.168.1.12, and I gave the board an IP of 192.168.1.7:

ci20# setenv serverip 192.168.1.12
ci20# setenv ipaddr 192.168.1.7

Now we can instruct uboot to load our file:

ci20# tftpboot 192.168.1.12:hello.bin
Load address: 0x88000000
Loading: #
         12.7 KiB/s
done
Bytes transferred = 144 (90 hex)

Note that the default load address is at 0x88000000, which isn’t what we asked for. However, it’s not a problem, because this address and 0xa8000000 both refer to the same location, but the former is cached.

If you get this far, cross your fingers, and type:

ci20# go 0xa8000000
## Starting application at 0xA8000000 …

You should get a pretty purple (well, pinkish, really) LED, with no operating system required.

Step 6: Future work

Some things one might do to expand on this:

  • Enable caches and work from cached memory
  • Write to the uart, to make a true “Hello, world!” program
  • Set up a timer and flip the LED from the timer interrupt routine
  • Read up on MIPS TLB refil and run the LED flipping function from KUSEG (also known as user space) by mapping memory in using that function
  • Use the timer to switch between executing one function which turns the LED red, and another which turns it blue…

Complete code

Is available from Github:

https://github.com/nfd/ci20-hello-world