How do I write a bare-metal RISC-V program to go from boot to writing to the UART0 on QEMU's `sifive_e` machine?

795 Views Asked by At

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's virt 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 the HiFive1 RevB board?
0

There are 0 best solutions below