- Code Samples
- Basic C++ Start-up
- RISC-V Start-up Specifics
- C/C++ Start-up Specifics
- Dead End
These code samples relate to this article:
Basic C++ Start-up
From processor reset to entering the
main() function some work
needs to be done to allow a C or C++ program to execute. You will find that generally the
device SDK/BSP will provide code or libraries to care of this. However, as RISC-V
gives you the a great chance to customize a CPU or build a custom SoC, you may also find it useful to
understand this bootstrapping process. I think you will agree it’s easier to understand C++ than assembly.
You will find an example of a stripped down bare-metal test program is in the scratchpad repo. The whole example is implemented in C++ so the high level program flow is easier to understand.
These are the relevant files for this post:
- baremetal-startup-cxx/src/startup.cpp : The start-up code.
- baremetal-startup-cxx/src/linker.lds : An example linker file. This is taken from SiFive’s HiFive RevB BSP.
NOTE - These examples are intended to give an overview for experimenting with RISC-V systems. They are not intended for production code.
RISC-V Start-up Specifics
The initial entry code needs to be in assembler, GCC’s extended
used to embedded this in the C++ source file. The
enter() function must be placed in the location where
code execution starts by specifying the linker section. In this
.text.metal.init.enter linker section is used to place
it at the entry point. Unlike platforms like ARM Cortex-M, the entry point is not specified by the RISC-V
specification, or in the interrupt vector table, but is a vendor specific configuration.
The function is marked
naked. At this point there is no stack and no variables (global or local) can be accessed.
- The stack pointer is configured with a location
_spfrom the linker script. This allows a non
nakedC function to be called.
- The global pointer is configured with a location
_global_pointer$from the linker script. The GCC compiled code relies on this to access global variables. (see https://www.sifive.com/blog/all-aboard-part-3-linker-relaxation-in-riscv-toolchain)
This example is for a single core, so it does not try and identify the current hart but just proceeds to jump to the main startup function implemented in C++. See the freedom-metal entry code for a more complete example that considers multi-hart start-up.
C/C++ Start-up Specifics
Before any standard C or C++ code can be called the run time environment needs to be initialized. The linker defines a number of regions in memory that the start-up file uses to perform initialization.
bssregion contains global variables with no initial value. The SRAM allocated to these variables is cleared to 0.
datasection contains global variables with initial values. These values are copied from read-only memory (FLASH/ROM) to SRAM.
itimsection is a code section that is to be moved to SRAM to improve performance.
initarray is a table of constructor function pointers to construct global variables. Beware the static initialization order.
Once those regions are initialized C/C++ code can be run,
main() is called.
main() function should never exit, but in the event it does a busy loop is invoked. The
wfi instruction should put the core in low power mode when it’s idle - but that is implementation specific.
This C++ start-up code is based on examples in chapter 8 of Christopher Kormanyos’s Real Time C++, code examples for AVR, ARM, Renesas etc are on github.
A more complete start-up environment is given by SiFive in their Freedom Metal SDK. The linker script used here is from the Freedom E-SDK.
ARM’s CMSIS gives a good SoC independent minimal bare metal initialization examples for ARM Cortex-M devices. e.g. Cortex-M0 start-up. I find this an easier way to understand the start-up flow required for a C runtime environment.