Feb 10
2015
2015
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
$ 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: