Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

This post is a draft for Medium.

In the last post, we set up the development environment. This post is about how the RISC-V core executes our program.

How do we go from reset to entering the main() function in C++ in RISC-V? Startup code is generally not something you need to worry about, however, it is of interest when bringing up a device from scratch.

Can we write our own startup code in pure C++?

Booting a RISC-V Core

Bootup

Booting an OS is much more complex, but the entry point relies on the same principles.


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

You can see these specifics handled in 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  (
                      ".option push;"
                      ".option norelax;"
                      "la    gp, __global_pointer$;"
                      ".option pop;"
                      "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:

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::uint8_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_source_start, // cppcheck-suppress mismatchingContainers
              &metal_segment_data_source_start + (&metal_segment_data_target_end-&metal_segment_data_target_start),
              &metal_segment_data_target_start);
    // Initialize the .itim section (code moved from flash to SRAM to improve performance)
    std::copy(&metal_segment_itim_source_start, // cppcheck-suppress mismatchingContainers
              &metal_segment_itim_source_start + (&metal_segment_itim_target_end - &metal_segment_itim_target_start),
              &metal_segment_itim_target_start);
    // Call constructors
    std::for_each( &__init_array_start,
                   &__init_array_end, 
                   [](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.

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.


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++.