Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

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:

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.

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.

void _enter(void)  __attribute__ ((naked, section(".text.metal.init.enter")));

void _enter(void)   {
    // Setup SP and GP
    // The locations are defined in the linker script
    __asm__ volatile  ("la    gp, __global_pointer$;"
                      "la    sp, _sp;"
                      "jal   zero, _start;"
                      :  /* output: none */
                      : /* input: none */
                      : /* clobbers: none */); 
    // This point will not be executed, _start() will be called with no return.

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.

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.

// At this point we have a stack and global pointer, but no access to global variables.
void _start(void) __attribute__ ((noreturn));
void _start(void) {

    // Init memory regions
    // Clear the .bss section (global variables with no initial values)
           (metal_segment_bss_target_end - metal_segment_bss_target_start));

    // Initialize the .data section (global variables with initial values)

    // Initialize the .itim section (code moved from flash to SRAM to improve performance)

    // Call constructors
    for (unsigned int i=0; i< (__init_array_start-__init_array_end)/sizeof(function_t); i++) {


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.

void _Exit(int exit_code) __attribute__ ((noreturn));
void _Exit(int exit_code) {
    // Halt
    while (1) {
        __asm__ volatile ("wfi");


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.