Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

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 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++

The driver for this post is on github baremetal-startup-cxx/src/startup.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.