4. 启动代码与运行时初始化
当 RISC-V 处理器完成复位后,它并不会自动运行 C 语言代码。CPU 首先从复位向量取指,执行一段用汇编编写的启动代码,完成栈指针设置、数据段搬运、BSS 段清零、时钟与 PLL 配置等关键动作,最后才跳转到 C 语言入口函数。理解这段“从硬件到 C 语言”的过渡过程,是裸机外设开发的基础。本章围绕启动代码的完整执行链路展开,帮助读者建立清晰的运行时初始化模型。
一、核心原理:复位向量与启动链路
RISC-V 复位后,PC 被设置为芯片规定的复位地址,通常位于 Flash 或 ROM 的起始位置。该地址处存放的正是复位向量,它一般是一条跳转指令,指向真正的启动代码入口。启动代码运行在物理地址空间,尚未建立完整的 C 运行环境,因此必须使用汇编编写,且不能依赖任何未初始化的全局变量。
启动代码的核心任务可以概括为“搭环境、搬数据、清内存、配时钟、进 C 语言”。其中栈指针设置是第一步,因为后续任何函数调用、局部变量、中断保存都需要栈。数据段搬运负责把存储在 Flash 中的已初始化全局变量复制到 RAM,BSS 段清零则保证未初始化全局变量初值为 0。时钟与 PLL 配置决定 CPU 和外设的工作频率,必须在跳转 C 语言入口前完成,否则后续延时、串口波特率等都会失准。
二、方法流程:启动代码的典型实现
下面以一段典型的 RISC-V 启动汇编为例,展示从复位向量到 C 语言入口的完整流程。代码中使用了链接脚本导出的符号,如 _stack_top、_data_lma、_data_start、_data_end、_bss_start、_bss_end,这些符号由链接脚本定义,启动代码直接引用。
.section .text.start
.globl _start
_start:
/* 1. 设置栈指针 */
la sp, _stack_top
/* 2. 搬运 .data 段:Flash -> RAM */
la a0, _data_lma
la a1, _data_start
la a2, _data_end
1:
bgeu a1, a2, 2f
lw t0, 0(a0)
sw t0, 0(a1)
addi a0, a0, 4
addi a1, a1, 4
j 1b
2:
/* 3. 清零 .bss 段 */
la a1, _bss_start
la a2, _bss_end
3:
bgeu a1, a2, 4f
sw zero, 0(a1)
addi a1, a1, 4
j 3b
4:
/* 4. 配置时钟与 PLL */
call clock_init
/* 5. 跳转到 C 语言入口 */
call main
/* 6. main 返回后进入死循环 */
5:
j 5b
这段代码结构清晰,每一步都对应一个明确的运行时初始化目标。需要注意的是,clock_init 本身是一个 C 函数,但它被调用时栈已经可用,且不依赖任何全局变量,因此可以安全执行。如果 PLL 配置需要较长时间锁定,应在函数内部加入等待循环,而不是在汇编中盲目延时。
三、案例场景:从 Flash 启动的完整初始化
假设某 RISC-V MCU 的 Flash 起始地址为 0x0800_0000,RAM 起始地址为 0x2000_0000,栈顶设置为 0x2000_8000。链接脚本将 .data 段的加载地址放在 Flash 中,运行地址放在 RAM 中;.bss 段紧跟在 .data 之后。启动代码执行时,先设置 sp = 0x2000_8000,然后把 Flash 中的 .data 初值复制到 RAM,再把 .bss 区域全部写 0。接着调用 clock_init,将系统时钟从内部 RC 振荡器切换到 24 MHz 外部晶振经 PLL 倍频后的 144 MHz。最后调用 main,进入 C 语言世界。
main 之前就跑飞,优先检查栈指针是否指向合法 RAM 区域、.data 搬运长度是否正确、.bss 清零范围是否越界。使用调试器查看 sp 和 PC 是最直接的手段。
四、误区与复盘
初学者在启动代码阶段常犯的错误包括:忘记设置栈指针就直接调用函数,导致压栈写到非法地址;.data 搬运时按字节复制但长度计算错误,造成数据错位;.bss 清零范围包含了栈区域,把栈内容意外清零;PLL 配置后没有等待锁定就切换时钟源,导致 CPU 取指不稳定。这些问题的共同点是“顺序错误”或“边界错误”,因此在编写启动代码时,必须严格按照“栈 → 数据 → 内存 → 时钟 → C 入口”的顺序执行。
| 初始化步骤 | 关键操作 | 依赖条件 | 常见错误 |
|---|---|---|---|
| 设置栈指针 | sp = _stack_top | 链接脚本定义栈顶 | 栈顶未对齐或指向 Flash |
| 搬运 .data | Flash → RAM 复制 | LMA 与 VMA 正确 | 长度计算错误、按字节/字混淆 |
| 清零 .bss | RAM 区域写 0 | _bss_start/_bss_end 正确 | 范围包含栈区或外设寄存器 |
| 配置时钟/PLL | 写 PLL 寄存器并等待锁定 | 晶振已起振 | 未等待锁定就切换时钟源 |
| 跳转 C 入口 | call main | 栈、数据、时钟就绪 | main 返回后无死循环 |