4. 时钟系统配置:PLL锁相环原理、系统时钟树分析、为DDR与外设提供时钟

时钟,是嵌入式系统的“心跳”。

Bootloader 里如果时钟配错了,后面所有外设、DDR、CPU 频率全都会乱套。我见过太多工程师在移植 U-Boot 时,卡在时钟配置这一步好几天。说白了,时钟树没理清,后面全是白干。

4.1 PLL锁相环原理——从“抖”到“稳”

先聊聊 PLL 锁相环。很多同学觉得它很玄,其实没那么复杂。

PLL 的核心任务就一个:把低频的参考时钟,倍频成高频的稳定时钟。比如外部晶振只有 24MHz,但 CPU 要跑 1.2GHz,怎么办?靠 PLL。

一个典型的 PLL 结构包含四个部分:

  • 鉴频鉴相器(PFD):比较参考时钟和反馈时钟的相位差。
  • 电荷泵(CP):把相位差转换成电流信号。
  • 环路滤波器(LF):滤掉高频噪声,输出平滑的控制电压。
  • 压控振荡器(VCO):根据控制电压调整输出频率。

嗯,这里有个关键点:VCO 的输出频率 = 参考频率 × 倍频系数 / 分频系数。这个公式在配置寄存器时天天用。

我的经验:调试 PLL 时,别一上来就追求最高频率。我习惯先配一个中间值,比如 400MHz,确认锁定正常后,再往上调。否则 VCO 没锁住,系统直接死给你看。

我曾经在一个项目中,因为 PLL 的环路滤波器参数没配好,导致输出时钟抖动过大,DDR 读写偶尔出错。查了三天,最后用示波器看时钟波形才发现——嗯,从那以后,我每次配 PLL 都会先看一眼 VCO 的锁定状态寄存器。

4.2 系统时钟树分析——理清“谁给谁供时钟”

现代 SoC 的时钟树,说白了就是一张“时钟分发网”。

以 i.MX6ULL 为例,它的时钟树大概长这样:

i.MX6ULL 时钟树简化结构 外部晶振 24MHz OSC 振荡器 PLL1 (ARM PLL) PLL2 (系统 PLL) PLL3 (USB PLL) 分频器 / 选择器 分频器 / 选择器 分频器 / 选择器 ARM Core 时钟 AHB / IPG 时钟 USB / 外设时钟 说明:每个 PLL 输出后经过分频/选择,供给不同的总线域和外设 配置时需注意各总线最大频率限制,避免超频

你看,从外部晶振出发,经过 OSC 后分三路:

  • PLL1:专供 ARM Core,频率最高。
  • PLL2:系统 PLL,供给 AHB、IPG 等总线。
  • PLL3:USB PLL,也常被外设借用。

每个 PLL 后面还有一堆分频器和选择器。你想想看,如果选错了时钟源,或者分频系数配错了,DDR 可能直接初始化失败。

注意:配置时钟树时,一定要先确保当前使用的时钟源是稳定的。切换时钟源时,建议先切到临时时钟(比如 24MHz 晶振直通),等新 PLL 锁定后再切回来。否则系统会在切换瞬间“断粮”。

4.3 为DDR提供时钟——时序的命脉

DDR 对时钟的要求极其苛刻。频率、相位、抖动,哪一项不达标,系统就跑不稳。

在 Bootloader 里配 DDR 时钟,我一般按这个步骤来:

  1. 确定 DDR 类型和频率:DDR3 还是 DDR4?最高支持多少 MHz?
  2. 选择 PLL 源:通常用系统 PLL(PLL2)或专门的 DDR PLL。
  3. 配置分频系数:算出目标频率对应的分频值。
  4. 设置 DDR 控制器时钟:包括 DRAM 时钟、PHY 时钟、参考时钟。
  5. 调整相位偏移:确保 DQS 与时钟对齐。

举个例子,某款芯片的 DDR 时钟配置代码片段:

/* 配置 DDR PLL 为 528MHz */
reg_write(PLL_DDR_CTRL, 0x0000200D);  // 使能 PLL,设置倍频系数

/* 等待 PLL 锁定 */
while (!(reg_read(PLL_DDR_STATUS) & 0x01));

/* 设置 DDR 时钟分频,目标 264MHz */
reg_write(CCM_CSCDR1, (reg_read(CCM_CSCDR1) & ~0x1F) | 0x01);

/* 使能 DDR 时钟输出 */
reg_write(CCM_CCGR1, reg_read(CCM_CCGR1) | (0x3 << 12));

这段代码看着简单,但坑不少。我曾经因为忘记等待 PLL 锁定,直接往下配分频,结果 DDR 初始化死活过不去。后来加了 while 循环等锁定标志,问题就解决了。

关键点:DDR 时钟的相位对齐非常重要。如果 DQS 和时钟的相位偏差太大,DDR 控制器读回来的数据全是错的。我习惯在调试时先用 DDR 训练工具跑一遍,确认眼图合格后再固化配置。

4.4 为外设提供时钟——别让外设“饿着”

外设时钟配置,说白了就是“谁要用,就给谁开门”。

每个外设都有对应的时钟门控寄存器。比如 UART、I2C、SPI、Ethernet,它们各自挂在不同的时钟总线上。配置时要注意:

  • 时钟源选择:有些外设可以选 PLL2 或 PLL3 作为时钟源。
  • 分频系数:外设的工作频率不能超过其最大规格。
  • 时钟门控:必须使能对应外设的时钟门控位,否则外设寄存器读出来全是 0。

我见过一个案例:同事移植 U-Boot 时,UART 怎么都不输出。查了半天,发现是时钟门控没开。嗯,这种问题其实很常见,因为芯片上电后默认大部分外设时钟是关闭的。

下面是一个典型的外设时钟配置表:

外设 时钟源 最大频率 门控寄存器
UART1 PLL3 / 24MHz 5 MHz CCM_CCGR0
I2C1 PLL2 / IPG 400 kHz CCM_CCGR1
SPI1 PLL2 / AHB 30 MHz CCM_CCGR2
Ethernet PLL2 / ENET 125 MHz CCM_CCGR3
我的习惯:在 Bootloader 里,我不会一股脑把所有外设时钟都打开。只开当前需要的,比如 UART、DDR、Timer。其他外设等 Linux 内核启动后再由驱动去配。这样既省电,也减少干扰。

4.5 避坑指南——时钟配置常见问题

做 Bootloader 时钟配置这些年,我踩过不少坑。挑几个典型的说说:

  • PLL 未锁定就使用:后果是系统跑飞。解决办法:配完 PLL 后一定要轮询锁定标志。
  • 时钟源切换顺序错误:先切到临时时钟,再切到新 PLL。顺序反了,系统直接挂。
  • 分频系数计算错误:DDR 频率超了上限,导致数据损坏。建议用公式手动算一遍,别光靠工具。
  • 忘记使能时钟门控:外设寄存器读出来全是 0。调试时先检查门控寄存器。

我曾经在一个项目中,因为时钟树配置里漏了一个分频器的使能位,导致 AHB 总线频率只有预期的一半。系统能跑,但性能差得离谱。查了两天才找到原因——嗯,从那以后我配时钟树都会对着参考手册逐位核对。

最后提醒:不同芯片的时钟树差异很大。移植 Bootloader 时,一定要先读懂芯片手册里的时钟章节,别拿上一款芯片的配置直接套用。我见过有人把 i.MX6 的时钟配置直接搬到 i.MX7 上,结果 DDR 初始化失败——两个芯片的 PLL 结构完全不同。