4、硬件初始化概述:从复位到C语言环境的过渡
嵌入式系统上电或复位后,CPU 首先执行的并不是 main() 函数,而是一段精心编写的汇编启动代码。这段代码承担着从硬件复位状态到高级语言运行环境之间的“桥梁”职责。对于 Bootloader(如 U-Boot)而言,启动代码的正确性直接决定了后续所有软件逻辑能否运行。本章将深入剖析这一过渡过程,重点讲解汇编启动代码的职责、栈指针(SP)的设置方法、BSS 段清零的必要性,以及如何安全地跳转到 C 语言入口。
4.1 核心原理:为什么需要汇编启动代码
当处理器复位引脚被释放后,CPU 会从预设的复位向量地址取指执行。此时 C 语言运行环境尚未建立,具体表现为:栈指针(SP)为未知值、全局变量未初始化、中断未配置、MMU 和 Cache 处于关闭状态。如果直接跳转到 C 代码,函数调用将无法进行(没有栈),全局变量访问将得到错误结果(BSS 未清零),系统行为完全不可预测。
汇编启动代码的核心目标可以归纳为以下四点:
- 建立基本执行环境:设置 CPU 工作模式(如 ARM 的 SVC 模式)、关闭中断、配置关键状态寄存器。
- 初始化内存子系统:对于使用 DDR 的嵌入式系统,需要先初始化 DDR 控制器,否则无法访问大容量内存。
- 准备 C 语言运行环境:设置栈指针、清零 BSS 段、复制数据段,使全局变量和函数调用可用。
- 跳转到高级入口:将控制权移交给 C 代码中的入口函数(如 U-Boot 的
_start跳转到board_init_f)。
值得注意的是,启动代码通常分为两个阶段:第一阶段是极简的汇编代码,完成最基础的硬件初始化和环境准备;第二阶段可能仍包含部分汇编,但主要逻辑已切换到 C 语言。这种分层设计既保证了启动的可靠性,又提高了代码的可维护性。
4.2 操作流程:汇编启动代码的关键步骤
以 ARM Cortex-A 系列处理器和 U-Boot 为例,启动代码的典型流程如下表所示:
| 步骤 | 操作内容 | 关键指令/寄存器 | 说明 |
|---|---|---|---|
| 1 | 设置异常向量表 | ldr pc, =reset_handler |
确保复位、中断、未定义指令等异常有正确的跳转目标 |
| 2 | 切换 CPU 模式 | cpsr / cps 指令 |
切换到 SVC 模式,关闭 IRQ 和 FIQ 中断 |
| 3 | 初始化关键硬件 | 写 CCM、DDR 控制器寄存器 |
时钟使能、DDR 控制器配置(若需要) |
| 4 | 设置栈指针 | ldr sp, =__stack_top |
为每种 CPU 模式分配独立栈空间,并确保对齐 |
| 5 | 清零 BSS 段 | ldr r0, =__bss_start / str r2, [r0], #4 |
循环将 BSS 段所有内存单元写 0 |
| 6 | 复制数据段 | ldmia / stmia |
将已初始化全局变量的初值从 ROM 拷贝到 RAM |
| 7 | 跳转 C 入口 | bl main 或 bl board_init_f |
使用分支链接指令,保存返回地址 |
在 U-Boot 中,arch/arm/lib/vector.S 定义了异常向量表,arch/arm/cpu/armv7/start.S 包含了复位后的启动流程。开发者需要根据具体 SoC 的参考手册调整时钟、DDR 等初始化参数。
4.3 案例拆解:栈指针设置与 BSS 清零的工程实践
下面以一个基于 i.MX6ULL 处理器的嵌入式系统为例,展示启动代码中栈指针设置和 BSS 清零的具体实现。该处理器内部有 128KB OCRAM,DDR 容量为 512MB,U-Boot 被烧录在 SD 卡中。
场景约束:系统上电后,DDR 尚未初始化,因此启动代码必须先在 OCRAM 中运行,完成 DDR 初始化后再将自身重定位到 DDR 中。栈指针需要分别设置:在 OCRAM 阶段使用 OCRAM 顶部的临时栈,重定位后切换到 DDR 中的正式栈。
汇编代码片段(简化版):
_start:
@ 设置 CPU 为 SVC 模式,关闭中断
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
@ 临时栈:使用 OCRAM 顶部(0x00900000 为 OCRAM 起始,大小 128KB)
ldr sp, =0x00920000
@ 初始化 DDR(此处省略具体寄存器配置)
bl ddr_init
@ 重定位 U-Boot 到 DDR(此处省略拷贝代码)
@ 设置正式栈:DDR 顶部向下 4MB 处
ldr sp, =0x8FF00000
@ 清零 BSS 段
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
bss_loop:
cmp r0, r1
bge bss_done
str r2, [r0], #4
b bss_loop
bss_done:
@ 跳转到 C 代码入口
bl board_init_f
在这个案例中,栈指针的设置经历了两个阶段。第一阶段使用 OCRAM 顶部的临时栈,因为此时 DDR 不可用;第二阶段在 DDR 初始化并完成重定位后,切换到 DDR 高地址处的正式栈。BSS 清零使用了一个简单的循环,将 __bss_start 到 __bss_end 之间的每个 32 位字写入 0。链接脚本(.lds)中定义了这两个符号的地址。
实际调试中,如果发现全局变量初始值不对,通常是因为数据段复制遗漏;如果函数调用崩溃,则要检查栈指针是否指向了有效内存且对齐正确。
4.4 常见误区与总结复盘
在 Bootloader 启动代码的开发与调试中,以下问题最为常见:
误区一:栈空间不足或溢出。栈大小分配过小,或者中断嵌套过深,会导致栈指针越过栈底,破坏相邻内存区域的数据。建议在链接脚本中为栈区域预留足够空间,并在调试阶段使用栈填充模式(如 0xAA)来检测溢出。
误区二:BSS 清零遗漏或范围错误。如果链接脚本中 __bss_start 和 __bss_end 定义错误,或者汇编代码中的循环条件写错,会导致部分全局变量未被清零,程序运行逻辑混乱。务必检查链接脚本的符号导出和汇编循环的边界条件。
误区三:栈指针未对齐。ARM 架构要求栈指针在函数调用时保持 8 字节对齐(AAPCS 标准),否则调用浮点或 64 位操作时可能触发对齐异常。设置 SP 时应使用 bic sp, sp, #7 强制对齐。
总结复盘时,建议开发者遵循以下检查清单:
- 确认复位向量地址与 SoC 的启动模式匹配(如从 SD 卡、NOR Flash 或内部 ROM 启动)。
- 验证 CPU 模式切换后中断确实被关闭,避免初始化过程中被意外中断打断。
- 使用 JTAG 调试器单步执行启动代码,观察 SP、PC 和关键寄存器的变化。
- 在 BSS 清零后设置一个软件断点,检查某个全局变量的值是否为 0。
- 确认跳转到 C 入口时,LR 寄存器保存了正确的返回地址,以便异常时回溯。
从复位到 C 语言环境的过渡是 Bootloader 中最基础也最关键的一环。汇编启动代码虽然简短,但每一行都直接影响系统的稳定性。只有深刻理解栈指针、BSS 清零和数据段复制的原理,并掌握调试方法,才能为后续的硬件初始化和操作系统引导打下坚实基础。