RISC-V: A Baremetal Introduction using C++. Overview.

Blinky in C++

Here we have blinky on SiFive HiFive1 Rev B development board, built and loaded via Platform IO.

  • RISC-V has a standard timer. However, there is no standard RISC-V GPIO. On this device, we use SiFive’s GPIO.
  • The C++ drivers are using template parameters to configure the drivers for the hardware at compile time. Two methods are used here:
  • For the timer, the template parameter is a trait, a struct that exists purely to isolate the implementation details. Both the address map and base clock frequency are parameterized as these are not standardized for RISC-V.
  • For the GPIO we’re simply passing a value, SIFIVE_GPIO0_0, as the base address of the device as a template parameter.
  • By using C++ templates there is no need to store those parameters in the instance, so these instantiations consume no resources.
    struct mtimer_address_spec {
static constexpr std::uintptr_t MTIMECMP_ADDR = 0x20004000;
static constexpr std::uintptr_t MTIME_ADDR = 0x2000BFF8;
};
struct mtimer_timer_config {
static constexpr unsigned int MTIME_FREQ_HZ=32768;
};
static constexpr uintptr_t SIFIVE_GPIO0_0 = 0x10012000;
...
driver::timer<mtimer_address_spec, mtimer_timer_config> mtimer;
driver::sifive_gpio0_0_dev<SIFIVE_GPIO0_0> gpio_dev;
  • The timer is set to interrupt after a one-second timeout. C++’s std::chrono is used to convert from human-readable time into hardware clocks behind the scenes at compile time.
  • RISC-V defines a standard machine mode timer with a compare and time counter register.
mtimer.set_time_cmp(std::chrono::seconds{1});
  • This driver is not hiding the hardware details, it simply exposes the registers.
  • The pin assignment details are hidden with a variable, LED_MASK_WHITEdefined as a C++ constexpr. A constexpr has zero runtime cost and allows us to avoid the issues of the C Pre-processor. Modern C++ has almost no need to use the C++ preprocessor.
    static constexpr int LED_RED=22;
static constexpr int LED_GREEN=19;
static constexpr int LED_BLUE=21;
static constexpr unsigned int LED_MASK_WHITE=
bitmask(LED_RED)|bitmask(LED_GREEN)|bitmask(LED_BLUE);
...
gpio_dev.output_val &= ~(LED_MASK_WHITE);
gpio_dev.output_en |= (LED_MASK_WHITE);
  • RISC-V defines a standard system register for interrupt vector configuration.
  • As this is modern C++ we can use a lambda function that avoids the need to share data between the interrupt and main function via globals.
  • The lambda captures its context (that is the variables in the current scope) via references.
  • The details of loading the lambda function are hidden in the irq_handler class’s templated constructor.
   static const auto handler = [&] (void) 
{
auto this_cause = riscv::csrs.mcause.read();
// ...more code...
}
irq::handler irq_handler(handler);
  • The RISC-V defines system registers for the timer interrupt (mie.mti) and global interrupt (mstatus.mie) enables.
  • RISC-V system registers are accessed via special instructions, and these C++ statements will compile to those instructions.
  • We can use a C++ class riscv::csr::all instantiated as riscv::csrs to provide an abstraction for the RISC-V system registers.
    #include "riscv-csr.hpp"
...
riscv::csrs.mie.mti.set();
riscv::csrs.mstatus.mie.set();
  • There is no C++ here, just the wait for interrupt instruction. More program logic and abstraction could be added here, but blinky does not need it.
    do {
__asm__ volatile ("wfi");
} while (true);
  • The interrupt handler reads the mcause system register to de-multiplex the cause of the interrupt.
  • The RISC-V machine timer is not auto-reloading, so the handler must reload it.
  • While this example uses a single IRQ handler, RISC-V supports both multiplexed and vectored interrupts.
  • The GPIO output_val bits are toggled to blink the LED. The register interface is simple, so no more abstraction is added.
auto this_cause = riscv::csrs.mcause.read();
if (this_cause &
riscv::csr::mcause_data::interrupt::BIT_MASK) {
this_cause &= 0xFF;
switch (this_cause) {
case riscv::interrupts::mti :
mtimer.set_time_cmp(std::chrono::seconds{1});
timestamp =
mtimer.get_time<
driver::timer<>::timer_ticks>().count();
gpio_dev.output_val ^= (LED_MASK_WHITE);
break;
}
}

The Complete Example

Putting together the above steps we can write the main() function that will set up a one-second timer and blink the LED.

int main(void) {// Device drivers
driver::sifive_gpio0_0_dev<SIFIVE_GPIO0_0> gpio_dev;
driver::timer<> mtimer;
// Device Setup // Save the timer value at this time.
auto timestamp =
mtimer.get_time<driver::timer<>::timer_ticks>().count();
// Setup timer for 1 second interval
mtimer.set_time_cmp(std::chrono::seconds{1});
// Enable GPIO
gpio_dev.output_val &= ~(LED_MASK_WHITE);
gpio_dev.output_en |= (LED_MASK_WHITE);
// The periodic interrupt lambda function.
// The context (drivers etc) is captured via reference using [&]
static const auto handler = [&] (void)
{
// In RISC-V the mcause register stores the
// cause of any interrupt or exception.
auto this_cause = riscv::csrs.mcause.read();
// For simplicity non-vectored interrupt mode is used.
// The top bit of the mcause register indicates
// if this is an interrupt or exception.
if (this_cause &
riscv::csr::mcause_data::interrupt::BIT_MASK) {
this_cause &= 0xFF;
// De-multiplex the interrupt
// The cause register LSBs hold an integer value
// that represents the interrupt source
switch (this_cause) {
case riscv::interrupts::mti :
// A machine timer interrupt
// RISC-V machine mode timer interrupts
// are not repeating.
// Set the timer compare register to the
// current time + one second
mtimer.set_time_cmp(std::chrono::seconds{1});
// Save the timestamp as a raw counter in
// units of the hardware counter.
// While there is quite a bit of code here,
// it can be resolved at compile time to a
// simple MMIO register read.
timestamp =
mtimer.get_time<
driver::timer<>::timer_ticks>().count();
// Xor to invert. This can be compiled to a
// write to the toggle register via
// operator overloading.
gpio_dev.output_val ^= (LED_MASK_WHITE);
break;
}
}
};
// Install the above lambda function as the machine mode
// IRQ handler.
irq::handler irq_handler(handler);
// Enable interrupts
riscv::csrs.mie.mti.set();
// Global interrupt enable
riscv::csrs.mstatus.mie.set();
// Busy loop
do {
__asm__ volatile ("wfi");
} while (true);
return 0; // Never executed
}

Last Words

It’s compact C++ code but does not hide the operation of the hardware.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Phil Mulholland

Phil Mulholland

Experienced in Distributed Systems, Event-Driven Systems, Firmware for SoC/MCU, Systems Simulation, Network Monitoring and Analysis, Automated Testing and RTL.