4. FSBL与BootROM:启动流程详解、FSBL源码分析、第一级引导加载程序配置
Zynq的启动过程,说白了就是芯片从「断电」到「跑起Linux」的完整链条。很多初学者一上来就对着Petalinux敲命令,结果板子起不来,卡在某个阶段半天找不到原因。我当年也踩过这个坑——后来老老实实把BootROM和FSBL啃了一遍,才明白问题出在哪。
这一章,我们就来拆解这个启动链条。我会带着你从BootROM开始,一路走到FSBL,把每一级引导程序到底干了什么、怎么配置、源码里哪些是关键路径,都捋清楚。
4.1 启动流程全景图
先看一张整体流程图。我习惯把Zynq的启动想象成「接力赛」:BootROM是第一棒,FSBL是第二棒,U-Boot或ATF是第三棒。每一棒都有固定的交接规则。
4.2 BootROM:芯片的第一口「奶」
BootROM是Zynq上电后第一个执行的代码。它固化在芯片内部的ROM里,你改不了它,也删不掉它。它的任务很简单:把FSBL从外部存储加载到OCM(On-Chip Memory)里,然后跳过去执行。
我记得第一次调试启动问题时,板子死活起不来,串口什么输出都没有。后来用示波器量了Boot Mode引脚的电平,才发现是拨码开关接触不良,BootROM读到了错误的启动模式——它以为要从NAND启动,但板子上根本没焊NAND。嗯,这种低级错误,犯过一次就记住了。
4.2.1 BootROM的工作流程
- 上电复位:PS_POR_B引脚释放后,BootROM开始执行。
- 读取Boot Mode引脚:根据MIO[6:2]的电平,决定从哪种介质加载FSBL。
- 初始化存储控制器:比如QSPI控制器、SD控制器、NAND控制器等。
- 搜索Boot Image:在存储介质的固定偏移位置查找合法的Boot Image头。
- 加载到OCM:将FSBL代码从存储介质拷贝到OCM(地址0xFFFF0000)。
- 跳转执行:设置CPU的PC指针到OCM中的FSBL入口地址。
slcr.BOOT_MODE。这个寄存器在BootROM阶段就已经被写入了。我在调试时经常用这个来确认硬件配置是否正确。
4.2.2 Boot Image的格式
BootROM能识别的Boot Image有固定的格式。说白了,就是一段头部信息加上实际的FSBL二进制。头部里包含了校验和、长度、目标地址等关键信息。
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0x00 | Image Header Table | 分区表,描述每个分区的偏移和长度 |
| 0x20 | Boot Header | 包含宽度检测、加密标志、目标CPU等 |
| 0x40 | Partition Header #0 | FSBL分区的描述信息 |
| ... | FSBL Binary | 实际的FSBL可执行代码 |
这里有个坑:BootROM要求Boot Image必须存储在介质的固定偏移位置。比如QSPI Flash是偏移0x0,SD卡是偏移0x0(但需要FAT32文件系统)。如果你把镜像烧错了位置,BootROM找不到合法的头部,就会卡死在BootROM里——串口没有任何输出。
4.3 FSBL:第一级引导加载程序
FSBL(First Stage Boot Loader)是用户可配置的第一级引导程序。它由Xilinx SDK或Vitis生成,源码在Xilinx的GitHub上可以找到。我个人习惯把FSBL的源码下载下来,直接在里面加调试打印——这样能清楚地看到每一行代码的执行情况。
4.3.1 FSBL的主要职责
- 初始化PS基本外设:包括DDR控制器、UART、时钟、PLL等。
- 加载PL Bitstream:如果硬件设计中有FPGA逻辑,FSBL负责把Bitstream从存储介质加载到PL。
- 加载第二级引导程序:比如U-Boot或ARM Trusted Firmware(ATF)。
- 跳转到下一级:设置好CPU状态,跳转到SSBL的入口地址。
4.3.2 FSBL源码关键路径分析
FSBL的源码主要位于 fsbl/src/ 目录下。核心文件就几个,我带你过一遍最关键的。
1. main.c —— 入口函数
int main(void)
{
/*
* 第一步:初始化PCAP控制器
* PCAP是PS访问PL的接口
*/
PcapInit();
/*
* 第二步:加载并解析Boot Image
* 从存储介质中读取分区表
*/
FsblBootInit();
/*
* 第三步:处理每个分区
* 根据分区类型执行不同操作
*/
FsblHandoff();
return 0;
}
这个流程看着简单,但实际执行时有很多细节。比如 FsblBootInit() 里会调用 FfsBootInit(),这个函数会遍历所有分区,找到类型为 IMAGE_TYPE_BOOT_IMAGE 的分区。
2. fsbl_handoff.S —— 跳转汇编
这是FSBL最后执行的代码,负责跳转到下一级引导程序。我当初看这段汇编时,最困惑的是为什么要清空一些寄存器——后来才明白,这是为了给下一级程序一个干净的运行环境。
_fsbl_handoff:
/* 清空通用寄存器 */
mov r0, #0
mov r1, #0
/* ... 省略中间部分 ... */
/* 设置下一级入口地址 */
ldr r0, =NextStageAddress
ldr r1, [r0]
/* 跳转 */
bx r1
fsbl_printf() 函数。这个函数通过UART输出,前提是你已经在硬件设计里使能了UART。我曾经在 FsblBootInit() 前后各加一句打印,用来定位是加载阶段还是跳转阶段出了问题。
4.3.3 FSBL的配置选项
FSBL的行为可以通过Xilinx SDK/Vitis中的BSP设置来配置。常用的配置项如下:
| 配置项 | 可选值 | 说明 |
|---|---|---|
| FSBL_LOAD_BITSTREAM | TRUE / FALSE | 是否加载PL Bitstream |
| FSBL_WDT_ENABLE | TRUE / FALSE | 是否使能看门狗定时器 |
| FSBL_PERF_ENABLE | TRUE / FALSE | 是否使能性能测量(打印各阶段耗时) |
| FSBL_DEBUG_PRINT | TRUE / FALSE | 是否使能调试打印 |
我建议在开发阶段把 FSBL_DEBUG_PRINT 和 FSBL_PERF_ENABLE 都打开。这样你能看到FSBL每一步的耗时,比如加载Bitstream花了多少毫秒,初始化DDR花了多少。这些数据对优化启动速度很有帮助。
4.4 常见启动问题与排查方法
做Zynq项目,启动问题是最常见的。我整理了几个典型场景,你遇到了可以直接对照排查。
可能原因:BootROM阶段就卡住了。
排查步骤:
1. 检查Boot Mode引脚电平是否正确。
2. 检查存储介质中Boot Image的偏移位置是否正确。
3. 用示波器测量QSPI/SD的时钟信号,确认BootROM是否在读取。
可能原因:DDR初始化失败,或者Bitstream加载出错。
排查步骤:
1. 检查DDR型号和硬件设计中的参数是否匹配。
2. 检查Bitstream文件是否损坏,重新生成并烧录。
3. 在FSBL源码的
DdrInit() 前后加打印,定位具体卡在哪一步。
可能原因:FSBL加载U-Boot的地址不对,或者U-Boot本身有问题。
排查步骤:
1. 确认U-Boot的加载地址是否与FSBL中配置的一致。
2. 检查U-Boot的编译选项,确保是针对Zynq平台编译的。
3. 用
bootgen 工具重新生成Boot Image,确保分区顺序正确。
4.5 实战:手动构建一个最小FSBL工程
说了这么多理论,我们来点实际的。下面是一个最小FSBL工程的构建步骤,你可以跟着操作一遍。
- 打开Vitis,创建一个新的应用工程。
- 选择模板:在模板选择界面,选择「Zynq FSBL」。
- 配置BSP:在BSP设置中,打开
FSBL_DEBUG_PRINT和FSBL_PERF_ENABLE。 - 编译:直接编译,生成
fsbl.elf。 - 生成Boot Image:使用
bootgen工具,将fsbl.elf、bitstream.bit、u-boot.elf打包成一个BOOT.BIN。 - 烧录:将
BOOT.BIN烧录到SD卡或QSPI Flash中。 - 上电测试:连接串口,观察输出。
如果你看到类似下面的输出,说明FSBL跑起来了:
Xilinx Zynq FSBL
FSBL: Stage 1: Loading Bitstream
FSBL: Bitstream loaded successfully
FSBL: Stage 2: Loading U-Boot at 0x04000000
FSBL: Jumping to U-Boot at 0x04000000
FsblHandoff() 函数里,我会加一个延时循环——等待3秒,如果检测到某个GPIO引脚被拉低,就进入一个「调试模式」,打印更多信息。这样在生产环境下可以关闭调试打印,遇到问题时又能通过硬件触发进入调试模式,非常实用。
好了,这一章的内容就到这里。BootROM和FSBL是Zynq启动的基石,理解了它们,你就能看懂启动过程中每一步到底在干什么。下次板子起不来的时候,别急着怀疑硬件——先看看BootROM有没有读到正确的镜像,FSBL有没有正确初始化DDR。很多时候,问题就出在这些「理所当然」的环节里。