Contents
Baremetal RISC-V Renode Series
I'm exploring the line between hardware and software by creating a series of demos within a minimal, free and open source environment. These demos span from blinking an LED to implementing a toy operating system. The goal is to minimize parts of the system that we take for granted and gain a better understanding of computers and operating systems.
In Part 1, we setup a bare minimum LED blinking example to demonstrate how to compile your development environment and debug the software in real-time using GDB.
Background
In this part we'll start working towards operating system features, specifically being able to share a CPU with multiple processes simultaneously. I call this toy operating system KohlerOS or KOS, pronounced chaos, for short.
At a minimum, the there are a few things required to demonstrate multitasking. First you need a way to define a program. In linux, this would be an ELF binary, on windows it would PE format. The job of both of these are to deliver native machine code with enough metadata to launch and link with other binaries such as libraries. Next you need a way to actually start and manage a program running within particular region of memory. This we call a process..
Program
We are using the UART from the Litex project. Litex is a High Level HDL project that makes it easy to design a system on a ship and target both simulations and FPGA.
Because we are dealing with virtual hardware there isn't a datasheet. Instead we have 3 different code repositories that are useful for understanding the virtual hardware.
- The actual Litex hardware description for the UART
- A UART driver also provided by the Litex project
- The Renode project provides a software emulation of the Litex UART. This implements the hardware functionality without have to do a full verilog or gate-level simulation.
To get the offsets and registers the easiest way was to look at the Renode emulation directly. You can see that
private enum Registers : long
{
RxTx = 0x0,
TxFull = 0x04,
RxEmpty = 0x08,
EventStatus = 0x0c,
EventPending = 0x10,
EventEnable = 0x14,
}
The actual configuration of the UART hardware is a very simple addition:
vexriscv.repl
...
uart: UART.LiteX_UART @ sysbus 0x60001800
-> cpu@2
All we needed to decide was where in memory to map the hardware, and what interrupt number to wire it to.
A quick note about using C
This example is going to use C functions in addition to assembly.
Since we are baremetal we need to set up the stack pointer ourselves.
baremetal.s
# setup a stack pointer
la sp, memtop
Interrupt Handling
Interrupts are an asynchronous way to externally trigger the CPU to jump.
Typically they jump to a particular memory location, or a location + an offset based on the reason for the interrupt.
RISC-V interrupts
RISC-V interrupts come in two flavors, the original Core Local Interrupter (CLINT), and the Core Local Interrupt Controller (CLIC). The difference between the two, and much more, is described in the sifive interrupt cookbook.
Driver Code
All that is left is to write the code to actually interact with the hardware.
Note that we are going for understandability, not performance, so we are creating an unbuffered solution here.
Define a hardware register map to memory.
baremetal.c
typedef struct
{
uint32_t RxTx;
uint32_t TxFull;
uint32_t RxEmpty;
uint32_t EventStatus;
uint32_t EventPending;
uint32_t EventEnable;
} UART;
const uint32_t TxEvent = 0b01;
const uint32_t RxEvent = 0b10;
volatile UART *const uart = (UART *)0x60001800;
We need to set a flag in the UART to enable interrupt events.
void init_uart()
{
uart->EventEnable = RxEvent;
}
This is called in during startup, right before the final wfi spin-loop.
...
# set mie.MEIE=1 (enable M mode external interrupts)
li t0, 0b0000100000000000
csrrs zero, mie, t0
call init_uart
wait_for_interrupt:
wfi
j wait_for_interrupt
...
Then we just need to specify what to do when an interrupt comes in.
In the real world you would need to check the reason code and figure out:
- What type of interrupt are we handling
- What is the reason for the interrupt?
We can safely ignore this for our demo because the only source of interrupts will be the UART receiving a character.
void interrupt_handler()
{
fancy_char((char)uart->RxTx);
}
For fun, we echo the transmitted character surrounded by an ASCII art border.
void fancy_char(char c)
{
char s[] = "\n###\n\r#X#\n\r###\n\r\n\r";
s[7] = c;
puts(s);
}
void putc(char c)
{
uart->RxTx = c;
}
void puts(char *str)
{
while (*str != '\0')
putc(*str++);
}
Run the example
Ensure you have the setup from Part 1 completed.
Switch to the folder 3_uart
In one terminal run:
$ make start
then in another terminal:
$ make uart-poll
then you can send characters via the UART connection.