4. ARMv8内存管理与MMU
U-Boot 移植,说白了就是让 Bootloader 在你的板子上「活过来」。我做了这么多年,最深的体会是:时钟和 DDR 是门槛,串口是眼睛,网络是手脚。这四关过不了,后面全是白搭。
今天咱们就按实际移植的顺序,把这四个核心模块拆开揉碎了讲。我会把我在项目里踩过的坑、总结的经验都放进来。
4.1 时钟与 PLL 配置:让芯片先跑起来
芯片上电后,默认时钟通常很低。你得先配置 PLL,把 CPU、总线、外设的时钟提上去。这是所有后续操作的基础。
4.1.1 时钟树分析
拿到一个 SoC 的参考手册,第一件事就是找时钟树图。你得搞清楚:
- 输入时钟源:板子上是 24MHz 晶振还是 25MHz?
- PLL 类型:ARM PLL、DDR PLL、PERI PLL…… 它们分别给谁供电?
- 分频链:从 PLL 输出到最终模块时钟,中间经过几级分频?
我个人习惯,先把时钟树画成一张表,对照着写代码,不容易乱。
关键原则:配置 PLL 时,一定要先确认输入时钟稳定,再操作 PLL 锁定。锁定后,等一段时间(通常几百微秒),再切换时钟源。顺序错了,芯片直接死给你看。
4.1.2 PLL 配置实战
以某款 ARMv8 芯片为例,配置 ARM PLL 到 1.2GHz 的典型步骤:
- 读取当前 PLL 状态,确保未锁定。
- 写 PLL 控制寄存器,设置倍频系数(比如 1200MHz / 24MHz = 50 倍)。
- 等待 PLL 锁定标志位置位。
- 切换 CPU 时钟源到新 PLL。
/* 伪代码示例:配置 ARM PLL */
void pll_config_arm(void) {
// 1. 进入 PLL 配置模式
writel(0x2, &pll->con);
// 2. 设置倍频系数 (MDIV=50, PDIV=1, SDIV=0)
writel((50 << 16) | (1 << 8) | (0 << 0), &pll->con);
// 3. 等待锁定
while (!(readl(&pll->status) & LOCK_BIT));
// 4. 切换时钟源
clk_set_parent(clk_get("arm_clk"), clk_get("arm_pll"));
}
我曾经在一个项目里,因为没等 PLL 锁定就切了时钟源,结果 CPU 跑飞,串口打印全是乱码。查了整整一天才找到原因。从那以后,我每次都在 PLL 锁定后加一个 10ms 的延时,确保万无一失。
4.2 DDR 初始化:内存是系统的基石
DDR 初始化是 U-Boot 移植里最复杂、最容易出问题的环节。没有内存,你连 printf 都跑不起来。
4.2.1 DDR 控制器配置要点
DDR 控制器的寄存器多如牛毛,但核心参数就几个:
| 参数 | 说明 | 常见坑点 |
|---|---|---|
| 时序参数 (tRCD, tRP, tRAS...) | 从 DDR 颗粒 datasheet 获取 | 不同频率下时序不同,别照搬 |
| 刷新率 (REFI) | DDR 需要定期刷新 | 刷新间隔太短会降低性能 |
| ODT (片上端接) | 信号完整性关键 | ODT 值不对,高频下会出错 |
| ZQ 校准 | 输出驱动强度校准 | 必须等 DDR 初始化完成后做 |
你想想看,这些参数任何一个错了,DDR 要么初始化失败,要么跑起来后随机死机。调试起来非常痛苦。
4.2.2 初始化流程
标准的 DDR 初始化流程,我总结为「五步走」:
- 上电与复位:给 DDR 颗粒供电,释放复位信号。
- 时钟使能:提供差分时钟 (CK/CK#)。
- 控制器配置:写时序、刷新、ODT 等参数。
- DDR 初始化序列:发送 MRS (模式寄存器设置) 命令。
- ZQ 校准:完成校准后,DDR 才可用。
/* DDR 初始化核心代码片段 */
void ddr_init(void) {
// 1. 配置 DDR 控制器时序
ddr_set_timing(tRCD, tRP, tRAS, tWR);
// 2. 设置刷新率
ddr_set_refresh(7.8); // 7.8us 刷新周期
// 3. 发送 MRS 命令
ddr_send_mrs(MR0, DDR_BL8 | DDR_CL_11);
ddr_send_mrs(MR1, DDR_ODT_40OHM);
// 4. 等待 ZQ 校准完成
while (!(readl(&ddr->status) & ZQ_DONE));
}
调试技巧:如果 DDR 初始化失败,先检查时钟是否稳定,再检查时序参数。我习惯在 DDR 初始化后,写一个简单的内存测试(比如写 0xAA、0x55 模式),读回来验证。能快速定位问题。
4.3 串口驱动适配:让 U-Boot 开口说话
串口是 U-Boot 的「眼睛」。没有串口输出,你根本不知道系统跑到哪一步了。
4.3.1 串口硬件初始化
串口驱动适配,核心就是配置 UART 控制器的波特率、数据位、停止位、校验位。以 115200 波特率为例:
/* 串口初始化 */
void uart_init(void) {
// 1. 使能 UART 时钟
clk_enable("uart0_clk");
// 2. 配置引脚复用
pinmux_set(PIN_UART0_TX, MODE_ALT1);
pinmux_set(PIN_UART0_RX, MODE_ALT1);
// 3. 设置波特率 (假设时钟 50MHz)
uart_set_baudrate(115200, 50000000);
// 4. 使能发送和接收
writel(UART_CTRL_TXE | UART_CTRL_RXE, &uart->ctrl);
}
嗯,这里要注意:波特率计算一定要精确。如果时钟频率不是整数倍,实际波特率会有误差。误差超过 3%,串口就可能丢数据。
4.3.2 调试输出函数
U-Boot 的 putc() 函数是串口驱动的核心。它负责发送一个字符。实现很简单:
void uart_putc(char c) {
// 等待发送缓冲区空
while (!(readl(&uart->status) & TX_EMPTY));
// 写数据
writel(c, &uart->data);
// 如果是换行符,加一个回车
if (c == '\n')
uart_putc('\r');
}
我曾经在一个项目里,串口能输出,但输入死活不行。查了半天,发现是 RX 引脚的中断没使能。U-Boot 的串口输入通常用轮询方式,但有些 SoC 需要额外配置接收触发条件。这个坑,我印象很深。
4.4 网络驱动调试:让 U-Boot 联网
网络驱动是 U-Boot 移植的「高阶关卡」。有了网络,你才能通过网络下载内核、文件系统,调试效率翻倍。
4.4.1 网络驱动架构
U-Boot 的网络驱动架构,说白了就是三层:
- 底层:MAC 控制器驱动,负责收发数据帧。
- 中间层:PHY 驱动,负责管理物理层。
- 上层:协议栈,负责解析 ARP、IP、UDP 等协议。
移植时,你主要关注底层和中间层。上层协议栈 U-Boot 已经帮你做好了。
4.4.2 PHY 驱动适配
PHY 芯片种类繁多,但接口是标准的 MDIO。适配 PHY 驱动,核心是:
- 通过 MDIO 总线读写 PHY 寄存器。
- 读取 PHY ID,确定 PHY 型号。
- 配置 PHY 的工作模式(如 1000Mbps、全双工)。
- 等待链路建立。
/* PHY 初始化示例 */
int phy_init(void) {
u16 phy_id;
// 1. 读取 PHY ID
phy_id = phy_read(PHY_ADDR, PHY_ID_REG);
if (phy_id != EXPECTED_PHY_ID) {
printf("PHY ID mismatch: 0x%04x\n", phy_id);
return -1;
}
// 2. 配置自动协商
phy_write(PHY_ADDR, PHY_ANAR, AN_1000BASE_T_FULL | AN_100BASE_TX_FULL);
phy_write(PHY_ADDR, PHY_BMCR, BMCR_ANENABLE | BMCR_ANRESTART);
// 3. 等待链路建立
while (!(phy_read(PHY_ADDR, PHY_BMSR) & BMSR_LINK_STATUS));
return 0;
}
4.4.3 常见调试问题
网络驱动调试,我遇到过最多的问题就是「ping 不通」。原因通常有:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| PHY 无法建立链路 | PHY 地址错误、复位引脚未拉高 | 用示波器看 MDIO 波形 |
| 能 ping 通,但丢包严重 | MAC 的 DMA 描述符配置错误 | 检查描述符链表的地址对齐 |
| 能收到包,但发不出去 | TX 描述符状态未正确更新 | 检查中断处理或轮询逻辑 |
| ARP 请求无响应 | MAC 地址未正确设置 | 检查环境变量 ethaddr |
调试技巧:我习惯在 U-Boot 里加一个「网络回环测试」命令。让板子自己给自己发包,验证 MAC 的收发通路是否正常。能快速排除 PHY 和链路的问题。
4.5 本章知识体系
下面这张图,把本章四个核心模块的关系画清楚了。你可以对照着看,理解它们之间的依赖顺序。
好了,这一章的内容就到这儿。时钟、DDR、串口、网络,这四个模块是 U-Boot 移植的「四座大山」。翻过它们,你的板子就能跑起来了。剩下的,就是根据具体硬件做微调。