Background
I'm trying to understand the boot process for a simple RISC-V computer in detail. I'm not interested in getting all the way to an operating system: I just want to know what happens between poweron and running a bare-metal program to write some data to the UART device.
Since booting is hardware-specific, I picked the SiFive HiFive1 RevB because:
- It's relatively cheap
- It has both real hardware and can be emulated via QEMU's
sifive_e
machine
(However, if there is a more appropriate board, I would also like to know)
My goal is to understand all of the code involved in the boot process. I am particularly confused about the role of the following:
- Firmware
- The bootloader
- QEMU's
-bios
flag - QEMU's
-kernel
flag
Desired outcome
As proof that I understand all the pieces going into the boot process, I would like to write a program which writes "Hello World" to the UART0 device, and then halts (either with a busy loop, or somehow by powering off the board).
Ideally, I would write all the pieces needed to make this happen in RISC-V assembly. I would also like to avoid using any high level languages, toolchains, and linkers.
A working version for the QEMU virt
machine
I have a simple program which works as expected on QEMU's virt
machine.
It's a binary file test.bin
of RISC-V instructions produced by a python script--it is not produced by gcc or another compiler.
Here is the contents of the file according to objdump:
> objdump -b binary --architecture=riscv -D test.bin
test.bin: file format binary
Disassembly of section .data:
0000000000000000 <.data>:
0: 00100093 li ra,1
4: 01c09093 slli ra,ra,0x1c
8: 01300113 li sp,19
c: 00c11113 slli sp,sp,0xc
10: 04800113 li sp,72
14: 00208023 sb sp,0(ra)
18: 06500113 li sp,101
1c: 00208023 sb sp,0(ra)
20: 06c00113 li sp,108
24: 00208023 sb sp,0(ra)
28: 06c00113 li sp,108
2c: 00208023 sb sp,0(ra)
30: 06f00113 li sp,111
34: 00208023 sb sp,0(ra)
...snip
This generates the UART0 address (0x10000000
) of the virt
machine and places it in register R1
.
It then simply writes each byte of "Hello, World" to that address.
I run this file as follows:
> qemu-system-riscv32 -nographic -machine virt -bios test.bin
Hello, World!
QEMU 7.1.0 monitor - type 'help' for more information
(qemu) quit
The final two lines of output here are because QEMU appears to hang, and I use C-a c; quit
to exit.
I can also run the image using the -kernel
flag:
> qemu-system-riscv32 -nographic -machine virt -kernel test.bin
Hello, World!
Hello, World!
Hello, World!
...
In this case, it loops printing "Hello World" until I quit.
I assume this is because the virt
machine has a default bios which sets up handlers for illegal instructions and restarts my program: since test.bin
does not busy loop at the end, this triggers the handler and restarts.
Question 1
- If I use
-bios test.bin
, is my code the first code running when the CPU boots, or does some other code run first? (For example "firmware") - Is there a "firmware stage" that happens before the BIOS?
- What does the default
-bios
for QEMU'svirt
machine do?
Porting to the sifive_e
machine
I expected that I could just port this code to the sifive_e
machine by changing the UART0 address to 0x10013000
Here is the objdump for the modified code sifive_e.bin
, which computes the UART address by adding together R1
and R2
:
0: 00100093 li ra,1
4: 01c09093 slli ra,ra,0x1c
8: 01300113 li sp,19
c: 00c11113 slli sp,sp,0xc
10: 002080b3 add ra,ra,sp
14: 04800113 li sp,72
18: 00208023 sb sp,0(ra)
1c: 06500113 li sp,101
20: 00208023 sb sp,0(ra)
However, when I run this as follows:
qemu-system-riscv32 -nographic -machine sifive_e -bios sifive_e.bin
QEMU produces no output and appears to hang. This is also the case if I run it like this:
qemu-system-riscv32 -nographic -machine sifive_e -kernel sifive_e.bin
Question 2
- What is missing to make this example work on the
sifive_e
machine? - Where can I find documentation on the boot process for
sifive_e
? - Does the boot process of the QEMU
sifive_e
machine really correspond to that of theHiFive1 RevB
board?