Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

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.

    // Enable MIE.MTI
    csr_set_bits_mie(MIE_MTI_BIT_MASK);

    // Global interrupt enable 
    csr_set_bits_mstatus(MSTATUS_MIE_BIT_MASK);
    
    // Setup first Interrupt
    mtimer_set_raw_time_cmp(MTIMER_SECONDS_TO_CLOCKS(1));

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.

static void irq_entry(void)  {
    uint_xlen_t this_cause = csr_read_mcause();
    if (this_cause &  MCAUSE_INTERRUPT_BIT_MASK) {
        // Known exceptions
        switch (this_cause & 0xFF) {
        case RISCV_INT_MASK_MTI :
            // Timer exception, keep up the one second tick.
            mtimer_set_raw_time_cmp(MTIMER_SECONDS_TO_CLOCKS(1));
            break;
        }
    }
}

The C timer driver also has a function mtimer_get_raw_time() to return a 64bit raw timestamp from mtime.

    uint64_t timestamp = mtimer_get_raw_time();

Driver Implementation

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.

The address of the timer register is platform dependent. These are for the SiFive CLINT implementation.

#define RISCV_MTIMECMP_ADDR (0x2000000 + 0x4000)
#define RISCV_MTIME_ADDR    (0x2000000 + 0xBFF8)

Read the current timer value:

uint64_t mtimer_get_raw_time(void) {
#if ( __riscv_xlen == 64)
    volatile uint64_t *mtime = (volatile uint64_t *)(RISCV_MTIME_ADDR);
    return *mtime;
#else
    volatile uint32_t * mtimel = (volatile uint32_t *)(RISCV_MTIME_ADDR);
    volatile uint32_t * mtimeh = (volatile uint32_t *)(RISCV_MTIME_ADDR+4);
    uint32_t mtimeh_val;
    uint32_t mtimel_val;
    do {
        mtimeh_val = *mtimeh;
        mtimel_val = *mtimel;
    } while (mtimeh_val != *mtimeh);
    return (uint64_t) ( ( ((uint64_t)mtimeh_val)<<32) | mtimel_val);
#endif
} 

Set the timer compare value to generate an interrupt:

void mtimer_set_raw_time_cmp(uint64_t clock_offset) {
    uint64_t new_mtimecmp = mtimer_get_raw_time() + clock_offset;
#if (__riscv_xlen == 64)
    volatile uint64_t *mtimecmp = (volatile uint64_t*)(RISCV_MTIMECMP_ADDR);
    *mtimecmp = new_mtimecmp;
#else
    volatile uint32_t *mtimecmpl = (volatile uint32_t *)(RISCV_MTIMECMP_ADDR);
    volatile uint32_t *mtimecmph = (volatile uint32_t *)(RISCV_MTIMECMP_ADDR+4);
    *mtimecmph = 0xFFFFFFFF;  // cppcheck-suppress redundantAssignment
    *mtimecmpl = (uint32_t)(new_mtimecmp & 0x0FFFFFFFFUL);
    *mtimecmph = (uint32_t)(new_mtimecmp >> 32); // cppcheck-suppress redundantAssignment
#endif
}

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.

// Instanciate Timer driver 
static driver::timer<> mtimer;
// Set timeout
mtimer.set_time_cmp(std::chrono::seconds{1});

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

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

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.

    struct default_timer_config {
        // See
        // freedom-e-sdk/bsp/sifive-hifive1-revb/core.dts
        // psdlfaltclk: clock@6
        // Fixed to 32Khz
        static constexpr unsigned int MTIME_FREQ_HZ=32768;
    };

    /** Duration of each timer tick */
    using timer_ticks = std::chrono::duration<int, std::ratio<1, CONFIG::MTIME_FREQ_HZ>>;

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.

        uint64_t get_raw_time(void) {
            if constexpr ( __riscv_xlen == 64) {
                // Directly read 64 bit value
                auto mtime = reinterpret_cast<volatile std::uint64_t *>(ADDRESS_SPEC::MTIME_ADDR);
                return *mtime;
            } else {
                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;
            } 
        }

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.

        void set_raw_time_cmp(uint64_t clock_offset) {
            // First of all set 
            auto new_mtimecmp = get_raw_time() + clock_offset;
            if constexpr ( __riscv_xlen == 64) {
                // Single bus access
                auto mtimecmp = reinterpret_cast<volatile std::uint64_t *>(ADDRESS_SPEC::MTIMECMP_ADDR);
                *mtimecmp = new_mtimecmp;
            } else {
                auto mtimecmpl = reinterpret_cast<volatile std::uint32_t *>(ADDRESS_SPEC::MTIMECMP_ADDR);
                auto mtimecmph = reinterpret_cast<volatile std::uint32_t *>(ADDRESS_SPEC::MTIMECMP_ADDR+4);
                // AS we are doing 32 bit writes, an intermediate mtimecmp value may cause spurious interrupts.
                // Prevent that by first setting the dummy MSB to an unacheivable value
                *mtimecmph = 0xFFFFFFFF;  // cppcheck-suppress redundantAssignment
                // set the LSB
                *mtimecmpl = static_cast<uint32_t>(new_mtimecmp & 0x0FFFFFFFFUL);
                // Set the correct MSB
                *mtimecmph = static_cast<uint32_t>(new_mtimecmp >> 32); // cppcheck-suppress redundantAssignment
            }
        }

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.