- Code Samples
- The Machine Level ISA Timer
- Bare-Metal Timer Access in C
- Bare-Metal Timer Access in C++
- Running the Example
Code Samples
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
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 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
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.
Bare-Metal Timer Access in C
Driver Usage
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.
The mti
pending bit is cleared by updating mtimecmp
.
The C timer driver also has a function mtimer_get_raw_time()
to return a 64bit raw timestamp from mtime.
Driver Implementation
The C driver for this post is on github baremetal-startup-c/src/timer.c.
The driver has 2 functions:
- Raw write to mtimecmp.
- Raw read of mtime.
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++
Driver Usage
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.
Driver Implementation
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 mtime
register lower word is does not overflow while we are reading the
upper word.
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 mti
interrupt.