4、时钟与复位配置:时钟树分析、display clock tree、PLL配置、复位信号处理

显示驱动能不能跑起来,时钟和复位是命根子。我见过太多工程师花了两周调显示参数,最后发现是PLL没锁住,或者复位信号时序不对。说白了,时钟不对,显示就是黑屏;复位不对,硬件就卡在中间态。

这一节我们就把显示子系统的时钟树掰开揉碎了讲。你跟着我走一遍,以后遇到显示时钟问题,心里就有底了。

4.1 显示时钟树整体架构

一个典型的嵌入式显示系统,时钟路径大概长这样:

显示时钟树整体架构 晶振 OSC (24MHz) PLL (VCO ~1.2GHz) 分频器 /M /N /P 时钟门控 CG Pixel Clock (DCLK) AHB/AXI 总线时钟 接口时钟 (MIPI/HDMI) 说明:实际SoC中可能包含多个PLL和分频链,这里展示的是最简通用模型

从晶振到最终输出,中间经过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配置、复位时序、时钟门控,这三个点抓住了,显示时钟树就稳了。