This post is a draft for Medium.

Following on from setting up the development environment in the previous post.

How do we go from from reset in RISC-V to entering the main() function in C++. Startup code is generally not something you need to worry about, however it is of interest when bringing up a device from scratch. It’s usually written in assembly and distributed by the device vendor, but RISC-V opens up the chance to

So, can we write such code in pure C++?

This C++ start-up code is based on examples in chapter 8 of Christopher Kormanyos’s Real Time C++. His code examples for AVR, ARM, Renesas etc are on github. He shows we can implement most of the code in C++.

Booting a RISC-V Core

Let’s start by looking at the RISC-V specifics needed to boot. This is where the software starts executing.

  • Where does the execution start at reset?

    RISC-V does not define a standard entry point or reset vector. For SiFive’s core we can locate the entry code in the linker section .text.metal.init.enter, but this will change for other RISC-V cores.

  • How is the stack created?

    The stack pointer sp needs to be initialized manually with a move (Unlike the Cortex-M). The linker script defines a global symbol _sp for the top of the stack, and reserves __stack_size below it.

  • Do any other registers need to be initialized?

    The GCC compiled code for RISC-V requires the global pointer gp to be configured with a location, _global_pointer$, from the linker script, the code generator relies on this to offset access to global variables. (see this post for more details)

  • What about multi-core startup?

    RISC-V has a concept of a hart to represent each execution context (thread, core, etc..). This example will only work with one hart.

You can see these specifics handled the small function below using inline assembly.

extern "C" void _enter(void)  __attribute__ ((naked, section(".text.metal.init.enter")));
extern "C" void _start(void) __attribute__ ((noreturn));
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 %0 */
                      : /* input: none */
                      : /* clobbers: none */); 
    // This point will not be executed, _start() will be called with no return.
}

A few details:

  • The sp and gp are aliases to general purpose registers and these are defined by the ABI and code generation conventions.
  • The zero register is an alias to r0, however this is defined as constant 0 by the hardware specification, not just by convention.
  • The la instruction is an pseudo instruction for loading addresses.

Initializing the C++ World

The enter() function was pure assembly, when do we get to use C++ as promised?

Once we reach the start() function we can use SOME C++, we have a stack so we can use local variables. Anything that does not use the heap, globals or rely on static initialization should be available. In this case we can use the std::fill, std::copy and std::for_each functions from the <algorithm> header.

What needs to be initialized? The SRAM at reset has no defined value, any globals will be in an undefined state. To initialize them the value is either cleared to zero (.bss), or comes from a default value stored in the program image (.data), or from an initialization function (constructors).

extern "C" std::uintptr_t metal_segment_bss_target_start, metal_segment_bss_target_end, metal_segment_data_source_start .. metal_segment_itim_target_end;
extern "C" function_t *__init_array_start, *__init_array_end;
// At this point we have a stack and global pointer, but no access to global variables.
void _start(void) {
    // Init memory regions
    // Clear the .bss section (global variables with no initial values)
    std::fill(&metal_segment_bss_target_start, // cppcheck-suppress mismatchingContainers
              &metal_segment_bss_target_end,
              0U);
    // Initialize the .data section (global variables with initial values)
    std::copy(&metal_segment_data_target_start, // cppcheck-suppress mismatchingContainers
              &metal_segment_data_target_end,
              &metal_segment_data_source_start);
    // Initialize the .itim section (code moved from flash to SRAM to improve performance)
    std::copy(&metal_segment_itim_target_start, // cppcheck-suppress mismatchingContainers
              &metal_segment_itim_target_end,
              &metal_segment_itim_source_start);
    // Call constructors
    std::for_each( __init_array_start,
                   __init_array_end, 
                   [](const function_t pf) {pf();});
    // Jump to main
    auto rc = main();
    // Don't expect to return, if so busy loop in the exit function.
    _Exit(rc);
}

What is happening above? It’s initialing the regions of memory for the C++ program. The linker script has defined the locations, but we need to initialize the SRAM which is in an undefined state at startup.

  • The linker script used here is from the Freedom E-SDK.
  • 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.

Conclusion

This is the absolute bare minimum needed to run a program, but from here we can call main() and start the program.

Could we implement the startup routines in pure C++? Not at all, but we have benefited from the abstraction of C++.

The complete source code is here

The next post will look at RISC-V system registers and how to use C++ to abstract special instructions.