
As competition in the microprocessor market intensifies, the RISC-V instruction set has garnered increasing attention. Although RISC-V is not the first open-source Instruction Set Architecture (ISA), it is the first ISA that allows flexible selection of instruction sets based on actual application scenarios. The RISC-V architecture can accommodate all application scenarios, from high-performance server CPUs to ultra-low-power embedded CPUs for sensors.
Typically, the startup code for a processor is designed using assembly language. The reasons include: during the processor startup phase, the C runtime environment has not been initialized; code implemented in assembly language is not affected by the compiler; certain special register operations cannot be obtained through C compilation; and some special designs of the processor are not conducive to the use of C language. This article will address the aforementioned issues and demonstrate a method for designing startup code for the RISC-V processor using C language.
To discuss the issues more clearly and facilitate readers’ understanding of certain processes, this article takes the N205 series core based on the RV32IMC instruction set from ChipRise Technology as the target processor, starting from the C language startup code of the Cortex-M core from ARM in the IAR Embedded Workbench for ARM[1] (hereinafter referred to as IAR) environment, and gradually introducing and implementing the C language startup code for the N205 series core in the SEGGER Embedded Studio[2] (hereinafter referred to as SES) environment.
01C Language Startup Code for Cortex-M Core in IAR Environment
The Cortex series cores are ARM’s most successful product line to date, including A, R, and M types, with the M series primarily targeting the microcontroller market. The Cortex-M cores have the following characteristics: the core includes an advanced interrupt controller; during interrupt response, the processor hardware saves and restores the corresponding registers; the first element in the vector table is the stack address, while the others are entry addresses for exceptions or interrupt functions; the contents of the vector table are automatically loaded by hardware.
The content shown in Code Segment 1 is the startup code developed in C language for the Cortex-M core in the IAR environment.
【Code Segment-1】
#pragma language=extended ❶
–snip–
void ResetISR(void); ❷
–snip–
extern void __iar_program_start(void); ❸
static unsigned long pulStack[64] @”.noinit”; ❹
typedef union ❺
{
void (*pfnHandler)(void);
unsigned long ulPtr;
}
uVectorEntry;
__root const uVectorEntry __vector_table[] @”.intvec” = ❻
{
{ .ulPtr = (unsigned long)pulStack + sizeof(pulStack) }, ❼
ResetISR,
–snip–
};
–snip–
void ResetISR(void)
{
__iar_program_start();
}
Here is a brief analysis of the above code:
❶ is an IAR #pragma directive.
❷ is the declaration of the reset function, which is the first code executed after the processor reset and is sometimes referred to as the reset entry function.
❸ is the declaration of an IAR system function, __iar_program_start, which primarily initializes the C runtime environment and calls the main system function.
❹ uses the IAR @ operator to define the system stack area.
❺ declares the union type for the vector table.
❻ uses the IAR object attributes and @ operator to define the vector table, where the first element ❼ stores the stack base address, and subsequent elements are function addresses.
From the analysis process above, it can be seen that the necessary tasks for the startup code include defining the stack area, defining and initializing the vector table, defining and implementing the system reset function, and initializing the stack pointer or stack register. Depending on the architecture of the processor, some of these operations need to be completed by software, while others are automatically loaded by hardware.
Additionally, the content related to IAR directives, object attributes, etc., is not within the scope of this article and can be referenced as needed. Here are two tips: the compilation system of the IAR environment is developed by IAR itself, so the directive symbols in the example code are not applicable to GCC; some directives may vary due to different versions of the IAR environment.
02Necessary Knowledge for Implementing C Language Startup Code for RISC-V Core in SES Environment
As mentioned earlier, RISC-V is an instruction set rather than a specific design implementation, which is quite different from the previously discussed Cortex-M series cores. Simply put, processors from different manufacturers based on the same Cortex-M core may not have significant differences at the core level, but RISC-V processors developed by different manufacturers with the same instruction set each have their own characteristics: on one hand, the specific implementations of the same functionality may differ; on the other hand, different manufacturers can implement different instruction extensions.
Compared to the Cortex-M core, some characteristics of RISC-V processors include: different manufacturers have unique implementations of interrupt controllers; during interrupt response, the processor hardware does not save the context, requiring software to complete this function; the vector table may have significant differences based on the manufacturer, where the first address of the vector table may store instructions rather than addresses.
When switching between Cortex-M core processors from different manufacturers, due to the consistency of the processor core, the startup code typically requires little modification. Therefore, designing startup code using assembly or C language seems to make little difference. However, to reduce the complexity of switching between different manufacturers’ RISC-V processors, using C language to develop startup code is an effective approach.
As previously mentioned, the necessary tasks for startup code include defining the stack area, defining and initializing the vector table, defining and implementing the system reset function, and initializing the stack pointer or stack register, etc. In the C startup code for the aforementioned Cortex-M core, IAR provides the interface __iar_program_start, which hides almost all details. In the SES environment, there is no such interface available, so to implement the C language startup code for the RISC-V processor, knowledge related to the compiler and linker is required.
(1) GCC Inline Assembly
The CSR registers in the RISC-V processor require special instructions to access, which the C compiler cannot generate. Therefore, several assembly instructions still need to be inserted into the C language startup code. To achieve interaction between assembly instructions and C language, GCC inline assembly must be used, as illustrated below:
asm volatile ( ❶
“csrw 0x307, %0” ❷
: ❸
:”r”(vector_base) ❹
: ❺
);
Where: ❶ asm is the GCC inline assembly keyword, volatile is a modifier; ❷ the assembly instruction list is enclosed in double quotes; if there are multiple instructions, they can be separated by “\n”; where %0 represents the first value in the input operands list; ❸ is an optional output operands list; ❹ is an optional input operands list, where “r” indicates the use of a compiler-allocated register to store the variable vector_base; ❺ is an optional list of affected registers.
(2) Section and Initialization
In simple terms, linking sections in the object file creates the executable file. By default, the compiler creates standard sections. Table 1 provides a brief overview of standard sections.
Table 1 Overview of Standard Sections

From Table 1, it can be seen that the executable code of the program is stored in the .text section, while initialized global and static variables are stored in the .data section.
A typical SoC system usually contains two types of memory, namely ROM and RAM. For modern processors, these two parts are typically Flash and SRAM. Under power-off conditions, SRAM cannot retain data, so the initial values of variables in C language need to be stored in Flash. After the system is powered on, the initialization code copies the initialized data from Flash to the target addresses in SRAM. As mentioned earlier, this is one of the important tasks of the initialization code.
Next, we will explain how to find the location of the initialization data in Flash and reference it in C language.
(3) C Language Access to Linker Variables
From the perspective of the linker, the initial value stored in Flash is called the LMA (Load Memory Address), while the runtime address of the variable in SRAM is called the VMA (Virtual Memory Address). A linker script is a file used to describe the distribution of processor memory, the inclusion relationships of each section and standard sections, and the respective LMA and VMA addresses or storage areas.
Code Segment 2 is a snippet of a standard linker script. This snippet will be used to explain C language access to linker variables.
【Code Segment-2】
MEMORY
{
–snip–
}
SECTIONS
{
–snip–
__data_load_start__ = ALIGN(__srodata_end__ ,4);
.data ALIGN(__RAM_segment_start__ , 4) :AT(ALIGN(__srodata_end__ , 4))
{
__data_start__ = .;
*(.data .data.*)
}
__data_end__ = __data_start__ + SIZEOF(.data);
__data_size__ = SIZEOF(.data);
__data_load_end__ = __data_load_start__ + SIZEOF(.data);
–snip–
}
In Code Segment 2, linker script variables __data_load_start__, __data_start__, and __data_end__ are defined. Here, __data_load_start__ represents the LMA address, while __data_start__ represents the VMA address. There are two methods to access these variables in C language: declare the linker script variable as a data type, for example, declare extern uint32_t __data_load_start__; and obtain the value of the variable through &__data_load_start__; or declare the linker script variable as an array, for example, declare extern uint32_t __data_load_start__[]; and obtain the value of the variable through __data_load_start__.
(4) Function Attributes
In general, the compiler automatically generates prologue and epilogue sequences for each function, which includes stack operations at the function header and corresponding operations at the function end. A significant issue is that during the execution of the reset function in C language startup code, the stack pointer or stack register may not have been initialized, and stack operations at this point may lead to illegal address access by the processor, causing the program to crash. Furthermore, as previously mentioned, the reset entry of the RISC-V processor may save a jump instruction rather than an address, and a short jump address can ensure that the jump is completed with a single instruction.
Given the above reasons, it is necessary to use relevant function attributes to inform the compiler to eliminate the default function sequence and specify the section. The following form of reset function definition can meet this requirement:
void __attribute__((section(“.init”), naked)) reset_handler() {
–snip–
};
03Example of C Language Startup Code for RISC-V Core
The previous content introduced relevant background knowledge and technical means, and now an actual framework program will be used to demonstrate the C language startup code for the RISC-V processor. Code Segment 3 is the implementation of the C language startup code, and Code Segment 4 is the vector table. All key points in the code have been introduced previously, so they will not be repeated here.
【Code Segment-3】
#include “riscv_encoding.h”
#include
–snip–
extern uint32_t __data_load_start__;
–snip–
extern uint32_t __bss_start__;
–snip–
extern void (*const vector_base[])(void);
extern void main(void);
–snip–
const struct {
uint32_t* load;
uint32_t* start;
uint32_t* end;
} dsection[3] = {
–snip–
};
const struct {
uint32_t* start;
uint32_t* end;
} bsection[3] = {
–snip–
};
void __attribute__((section(“.init”), naked)) reset_handler() {
register uint32_t *src, *dst;
–snip–
/* Inline Assembly */
asm volatile(“csrw 0x307,%0”::”r”(vector_base));
–snip–
asm volatile(“la gp, __sdata_start__+0x800”);
asm volatile(“la sp,__stack_end__”);
/* System clock initialization and so on */
init();
/* Copy initialized values to RAM */
if(&__vectors_load_start__ !=&__RAM_segment_start__) {
for(uint8_t idx = 0; idx < 3; idx++) {
src = dsection[idx].load;
dst = dsection[idx].start;
while(dst < dsection[idx].end) {
*dst = *src;
dst++;
src++;
}
}
}
/* Clear .bss area */
for(uint8_t idx=0; idx < 3; idx++) {
dst = bsection[idx].start;
while(dst < bsection[idx].end) {
*dst = 0U;
dst++;
}
}
/* Call main function */
main();
}
–snip–
【Code Segment-4】
.section .vectors, “ax”
–snip–
.globl vector_base
vector_base:
j reset_handler
.align 2
.word 0
–snip–
04Conclusion
Typically, semiconductor manufacturers provide startup code for processors in their accompanying software development packages, which leads many embedded developers to focus more on the application code implementation and neglect the existence of startup code. Since the startup code provided by manufacturers is almost all written in assembly language, many developers mistakenly believe that startup code must be developed using assembly language.
In fact, most processor startup code can be developed using C language, and the code efficiency is nearly indistinguishable from assembly. In engineering practice, many deep-level developments require modifications or rewrites of the startup code. C language-based code can save developers time and effort in learning assembly instructions, while also being more efficient in subsequent upgrades and maintenance.
Supplementary Knowledge:
[1] Considering that the development of the Cortex-M series architecture often uses environments like IAR and MDK, this example uses the IAR environment.
[2] Given that the current RISC-V integrated development environments are mostly based on Eclipse, and SEGGER Embedded Studio is based on its own architecture, which is convenient and powerful, this article takes SES as an example. Additionally, the compilation systems in RISC-V development environments, including SES, are based on GCC, so the methods discussed in this article are also applicable to other development environments.
[3] If macros need to be used in GCC inline assembly code, a method called “double macro definition” must be used, as shown below:
#define CSR_MTVT 0x307
#define STR(R) #R
#define XSTR(R) STR(R)
/*asm volatile(“csrw 0x307, %0”::”r”(vector_base)); */
asm volatile(“csrw ” XSTR(CSR_MTVT) “,%0”::”r”(vector_base));
Author Profile:
Tang Sichao is currently the software development manager at Beijing Zhichun Technology Co., Ltd., responsible for the toolchain and embedded development for artificial intelligence chips, with 14 years of experience in hardware circuit design and software development, specializing in the integrated use of processor, compiler systems, and operating system design and development.


1.Things About Embedded Employment~
2.FreeRTOS 10.3.0 Official Release, Source Code Migrated to Github!
3.The Second Issue of 2020 “Microcontrollers and Embedded Systems Applications” Electronic Journal is Freshly Released!
4.Quick, Simple, and Reliable! Let’s Experience the Capacitive Touch Solution of the KE Series MCU
5.Arm Announces the Launch of Cortex-M55 Core and Ethos-U55 microNPU, Targeting Low-Power Edge AI
6.What is the Relationship Between ANSI C, ISO C, and Standard?