- Code Samples
- The Machine Level ISA Timer
- Bare-Metal Timer Access in C
- Bare-Metal Timer Access in C++
- Running the Example
These code samples relate to this article:
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 mtimecmp.
To get an interrupt one second from now, you simply need to set
mtime + 1 second.
The programming model is quite simple - when
mti interrupt. The
mtime register is counter that increases
monotonically - forever. The
mtimecmp is continuously compared to
it. As both registers are 64 bits there is no concern about overflow.
While most system registers are accessed via special instructions
mtimecmp, are accessed via MMIO (memory mapped IO). The
register depends on a global real time clock, and may need to be placed on a bus
shared by many cores.
Bare-Metal Timer Access in C
For our driver the
mtimer_set_raw_time_cmp() is passed a timeout RELATIVE to the current time. To enable the interrupt mie.mti and mstatus.mie need to be set.
The MTI interrupt does not repeat. The ISR needs to reset the timer compare register mtimecmp at each timeout.
mti pending bit is cleared by updating
The C timer driver also has a function
mtimer_get_raw_time() to return a 64bit raw timestamp from mtime.
The C driver for this post is on github baremetal-startup-c/src/timer.c.
The driver has 2 functions:
These each have two implementations.
- A 64 bit version that performs single read/write accesses.
- A 32 bit version that performs low and high word read/write accesses, while taking care there is no overflow between acceses.
The address of the timer register is platform dependent. These are for the SiFive CLINT implementation.
Read the current timer value:
Set the timer compare value to generate an interrupt:
Bare-Metal Timer Access in C++
As for the C driver the time is passed relative to the current time. The
std::chrono time units can be used to pass a readable timeout.
The interrupt enable is described above and can be seen in src/main.cpp.
The driver for this post is on github baremetal-startup-cxx/src/timer.cpp.
The two registers are accessed via MMIO. The RISC-V spec does not specify a standard address, so they may be mapped to any address location. The addresses here are from the SiFive SVD file, where they are located in the Core Local Interrupt Controller (CLIC).
There is no standard register for the timer period, so again it’s defined as a constant. This info was found in the SiFive device tree file.
There are two core functions defined to access the timer, one to read the timer, the other to update the compare register.
The function below reads the raw timer value. A simple version is
implemented for when an RV64 target is
detected. A more complex version is needed to ensure the
register lower word is does not overflow while we are reading the
The function below writes to the raw compare value. Again, a simple
version is implemented for when an RV64
target is detected. A more complex version is
needed to ensure the
mtimecmp register does not cause a spurious
interrupt when the low word is written. This is done by writing an
impossibly large value to the upper word.
The remaining methods in the class implement C++ specific conversion from raw value to a time duration with defined units.
Running the Example
The C example program and C++ example program can be compiled with CMake and a RISC-V cross compiler, such as xPack GNU RISC-V Embedded GCC. It has been tested with a SiFive HiFive RevB, but is designed to be a generic 32 bit RISC-V example.
It enables a period 1 second machine timer, and the ISR
irq_entry demultiplexes the interrupt cause and saves the timestamp value to a global variable
timestamp on an