This post is a draft for Medium.

The RISC-V machine mode timer and timing keeping using the C++ std::chrono library.

How does RISC-V keep time? How can we perform a periodic task with no operating system?

You may take for granted that you can simply ask the operating system to sleep and wake you up in a second. If have programmed bare-metal systems, you’ll understand it’s not as straight forward as calling sleep().

The Machine Level ISA Timer

The RISC-V machine level ISA defines a real-time counter. It is defined as two MMIO system registers mtime and mtimer .

To get an interrupt one second from now, you simply need to set mtimecmp to mtime + 1 second.

The programming model is quite simple - when mtimecmp >= mtime you get an mti interrupt. The mtime register is counter that increases monotonically - forever. The mtimecmp is continously compared to it. As both registers are 64 bits there is no concern about overflow.

While most system registers are accessed via special instructions mtime and mtimecmp, are accessed via MMIO (memory mapped IO). The mtime register depends on a global real time clock, and may need to be placed on a bus shared by many cores.

There is one remaining question, how do we know what 1 second corrosponds to in mtime counts?

Timekeeping in Modern C++

Modern C++ includes the std::chrono library, and std::chrono::literals that allow us to think in terms of human time, not machine time. For embedded systems, time is a first order concern so it is great that C++ makes it a standard part of the language.

Can we have a driver that simply lets as program “give me an interrupt in one second”?

Let’s look at the driver timer.hpp. We can start by definining the period of the mtime clock in C++ terms, via std::chrono::duration. I’ve made this a template as each RISC-V implementation is free to choose it’s mtime clock period. In this case were using a Sifive device, so we can read the clock period from the BSP device tree.

The driver::timer::timer_ticks declaration is the period of mtime. It defines the period as a ratio.

namespace driver {
    struct default_timer_config {
        static constexpr unsigned int MTIME_FREQ_HZ=32768;
    };
    template<class CONFIG=default_timer_config> class timer {
        /** Duration of each timer tick */
        using timer_ticks = std::chrono::duration<int, std::ratio<1, CONFIG::MTIME_FREQ_HZ>>;
    }       
}

Next, how can we convert these timer ticks to another time base? std::chrono::duration_cast does the job. std::chrono::duration_cast<timer_ticks>(time_offset) ratio of the number of seconds to clocks in one second.

If we have a timer value from mtime and want to convert to microseconds, then we use:

uint64_t value_from_mtime = ...;
auto value_in_ms = std::chrono::duration_cast<std::chrono::microseconds>( driver::timer::timer_ticks(value_from_mtime) );

Alternatively to convert from microseconds to a hardware timer value for mtimecmp then we use:

auto time_offset = std::chrono::microseconds(???);
uint64_t  value_of_mtimecmp = std::chrono::duration_cast<timer_ticks>(time_offset).count();

It’s all computed at compile time, so no run-time cost is incurred.

Reading/Writing MMIO Registers in C++

There is not much differnce between accessing MMIO registers in C, and C++. One advantage C++ has is the availability of templates. As RISC-V’s timer registers are not at a fixed address (absolute or relative to each other), re-usable code should be parameterized.

In C we could use a structure to define the location of each register with a run time cost, or a set of pre-procesor macros to make this zero-cost, however in C++ we can pass a structure via a template parameter at zero cost.

struct mtimer_address_spec {
        static constexpr std::uintptr_t MTIMECMP_ADDR = 0x2000000 + 0x4000;
        static constexpr std::uintptr_t MTIME_ADDR = 0x2000000 + 0xBFF8;
};

template<class ADDRESS_SPEC=mtimer_address_spec> 
void set_raw_time_cmp(uint64_t clock_offset) {
   // Single bus access
   auto mtimecmp = reinterpret_cast<volatile std::uint64_t *>(ADDRESS_SPEC::MTIMECMP_ADDR);
   *mtimecmp =  *mtimecmp + clock_offset;
}

64 Bit Registers Access on a 32 Bit Bus.

There is a small complication accessing timer registers, they are 64 bits wide and time tends to update constantly while our program is executing. On a 32 bit system we can only access 1/2 of the register at a time.

Imagine this sequence.

  1. The mtime is 0x0000_0000_FFFF_FFFF.
  2. We read the top 32 bits, 0x0000_0000
  3. We save this into our register t0.
  4. The real time clock ticks.
  5. The mtime is 0x0000_0001_0000_0000.
  6. We read the bottom 32 bits, 0x0000_0000.
  7. We save this into our register t1.
  8. We check the time in t0:t1, it’s 0x0000_0000_0000_0000!

This is one of the problems with bare-metal programing, we are communicating with hardware devices that are operating asynchronous to us, and can mess with out address space at will.

What can we do to deal with this? The upper bytes in mtime are very unlikely to change from read to read, so we can loop while there is a difference between reads. As the variable is marked volatile the compiler knows to keep reading it from “memory” each time. (The one acceptable use of volatile in C++…)

auto mtimel = reinterpret_cast<volatile std::uint32_t *>(ADDRESS_SPEC::MTIME_ADDR);
auto mtimeh = reinterpret_cast<volatile std::uint32_t *>(ADDRESS_SPEC::MTIME_ADDR+4);
uint32_t mtimeh_val;
uint32_t mtimel_val;
do {
    // There is a small risk the mtimeh will tick over after reading mtimel
    mtimeh_val = *mtimeh;
    mtimel_val = *mtimel;
    // Poll mtimeh to ensure it's consistent after reading mtimel
    // The frequency of mtimeh ticking over is low
} while (mtimeh_val != *mtimeh);
return (static_cast<std::uint64_t>(mtimeh_val)<<32)|mtimel_val;

There are similar issues writing to mtimecmp that can cause spurious interrupts. Fortunately the RISC-V spec gives us an example of the code required to avoid this issue….. in RISC-V assembly.

Conclusion

The timer driver covers a few core topics in bare-metal programming.

  • MMIO access and static polymorphism.
  • Hardware real time clocks.
  • Converting clock frequencies and periods to human readable units.
  • Configuring drivers via templates and constexpr.
  • The need to think about bus transfer sizes and asynchronous operation of hardware.

All of them can be handled in C++.