RISC-V: A Baremetal Introduction using C++. System Registers.

This is the fifth post in a series.

What are system registers in RISC-V? How can we access them with modern C++?

System registers require special instructions to access, so unlike memory-mapped registers (MMIO) we can’t just cast a pointer to memory to get access them in C++.

Do we need to embed inline assembly in our code, destroying the flow of our clean C++? No, with some abstraction we can write code like this:

RISC-V Special Instructions and C++

How does the above code generate custom instructions? The riscv-csr.hpp header provides the abstractions. That’s a huge file, but it is generated from a much more compact template file templates/riscv-csr.hpp.

To understand how it works let's look at how just one system register, such as mtvec, can be written. The csrw instruction will write to the register, and the assembler can recognize mtvec and encode it to register number 0x0305.

GCC inline assembler is required, but we can hide it within a static inline method of a struct.

The function above is fine, and we could use it as-is, but it’s not very C++-ish (modern or classic), and it will get messy once we try and write immediate values, or do atomic write to bitfields in CSRs. (Those have their own instructions such as csrwi and csrrw).

You may have seen the function was declared with a generic name, write(), and not mtvec_write() or something specialized. Instead, the target system register was scoped was provided by a traits-like structure, mtvec_ops {} .

This will enable us to do some generic programming. We can achieve that by declaring similar structs with methods of the same name, write(), for all special instructions. Then we use the structure as a template parameter to a generic register access class, such as read_write_reg.

Finally, this templated class read_write_reg<mtvec_ops> can be aliased via using and we can give it a simple name, such as mtvec_reg.

For simplicity, the whole read_write_reg class is not shown here, just the write function and instantiation.

We can now write to the interrupt vector in clean C++ code.

RISC-V CSRs and Bit Level Access

We’ve now seen how mtvec.write( ...); works. How about accessing fields?

RISC-V has a set of atomic read and write or set/clear bits instructions. These can be used to modify fields of system registers.

As you would expect, with the same method as above C++ can abstract those instructions. Using constexpr we can use compile-time conditional code to select instructions.

While that is very verbose, it is easy to generate. We can now enable the interrupt vector in clean C++ code.

Conclusion

So were we able to do this in pure C++? Yes (with a touch of inline assembly). In fact, using C++ has opened up programmable compile-time optimizations implemented at the instruction level that would not be possible in C.

As for RISC-V? Another topic that can be explored using this method is custom instruction extensions. I will explore that later.

The next post will explore the machine mode timer.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Phil Mulholland

Experienced in Distributed Systems, Event-Driven Systems, Firmware for SoC/MCU, Systems Simulation, Network Monitoring and Analysis, Automated Testing and RTL.