Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

This post is a draft. Final version on Medium.

As described in Part 1, a simple C++ application to blink an LED, what does this look like with no operating system?

Blinky in C++

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

Blinky

Let’s look at the program flow, and the C++ and RISC-V features used. All code here is C++. The drivers and startup routine not shown here are also C++.

(1) Instantiating the drivers for the timer and GPIO.

    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;

(2) Initializing the timer.

mtimer.set_time_cmp(std::chrono::seconds{1});

(3) Enable the GPIO.

    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);

(4) Declare an interrupt handler.

   static const auto handler = [&] (void) 
        { 
            auto this_cause = riscv::csrs.mcause.read();
            // ...more code...
        } 
    irq::handler irq_handler(handler);

(5) Enable interrupts via system registers.

    #include "riscv-csr.hpp"
    ...
    riscv::csrs.mie.mti.set();
    riscv::csrs.mstatus.mie.set();

(6) Enter a busy loop and stay there.

    do {
        __asm__ volatile ("wfi");  
    } while (true);

(7) Handle the timer interrupt.

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.

I hope this post has demonstrated the basics of a low-level program for the RISC-V and the capabilities of using modern C++ for efficient low-level programming.

Certainly, more abstraction could be used to hide details of the architecture and operation — but that would defeat some of the purpose of this example for learning RISC-V!

Feel free to try it out via the project on GitHub and read the next post to find out how to compile and load the project.

The next post describes the development environment for RISC-V C++.