已验证 创建 2026/09/11 更新 2026/09/11

屏幕帧率精确匹配

#mtk

[MTK]屏幕帧率精确匹配:不修改 Porch 和 .clock,使用 data_rate_khz 精调 PLL

一、问题背景

当前 LCD 实测刷新率约为:

60.066 Hz

项目要求刷新率达到:

60 ± 0.01 Hz

也就是允许范围:

59.99 ~ 60.01 Hz

因此当前 60.066 Hz 偏高,需要把 MIPI DSI 的实际输出速率稍微降低。

这次调试有一个重要原则:

不修改 H/V Porch,不修改 drm_display_mode 的 .clock,只通过 data_rate_khz 对 MIPI PLL 做更细粒度的调整。

这样做的好处是不会改变面板原本的 H/V Timing,只微调 DSI PHY 的发送速率。


二、先理解为什么刷新率会变化

对于固定的显示 Timing:

Htotal = HACT + HFP + HSA + HBP
Vtotal = VACT + VFP + VSA + VBP

理论刷新率大致为:

FPS = Pixel Clock / (Htotal × Vtotal)

正常情况下,可以通过修改:

HFP / HBP / HSA
VFP / VBP / VSA
.clock

来改变刷新率。

但是本项目不希望修改这些参数。

因此采用另外一种方式:

调整 MIPI DSI PLL Data Rate

当 H/V Timing 不变时,MIPI 发送速率稍微降低,实际扫描周期也会略微拉长,因此实测刷新率可以相应降低。


三、原来的 MIPI 参数

Panel 驱动:

kernel_device_modules-6.12/drivers/gpu/drm/panel/
panel-hjr132037d-wqxga2400-dsi-vdo.c

原来的核心参数:

static struct mtk_panel_params ext_params = {
	.pll_clk = 355,
	.data_rate = 355 * 2,
};

也就是:

pll_clk = 355 MHz

data_rate
= 355 × 2
= 710 MHz

等价于:

710000 kHz

当前在这个速率下实测:

60.066 Hz

四、为什么直接修改 data_rate 不够精确

注意:

.data_rate = 710,

它的单位实际上是:

MHz

所以它只能比较方便地调整成:

710 MHz
709 MHz
708 MHz
...

比如从:

710 MHz

直接变成:

709 MHz

变化量是:

1 MHz = 1000 kHz

对于刷新率只允许:

±0.01 Hz

这种要求来说,1 MHz 的调整粒度可能太粗。

我们真正希望的是类似:

710000 kHz
↓
709220 kHz

只降低:

780 kHz

因此需要使用:

.data_rate_khz

进行 kHz 级别的精调。


五、Panel 参数修改

修改:

static struct mtk_panel_params ext_params = {
	.pll_clk = 355,
	.data_rate = 355 * 2,
	.data_rate_khz = 709220,
};

此时三个参数可以这样理解:

pll_clk      = 355 MHz
data_rate    = 710 MHz
data_rate_khz = 709220 kHz

其中:

.data_rate = 710

仍然保留,用于兼容原来的 MHz 参数体系。

真正用于此次精调的是:

.data_rate_khz = 709220

也就是:

709.220 MHz

六、为什么计算得到 709220 kHz

当前:

MIPI Data Rate = 710000 kHz
实测 FPS       = 60.066 Hz

目标:

60.000 Hz

因为这里做的是小范围线性微调,可以近似认为:

新 FPS / 旧 FPS
≈
新 Data Rate / 旧 Data Rate

所以:

新 Data Rate
=
旧 Data Rate × 目标 FPS / 当前 FPS

代入:

= 710000 × 60 / 60.066
≈ 709220 kHz

也可以反过来预测修改后的 FPS:

60.066 × 709220 / 710000
≈ 60.00001 Hz

因此理论上:

FPS ≈ 60.00001 Hz

满足:

60 ± 0.01 Hz

要求。


七、为什么光在 Panel 里面写 data_rate_khz 还不够

这是这次修改最关键的地方。

Panel 里面虽然配置了:

.data_rate_khz = 709220,

但最终真正设置 MIPI PLL 的代码位于 MTK DRM/MIPI PHY 驱动。

相关文件:

kernel_device_modules-6.12/drivers/gpu/drm/mediatek/
mediatek_v2/platform/mtk_drm_6789.c

以及:

kernel_device_modules-6.12/drivers/gpu/drm/mediatek/
mediatek_v2/v1/platform/mtk_drm_6789.c

最终都会进入:

mtk_mipi_tx_pll_prepare_mt6789()

这里才是真正准备 MIPI PLL 参数的位置。


八、原来的代码逻辑

原代码大致为:

rate = (mipi_tx->data_rate_adpt) ?
	mipi_tx->data_rate_adpt :
	mipi_tx->data_rate / 1000000;

rate_khz = (mipi_tx->data_rate_adpt) ?
	mipi_tx->data_rate_adpt * 1000 :
	mipi_tx->data_rate / 1000;

先看:

rate

它的单位是:

MHz

而:

rate_khz

单位是:

kHz

原来的 rate_khz 只能从两个地方取值:

data_rate_adpt × 1000

或者:

data_rate / 1000

也就是说,原代码并没有优先使用我们新增的:

data_rate_khz_adpt

因此即使 Panel 配置:

.data_rate_khz = 709220,

PLL 代码如果不支持这个参数,最后仍然可能按照:

710000 kHz

运行。

这样 Panel 里的 data_rate_khz 就无法真正实现精调。


九、修改后的核心逻辑

修改为:

rate_khz = (mipi_tx->data_rate_khz_adpt) ?
	mipi_tx->data_rate_khz_adpt :
	((mipi_tx->data_rate_adpt) ?
	mipi_tx->data_rate_adpt * 1000 :
	mipi_tx->data_rate / 1000);

把它拆开看就非常容易理解:

if (mipi_tx->data_rate_khz_adpt)
	rate_khz = mipi_tx->data_rate_khz_adpt;
else if (mipi_tx->data_rate_adpt)
	rate_khz = mipi_tx->data_rate_adpt * 1000;
else
	rate_khz = mipi_tx->data_rate / 1000;

因此新的优先级变成:

data_rate_khz_adpt
        ↓
data_rate_adpt
        ↓
data_rate

也就是:

kHz 精确值
优先级最高

↓

MHz 调整值

↓

默认 Data Rate

十、以本项目实际参数举例

Panel:

.pll_clk = 355,
.data_rate = 710,
.data_rate_khz = 709220,

如果上层已经把 Panel 的:

data_rate_khz

正确传递到:

mipi_tx->data_rate_khz_adpt

那么最终:

mipi_tx->data_rate_khz_adpt = 709220;

进入:

mtk_mipi_tx_pll_prepare_mt6789()

以后:

rate_khz = 709220;

PLL 最终按照:

709220 kHz

计算,而不是原来的:

710000 kHz

这就是此次修改真正生效的关键。


十一、为什么 rate 还是 710 MHz?

这里很容易产生疑惑。

代码中:

rate = (mipi_tx->data_rate_adpt) ?
	mipi_tx->data_rate_adpt :
	mipi_tx->data_rate / 1000000;

可能仍然得到:

rate = 710 MHz

而:

rate_khz = 709220 kHz

看起来好像矛盾:

rate     = 710 MHz
rate_khz = 709.220 MHz

其实不矛盾。

rate 是原来 MHz 级的粗粒度参数,而新增的:

rate_khz

用于更精细的 PLL 计算。

也就是说:

rate

主要用于原有逻辑、范围判断以及兼容。

而真正希望增加精度的地方使用:

rate_khz

从:

整数 MHz

提升到:

整数 kHz

精度提高了:

1000 倍

十二、日志也进行了修改

原日志:

DDPINFO(
	"prepare: %u MHz, mipi_tx->data_rate_adpt: %d MHz, mipi_tx->data_rate : %d MHz\n",
	rate,
	mipi_tx->data_rate_adpt,
	(mipi_tx->data_rate / 1000000));

修改以后增加:

rate_khz

打印:

DDPINFO(
	"prepare: %u MHz (%u kHz), mipi_tx->data_rate_adpt: %d MHz, mipi_tx->data_rate : %d MHz\n",
	rate,
	rate_khz,
	mipi_tx->data_rate_adpt,
	(mipi_tx->data_rate / 1000000));

这样抓 Kernel log 时,就可以确认 PLL 实际拿到的精确速率。

理想情况下应该看到类似:

prepare: 710 MHz (709220 kHz) ...

最重要的是确认:

709220 kHz

是否真正传到了底层。

如果日志仍然打印:

710000 kHz

说明:

panel .data_rate_khz

到:

mipi_tx->data_rate_khz_adpt

之间的数据传递链路还有问题。


十三、完整数据流要这样理解

本次修改的整体数据流可以画成:

Panel Driver
    │
    │
    ├── pll_clk = 355
    │
    ├── data_rate = 710 MHz
    │
    └── data_rate_khz = 709220 kHz
                     │
                     ▼
             MTK DRM Panel Ext
                     │
                     ▼
          data_rate_khz_adpt
                     │
                     ▼
       mtk_mipi_tx_pll_prepare_mt6789()
                     │
                     ▼
          rate_khz = 709220
                     │
                     ▼
             MIPI PLL 计算
                     │
                     ▼
              DSI PHY 输出
                     │
                     ▼
             LCD 实际刷新率
                     │
                     ▼
               ≈ 60.000 Hz

所以真正要检查的是整条链路,而不能只看:

.data_rate_khz = 709220

这一行。


十四、为什么两个 mtk_drm_6789.c 都要修改

当前代码树里面存在:

mediatek_v2/platform/mtk_drm_6789.c

和:

mediatek_v2/v1/platform/mtk_drm_6789.c

两个版本。

它们里面都有:

mtk_mipi_tx_pll_prepare_mt6789()

因此这次两个地方做了相同修改。

目的就是保证实际编译使用哪个版本时:

data_rate_khz_adpt

都能够生效。

否则可能出现:

改了一个文件
↓
实际编译的是另一个文件
↓
data_rate_khz 完全没有生效

这种情况。


十五、这次修改本质上改了什么

一句话总结:

原来的 MTK MIPI PLL 主要按照 MHz 粒度配置,现在增加 data_rate_khz_adpt 优先级,使 PLL 可以按照 kHz 粒度配置,从而对实际刷新率进行更精细的调整。

原来:

710 MHz
709 MHz
708 MHz

调整步进约:

1 MHz

现在可以:

710000 kHz
709900 kHz
709800 kHz
709220 kHz
...

调整步进:

1 kHz

因此特别适合:

60.066 Hz

这种只需要微调到:

60.000 Hz

的场景。


十六、为什么不直接修改 Porch

例如修改:

HFP
HBP
VFP
VBP

也可以改变刷新率。

但是会直接改变 Panel Timing。

可能带来:

Panel 时序偏离规格书
显示稳定性变化
TE/DSI Timing 变化
兼容性问题
不同阶段 Timing 不一致

而当前:

H/V Timing 本身没有问题

只是:

实测 FPS = 60.066 Hz

稍微偏高。

因此更合理的思路是:

保持 Timing 不变
        +
只微调 PLL

也就是这次:

.data_rate_khz = 709220

的意义。


十七、为什么不修改 .clock

.clock 属于显示模式的 Pixel Clock,例如:

static const struct drm_display_mode default_mode = {
	.clock = xxx,
	.hdisplay = 1600,
	...
};

修改 .clock 同样会影响理论刷新率。

但 MTK DSI 实际 PHY 时钟还受到:

pll_clk
data_rate
data_rate_khz

等参数控制。

如果当前 H/V Timing、.clock 已经作为正式 Panel Timing 使用,只为了修正:

0.066 Hz

的误差,没有必要把整套显示 Timing 都改掉。

因此此次采用:

.clock             不动
H/V porch          不动
pll_clk            基本不动
data_rate          保留
data_rate_khz      微调

是影响范围更小的一种方案。


十八、实际调试方法

第一次可以按照比例公式计算:

新 Data Rate
=
旧 Data Rate × 目标 FPS / 实测 FPS

例如:

当前:

FPS = 60.066
Data Rate = 710000 kHz

目标:

FPS = 60.000

计算:

710000 × 60 / 60.066
≈ 709220 kHz

配置:

.data_rate_khz = 709220,

然后:

重新编译
→ 烧录
→ 测量刷新率

十九、如果实测仍有误差怎么继续调

公式仍然一样。

例如修改到:

709220 kHz

以后实测:

60.008 Hz

那么下一次:

新 Data Rate
=
709220 × 60 / 60.008

再根据结果继续调整。

可以记住一个简单规律:

实际 FPS 偏高
→ 降低 data_rate_khz

实际 FPS 偏低
→ 提高 data_rate_khz

例如:

60.03 Hz
→ data_rate_khz 往下降

59.97 Hz
→ data_rate_khz 往上加

二十、调试时重点检查什么

首先检查 Panel:

.data_rate_khz = 709220,

然后检查 Kernel log 中:

rate_khz

应该真正变为:

709220

最后再测:

实际 FPS

所以排查顺序应该是:

Panel 参数
    ↓
data_rate_khz 是否传递
    ↓
data_rate_khz_adpt 是否有值
    ↓
rate_khz 是否为 709220
    ↓
PLL 是否使用 rate_khz
    ↓
实测 FPS

不要一开始看到 FPS 没变化就继续乱改:

Porch
clock
pll_clk

先确认:

709220

到底有没有真正进入 PLL。


二十一、需要特别注意的 Git Diff

此次 diff 中还出现了:

old mode 100644
new mode 100755

这不是代码逻辑修改。

它表示文件权限从:

100644
普通文件

变成了:

100755
可执行文件

对于:

mtk_drm_6789.c

这种 C 源文件来说,一般不需要可执行权限。

因此如果不是刻意修改,建议恢复:

chmod 644 kernel_device_modules-6.12/drivers/gpu/drm/mediatek/mediatek_v2/platform/mtk_drm_6789.c

以及:

chmod 644 kernel_device_modules-6.12/drivers/gpu/drm/mediatek/mediatek_v2/v1/platform/mtk_drm_6789.c

避免提交中出现无意义的:

100644 → 100755

权限变化。


二十二、最终修改总结

Panel

static struct mtk_panel_params ext_params = {
	.pll_clk = 355,
	.data_rate = 355 * 2,
	.data_rate_khz = 709220,
};

MIPI PLL

原来:

rate_khz = (mipi_tx->data_rate_adpt) ?
	mipi_tx->data_rate_adpt * 1000 :
	mipi_tx->data_rate / 1000;

修改为:

rate_khz = (mipi_tx->data_rate_khz_adpt) ?
	mipi_tx->data_rate_khz_adpt :
	((mipi_tx->data_rate_adpt) ?
	mipi_tx->data_rate_adpt * 1000 :
	mipi_tx->data_rate / 1000);

新的优先级:

data_rate_khz_adpt
        >
data_rate_adpt
        >
data_rate

目的:

MIPI PLL 从 MHz 级调节
        ↓
支持 kHz 级精调

二十三、这次经验最值得记住的地方

遇到:

LCD 目标 60 Hz
实测 60.066 Hz
要求 ±0.01 Hz

不要看到刷新率不准就立即修改:

HFP/HBP
VFP/VBP
.clock

如果 Panel Timing 已经正确,可以优先考虑:

保持 H/V Timing
保持 .clock
        ↓
使用 data_rate_khz
        ↓
微调 MIPI PLL

计算方法:

新 Data Rate
=
旧 Data Rate × 目标 FPS / 当前 FPS

本案例:

710000 × 60 / 60.066
≈ 709220 kHz

预测:

60.066 × 709220 / 710000
≈ 60.00001 Hz

最终目标:

60 ± 0.01 Hz

一句话复盘

data_rate 适合 MHz 级粗调,data_rate_khz 适合 kHz 级精调;本次通过让 MTK MIPI PLL 优先使用 data_rate_khz_adpt,在不修改 H/V Porch 和 .clock 的情况下,将 710000 kHz 微调到 709220 kHz,从而把实测约 60.066 Hz 修正到约 60.000 Hz。