Łukasz Adamczak

Łukasz Adamczak

One developer's journeys through code

Tinker Board bare metal: build, boot, and hang

Apr 24, 2020
  • Bare-metal

My recent interest in low-level programming got me to dust off the Tinker Board and start learning to talk to bare metal. I hope to share all I know so far, and learn more along the way.

What I’d like to cover:

  1. Build and boot a bare metal ARM binary ← this article
  2. JTAG debugging
  3. The stack & C environment setup
  4. Basic peripherals - UART, GPIO
  5. MMU, caches, and cores

Please note I’m quite new to this. There may be inaccuracies or just plain errors. I’d love to hear at lukasz@THISDOMAIN when you find them.

What’s a Tinker Board?

A Tinker Board ready to be tinkered with

The ASUS Tinker Board is a single-board computer based on the Rockchip RK3288 SoC. While not nearly as popular as the Raspberry Pi, it has quite a few things going for it:

In this part

We set up the cross-compilation toolchain, build an ARM binary, burn it to an SD card and boot on the board.

The code

There’s more setup than programming work today, so we keep code to a minimum. The program will first do nothing and then do more nothing in a busy loop.

.globl _start

_start:
  b  main

main:
  b  main

This is ARM assembly equivalent of:

10 GOTO 20
20 GOTO 20

Why do we need the first branch at all? One of the intricacies of RK3288 is that it expects "RK32" as the first 4 bytes in the binary. In our build process, the first b main (compiled to 0xeaffffff) will actually be overwritten by "RK32". We could just stick four zero bytes in the source code, but that would make it even more confusing.

Now we need to tool up.

Toolchain

If you’re on a recent Linux distribution, your package manager might provide an ARM cross-compilation toolchain for you. On Ubuntu that’s gcc-arm-none-eabi. On Arch, arm-none-eabi-gcc.

The arm-none-eabi variant is preferred and tailored to bare metal targets, but I’ve had success with arm-linux-gnueabihf-gcc from Linaro as well. We’re not using any ABI here really, and we strip everything except for raw ARM code, so it doesn’t really matter at this point.

Confirm it’s installed by running:

$ arm-none-eabi-gcc --version
arm-none-eabi-gcc (Arch Repository) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Build

Save the source file to main.S and build using:

$ arm-none-eabi-gcc -nostdlib -Ttext=0xff704000 main.S -o boot.elf

We need -nostdlib because we provide our own entry point (_start) and we don’t want any code linked from the toolchain’s standard library. The -Ttext=0xff704000 sets the base address for .text (code) sections. See the Boot section below for the significance of this address. We can confirm that worked by checking the disassembly:

$ arm-none-eabi-objdump -d boot.elf

boot.elf:     file format elf32-littlearm


Disassembly of section .text:

ff704000 <_start>:
ff704000:       eaffffff        b       ff704004 <main>

ff704004 <main>:
ff704004:       eafffffe        b       ff704004 <main>

The _start entry point is placed at the specified 0xff704000 address, and main starts 4 bytes after that.

Image

To turn this code into a bootable image for the Tinker Board, we need another tool called mkimage. It’s developed as part of Das U-Boot, and packaged on many distributions - u-boot-tools for Ubuntu or uboot-tools for Arch.

Each SoC has its own requirements for bootable media. This means specific headers, alignments, signatures etc. mkimage handles this for us. We only need to provide it with the binary and instruct it to build an SD card image for RK3288.

First, extract raw code from boot.elf.

$ arm-none-eabi-objcopy -O binary boot.elf boot.bin

The resulting boot.bin should be 8 bytes long and contain only the two branch instructions:

$ hexdump -C boot.bin
00000000  ff ff ff ea fe ff ff ea                           |........|
00000008

Now feed that into mkimage:

$ mkimage -n rk3288 -T rksd -d boot.bin boot.img

Options are quite straightforward. We specify the correct type with -n rk3288 and -T rksd (as in “Rockchip SD card image”) and provide data from -d boot.bin.

Boot

Before we burn boot.img to the SD card, a few words about the RK3288 boot process. After power on, the first code to run is the internal boot ROM. It goes over storage devices e.g. eMMC (for Tinker Board S), SPI flash or SD card and looks for a valid bootable image. When it finds one (and we now have one, thanks to mkimage), it loads the binary into internal SRAM starting at address 0xff704000, and starts executing code starting from 0xff704004.

What’s the “internal SRAM” you ask? Well despite the 2GB of DDR3 memory on the board, the only memory available to us right after power on is 96KB of internal static RAM in the SoC, mapped between 0xff700000-0xff718000. The idea is that this should be enough for a small piece of code to initialize the DDR memory and have further work continue from there. I hope to figure out how to do that soon. For now however, we’ll use those 96KB as the only RAM we’ve got and treat the SoC as a big microcontroller.

One last caveat: the boot image needs to be placed at sector 64 of the SD card. That’s where RK3288 will be looking for it, so that’s where we put it:

# Triple-check which device is your SD card. For me it's /dev/sdb.
# - seek=64 means start writing at sector 64
# - oflag=sync will ensure data is actually written to card before finishing

$ sudo dd if=boot.img of=/dev/sdb seek=64 oflag=sync

Power on

Insert the SD card into the Tinker Board and power on. Voila! Our code is now running on the RK3288, happily doing nothing in particular. And yes, you will have to trust me here because we have no way to prove it. The only output so far is heat coming out of the CPU. In the next part I will show how to connect a JTAG adapter and see what’s happening inside.

References

Source files to the examples are available on my GitHub at czak/bare-tinker.