启动流程第一阶段(汇编部分):start.S深度解析
大家好,我是你们的嵌入式底层工程师老K。今天咱们来啃一块硬骨头——U-Boot启动流程的第一阶段,也就是汇编部分。说白了,就是分析那个让无数人头疼的start.S文件。
我记得刚入行那会儿,第一次打开start.S,满屏的汇编指令看得我头皮发麻。但后来我发现,只要你抓住了核心脉络,这东西其实没那么玄乎。今天我就带着大家,把CPU初始化、中断向量表设置、SVC模式切换这几个关键点,一个一个掰开揉碎了讲清楚。
核心要点:第一阶段汇编代码负责从零开始搭建C语言运行环境。说白了,就是让CPU从复位后的“混沌状态”进入一个有序、可控的状态。
1. 中断向量表:CPU的“紧急通讯录”
CPU复位后,第一件事就是去找中断向量表。你可以把它想象成一本“紧急通讯录”——当各种异常事件发生时(比如复位、未定义指令、数据访问异常等),CPU会按照这个表跳转到对应的处理函数。
在ARM架构中,中断向量表通常放在起始地址0x00000000处。但U-Boot里有个小技巧——它会把向量表复制到RAM的高地址处,然后通过设置CP15协处理器来重映射。为什么要这么做?嗯,这里有个坑:如果向量表只放在Flash里,一旦你启用了MMU或者进入了某些特殊模式,CPU可能找不到处理函数。我曾经在一个项目中遇到过这个问题,板子一跑就死,查了三天才发现是向量表位置没配好。
// 典型的ARM中断向量表布局(start.S中)
_start:
b reset // 0x00: 复位
ldr pc, _undefined_instruction // 0x04: 未定义指令
ldr pc, _software_interrupt // 0x08: 软件中断
ldr pc, _prefetch_abort // 0x0C: 预取中止
ldr pc, _data_abort // 0x10: 数据中止
ldr pc, _not_used // 0x14: 保留
ldr pc, _irq // 0x18: 普通中断
ldr pc, _fiq // 0x1C: 快速中断
注意看,复位向量用的是b reset(相对跳转),而其他向量用的是ldr pc, xxx(绝对跳转)。为什么?因为复位后CPU可能还没初始化好内存控制器,绝对跳转会出问题。而b指令是位置无关的,不管代码在哪儿都能跳对。这个细节,很多新手会忽略。
3. 设置SVC模式:给CPU一个“管理员身份”
ARM处理器有多种运行模式,比如用户模式(USR)、系统模式(SYS)、中断模式(IRQ/FIQ)等。但U-Boot启动时,必须切换到SVC模式(超级管理员模式)。为什么?因为只有SVC模式才能访问所有系统资源,配置CP15协处理器,设置MMU页表等。
我个人习惯在复位后立即切换模式,免得后面出幺蛾子。切换方法很简单——修改CPSR寄存器。
// 切换到SVC模式,并关闭所有中断
reset:
mrs r0, cpsr // 读取当前程序状态寄存器
bic r0, r0, #0x1F // 清除模式位(低5位)
orr r0, r0, #0xD3 // 设置模式位:SVC模式(0x13) + 禁止IRQ(0x40) + 禁止FIQ(0x80)
msr cpsr, r0 // 写入CPSR,完成模式切换
这里有个小细节:0xD3这个值,二进制是1101 0011。其中低5位10011就是SVC模式的编码。而0x40和0x80分别对应IRQ和FIQ的禁止位。启动阶段为什么要关中断?你想想看,这时候内存都还没初始化好,中断来了你连堆栈都没有,一压栈就崩了。所以,先关掉所有中断,等环境搭好了再开。
个人经验:我建议你在切换模式后,顺手把各个模式下的堆栈指针(SP)也初始化一下。虽然当前只用SVC模式,但万一后面调试时需要切到其他模式,堆栈没设好会直接死机。我曾经就因为这个,浪费了一整天。
3. CPU初始化:让硬件“醒过来”
CPU初始化是第一阶段最核心的工作。主要包括:
- 关闭MMU和Cache:刚复位时,MMU和Cache都是关闭的,但有些CPU默认会开启。我们需要显式关闭它们,因为此时页表还没建立,开MMU会导致地址翻译错误。
- 设置时钟:配置CPU主频、总线频率、外设时钟等。不同芯片的时钟树差异很大,这块需要参考具体芯片手册。
- 初始化内存控制器:配置DDR/LPDDR的时序参数,让内存可用。这是最关键的一步,因为后续的代码都要在内存里跑。
// 关闭MMU和Cache(CP15协处理器操作)
cpu_init_crit:
mrc p15, 0, r0, c1, c0, 0 // 读取系统控制寄存器
bic r0, r0, #0x00000005 // 关闭MMU(bit0)和Data Cache(bit2)
bic r0, r0, #0x00001000 // 关闭Instruction Cache(bit12)
mcr p15, 0, r0, c1, c0, 0 // 写回控制寄存器
dsb // 数据同步屏障,确保操作完成
isb // 指令同步屏障,刷新流水线
这里要注意dsb和isb指令。很多新手会忽略它们,结果发现配置没生效。说白了,dsb保证前面的写操作都完成了,isb保证后面的指令重新读取。没有这两条,CPU可能还在用旧的配置执行指令,那不乱套了吗?
避坑指南:我曾经在一个项目中,因为忘记在关闭MMU后执行isb,导致后续的代码跳转到了错误的地址。板子表现很奇怪——有时候能启动,有时候不能。后来用JTAG单步跟踪才发现,流水线里还残留着MMU开启时的指令。加上isb后,问题立刻解决。
4. 代码重定位:从Flash搬到RAM
U-Boot的代码通常存储在Flash或SD卡里,但Flash的读取速度太慢,而且有些Flash不支持随机访问。所以,我们需要把U-Boot代码从Flash复制到RAM中运行。这个过程就叫“重定位”。
重定位的代码通常写在start.S的末尾,用C语言函数board_init_f()来调用。但在汇编阶段,我们只需要准备好重定位的条件:
- 确保内存控制器已经初始化
- 计算代码的源地址(Flash)和目标地址(RAM)
- 设置好堆栈指针(SP),因为后面要调用C函数了
// 设置堆栈指针,准备跳转到C代码
ldr sp, _TEXT_BASE // 设置堆栈基址,通常在RAM的高地址处
bl board_init_f // 跳转到C语言阶段的初始化函数
这里_TEXT_BASE是U-Boot在RAM中的运行地址,一般在配置文件中定义。比如对于某些ARM芯片,这个值可能是0x80800000。堆栈指针设置在它下面,向下生长。
5. 第一阶段流程图
为了让大家更直观地理解整个流程,我画了一张流程图。这张图把从复位到跳转到C语言阶段的关键步骤都串起来了。
6. 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 复位后板子没反应 | 中断向量表设置错误,或复位向量跳转地址不对 | 检查链接脚本,确认_start符号在0地址处 |
| 切换到SVC模式后死机 | CPSR模式位设置错误,或堆栈指针未初始化 | 用JTAG读出CPSR值,确认模式位为0x13 |
| 关闭MMU后程序跑飞 | 未执行dsb和isb指令 |
在修改CP15寄存器后,立即添加这两条指令 |
| 内存初始化后读写异常 | 时序参数配置错误,或DDR电压不对 | 使用芯片厂商提供的DDR训练工具生成参数 |
我的建议:如果你在移植U-Boot时遇到第一阶段的问题,别急着改代码。先用串口打印或者JTAG单步跟踪,确认CPU执行到了哪一步。我个人的习惯是,在start.S的关键位置加上GPIO翻转的代码——比如初始化内存前点亮一个LED,初始化后熄灭它。这样不用调试器也能知道程序跑到哪儿了。这招在量产调试时特别管用。
好了,关于U-Boot启动第一阶段的内容,今天就讲到这里。这部分虽然全是汇编,但逻辑其实很清晰——就是让CPU从“裸奔”状态进入一个受控的环境,为后续的C语言代码铺好路。你只要记住:中断向量表是入口,SVC模式是权限,CPU初始化是基础,重定位是目标。把这四个点抓住了,start.S就不再是天书了。