Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

Code Samples

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:

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.

extern "C" 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  (
        ".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.

}

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.

Once those regions are initialized C/C++ code can be run, main() is called.

// At this point we have a stack and global poiner, but no access to global variables.

extern "C"  [[noreturn]] void _start(void) noexcept;
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();

    // Call destructors

    std::for_each( &__fini_array_start,
                   &__fini_array_end, 
                   [](function_t pf) {(pf)();});


    // Don't expect to return, if so busy loop in the exit function.

    _Exit(rc);
}

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.

extern "C" [[noreturn]] void _Exit(int exit_code) noexcept __attribute__ ((noinline));
void _Exit(int exit_code) { 
    // Halt

    while (true) {
        __asm__ volatile ("wfi");
    }
}

References

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.