Code Samples
These code samples relate to this article:
Basic C Start-up
This describes a basic generic example of a RISC-V startup routine written in C.
While generally the device SDK/BSP will provide code to take care of this, RISC-V lowers the barrier to building new cores and systems, so it is worth understanding and customizing the low level process.
An example of a stripped down bare-metal C test program is in the scratchpad repo. Rather than mix assembly and C source files, it is all in C for simplicity.
These are the relevant files for this article:
- 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
inline assembler
keyword is
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
example the .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
_sp
from the linker script. This allows a nonnaked
C 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 Start-up Specifics
Before any standard 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.
- The
bss
region contains global variables with no initial value. The SRAM allocated to these variables is cleared to 0. - The
data
section contains global variables with initial values. These values are copied from read-only memory (FLASH/ROM) to SRAM. - The
itim
section is a code section that is to be moved to SRAM to improve performance. - The
init
array is a table of constructor function pointers to construct global variables.
For simplicity memcpy()
and memset()
from the C library are
used. Startup routines often directly implement these functions to
remove such a dependency.
Once those regions are initialized C code can be run, main()
is called.
Dead End
The 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.
References
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.