Fork me on GitHub
0%

Ingenic X2600 RISCV 编译&启动解析

源码链接:https://gitee.com/ingenic-dev/libbare-cpu/tree/develop/master/cpu/core-riscv

简介

Ingenic X2600 是一款双核异构的芯片,主核使用主频1.2GHz的MIPS架构的Xburst2,小核使用主频600MHz的RISCV架构的Victory。其中对于RISCV小核无法做到单独使用,依赖于大核的启动对其进行加载和通讯,软件SDK采用libbare-cpu对其进行编程。本篇主要对跑在RISCV核上的FreeRTOS程序的代码编译和第一个程序的执行等内容进行分析

代码编译

具体的代码编译规则是由CMakeLists.txt 和 Makefile两种方式进行构建的,本部分对此不做解释,而是对链接脚本文件ld.lds文件做分析

ld.lds 文件(也叫 Linker Script)是 GCC 编译器中 ld 链接器使用的链接脚本,用来控制最终生成的可执行文件或固件的 内存布局,在CMakefiles.txt中可以通过target_link_libraries的-T参数将链接脚本文件指定,而Makefile也可以通过-T参数进行指定传递给LDFALGS进一步给gcc进行编译

本部分展示的ld文件来源于2025年5月25日版本,如果需要查看最新的内容参阅:https://gitee.com/ingenic-dev/libbare-cpu/blob/develop/master/cpu/core-riscv/ld.lds#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// GNU链接器脚本 中的一个指令,用来指定最终输出文件的目标架构(Target Architecture)
OUTPUT_ARCH(riscv)

// GNU链接器脚本 中的一个高级指令,用于将多个库文件打包在一起,让链接器尝试多次解析符号依赖,解决复杂的循环依赖问题
// 这里是 newlib 的正确链接的依赖
GROUP( -lc -lgloss -lgcc -lsupc++ )

// 指定库文件搜索路径
SEARCH_DIR(.)
// 避免链接器生成 .dynamic 段,解决报错,确保最终是纯静态链接的输出
__DYNAMIC = 0;

// 指定程序的入口点符号的指令,即告诉链接器程序从哪个函数(或地址)开始执行
ENTRY(_reset_entry)

// MEMORY 段用于定义目标芯片内存布局,包括内存的起始地址和小。因为链接器需要知道代码和数据要放在哪块物理内存中。
MEMORY
{
instrram : ORIGIN = 0x07f00000, LENGTH = 0x50000
cpusram : ORIGIN = 0x12400000, LENGTH = 0x2000
}

// 声明_stack_start符号指示栈起始位置(栈顶, 栈地址向下增长)
_stack_start = ORIGIN(instrram) + LENGTH(instrram);

/* We have to align each sector to word boundaries as our current s19->slm
* conversion scripts are not able to handle non-word aligned sections. */

// 定义输出文件中各段的布局和分布,包括 .text、.data、.bss 等代码和数据段应放入哪个内存区域、在哪些位置
SECTIONS
{
// 定义一个vector的段,4地址对齐,并且定一个符号 __reset_entry 内容是当前地址+0x80
.vectors :
{
. = ALIGN(4);
_reset_entry = . + 0x80;
KEEP(*(.vectors))
} > instrram
// 代码段
.text : {
. = ALIGN(4);
_stext = .;
*(.text*) // * 是通配符语法,表示“来自所有文件”
_etext = .;
__CTOR_LIST__ = .;
LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
*(.ctors) // constructors 构造函数
LONG(0)
__CTOR_END__ = .;
__DTOR_LIST__ = .;
LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
*(.dtors) // destructors 析构函数
LONG(0)
__DTOR_END__ = .;
*(.lit) // literal data 常量数据
*(.shdata) // share data 共享数据
_endtext = .;
} > instrram

/*--------------------------------------------------------------------*/
/* 全局 构造/析构函数 的段 */
/*--------------------------------------------------------------------*/
// 预初始数组段
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
} > instrram
// 初始化数据段
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array ))
PROVIDE_HIDDEN (__init_array_end = .);
} > instrram
// 初始化结束段
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array ))
PROVIDE_HIDDEN (__fini_array_end = .);
} > instrram
// GOT表(动态链接需要使用,这里是在静态链接中实现加载跳转)段
.got : {
__got_start = .;
*(.got)
*(.got.plt)
__got_end = .;
} > instrram
// 资源表段(Ingenic用于大小核通讯中使用)
.resource_table : {
. = ALIGN(4);
__rsc_table_start = .;
KEEP (*(.resource_table*))
__rsc_table_end = .;
} > instrram
// 中断处理段
.interrupt_handler : {
. = ALIGN(4);
KEEP (*(.interrupt_handler*))
} > instrram
// IPC 通讯环形缓冲区段(用于IPC通讯)
// NOLOAD 表示:链接器不会将此段内容加载到最终生成的二进制文件中(例如 ELF 的 LOAD 标记会被取消)
// 虽然不加载但是将占位
.ipc_ring_mem (NOLOAD):{
. = ALIGN(4);
KEEP (*(.ipc_ring_mem*))
} > cpusram
// Read Only 只读段
.rodata : {
. = ALIGN(4);
*(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*)))
} > instrram
// Share BSS共享的BSS段的定义(Maybe)
.shbss :
{
. = ALIGN(4);
*(.shbss)
} > instrram
// relocation data 重定位后的段
.data.rel : {
__data_rel_start = .;
*(.data.rel.*)
__data_rel_end = .;
} > instrram
// 数据段
.data : {
. = ALIGN(4);
sdata = .;
_sdata = .;
*(.data);
*(.data.*)
edata = .;
_edata = .;
} > instrram
// 函数符号序列段
.func_symbol_index :
{
. = ALIGN(16);
__func_symbol_index_start = .;
KEEP(*(SORT(.func_symbol_index*)))
__func_symbol_index_end = .;
} > instrram
// 函数符号字串段
.func_symbol_str :
{
__func_symbol_str_start = .;
KEEP(*(SORT(.func_symbol_str*)))
__func_symbol_str_end = .;
} > instrram
// BSS段
.bss :
{
. = ALIGN(4);
_bss_start = .;
*(.bss)
*(.bss.*)
*(.sbss)
*(.sbss.*)
*(COMMON)
_bss_end = .;
} > instrram
// STABS 调试格式 的符号表段,存储调试符号信息(变量名、函数名、类型等)
.stab 0 (NOLOAD) :
{
[ .stab ]
}
// .stabstr 是 .stab 段中符号名称的字符串表
.stabstr 0 (NOLOAD) :
{
[ .stabstr ]
}
}

对于以上诸多的段代码,至于链接器会将哪些内容放到对应的段,有些是通识定义比如:(函数体)默认放在 .text 段;初始化的全局变量放 .data;未初始化的全局变量放 .bss;常量字符串放 .rodata,而自定义的段可以通过__attribute__((section(".my_section"))) 来显式声明

启动流程

由于使用FreeRTOS所以启动过程较复杂,主要包括的文件包括start.S、genex.S、portASM.S文件。对于文件内容较多的不做全部解析,只对重要的语句进行解释

  • start.S: 通常是启动代码(startup code)文件,上电或复位之后执行的第一段汇编代码
  • genex.S: 可能是generated exceptions生成的异常的缩写,里面内容则是与系统异常处理相关的汇编代码
  • portASM.S: 通常是“porting assembly”的缩写,移植层的汇编代码,用于对接到FreeRTOS的汇编代码

启动汇编代码

start.S(或 start.s)是启动汇编文件,在嵌入式系统、操作系统内核、引导程序等项目中极其关键。它是系统上电或重启后 第一段被执行的代码

本部分展示的ld文件来源于2025年5月25日版本,如果需要查看最新的内容参阅:https://gitee.com/ingenic-dev/libbare-cpu/blob/develop/master/cpu/core-riscv/start.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

// 声明 全局变量reset_handler 外部变量__MTVEC
.global reset_handler
.extern __MTVEC

// 告诉汇编器接下来的代码放到 代码段中
.section .text

// reset_handler 符号内容
reset_handler:
// mv rd, rs (将rs寄存器内容复制到rd中)
// 以下代码是将x1 - x31寄存器的值清0
mv x1, x0
mv x2, x1
mv x3, x1
mv x4, x1
mv x5, x1
mv x6, x1
mv x7, x1
mv x8, x1
mv x9, x1
mv x10, x1
mv x11, x1
mv x12, x1
mv x13, x1
mv x14, x1
mv x15, x1
mv x16, x1
mv x17, x1
mv x18, x1
mv x19, x1
mv x20, x1
mv x21, x1
mv x22, x1
mv x23, x1
mv x24, x1
mv x25, x1
mv x26, x1
mv x27, x1
mv x28, x1
mv x29, x1
mv x30, x1
mv x31, x1

// Register x1 = 8 ---> 0x300 MIE 全局中断使能位
// 0x300 MRW mstatus 机器状态寄存器
li x1, 8
csrw 0x300, x1

// Register x1 = __MTVEC ---> 0x305 MRW mtvec 机器陷阱处理程序基地址
// mtvec(Machine Trap-Vector Base Address Register)符号别名或宏定义,用来设置中断/异常的跳转入口地址
// __MTVEC 定义在genx.S
la x1, __MTVEC
csrw 0x305, x1

mv x1, x0

// 将 _stack_start - 8 给到x2(stack pointer,sp寄存器)
// _stack_start 定义在哪里 ld.lds 中
la x2, (_stack_start - 8)

// _start符号内容
_start:
.global _start

// 以下内容是用于清除BSS段,主要逻辑是将bss起始位置加载,将该部分内容清0
// _bss_start 和 _bss_end 定义于 ld.lds 文件中
la x26, _bss_start
la x27, _bss_end
bge x26, x27, zero_loop_end

zero_loop:
sw x0, 0(x26)
addi x26, x26, 4
blt x26, x27, zero_loop
zero_loop_end:


// main_entry符号内容
main_entry:
// 1. 清除 mstatus bit11 bit12 (清除 MPP 字段(Machine Previous Privilege) 切换到用户态 U
// 2. 将label-1的地址写入 mepc中 机器异常程序计数器
// 3. 用于从 机器模式(M-mode) 返回到之前的 特权模式。这通常发生在异常或中断处理后,目的是恢复处理前的程序状态

// 1f 表示“从当前位置向前查找编号为 1 的标签”;1b 表示“从当前位置向后查找编号为 1 的标签”
li t0, 0x00001800
csrc mstatus, t0
la t0, 1f
csrw mepc, t0
mret

1:
// 跳转到c_main函数,参数argc = argv = 0,返回地址保存到x1中
addi x10, x0, 0
addi x11, x0, 0
jal x1, c_main
1:
// # wfi 是 Wait For Interrupt 的缩写,它用于将处理器置于等待中断状态,直到发生中断或异常
wfi
j 1b

异常处理代码

Ingenic-x2600支持将中断放置到RISCV-RAM中执行,而不在DDR执行,这样就减少了DDR的压力对中断实时性的影响。该功能的实现需要使能CONFIG_IRQ_IN_TCSM宏并配置CMAKE编译,这里不对此部分作说明,因此代码说明部分将删除掉对应的宏控内容

本部分展示的ld文件来源于2025年5月25日版本,如果需要查看最新的内容参阅:https://gitee.com/ingenic-dev/libbare-cpu/blob/develop/master/cpu/core-riscv/port_freertos/genex.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    .extern reset_handler

// 以下代码放到vector段
// a:表示该段是 可分配(allocatable)的,也就是说该段会被分配内存
// x:表示该段是 可执行(executable)的,意味着它包含的代码是可执行的
.section .vectors,"ax"
// RISC-V 汇编器中的一个伪指令,用于关闭 RVC(RISC-V Compressed Instructions)压缩指令模式
.option norvc;
.global __MTVEC
__MTVEC:
j handle_exception # 0x00 // 异常中断处理函数
j handle_exception # 0x04
j handle_exception # 0x08
j handle_soft # 0x0c // 软中断处理函数
j handle_exception # 0x10
j handle_exception # 0x14
j handle_exception # 0x18
j handle_mtimer # 0x1c // 定时器中断函数
j handle_exception # 0x20
j handle_exception # 0x24
j handle_exception # 0x28
j handle_interrupt # 0x2c // 外部中断函数
j handle_exception # 0x30
j handle_exception # 0x34
j handle_exception # 0x38
j handle_exception # 0x3c
j handle_mailbox # 0x40 // mailbox中断处理函数
j handle_exception # 0x44
j handle_exception # 0x48
j handle_exception # 0x4c
// 重复12次 handle_exception .rept - endr
.rept 12
j handle_exception # 0x50 ~ 0x7c
.endr
// 将当前位置设置为地址 0x80,也就是告诉汇编器:从地址 0x80 开始放接下来的内容。
.org 0x80
// 跳转到 reset_handler
j reset_handler

// 将当前位置设置为地址 0x200
.org 0x200
.word __rsc_table_start
.word __rsc_table_end
.word _reset_entry - 0x80

//将当前位置设置为地址 0x1000
.org 0x1000

对于以上设置的 __MTVEC 是 RISC-V 架构下的一个关键寄存器名称,它是 mtvec(Machine Trap-Vector Base Address Register) 的符号别名或宏定义,用来设置中断/异常的跳转入口地址,而不同地址位置的异常是需要满足RISCV标准定义的

riscv-execption

FreeRTOS汇编对接代码

本部分展示的ld文件来源于2025年5月25日版本,如果需要查看最新的内容参阅:https://gitee.com/ingenic-dev/libbare-cpu/blob/develop/master/cpu/core-riscv/port_freertos/portASM.S#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include "portContext.h"

.global xPortStartFirstTask
.extern vTaskSwitchContext

// xPortStartFirstTask 开始第一个任务
xPortStartFirstTask:
// 加载当前任务的 TCB 指针到 sp
lw sp, pxCurrentTCB
// 加载该任务栈顶的地址(stack pointer)
lw sp, 0( sp )
// 恢复 ra(return address)
lw x1, 0( sp )

lw x7, 4 * 4( sp ) /* t2 */
lw x8, 5 * 4( sp ) /* s0/fp */
lw x9, 6 * 4( sp ) /* s1 */
lw x10, 7 * 4( sp ) /* a0 */
lw x11, 8 * 4( sp ) /* a1 */
lw x12, 9 * 4( sp ) /* a2 */
lw x13, 10 * 4( sp ) /* a3 */
lw x14, 11 * 4( sp ) /* a4 */
lw x15, 12 * 4( sp ) /* a5 */
lw x16, 13 * 4( sp ) /* a6 */
lw x17, 14 * 4( sp ) /* a7 */
lw x18, 15 * 4( sp ) /* s2 */
lw x19, 16 * 4( sp ) /* s3 */
lw x20, 17 * 4( sp ) /* s4 */
lw x21, 18 * 4( sp ) /* s5 */
lw x22, 19 * 4( sp ) /* s6 */
lw x23, 20 * 4( sp ) /* s7 */
lw x24, 21 * 4( sp ) /* s8 */
lw x25, 22 * 4( sp ) /* s9 */
lw x26, 23 * 4( sp ) /* s10 */
lw x27, 24 * 4( sp ) /* s11 */
lw x28, 25 * 4( sp ) /* t3 */
lw x29, 26 * 4( sp ) /* t4 */
lw x30, 27 * 4( sp ) /* t5 */
lw x31, 28 * 4( sp ) /* t6 */
// 从任务堆栈中获取此任务的 xCriticalNesting 值
lw x5, CRITICAL_NESTING_OFFSET( sp )
// 将 xCriticalNesting 的地址加载到 x6 中
lw x6, pxCriticalNesting
// 恢复此任务的关键嵌套值
sw x5, 0( x6 )
// 初始 mstatus 进入 x5 (t0)
lw x5, MSTATUS_OFFSET( sp )
// 设置 MIE 位,以便第一个任务以启用中断的方式启动 - 必需,因为返回的是 ret 而不是 eret
addi x5, x5, 0x08
// 从这里启用中断!
csrrw x0, mstatus, x5
// 初始化 x5 x6 寄存器值
lw x5, 2 * 4( sp )
lw x6, 3 * 4( sp )
addi sp, sp, CONTEXT_SIZE
ret
/*-----------------------------------------------------------*/

// 定义 异常处理handler宏(已删除带有中断优先级的异常handler)
.macro __BUILD_INTERRUPT_HANDLER exception handler
.section .interrupt_handler.text, "ax"
.align 2
.global handle_\exception
.extern do_\exception
handle_\exception:
SAVE_INTERRUPT_CONTEXT
call do_\handler
RESTORE_INTERRUPT_CONTEXT
.endm

// 调用宏进行handler定义
__BUILD_INTERRUPT_HANDLER mailbox mailbox
__BUILD_INTERRUPT_HANDLER interrupt interrupt
__BUILD_INTERRUPT_HANDLER soft soft

__BUILD_INTERRUPT_HANDLER mtimer mtimer
.section .interrupt_handler.text, "ax"
.align 2
.global handle_exception
.extern do_exception
handle_exception:
SAVE_EXCEPTION_CONTEXT
// 把 mcause 的值读入寄存器 a0,trap的原因
csrr a0,mcause
// 异常代码 11 表示:ecall from machine mode(M-mode) == environment call
li t0, 11
// 如果 mcause != 11,跳转到 other_exception
bne a0, t0, other_exception
// mcause == 11 调用vTaskSwitchContext调度函数
call vTaskSwitchContext
RESTORE_EXCEPTION_CONTEXT
other_exception:
call do_exception
RESTORE_EXCEPTION_CONTEXT

整体梳理

  1. 编译部分

    通过CMAE指定 -T 参数可以指定链接文件,编译器将按照我们的规定的格式进行代码布局,如下图所示是基于以上文件编译出来的ELF文件,可以看到每个Section依次按照我们预期的布局进行排布,同时地址也是我们对应的内容区域

    程序入口地址 _reset_entry同样符合预期,偏移0x80位置

    这里还有一个需要注意的是 section to segment mapping,section是链接阶段的概念,而segment是运行时的概念,执行时相同属性的section会合并成一个segment加载到内存中,当然也可以手动指定

    elf elf-info
  2. 执行部分

    • 代码编译出来之后将根据segment装载到对应的地址位置
    • 解析elf文件头和其它信息
    • 跳到entry point地址进入到代码入口处

    这里简单看一下内存布局,vectors是第一个section因此__MTVEC符号对应的代码将放到0x7f00000位置(instrram内存区域),具体内容则是中断向量表 + 0x80的reset_handler + 0x200的资源表 —- 0x1000(4K对齐),至此vector的section结束

    当代码开始执行将跳到base+0x80执行也就是reset_handler,此时开始真正执行代码:初始化RISCV寄存器;开启mie;将中断向量表地址设置到RISCV MRW 机器陷阱处理寄存器里;初始化栈;清除BSS段;跳转到c_main函数,这里就可以调用到板级和应用相关的代码。

RISC-V通用寄存器

编号 名称 ABI 名称 用途描述
x0 zero 恒为 0(读为0,写无效)
x1 ra 返回地址(return address) 子程序返回地址
x2 sp 栈指针(stack pointer) 栈顶位置
x3 gp 全局指针(global pointer) 用于全局数据寻址
x4 tp 线程指针(thread pointer) 多线程环境中的线程本地存储地址
x5–x7 t0–t2 临时寄存器(caller saved)
x8 s0/fp 保存寄存器/帧指针
x9 s1 保存寄存器
x10–x17 a0–a7 函数参数/返回值(caller saved)
x18–x27 s2–s11 保存寄存器(callee saved)
x28–x31 t3–t6 临时寄存器(caller saved)

CSR寄存器

RISC-V 中的 CSR(Control and Status Registers,控制和状态寄存器) 是一种用于控制处理器行为和状态的特殊寄存器,广泛用于异常处理、中断、性能监控、特权级切换等系统功能。

🧠基本概念

  • CSR 是一个特殊的寄存器集合,用于配置 CPU 行为、保存运行状态。
  • 访问 CSR 只能通过特定的指令:CSRRW, CSRRS, CSRRC 等。
  • 通常只在特权级(如 M-mode、S-mode)中可用。

🧩常见 CSR 列表

  1. 系统控制类
CSR 名称 地址 描述
mstatus 0x300 M 模式状态寄存器
misa 0x301 ISA 特性描述寄存器
mie 0x304 中断使能控制
mtvec 0x305 异常/中断向量表入口地址
mscratch 0x340 保存临时数据(中断使用)
mepc 0x341 异常发生时的 PC(返回地址)
mcause 0x342 异常或中断的原因
mtval 0x343 与异常相关的值(如地址)
mip 0x344 中断挂起状态
  1. 性能计数类
CSR 名称 地址 描述
cycle 0xC00 Cycle 计数器(低 32 位)
time 0xC01 时间计数器(低 32 位)
instret 0xC02 已完成指令数