本篇定位:RISC-V 专题第 7 篇。本篇把"上电到 main 运行"的完整链路讲透:启动汇编必做 5 件事、多核启动、ROM 固化规则、链接脚本与启动的配合。读完后你能从零写一个 bare-metal 启动流程,能调任何启动死机。
一、启动链路总览
RISC-V MCU 的典型启动链路:
上电
│
▼
BootROM(出厂固化,只读)
│ - 硬件复位向量跳到这里
│ - 初始化最小环境(栈/时钟)
│ - 加载下一级(Flash/bootloader)到 RAM
│ - 跳转
▼
Bootloader(可选,多级)
│ - 初始化更多外设(DDR/外设时钟)
│ - 加载应用固件
│ - 跳转
▼
Application / RTOS
│ - _start 启动汇编
│ - 初始化运行时(.data/.bss/gp/mtvec)
│ - 调 main()
▼
main() 运行
各级职责分工
| 级 | 谁做 | 干什么 |
|---|---|---|
| BootROM | 芯片厂商 | 最小初始化 + 加载下一级 |
| Bootloader | 你 | 外设初始化 + 加载 app |
| App _start | 启动汇编 + runtime | 运行时初始化 + main |
二、启动汇编必做 5 件事(App _start)
这是本篇核心。App 启动汇编(crt0.s 或 startup.s)必须做 5 件事,缺一不可:
2.1 第 1 件:初始化 sp(栈指针)
asm
_start:
la sp, _stack_top # sp 指向栈顶(链接脚本定义)
- 栈用于函数调用、局部变量、中断保存
- sp 没初始化,任何 C 函数调用都崩(压栈写到野地址)
- 栈大小要够(中断嵌套深 + 任务多的场景),链接脚本里预留
2.2 第 2 件:初始化 gp(全局指针)⭐ RISC-V 特有
asm
.option push
.option norelax # 防 linker relaxation 把这步优化掉
la gp, __global_pointer$
.option pop
- gp 指向
.sdata段中点,用于 gp 相对寻址(②专题讲过) - 忘了设 gp,gp 相对寻址全崩------访问小全局变量读到野地址
.option norelax必须加:linker relaxation 会把la gp优化成mv gp,gp(无意义),必须禁掉这步 relaxation
gp 忘设是 RISC-V 启动常见崩点
ARM 没有 gp 概念,你 ARM 经验带过来容易漏这步。症状:启动早期随机崩溃,访问全局变量拿到错值。调试启动死机,先确认 gp 有没有设------这是 RISC-V 启动汇编最容易被漏的一步。
2.3 第 3 件:拷贝 .data 段(Flash → RAM)
asm
la t0, _sidata # .data 在 Flash 的加载地址(LMA)
la t1, _sdata # .data 在 RAM 的运行地址(VMA)
la t2, _edata
1: bgeu t1, t2, 2f
lw t3, 0(t0)
sw t3, 0(t1)
addi t0, t0, 4
addi t1, t1, 4
j 1b
2:
.data是已初始化全局变量,存储在 Flash(掉电保留),运行时要在 RAM(可读写)- 启动时从 Flash 拷到 RAM
- 不拷贝:.data 变量读到 Flash 的值,写它就崩(Flash 只读)
2.4 第 4 件:清零 .bss 段
asm
la t0, _sbss
la t1, _ebss
li t2, 0
1: bgeu t0, t1, 2f
sw t2, 0(t0)
addi t0, t0, 4
j 1b
2:
.bss是未初始化/初始化为 0 的全局变量,链接脚本不占 Flash(只记大小),运行时在 RAM- C 标准要求 .bss 默认 0
- 不清零:全局变量是随机值,所有
if (flag==0)类逻辑全错------常见诡异 bug 源
2.5 第 5 件:设置 mtvec(trap 入口)
asm
la t0, trap_vector
csrw mtvec, t0 # 配 trap 入口地址
- 没配 mtvec,任何异常/中断跳到默认地址(0 或保留),立刻死机套娃
- 必须在开中断前配好(开 MIE 后才有中断,但 trap 随时可能来,如 ecall/异常)
完整启动汇编骨架
asm
.section .text.init
.global _start
_start:
# 1. sp
la sp, _stack_top
# 2. gp(防 relaxation)
.option push
.option norelax
la gp, __global_pointer$
.option pop
# 3. 拷 .data
la t0, _sidata
la t1, _sdata
la t2, _edata
1: bgeu t1, t2, 2f
lw t3, 0(t0)
sw t3, 0(t1)
addi t0, t0, 4
addi t1, t1, 4
j 1b
2:
# 4. 清 .bss
la t0, _sbss
la t1, _ebss
li t2, 0
3: bgeu t0, t1, 4f
sw t2, 0(t0)
addi t0, t0, 4
j 3b
4:
# 5. mtvec
la t0, trap_vector
csrw mtvec, t0
# 6. (可选)初始化 FPU、cache、中断控制器
# 7. 调 main
call main
# main 返回(不应该),死循环
5: wfi
j 5b
三、链接脚本与启动的配合
启动汇编用的 _stack_top _sidata _sdata _edata _sbss _ebss __global_pointer$ 全是链接脚本定义的符号。
典型链接脚本结构
ld
ENTRY(_start)
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS {
.text : {
KEEP(*(.text.init)) /* 启动代码在前 */
*(.text .text.*)
*(.rodata .rodata.*)
} > FLASH
.data : {
_sdata = .; /* VMA: RAM 里运行地址 */
*(.data .data.*)
_edata = .;
} > RAM AT > FLASH /* LMA 在 Flash */
_sidata = LOADADDR(.data); /* .data 的 Flash 加载地址 */
.bss : {
_sbss = .;
*(.bss .bss.*) *(COMMON)
_ebss = .;
} > RAM
.sdata : { /* 小数据段,gp 寻址 */
*(.sdata .sdata.*)
} > RAM
.sbss : {
*(.sbss .sbss.*)
} > RAM
/* gp 指向 .sdata 中点 */
__global_pointer$ = . + 0x800; /* ±2KB 范围 */
/* 栈 */
_stack_top = ORIGIN(RAM) + LENGTH(RAM);
}
.data 的 LMA vs VMA
.data > RAM AT > FLASH:
- VMA(Virtual Memory Address)= RAM 地址,运行时在这里
- LMA(Load Memory Address)= Flash 地址,启动前在这里
_sidata = LOADADDR(.data)给启动汇编用,从这拷贝到_sdata。LMA/VMA 概念不懂,启动汇编拷贝段就写不对。
四、多核启动
RISC-V 多核(SMP)启动,所有核上电都进 BootROM,但通常只有一个核(核0)跑启动流程,其他核"park"等待。
4.1 主核(核0)流程
asm
_start:
csrr t0, mhartid
bnez t0, park # 非核0,去 park
# 核0:正常启动流程
la sp, _stack_top
# ... 5 件事 ...
call main
park:
# 非主核:等待唤醒
wfi # 低功耗等待中断
j park # 醒来再检查(或被主核引导)
4.2 主核唤醒从核
主核跑起来后,通过 MSI(写从核的 MSIP)或设置共享内存的启动地址,唤醒从核:
c
// 主核唤醒核1
*(uint32_t*)(CLINT + MSIP_OFFSET(1)) = 1; // 触发核1的 MSI
从核的 MSI handler 里跳转到应用代码。
五、ROM 固化规则
5.1 ROM 固化的三个硬条件
- 位置无关或固定地址:ROM 烧死后地址不变
- 不写自身所在段:ROM 只读
- 可写状态在 SRAM:函数的可写全局状态必须在 SRAM,不能跟着代码在 ROM
5.2 ROM 固化与启动的关系
- BootROM 本身就是最大的"固化代码"
- 你固化的库函数(
__divsi3/memcpy等)在 ROM,主程序链接时PROVIDE指向 ROM 地址 - 启动汇编调用的早期函数(如
memcpy拷 .data)若在 ROM,要先确认 ROM 可用
5.3 启动早期不能用的函数
启动汇编执行时,有些前提没建立,不能用:
- 不能调 printf(stdio 没初始化)
- 不能调 malloc(堆没初始化)
- 不能调依赖 .data 的函数(.data 还没拷)
- 不能调依赖全局对象的函数(C++ 构造没跑)
早期只能用:纯计算函数、ROM 固化的无状态函数、直接寄存器操作。
六、启动死机调试
启动阶段死机最难调(没有 printf)。方法:
6.1 串口/JTAG 早期输出
- 用 UART 寄存器直接输出字符(不用 printf,绕过 stdio)
- 在启动汇编每步后输出标记,定位卡在哪步
asm
_start:
la sp, _stack_top
uart_putc('1') # sp 设完
la gp, __global_pointer$
uart_putc('2') # gp 设完
# ...
6.2 JTAG 单步
- JTAG 调试器能在启动早期挂上(若芯片支持)
- 单步执行启动汇编,看哪步崩
6.3 常见启动死机
| 症状 | 可能原因 |
|---|---|
| 启动即崩,无输出 | sp 没设 / 复位向量错 |
| 访问全局变量崩 | gp 没设 |
| 全局变量值错 | .bss 没清零 |
| 写全局变量崩 | .data 没拷贝(写 Flash) |
| 开中断后崩 | mtvec 没配 |
| 随机崩 | 链接脚本段地址错 / 栈溢出 |
七、FreeRTOS 移植的启动钩子
FreeRTOS RISC-V port 在启动流程里挂钩:
7.1 FreeRTOS 需要的启动前提
- sp/gp/mtvec 已配(标准启动汇编)
- 定时器(CLINT mtimecmp/外设timer)可配
- 至少一个中断(MTI)使能
7.2 FreeRTOS 启动序列
c
// main 里
int main(void) {
hardware_init(); // 你的外设初始化
vPortDefineHeapRegions(); // FreeRTOS 堆
xTaskCreate(...); // 建任务
vTaskStartScheduler(); // 启动调度器
while(1); // 不应到这
}
vTaskStartScheduler() 内部:
- 配定时器中断(
vPortSetupTimerInterrupt配 mtimecmp) - 开全局中断(MIE)
- 切到第一个任务(用 mret 或类似机制切栈)
7.3 启动汇编里别开中断
- 启动汇编不开 MIE,等 FreeRTOS 调度器启动时再开
- 提前开中断,若 mtvec/handler 没就绪会崩
port 层就是连接"RISC-V 启动/trap/中断"和"FreeRTOS 调度"的胶水:① 配 mtvec 指向 FreeRTOS 的 trap entry;② trap entry 用 mscratch 切栈(④专题);③ tick 用 MTI;④ 任务切换在 trap 里完成。本专题 ③④⑤⑦ 合起来,就是 FreeRTOS port 的全部基础。
八、ARM vs RISC-V 启动对照
| ARM Cortex-M | RISC-V | |
|---|---|---|
| 复位向量 | 向量表第1项(初始 SP)+ 第2项(reset) | BootROM 跳 _start(或向量表) |
| 初始 SP | 硬件自动从向量表加载 | 软件在 _start 里 la sp |
| gp | 无 | 必须设(la gp, __global_pointer$) |
| .data 拷贝 | 启动代码做 | 启动代码做(一样) |
| .bss 清零 | 启动代码做 | 启动代码做(一样) |
| trap 入口 | VTOR 配 | mtvec 配 |
| 多核 | Cortex-M 多核少 | mhartid 判断主从 |
九、本篇小结
- 启动链路:BootROM → Bootloader → App _start → main
- 启动汇编必做 5 件事:sp / gp / .data拷贝 / .bss清零 / mtvec
- gp 必须设且防 relaxation(
.option norelax),RISC-V 特有易漏点 - 链接脚本的 LMA/VMA 概念:.data 在 Flash(LMA)运行在 RAM(VMA)
- 多核启动:mhartid 判断主从,从核 park,主核唤醒
- ROM 固化三硬条件:固定地址/不写自身段/状态在 SRAM
- 启动早期不能用 printf/malloc/依赖 .data 的函数
- FreeRTOS port 挂在启动流程上:mtvec/tick/MIE
速查表
| 启动步骤 | 代码 |
|---|---|
| 设 sp | la sp, _stack_top |
| 设 gp | .option norelax; la gp, __global_pointer$ |
| 拷 .data | 从 _sidata 拷到 _sdata~_edata |
| 清 .bss | _sbss~_ebss 清 0 |
| 配 trap | csrw mtvec, &trap_vector |
| 主核判断 | csrr t0, mhartid; bnez t0, park |
| 跳 main | call main |
| .data LMA | _sidata = LOADADDR(.data) |
| gp 范围 | __global_pointer$ = .sdata中点,±2KB |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。