Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

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

This post is a draft. Final version on Medium.

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++.