RISC-V: A Baremetal Introduction using C++. Development Environment

Phil Mulholland
4 min readMay 23, 2021


Following on from Part 2, how do we compile this project and run it?

For this series of posts, my platform is a SiFive HiFive1 Rev B development board. It’s equipped with a 320MHz RV32IMAC (FE310 core). For software build and debug the choice is Platform IO, an IDE integrated into VS Code.


To setup a new project in Platform IO just select the board, framework, and path for the files. For this exercise, the choices are HiFive1 Rev B, Freedom E SDK, and a custom path. Customization can be done via the platformio.ini file. We need to do this to configure our C++ version and baremetal environment.

In this example platformio.ini's build_flags has been extended with options to target embedded C++17. In particular:

  • -nostartfiles is used as this example includes a custom startup routine,
  • -std=c++17 for all the needed modern C++ features, and
  • -fno-threadsafe-statics to prevent threadsafe code from being emitted (we have no threads and these ensure static declarations are initialized only once).

The configuration build_flags captures compiler, pre-processor, and linker options, so we can add -Wl,-Map,blinky.map to generate a ma here alongside compiler options.

The complete set of build flags used is here:

build_flags = 

That configuration is all we need to build this project.

Adding a Post Compile Action

For this exercise, we’ll want to check exactly how the compiler is transforming our C++ code. Disassembling the output will show us that.

How can Platform IO do this? We need to edit platform.ini again, this time adding a helper script and additional targets.

extra_scripts = post_build.py
targets = disasm

The build system behind Platform IO is Scons. This is a Python-based system, so the post build script is a python file.

The Scons environment is used to find the path to the output file, ${BUILD_DIR}/${PROGNAME}.elf, and the toolchain objdump is found by modifying the path to${OBJDUMP}. The env.subst() will expand the environment variables, and env.Execute() will run the command.

def after_build(source, target, env): 
""" Run objdump on the target elf file and save the
output in the top dir.
cmd=" ".join([
objdump, "-SC","--file-start-context", "-w",
src_elf,">","${PROGNAME}.disasm"]) #

The target is given a name disasm via theenv.AddCustomTarget() method. This ties into the platformio.ini option targets = disasm above.

title="Disasm 2",
description="Generate a disassembly file on demand",

The above is all that is needed to compile the project. Loading it is simple, Platform IO takes care of it.

The project for this example is here. You should be able to load this project with Platform IO and try it out. With some small modifications, it should work on any other toolchain, IDE, or target.

The next post looks at how the core starts from reset and executes the program.

The rest of this post will look at other development environments and target platform options.

Other C++ Development Environments

PlaformIO as it’s a great way to get started with no messing around, but let’s explore a few other options that are available.

The standard toolchain for RISC-V is GCC, although LLVM RISC-V support is also available. Platform IO will automatically install SiFive’s toolchain and SDK.

If you are like most developers, an IDE may be your choice.

  • SiFive, like most processor vendors, has a GCC and Eclipse-based IDE, the Freedom Studio.
  • For a vendor-independent path GNU MCU Eclipse supports RISC-V compilation and debugging very well. I have personally used this to target proprietary RISC-V cores and simulators with success.

However, if like me your usual preference is command-line oriented, then Make or CMake may be your choice.

Compiling your own GCC is useful for those looking for bleeding-edge extension support, or to change variant that the default C libraries are compiled for. (For example, In the past I have used this option to target an RV32EC core before pre-compiled binaries were available.)

The IDEs above all use OpenOCD for debugging support, and this can be downloaded and compiled from OpenOCD source on GitHub, or binaries are available from xpack.

A complete list is maintained by the RISC-V org.

Other RISC-V Devices

These posts target SiFive’s HiFive1 Rev B as it’s easily available and easy to use. However, the point of programming RISC-V at this low level is to make use of the open architecture to build bigger custom systems. As I’ve been on the firmware side this list is not complete, but just a starting point:

The main IP vendors seem to be:

As RISC-V is an open architecture there are many other options.

  • Professionally my experience has been with IQonIC Works RISC-V IP. They provide a small RV32EC core targeted at deeply embedded applications, where stripped-down bare-metal programming is essential.
  • Many open-source options.

A complete list is maintained by the RISC-V org.



Phil Mulholland

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