第1章:Bootloader移植——U-Boot启动流程分析
做嵌入式Linux开发,Bootloader是第一道坎。
我记得刚入行那会儿,带我的师傅就说:「搞不定U-Boot,后面全是空中楼阁。」当时不以为然,直到自己亲手把一块板子搞成砖头……嗯,那次经历让我老老实实把U-Boot从头啃了一遍。
今天咱们就聊聊U-Boot的启动流程、板级配置怎么改,以及DDR、Timer、串口这些底层驱动怎么移植。说白了,就是让你拿到一块新板子,能快速让U-Boot跑起来。
1.1 U-Boot启动流程:从复位到命令行
U-Boot的启动,分两个阶段。
第一阶段是汇编写的,跟CPU架构强相关。第二阶段是C语言,可移植性好。我习惯把这两个阶段叫做「硬起」和「软起」。
第一阶段:汇编启动(SPL/TPL)
芯片上电后,CPU从复位向量地址开始执行。通常是0x00000000或者0xFFFF0000。这时候DDR还没初始化,代码只能在SRAM或者内部ROM里跑。
主要干这几件事:
- 设置CPU为SVC模式,关中断
- 初始化时钟、关MMU和Cache
- 初始化串口(方便打印调试信息)
- 初始化DDR控制器
- 把U-Boot主体从Flash拷贝到DDR
- 跳转到第二阶段入口
关键点:SPL(Secondary Program Loader)就是第一阶段的产物。很多新式芯片(比如i.MX6、AM335x)都要求用SPL,因为内部SRAM太小,装不下完整U-Boot。
代码入口一般在 arch/arm/cpu/armv7/start.S。我贴一段关键流程:
reset:
/* 设置SVC模式,关中断 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关MMU和Cache */
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x0005
mcr p15, 0, r0, c1, c0, 0
/* 初始化板级基本配置 */
bl board_init_f
这里有个坑——board_init_f 执行时,DDR还没好。所以这个函数里不能定义全局变量,也不能用栈太深。我曾经在这上面栽过跟头,全局变量一多,直接死机。
第二阶段:C语言启动
DDR初始化完成后,U-Boot把自己从Flash搬到DDR里。然后跳转到 board_init_r,这才是真正的C语言世界。
第二阶段做的事:
- 初始化剩余外设(网卡、I2C、SPI、MMC等)
- 设置环境变量(bootargs、bootcmd)
- 初始化命令行终端
- 进入主循环,等待用户输入
你想想看,从复位到出现「U-Boot>」提示符,中间经历了多少步骤?每一步出错,板子就变砖。所以调试U-Boot,串口打印是命根子。
1.2 板级配置修改:让你的板子被U-Boot认识
拿到一块新板子,U-Boot不认识它。你得告诉U-Boot:你的CPU是什么、DDR多大、时钟多少、GPIO怎么接。
这些信息放在哪里?在 board/ 和 include/configs/ 目录下。
1.2.1 创建板级目录
我一般这么干:
# 复制参考板配置
cp -r board/vendor/common board/vendor/myboard
# 修改目录名和文件
mv board/vendor/common board/vendor/myboard
# 修改Makefile,把目标名改成myboard
然后修改 include/configs/myboard.h。这个头文件是板级配置的核心。里面定义了:
- DDR大小和时序
- 时钟频率
- 串口基地址
- 环境变量分区位置
- 网络配置
我的习惯:先找一个跟你的板子最接近的参考板。比如用i.MX6ULL,就参考 mx6ull_14x14_evk。改配置比从头写快10倍。
1.2.2 关键配置项
| 配置宏 | 说明 | 典型值 |
|---|---|---|
| CONFIG_SYS_TEXT_BASE | U-Boot在DDR中的加载地址 | 0x87800000 |
| CONFIG_SYS_SDRAM_BASE | DDR基地址 | 0x80000000 |
| CONFIG_SYS_INIT_SP_OFFSET | 栈指针偏移 | 0x400000 |
| CONFIG_CONS_INDEX | 调试串口号 | 1(UART1) |
改完配置后,执行 make myboard_defconfig 生成 .config 文件。然后 make 编译。如果编译通过,恭喜你,至少语法没问题。
1.3 DDR驱动移植:最头疼的一步
DDR初始化是Bootloader移植里最坑的环节。没有之一。
为什么?因为DDR时序参数一错,板子直接死。而且死得悄无声息——串口都没初始化,你连错误信息都看不到。
1.3.1 DDR初始化流程
- 配置DDR控制器(比如i.MX6的MMDC)
- 设置时序参数(CAS、RAS、tRCD、tRP等)
- 校准DDR(写电平、读DQS门控)
- 启动DDR自刷新
代码一般在 board/vendor/myboard/ddr.c 里。我拿一个实际例子:
void ddr_init(void)
{
struct mmdc_p_regs *mmdc = (struct mmdc_p_regs *)MMDC_P0_BASE;
/* 1. 复位DDR控制器 */
writel(0x0, &mmdc->mdctl);
/* 2. 配置DDR时序 */
writel(0x0F, &mmdc->mdotc); /* 片选时序 */
writel(0x3333, &mmdc->mdcfg0); /* 配置参数0 */
writel(0x0000, &mmdc->mdcfg1); /* 配置参数1 */
/* 3. 启动DDR */
writel(0x04000000, &mmdc->mdctl);
/* 4. 等待DDR就绪 */
while (!(readl(&mmdc->mdctl) & 0x00010000));
}
注意:DDR时序参数必须跟你的DDR芯片数据手册一致。我曾经因为tRCD差了1个时钟周期,板子在高温下频繁死机。查了三天才找到原因。
1.3.2 调试DDR的技巧
我个人经验:
- 先用低速时钟初始化DDR,跑通了再提频
- 用读写测试验证DDR稳定性(U-Boot自带
mtest命令) - 如果板子有多个DDR颗粒,逐个测试
嗯,这里要注意:DDR校准参数跟PCB布线长度强相关。同一批板子,不同批次可能都要微调。量产时一定要留余量。
1.4 Timer驱动移植:系统的心跳
Timer是U-Boot的脉搏。延时、超时、看门狗都靠它。
U-Boot里Timer的接口很简单:
timer_init()— 初始化定时器get_timer()— 获取当前时间(毫秒)udelay()— 微秒级延时
移植时,你需要实现这三个函数。一般用芯片内部的通用定时器(GPT)或者系统节拍定时器(SysTick)。
以i.MX6为例:
#define GPT_BASE 0x02098000
#define GPT_CR (GPT_BASE + 0x00)
#define GPT_PR (GPT_BASE + 0x04)
#define GPT_SR (GPT_BASE + 0x08)
#define GPT_CNT (GPT_BASE + 0x24)
void timer_init(void)
{
/* 使能GPT时钟 */
writel(0x00000041, GPT_CR); /* 使能+24分频 */
writel(0x00, GPT_PR); /* 不分频 */
}
ulong get_timer(ulong base)
{
return readl(GPT_CNT) / 1000 - base;
}
避坑指南:我曾经遇到过Timer计数溢出的问题。如果你的Timer是32位,主频100MHz,那每43秒就溢出一次。记得在 get_timer() 里处理溢出,或者用64位计数器。
1.5 串口驱动移植:调试的生命线
串口是嵌入式开发者的眼睛。没有串口,你连U-Boot死在哪都不知道。
U-Boot的串口驱动在 drivers/serial/ 目录下。移植时主要实现:
serial_init()— 初始化UART控制器serial_putc()— 发送一个字符serial_getc()— 接收一个字符serial_tstc()— 检查是否有数据
以16550兼容UART为例:
#define UART_BASE 0x02020000
#define UART_RBR (UART_BASE + 0x00)
#define UART_THR (UART_BASE + 0x00)
#define UART_LSR (UART_BASE + 0x14)
void serial_init(void)
{
/* 设置波特率115200,8N1 */
writel(0x00, UART_BASE + 0x08); /* 关中断 */
writel(0x83, UART_BASE + 0x0C); /* 设置DLAB */
writel(0x01, UART_BASE + 0x00); /* 分频系数 */
writel(0x00, UART_BASE + 0x04);
writel(0x03, UART_BASE + 0x0C); /* 8位数据,1位停止 */
}
void serial_putc(char c)
{
while (!(readl(UART_LSR) & 0x20)); /* 等待发送缓冲区空 */
writel(c, UART_THR);
}
重要:串口波特率必须跟你的终端软件一致。我见过有人把波特率设成115200,终端却配了9600,结果打印全是乱码。排查了半天才发现是这种低级错误。
1.6 实战经验总结
说了这么多,最后分享几点我的实战心得:
- 先让串口说话 — 其他驱动可以慢慢调,串口必须第一个跑通。没有打印信息,你就像瞎子摸象。
- DDR参数要保守 — 先按最慢的时序配,跑起来再优化。别一上来就追求极致性能。
- 用好U-Boot的命令行 —
bdinfo看板级信息,md读内存,mw写内存,mtest测DDR。这些命令能帮你快速定位问题。 - 保留一个能用的参考板 — 我每次移植新板子,都会在旁边放一块开发板。出问题了,对比一下配置差异,往往能找到原因。
U-Boot移植说难也难,说简单也简单。难在DDR和时钟这些底层参数,简单在框架已经很成熟了。你只要照着参考板改,大部分工作就是复制粘贴+微调。
下一章咱们聊聊Linux内核的启动流程和移植要点。到时候见。