Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

This post is a draft for Medium.

This is the sixth post in a series, about 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). This is because 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 corresponds 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. This is a template as the mtime clock period is defined by the implementation. (For a SiFive device we can find the clock period and other parameters in 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 difference between accessing MMIO registers in C, and C++. One advantage C++ has is 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. Here that is done via template parameters.

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

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

Conclusion

The timer driver covers a few core topics in bare-metal programming and how C++ can provide an advantage.

The next post will look at handling interrupts.



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.