“RISC-V: A Baremetal Introduction using C++. Intro.
What does it look like to program with no operating system? Can we have direct access to hardware using a high-level language like C++? How does RISC-V work at the most stripped-back bare metal level?
This is a series of posts where I’d like to combine those topics for embedded systems programming. RISC-V and C++ have been evolving rapidly, and you will see modern C++ is a great way to explore RISC-V and develop embedded systems.
Is it possible to write pure modern C++ bare-metal firmware from the ground up for RISC-V?
RISC-V?
RISC-V has been getting a lot of publicity as a new ISA that’s taking on the entrenched players such as ARM and x86–64. Realistically, RISC-V won’t be displacing ARM in the standard platforms of the embedded or mobile space or replacing x86–64 on the desktop or server space anytime soon.
However, that is not to say RISC-V won’t gain much traction. I believe openness in computer hardware will be a transformative technology, much like Linux has shown the power of an open operating system. Linux never directly displaced the applications of desktop computers, but it completely transformed mobile and cloud computing. There is a lot of room for this architecture in many embedded applications, and we will be seeing applications that benefit from the open architecture grow and thrive on this platform.
RISC-V combines the simplicity of a minimal core architecture (RV32E, RV32I, and RV64I) with the ability to extend the architecture as required (e.g. _M_ultiply Instructions, _A_tomics, _C_ompressed instructions). Unlike, say the ARM Cortex-M, it is not designed for embedded systems, but the simplicity of machine mode and the RV32I/RV32E ISAs does suit embedded systems. This series of posts targets a RV32IMAC core.
As for my experience, a few years ago I was responsible for a firmware project for a custom SoC controlled by a small RISC-V RV32EC core. There was no room for an RTOS, so it was bare-metal. This was for an in-house RISC-V implementation, with in-house IP, and RISC-V was a new platform. It was a challenge, working out the details of this new architecture for such an embedded application.
I’d like to put back some of my knowledge related to RISC-V gained at that time in these posts. The knowledge can be applied to developing firmware for standard or custom RISC-V cores and SoCs.
Bare-metal?
Bare-metal programming for RISC-V machine mode is the target of these posts. That is, there is no operating system, everything we do will interact directly with the hardware.
Where can bare-metal programming be used? I first developed a taste for it a long time ago, when coding games in m68k assembler and directly accessing video and audio hardware. Since then some examples of where I’ve used it professionally are:
- Verification and bring up of processor cores and peripherals.
- Porting and debugging RTOSes for new processors, SoCs.
- Deeply embedded firmware for USB-PD/USB Type-C and power conversion.
- Debugging embedded Linux boot-up and drivers.
Is it directly useful for everyday mainstream software development? Absolutely not! But does it help you understand the core skills of software development, Absolutely! I believe understanding the underlying hardware model of computing opens up a deep understanding of software.
Modern C++?
The evolution of C++ has unleashed a modern low overhead language. While far more complex than C, the traditional language of embedded systems, modern C++ can achieve a higher-level abstraction of programming, with less cost in terms of memory usage and instruction count. In this post, I’m targeting C++17 but will look to C++20 in later posts.
The content in these posts has been inspired by reading Real-Time C++: Efficient Object-Oriented and Template Microcontroller Programming, although I have my own differing opinions on how some code should be structured.
The example for this series of posts is the classic blinky program. I’d like to use it to explore a number of topics of RISC-V and C++. Each post will focus on different aspects of RISC-V and modern C++ for embedded systems. As it turns out, there is a lot of work to program firmware from scratch, this is the breakdown:
Part 1. This Intro.
- About RISC-V, bare-metal programming, and C++.
Part 2. The main()
Program overview.
- What does a pure C++ application look like for embedded RISC-V?
Part 3. The Development Environment.
- Compiling, debugging, flashing to the RISC-V target.
- What target platform and toolchains are used for this exercise?
Part 4. Zero to main()
for RISC-V, in (almost) pure C++.
- How do RISC-V processors exit reset and set up the stack and global environment?
- How can we use the C++ algorithms library to simplify startup?
Part 5. The system registers of RISC-V and zero cost C++ abstractions.
- What do we need RISC-V system registers for? How can we access them?
- Can C++ hide the complexity of using special instructions?
Part 6. The RISC-V machine mode timer and timekeeping using the C++ std::chrono library.
- What does the standard RISC-V timer look like?
- How can we abstract timing using
std::chrono
in C++? Can C++ access MMIO registers?
Part 7. The basics of RISC-V interrupt handling, and C++ lambda functions.
- What is the basic interrupt model or RISC-V?
- What is a C++ lambda function? Can they handle interrupts in C++?
The Next Step.
Please read on to the next post to find out more.