4、时钟与复位配置:时钟树分析、display clock tree、PLL配置、复位信号处理
显示驱动能不能跑起来,时钟和复位是命根子。我见过太多工程师花了两周调显示参数,最后发现是PLL没锁住,或者复位信号时序不对。说白了,时钟不对,显示就是黑屏;复位不对,硬件就卡在中间态。
这一节我们就把显示子系统的时钟树掰开揉碎了讲。你跟着我走一遍,以后遇到显示时钟问题,心里就有底了。
4.1 显示时钟树整体架构
一个典型的嵌入式显示系统,时钟路径大概长这样:
从晶振到最终输出,中间经过PLL倍频、分频器降频、时钟门控开关。每个环节都可能出问题。我个人习惯是先看PLL能不能锁住,再看分频比对不对,最后查门控有没有被意外关掉。
4.2 PLL配置实战
PLL配置是时钟树里最核心的一步。配置错了,显示频率就不对,画面要么撕裂要么闪屏。
4.2.1 PLL计算公式
大部分SoC的PLL公式长这样:
VCO = OSC * M / N
Output = VCO / P
其中:
- OSC:晶振输入频率,常见24MHz或27MHz
- M:倍频系数(乘法器)
- N:预分频系数(除法器)
- P:后分频系数
- VCO:压控振荡器频率,通常有范围限制
关键约束:VCO频率必须在芯片手册规定的范围内。比如某款芯片要求VCO在800MHz~1.6GHz之间,你配了个600MHz,PLL根本锁不住。
4.2.2 实际配置案例
假设我们要输出148.5MHz的像素时钟(1080p@60Hz标准),晶振24MHz:
// 目标:148.5MHz
// 选 N=1, M=99, P=16
// VCO = 24 * 99 / 1 = 2376MHz → 超出范围,不行
// 选 N=2, M=99, P=8
// VCO = 24 * 99 / 2 = 1188MHz → 在范围内
// Output = 1188 / 8 = 148.5MHz → 完美
对应的设备树配置:
display_clocks: clock-controller@ff760000 {
compatible = "rockchip,rk3399-dclk";
reg = <0x0 0xff760000 0x0 0x1000>;
clocks = <&cru PLL_GPLL>, <&cru PLL_CPLL>;
clock-names = "gpll", "cpll";
assigned-clocks = <&cru PLL_VPLL>;
assigned-clock-rates = <1188000000>; // VCO频率
};
我的经验:配置PLL时,尽量让M和N都是整数,避免小数分频。小数分频虽然能精确出频率,但抖动大,显示效果会受影响。我曾经在一个项目里为了省一个分频器用了小数分频,结果画面边缘一直有毛刺,换了整数分频就好了。
4.3 显示时钟域划分
一个显示控制器内部通常有多个时钟域。我整理了一张表,方便你对照:
| 时钟域 | 典型频率 | 作用 | 注意事项 |
|---|---|---|---|
| Pixel Clock | 25~600MHz | 驱动像素逐行扫描 | 与分辨率直接相关,1080p@60Hz约148.5MHz |
| 总线时钟 | 100~400MHz | CPU/DMA读写帧缓冲区 | 太低会导致带宽不足,画面卡顿 |
| 接口时钟 | 同Pixel或倍频 | MIPI DSI/HDMI/LVDS串行传输 | MIPI DSI需要4倍Pixel Clock |
| AXI跨时钟域 | 异步 | 桥接总线与显示控制器 | 必须做异步FIFO,否则数据错乱 |
你想想看,如果总线时钟只有50MHz,但你要输出4K@60Hz,像素时钟都600MHz了,DMA根本来不及把数据从DDR搬到显示控制器。这就是典型的带宽瓶颈。
4.4 复位信号处理
复位比时钟更容易被忽略,但坑更多。我遇到过最离谱的一次,显示驱动加载完初始化序列,屏幕死活不亮,查了两天发现是复位引脚被GPIO拉高了,硬件复位信号根本没生效。
4.4.1 复位类型
- 硬件复位(HRESET):芯片引脚直接拉低,最彻底
- 软件复位(SRESET):写寄存器触发,不影响其他模块
- 电源复位(POR):上电时自动产生
- 看门狗复位:系统卡死时触发
4.4.2 复位时序要求
显示控制器对复位时序有严格要求。以某款MIPI DSI控制器为例:
// 复位时序要求
// 1. 复位低电平保持至少 10us
// 2. 复位释放后,等待 PLL 锁定(约200us)
// 3. 等待内部状态机稳定(约100us)
// 4. 然后才能开始配置寄存器
// 驱动中实现
static void dsi_reset_sequence(struct dsi_device *dsi)
{
// 拉低复位
writel(0, dsi->base + DSI_SRST_REQ);
udelay(20); // 保持20us,留余量
// 释放复位
writel(1, dsi->base + DSI_SRST_REQ);
mdelay(1); // 等待PLL锁定+状态机稳定
// 检查复位完成状态
if (!(readl(dsi->base + DSI_STATUS) & DSI_SRST_DONE))
dev_err(dsi->dev, "复位超时\n");
}
注意:复位释放后不能立刻写寄存器。我曾经在释放复位后只等了50us就开始配置,结果寄存器写进去的值读出来是乱的。后来查手册才发现内部状态机需要100us才能稳定。这个坑我踩过,你别再踩了。
4.4.3 设备树中的复位配置
display_subsystem: display-subsystem {
compatible = "rockchip,display-subsystem";
ports = <&hdmi_out>, <&dsi_out>;
// 复位控制
resets = <&cru SRST_HDMI>, <&cru SRST_DSI>;
reset-names = "hdmi", "dsi";
// 复位释放延迟
reset-delay-us = <200>;
};
4.5 常见时钟复位问题排查
我把这些年遇到过的时钟复位问题整理成了一张排查表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 黑屏,无信号 | PLL未锁定 | 读PLL锁定状态寄存器,用示波器测时钟输出 |
| 画面撕裂 | 像素时钟与帧率不匹配 | 计算实际像素时钟,对比VESA标准 |
| 画面偏移 | 复位时序不对,内部计数器未清零 | 检查复位释放后的等待时间 |
| 间歇性闪屏 | 时钟门控被意外关闭 | 检查CG寄存器,确认没有被其他驱动误操作 |
| 初始化失败 | 复位后寄存器未准备好 | 增加复位释放后的延迟,检查状态寄存器 |
我的调试习惯:遇到显示时钟问题,第一步不是看代码,而是拿示波器量时钟引脚。先确认有没有时钟,频率对不对,抖动大不大。硬件能告诉你的信息,比软件日志多得多。
4.6 时钟门控与动态频率调整
现代SoC为了省电,会在显示空闲时关闭时钟门控。但门控开关的时机很重要:
- 垂直消隐期(VBlank):可以安全关闭像素时钟
- 水平消隐期(HBlank):可以降低总线时钟频率
- 屏幕关闭时:可以关闭整个显示时钟域
// 动态时钟门控示例
static void display_set_clk_gating(struct display_device *disp, bool enable)
{
u32 val;
val = readl(disp->base + DISP_CLK_CTRL);
if (enable) {
// 等待当前帧结束(VBlank)
while (!(readl(disp->base + DISP_STATUS) & VBLANK_ACTIVE))
;
val |= CLK_GATE_EN;
} else {
val &= ~CLK_GATE_EN;
}
writel(val, disp->base + DISP_CLK_CTRL);
}
警告:不要在帧中间关闭时钟门控!否则会导致当前帧数据损坏,画面出现花屏。一定要等到VBlank信号到来再操作。
嗯,时钟和复位这部分内容就到这里。你把这些配置和排查方法记牢了,以后遇到显示时钟问题,至少知道从哪里下手。PLL配置、复位时序、时钟门控,这三个点抓住了,显示时钟树就稳了。