Bootloader at Home?

6 minute read

Published:

So far we looked at implementing firmware for bare metal use cases. But you may have seen that in most of the hardware SDK’s, they suggest using a bootloader to load your programs.

Introduction to Bootloaders

Bootloader is a simple program, which can load and start your application. The simplest analogy is that the bootloader has a placeholder for your application to boot successfully inside the processor once you update it in the correct place. This could be either from an external flash or EEPROM. Bootloader’s job is to copy the content from the read-only memory (ROM) to the executable memory (RAM).

Bootladers can isolate critical components from each other and security primitives such as confidentiality, integrity, and authenticity can be introduced to the firmware with the use of bootloaders. Bootloaders also support recovery from bad application updates.

In this tutorial, we will write a simple bootloader to work with our picoRV processor. This bootloader will start during the power-up of the processor and will make the program counter jump into the application code. Finally, we will simulate the design with our homemade bootloader with the picoRV processor.

Memory Map

We need to make sure that the bootloader is isolated from the application. The easiest way to achieve this is via the linker script. Note that the bootloader is a small program and does not need much space. Let’s give it about 4kB of memory.

/* memory_map.lds */
MEMORY
{
    BOOTROM (rx) : ORIGIN = 0x00100000, LENGTH = 0x004000
    APPROM (rx) : ORIGIN = 0x00104000, LENGTH = 0x3fc000
    RAM (xrw) : ORIGIN = 0x00000000, LENGTH = 0x20000
}

__bootrom_start__ = ORIGIN(BOOTROM);
__bootrom_size__ = LENGTH(BOOTROM);
__approm_start__ = ORIGIN(APPROM);
__approm_size__ = LENGTH(APPROM);

Bootloader needs to find the application. Therefore we have to share this information with the bootloader We can simply use a header file for that.

/* memory_map.h */
#pragma once

extern int __bootrom_start__;
extern int __bootrom_size__;
extern int __approm_start__;
extern int __approm_size__;

Bootloader Code

Let’s create a simple c program for the bootloader. In order to visually see our bootloader is working we can add some print statements to the UART. In this case, we have added the characters of the word Boot.

/* boot.c */
#include <inttypes.h>
#include "memory_map.h"


int main(void) {
  (*(volatile uint32_t*)0x02000004) = 104; // Set UART clock rate
  (*(volatile uint32_t*)0x02000008) = 0x42; // Write B to UART
  (*(volatile uint32_t*)0x02000008) = 0x6F; // Write o to UART
  (*(volatile uint32_t*)0x02000008) = 0x6F; // Write o to UART
  (*(volatile uint32_t*)0x02000008) = 0x74; // Write t to UART

   /* Do some bootloader stuff. */
 
  while (1) {}
}

Next from the bootloader, we need to set the program counter to the app starting address. For this, we will use assembly instructions. Specifically, since we are using the RISC-V based picoRV core we need to use the correct jump instruction from the RISC-V ISA. In this case the we need to jump to the app starting point with jal ra, __approm_start__

/* boot.c */
#include <inttypes.h>
#include "memory_map.h"


int main(void) {
  (*(volatile uint32_t*)0x02000004) = 104; // Set UART clock rate
  (*(volatile uint32_t*)0x02000008) = 0x42; // Write B to UART
  (*(volatile uint32_t*)0x02000008) = 0x6F; // Write o to UART
  (*(volatile uint32_t*)0x02000008) = 0x6F; // Write o to UART
  (*(volatile uint32_t*)0x02000008) = 0x74; // Write t to UART


   /* Do some bootloader stuff. */
  
  
  uint32_t app_start = (uint32_t)&__approm_start__; // Where is the application
 
  __asm__ ("jal ra, %[prog_count]" /* __asm__ ("jal ra, 0x00104000") */
                    : /* No outputs. */
                    : [prog_count] "i" (app_start) ); // Jump  to the application
 
  while (1) {}
}

More information about writing assembly with c can be found in Extended-Asm.

Finally, let’s create a Linker script for the bootloader informing the compiler to put out Bootloader to the BOOTROM. We can copy the Linker script from the tutorial3, and replace the >rom with >BOOTROM.

/* boot.lds */
INCLUDE memory_map.lds

SECTIONS {
    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        *(.rodata)
        *(.rodata*)
        *(.srodata)
        *(.srodata*)
        . = ALIGN(4);
        _etext = .;
        _sidata = _etext;
    } >BOOTROM
    
    .....

Application code

Now for the application side, we need to create a separate Linker Script. For that, we can copy the same Linker script and replace >rom with >APPROM.

/* app.lds */
INCLUDE memory_map.lds

SECTIONS {
    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        *(.rodata)
        *(.rodata*)
        *(.srodata)
        *(.srodata*)
        . = ALIGN(4);
        _etext = .;
        _sidata = _etext;
    } >APPROM
    
    .....

Now we need to compile the application and the bootloader separately.

Compile the Bootloader

Next, we compile the bootloader and app separately. Let’s compile the bootloader first.

riscv32-unknown-elf-gcc boot.c -c -mabi=ilp32 -march=rv32ic -Os --std=c99 -ffreestanding -nostdlib
riscv32-unknown-elf-gcc -Os -mabi=ilp32 -march=rv32imc -ffreestanding -nostdlib -o boot.elf -Wl,--build-id=none,-Bstatic,-T,boot.lds,-Map,boot.map,--strip-debug boot.o -lgcc

Then let’s compile the application. For this, we can use the simple “hello world” application that we used in tutorial2


riscv32-unknown-elf-gcc app.c -c -mabi=ilp32 -march=rv32ic -Os --std=c99 -ffreestanding -nostdlib
riscv32-unknown-elf-gcc start.S -c -mabi=ilp32 -march=rv32ic -o start.o
riscv32-unknown-elf-gcc -Os -mabi=ilp32 -march=rv32imc -ffreestanding -nostdlib -o app.elf -Wl,--build-id=none,-Bstatic,-T,app.lds,-Map,app.map,--strip-debug start.o app.o -lgcc

Next, we have to combine the two elf files into one bin/hex file. For that we use objcopy. Alternatively, You can simply copy the two hex files into one hex file following the order of boot.hex app.hex.

riscv32-unknown-elf-objcopy boot.elf --pad-to=0x4000 --gap-fill=0x00 -O verilog  boot.hex
riscv32-unknown-elf-objcopy app.elf -O verilog  app.hex
cat boot.hex app.hex > bootapp.hex

You can find all the code related to this tutorial in tutorial4 folder of the following GitHub repository.

Bootloader in Action

Now, let’s see our bootloader doing it’s job in the simulation.

First, we compile the hardware in the hw folder.

iverilog -s testbench -o ice.vvp  icebreaker_tb.v icebreaker.v ice40up5k_spram.v spimemio.v simpleuart.v picosoc.v picorv32.v spiflash.v -DNO_ICE40_DEFAULT_ASSIGNMENTS  `yosys-config --datdir/ice40/cells_sim.v`  

Then, simulate the hardware with the bootloader and our app.

vvp -N ice.vvp ../fw/bootapp.hex +firmware=../fw/bootapp.hex

This should produce the following output

0000000
Serial data: 'B'
Serial data: 'o'
Serial data: 'o'
Serial data: 't'
+50000 cycles
Serial data: 'H'
Serial data: 'E'
Serial data: 'L'
Serial data: 'L'
Serial data: 'O'
+50000 cycles
Serial data: ' '
Serial data: 'W'
Serial data: 'O'
Serial data: 'R'
Serial data: 'L'
Serial data: 'D'
Serial data: 13
Serial data: 10
+50000 cycles
+50000 cycles
+50000 cycles
+50000 cycles
icebreaker_tb.v:37: $finish called at 3000000000 (1ps)

Wallah! We have successfully created a bootloader at home! In future tutorials, we will discuss how to do more bootloader stuff with our simple bootloader.