高通SM6225亮度变化bug
qcom,mdss-dsi-bl-min-level = <1>;
qcom,mdss-dsi-bl-max-level = <4095>;
亮度调控的逻辑是啥呢?
- 上层写入一个0-1之间的值
- 再转成一个0-255之间的值,0-255是一个逻辑亮度值,0-255是一个上层的值,假设这个值叫做 brin
- 假设brin = 60 ,brin/255 = 实际PWM占空比(PWM占空比才是硬件亮度值)
- 再把实际PWM占空比输入到背光IC
- 背光IC根据PWM占空比来调控背光灯珠的实际亮度
- 人眼感觉到屏幕在变亮或变暗
亮度调控节点:/sys/class/backlight/panel0-backlight/brightness
正常的情况下呢,用户空间下对这个节点写0-255之间的值,亮度是会逐渐的从暗到亮的,
但是现在我遇到一个bug,我发现对这个节点写入40以上的值,亮度就不变化了,屏幕亮度的变化有问题,我从255写到0,让它逐渐变暗,发现在255-40这一段屏幕亮度几乎是不变的,几乎不会变暗。只有在40-0这一段才会有变化(亮度是看得出来是会变暗的)。现在就是这么个bug
我用Deepseek搜了下:
X:\disk\project_code\fh34\ap\VENDOR\vendor\qcom\opensource\display-drivers\msm\dsi\dsi_display.c
X:\disk\project_code\fh34\ap\VENDOR\vendor\qcom\opensource\display-drivers\msm\dsi\dsi_panel.c
看完这两个文件,问题的根因终于清楚了。你贴的代码里已经埋着答案了。
关键发现:驱动是纯线性的
在 dsi_panel.c 里,负责 PWM 背光的函数是这段:
static int dsi_panel_update_pwm_backlight(struct dsi_panel *panel, u32 bl_lvl)
{
...
period_ns = bl->pwm_period_usecs * NSEC_PER_USEC;
duty = bl_lvl * period_ns;
duty /= bl->bl_max_level;
...
}
这个换算就是纯线性的:duty = bl_lvl / bl_max_level * period。
你的 DTS 里 bl_max_level = 4095,所以:
- 写 4095 → duty = 100%
- 写 2048 → duty = 50%
- 写 40 → duty ≈ 1%
没有任何曲线补偿,没有任何 gamma 映射。
你手动测试现象的真正原因
你从 255 降到 40,屏幕几乎不变暗;40 到 0 才急剧变暗。原因就是:
- PWM 占空比是线性的,但人眼对亮度感知是对数的。
- 高占空比段(100% → 15%),物理光通量降了很多,但人眼感觉"只是暗了一点"。
- 低占空比段(15% → 0%),物理光通量降得不多,但人眼感觉"啪一下全暗了"。
这不是 bug,是 PWM 线性调光的物理特性。 所有用 PWM 调光且不做曲线补偿的项目,都会这样。
别的项目为什么"缓缓变暗"
两种可能:
可能一:别的项目在驱动里加了一条 gamma 曲线。
在写 PWM 之前,先对 bl_lvl 做一次非线性变换,把线性值转成符合人眼感知的值,再算 duty。你可以在别的项目里搜 dsi_panel_update_pwm_backlight 或 bl_lvl,看有没有类似 bl_lvl = curve[bl_lvl] 或 bl_lvl = (bl_lvl * bl_lvl) / 255 这样的代码。
可能二:别的项目用了 DCS 背光或 WLED 背光,不是 PWM。
DCS 背光(bl_ctrl_dcs)是直接给面板发亮度寄存器值,面板内部有自己的 gamma 曲线,所以看起来是平滑的。WLED 背光(bl_ctrl_wled)也有自己的调光曲线。
你的项目用的是 bl_ctrl_pwm,而且是纯线性 PWM,所以才会这样。
你现在能做的两种修法
修法一:在驱动里加一条软件 gamma 曲线(推荐,改动小)
在 dsi_panel_update_pwm_backlight 里,算 duty 之前,先对 bl_lvl 做一次非线性映射。最简单的做法:
/* 加一条 gamma 曲线,把线性亮度转成人眼感知亮度 */
u32 gamma_lvl;
gamma_lvl = (bl_lvl * bl_lvl) / bl->bl_max_level; /* 平方曲线,最简单的 gamma=2.0 */
duty = gamma_lvl * period_ns;
duty /= bl->bl_max_level;
这样高亮度段对应的 duty 会被拉大,低亮度段对应的 duty 会被压小,视觉上就均匀了。具体曲线可以调,常见的有 gamma 2.0、2.2,或者用查找表。
修法二:让上层 config.xml 的自动亮度曲线来补偿
但你现在测的是手动写 sysfs,走的是纯驱动路径,config.xml 管不到。所以要让手动调节也平滑,必须在驱动里加曲线。
为什么之前说 "config.xml" 没说到点上
因为你的平台没有 brightness-levels 表,驱动是线性映射。这种配置下,上层 config.xml 的曲线只能补偿自动亮度,补不了手动亮度。手动亮度的平滑度完全取决于驱动。
你现在该做的
- 确认别的项目是不是在
dsi_panel_update_pwm_backlight里加了曲线。如果是,直接抄过来。 - 如果别的项目也没加曲线,那他们可能用的是 DCS 或 WLED 背光。你可以对比一下别的项目的 DTS 里
qcom,mdss-dsi-bl-pmic-control-type是什么。 - 如果要加曲线,从平方曲线开始试,
gamma_lvl = bl_lvl * bl_lvl / bl_max_level,编译烧录后重新跑你的脚本,看 255→0 是否变均匀。
一句话总结
你的平台用的是纯线性 PWM 背光,没有 gamma 补偿,所以高段无感、低段陡降。别的项目要么在驱动里加了曲线,要么用了 DCS/WLED 背光。修复方向是在 dsi_panel_update_pwm_backlight 里加一条非线性映射曲线。
之前我搞展锐(UNISOC)的驱动时也遇到过一个问题,加了一个名叫gamma的什么东西后,问题就解决了,所以现在我决定把gamma2.0平方曲线加进去试下看看有没有效果。
在dsi_panel_update_pwm_backlight函数中,
static int dsi_panel_update_pwm_backlight(struct dsi_panel *panel,
u32 bl_lvl)
{
int rc = 0;
u32 duty = 0;
u32 period_ns = 0;
struct dsi_backlight_config *bl;
if (!panel) {
DSI_ERR("Invalid Params\n");
return -EINVAL;
}
bl = &panel->bl_config;
if (!bl->pwm_bl) {
DSI_ERR("pwm device not found\n");
return -EINVAL;
}
period_ns = bl->pwm_period_usecs * NSEC_PER_USEC;
++ //add gamma2.0
++ bl_lvl = (bl_lvl * bl_lvl) / bl->bl_max_level;
duty = bl_lvl * period_ns;
duty /= bl->bl_max_level;
现在正在编译,然后看看效果
20260912,编译完之后,亮度从80到0变暗变得更快了,
我现在需要用示波器测试下PWM占空比,
我看了下原理图,用的是艾为的一个背光芯片,原理很简单,有一根线叫做PWM,连接PMIC和背光IC,PMIC输出多少占空比到背光ic,LED灯珠就显示多少亮度。我想问下,我对/sys/class/backlight/panel0-backlight/brightness 写值,从0写到255,那么PWM占空比是会持续从0变大到100%的吗?这个是不是可以用示波器量PWM引脚的电平分析出来?
deepseek回答:
是的,可以直接用示波器量。
你写 0~255 时,PWM 引脚会发生什么
驱动里的换算链路是:
你写 0~255
↓ dsi_display_set_backlight 里的 bl_scale 换算
bl_lvl (0~4095)
↓ dsi_panel_update_pwm_backlight
duty = bl_lvl / bl_max_level * period_ns
↓ pwm_config()
PWM 控制器输出对应占空比的方波
↓ 引脚
背光 IC
所以:
- 写 0 → duty = 0,PWM 引脚恒低(或恒高,看极性)
- 写 128 → duty ≈ 50%
- 写 255 → duty = 100%,PWM 引脚恒高(或恒低)
占空比是随你写的值线性变化的,不是跳变。
示波器怎么量
- 找到 PWM 引脚,就是原理图上 PMIC 连到背光 IC 的那根线。
- 示波器探头接地,探针接 PWM 引脚。
- 写一个中间值,比如
echo 128 > brightness,看波形。 - 你应该看到周期固定、占空比约 50% 的方波。
你的 DTS 里:
qcom,bl-pmic-pwm-period-usecs = <49>;
周期约 49μs,对应频率约 20.4kHz。用示波器时把基调到 10~20μs/div 就能看清。
几个要注意的点
1. 极性
PWM 可能是高有效或低有效。如果写 255 时引脚是恒低,说明是低有效,写 0 反而是恒高。看波形就能确认。
2. 最小/最大限制
你 DTS 里有:
qcom,mdss-dsi-bl-min-level = <1>;
qcom,mdss-dsi-bl-max-level = <4095>;
写 0 可能被驱动或背光 IC 当成“关背光”,不输出 PWM。写 1 才是最小占空比。
3. 你加的平方曲线会改变占空比
加完 gamma 2.0 后,写 128 不再是 50% 占空比了:
bl_lvl = 128 * 255 / 255 ... 等等,这里要按 4095 算
实际链路是:你写 0~255 → 先换算到 0~4095 → 再过平方曲线 → 算 duty。
所以加曲线后,同样的写入值,占空比会变小。比如原来写 128 是 50%,加平方后可能只有 25% 左右。这是预期的,因为平方曲线就是要把低段的 duty 压下去。
用示波器验证的意义
- 确认 PMIC 真的在输出 PWM,而不是恒高/恒低。
- 确认占空比随写入值单调变化。
- 确认加曲线前后占空比的变化符合预期。
- 确认频率是 20.4kHz,如果不对,说明
bl-pmic-pwm-period-usecs没生效。
如果你量出来写 0~255 时占空比不连续或者中间有跳变,那才是真有问题。按你的 DTS 配置,应该是连续的。
你贴的 qpnp-lpg-pwm.c 里,__qpnp_lpg_calc_pwm_duty 函数末尾已经有一段别人加的占空比映射,而且这段代码就是导致你量到“0~40 就到 60%”的直接原因。
它做了什么
输入是 duty_ns,它先算出 duty_ratio(千分比):
duty_ratio 就是 duty_ns / period_ns 的千分比。
然后按三段做映射:
0 ~ 40(0% ~ 4%):映射到 10 ~ 15(1% ~ 1.5%)
40 ~ 120(4% ~ 12%):映射到 15 ~ 600(1.5% ~ 60%)
120 ~ 990(12% ~ 99%):映射到 600 ~ 990(60% ~ 99%)
这就是你量到“0~40 就到 60%”的原因:输入 duty_ratio 到 120(12%)时,输出已经是 600(60%)。
我先整理一下4个数字概念的关系
- 用户UI界面亮度条0-100
- 亮度节点(/sys/class/backlight/panel0-backlight/brightness)的值0-255
- 驱动层硬件亮度0-4095
- PWM引脚占空比0%-100%
用户界面滑动亮度调到最后屏幕亮度发生变化,就是走的1到4这四步。
正常情况下,pwm信号占空比是线性变化的,是一条直线,但是由于FH34-464的修改,就是改了pwm-qti-lpg这一个文件里的__qpnp_lpg_calc_pwm_duty函数,加了下面这一段修改导致我们现在的占空比不是线性的,下面这段代码插在上面我说的3-4步之间。
* 基于你的实测数据建立的精确映射:
* 输入占空比 | 亮度等级 | 目标占空比
* 4% (40) | 0% | 1.5% (15) (可微调)
* 12% (120) | 50% | 60% (600) (可微调)
* 99% (990) | 100% | 99% (990) (保持不变)
*/
if (duty_ratio <= 40) {
// 低于4%的输入统一映射到1.5%(最低亮度)
target_ratio = 15;
} else if (duty_ratio <= 120) {
// 4% ~ 12% 输入 → 1.5% ~ 60% 输出(线性插值)
target_ratio = 15 + (600 - 15) * (duty_ratio - 40) / (120 - 40);
} else {
// 12% ~ 99% 输入 → 60% ~ 99% 输出(线性插值)
target_ratio = 600 + (990 - 600) * (duty_ratio - 120) / (990 - 120);
}
亮度节点指的是:/sys/class/backlight/panel0-backlight/brightness
- 用户UI界面80%亮度时,对应的亮度节点的值是95,再对应的占空比大约是70%
- 用户UI界面75%亮度时,对应的亮度节点的值是73,再对应的占空比大约是67%
- 用户UI界面70%亮度时,对应的亮度节点的值是58,再对应的占空比大约是64%
- 用户UI界面65%亮度时,对应的亮度节点的值是48,再对应的占空比大约是62%
- 用户UI界面60%亮度时,对应的亮度节点的值是40,再对应的占空比大约是61%
- 用户UI界面0%亮度时,对应的亮度节点的值是10,再对应的占空比大约是1.36%
// 仅对背光通道(PM6125 PWM通道0)启用校正
if (lpg->lpg_idx == 0) {
u32 duty_ratio, target_ratio;
// 计算输入占空比(千分比,提高计算精度)
duty_ratio = div64_u64(duty_ns * 1000, period_ns);
/*
* 基于你的实测数据建立的精确映射:
* 输入占空比 | 亮度等级 | 目标占空比
* 4% (40) | 0% | 1.5% (15) (可微调)
* 12% (120) | 50% | 60% (600) (可微调)
* 99% (990) | 100% | 99% (990) (保持不变)
*/
if (duty_ratio <= 40) {
// 低于4%的输入统一映射到1.5%(最低亮度)
target_ratio = 15;
} else if (duty_ratio <= 120) {
// 4% ~ 12% 输入 → 1.5% ~ 60% 输出(线性插值)
target_ratio = 15 + (600 - 15) * (duty_ratio - 40) / (120 - 40);
} else {
// 12% ~ 99% 输入 → 60% ~ 99% 输出(线性插值)
target_ratio = 600 + (990 - 600) * (duty_ratio - 120) / (990 - 120);
}
// 安全限制:防止占空比超出合理范围
target_ratio = clamp(target_ratio, 10U, 990U); // 1% ~ 99%
// 转换回duty_ns格式
duty_ns = div64_u64(period_ns * target_ratio, 1000);
}
我想问下这段代码的逻辑到底是啥?
这段代码是一个三段折线映射,把“输入的占空比”强行改写成“另一个占空比”,目的是让屏幕在人眼看起来亮度变化更均匀。
它做的三件事
第一段:输入 0% ~ 4% → 输出固定 1.5%
if (duty_ratio <= 40)
target_ratio = 15;
输入占空比在 0~4%(对应 duty_ratio 0~40)时,不管输入多少,输出都是 1.5%(15/1000)。这相当于设了一个最低亮度门槛,避免低亮度段占空比太小导致背光 IC 不工作或闪屏。
第二段:输入 4% ~ 12% → 输出 1.5% ~ 60%
else if (duty_ratio <= 120)
target_ratio = 15 + (600 - 15) * (duty_ratio - 40) / (120 - 40);
输入 4%~12%,输出从 1.5% 线性拉到 60%。这一段是陡升段:输入只涨 8%,输出涨了 58.5%。这就是你量到“0~40 就冲到 60%”的原因——输入占空比刚到 12%,输出已经 60% 了。
第三段:输入 12% ~ 99% → 输出 60% ~ 99%
else
target_ratio = 600 + (990 - 600) * (duty_ratio - 120) / (990 - 120);
输入 12%~99%,输出从 60% 线性拉到 99%。这一段是平缓段:输入涨 87%,输出只涨 39%。所以 40~255 这一段占空比变化很慢。
整体效果
输入占空比 → 输出占空比
0% ~ 4% → 固定 1.5%
4% ~ 12% → 1.5% ~ 60% (陡升)
12% ~ 99% → 60% ~ 99% (平缓)
画出来是一条先平、再陡、再平的折线。
为什么要这么写
注释里写了“基于你的实测数据建立的精确映射”。它想解决的问题是:背光 IC 的亮度响应不是线性的。
- 低占空比段(0~4%),背光 IC 可能响应很弱或干脆不亮,所以直接给一个最低值 1.5% 让它亮起来。
- 中占空比段(4~12%),背光 IC 可能响应很敏感,稍微加点占空比就亮很多,所以这里输入小范围对应输出大范围。
- 高占空比段(12~99%),背光 IC 响应趋于饱和,输入大范围只对应输出小范围。
但这段代码的问题
- 它是三段折线,不是平滑曲线。段与段之间有拐点,亮度变化时会有“折线感”。
- 它和上层
dsi_panel.c的换算叠加了。上层已经做了一次 0~255 → 0~4095 的换算,这里又做了一次映射,两层叠加后行为很难预测。 - 它写死了数值,没有根据实际背光 IC 的 datasheet 来定,注释里说是“基于实测数据”,但实测数据可能只覆盖了少数几个点。
你现在该做的
先明确:这段代码是有意加的,还是别人随手加的?
- 如果是有意加的,找加它的人确认目的是什么,是不是为了配合背光 IC 的非线性。
- 如果是随手加的,先注释掉这段,让 PWM 恢复纯线性,再重新量你的脚本,看 0~255 对应的占空比是不是均匀的。
- 如果恢复线性后,人眼看起来还是高段无感、低段陡降,那再在这一层加一条平滑的曲线(比如 gamma 2.0),而不是用三段折线。
一句话
这段代码是在 PWM 驱动底层加的一条三段折线映射,把输入占空比改写成另一套占空比,目的是补偿背光 IC 的非线性,但实现方式是折线,且和上层换算叠加,导致你现在量到的现象。
“把输入占空比改写成另一套占空比”,输入占空比的输入是啥意思?谁给谁输入?
“输入”和“输出”都是同一个东西:PWM 的占空比。只是在这段代码里,它被改了两次。
完整链路
你写 brightness 节点
↓
dsi_panel.c 算出 duty_ns(第一版占空比)
↓
pwm_config() 调用 qpnp-lpg-pwm.c 的 __qpnp_lpg_calc_pwm_duty
↓
这段代码把 duty_ns 改写成新的 duty_ns(第二版占空比)
↓
写进 LPG 寄存器,PWM 引脚输出这个占空比
谁是“输入”,谁是“输出”
- 输入:
dsi_panel.c算出来的那个duty_ns,也就是你写 brightness 后,第一版占空比。在这段代码里,它被换算成duty_ratio(千分比)。 - 输出:这段代码改写后的
duty_ns,也就是target_ratio换算回去的那个值。这才是真正写进硬件、引脚上输出的占空比。
“输入”是上层驱动给你的,“输出”是你改写后给硬件的。 两者都是占空比,只是数值不同。
举个例子
你写 brightness = 128:
dsi_panel.c算出duty_ns,假设对应占空比 50%。- 这段代码收到
duty_ratio = 500(50% 的千分比)。 - 因为 500 > 120,走第三段:
target_ratio = 600 + (990-600) * (500-120) / (990-120) = 600 + 390 * 380 / 870 ≈ 770 - 输出占空比变成 77%。
输入 50%,输出 77%。 引脚上真正输出的是 77%,不是 50%。
所以“输入”是谁给的
是 dsi_panel.c 里的 dsi_panel_update_pwm_backlight 算出来的。你写 brightness 节点,最终会走到那里,算出第一版 duty,然后传给 PWM 驱动,被这段代码改写。
一句话
“输入占空比”是上层驱动算出来的原始占空比,“输出占空比”是这段代码改写后、真正写进硬件的占空比。两者都是占空比,只是中间被这段映射改了一次。
那如果没有这一段PWM映射的修改的话,会是什么样子的?
如果没有这段映射,就是纯线性:
输入占空比 → 输出占空比
0% → 0%
25% → 25%
50% → 50%
75% → 75%
100% → 100%
具体到你的链路
你写 brightness = 128:
dsi_panel.c算出 duty 对应 50%。- 直接写进硬件,引脚输出 50%。
写 64 → 25%,写 192 → 75%,完全成正比。
你之前量到的现象会变成
- 0~255 从 0% 线性涨到 100%
- 不会有“0~40 就冲到 60%”的陡升
- 也不会有“40~255 只从 60% 涨到 100%”的平缓
但纯线性也有问题
纯线性 PWM 占空比,人眼看起来是低段变化太快、高段变化太慢:
- 0% → 10%:人眼觉得“亮了很多”
- 90% → 100%:人眼觉得“几乎没变”
所以纯线性在视觉上也不均匀。这就是为什么有人加了那段映射——想补偿人眼的非线性感知。
所以你的选择
- 保留映射:视觉上可能更均匀,但这段是三段折线,有拐点,且和上层叠加,行为难调。
- 去掉映射:恢复纯线性,行为可预测,但视觉上高段无感、低段陡降。
- 去掉映射 + 在上层加平滑曲线:比如在
dsi_panel.c里加 gamma 2.0,这样只有一层映射,可控且平滑。
推荐第三种:去掉 qpnp-lpg-pwm.c 里的三段折线,在 dsi_panel.c 里加一条平滑的 gamma 曲线。这样只有一层补偿,逻辑清晰,也好调。
抛开这个问题不讲,如果想补偿人眼的非线性感知,那通常有啥办法呢
补偿人眼非线性感知,核心就一句话:把线性物理量(PWM 占空比、电流、电压)转成符合人眼感知的曲线。 常见做法分几类。
一、Gamma 曲线(最常用)
人眼对亮度的感知近似幂函数,所以用 gamma 校正:
感知亮度 = 物理亮度 ^ (1/gamma)
反过来,要让感知均匀,物理量要按 gamma 次方增长:
物理量 = 感知值 ^ gamma
常见 gamma 值:
- 2.0:最简单,平方曲线,计算量小。
- 2.2:sRGB 标准,显示行业通用。
- 2.4:影院、HDR 常用。
你的场景里,bl_lvl 是感知值(0~4095),duty 是物理量,所以:
duty = (bl_lvl / max) ^ 2.2 * period
这就是你之前加的平方曲线的升级版。
二、查找表(LUT)
把 0~4095 每个值对应的 duty 预先算好,存成一张表:
static const u16 gamma_lut[4096] = { 0, 1, 2, ... };
duty = gamma_lut[bl_lvl] * period / max;
优点:
- 曲线可以任意形状,不限于幂函数。
- 可以按实测数据逐点校准。
- 运行时无浮点运算,快。
缺点:
- 占内存(4096 个 u16 约 8KB)。
- 改曲线要重新编译。
高通、MTK 平台大多用这种方式。
三、分段线性折线
把曲线用几个拐点连成折线,就是你现在 qpnp-lpg-pwm.c 里那段:
0~4% → 固定 1.5%
4~12% → 1.5%~60%
12~99% → 60%~99%
优点:
- 实现简单,纯整数运算。
- 可以针对特定区间精细调整。
缺点:
- 拐点处不光滑,可能有可见的“折线感”。
- 段数多了调起来麻烦。
四、CIE 亮度曲线
比 gamma 更贴近人眼真实感知,用 CIE 1931 亮度函数或 CIELAB 的 L* 通道:
L* = 116 * (Y/Yn)^(1/3) - 16
反解出 Y 和 duty 的关系。适合对画质要求高的场景,计算复杂,一般用在专业显示器。
五、感知量化(PQ / HLG)
HDR 领域用的曲线,把绝对亮度映射到感知均匀的码值。手机背光一般用不到。
六、硬件自带曲线
很多背光 IC(包括艾为的芯片)内部就有 gamma 或 exponential 映射。你给它的 PWM 占空比会被它内部再映射一次。这种情况下:
- 驱动侧不用加曲线,让 IC 自己处理。
- 或者驱动侧加一条“逆曲线”,把 IC 的曲线抵消掉,再叠加你想要的曲线。
实际项目里的组合
通常是:
上层 config.xml:lux → brightness 曲线(自动亮度用)
↓
驱动 dsi_panel.c:brightness → duty 曲线(gamma 或 LUT)
↓
PWM 硬件:占空比 → 电流
↓
背光 IC:电流 → 实际光输出(可能自带曲线)
每一层都可能有曲线,设计时要明确哪一层负责补偿,避免多层叠加。
给你的建议
你现在的项目:
qpnp-lpg-pwm.c里已经有一条三段折线。dsi_panel.c里你加过 gamma 2.0,被下层覆盖了。
正确做法是二选一:
- 保留
qpnp-lpg-pwm.c的折线,把dsi_panel.c的曲线去掉,然后微调折线拐点。 - 去掉
qpnp-lpg-pwm.c的折线,在dsi_panel.c里加一条平滑的 gamma 2.2 或 LUT。
推荐第二种,逻辑清晰,只有一层补偿,好调也好维护。
哦,原来人眼对于手机屏幕亮度的变化感知是非线性的,不是线性的。
那就是说,手机的亮度变化曲线要做成非线性的,而不是线性的。
如果一台手机的亮度变化是线性的,那么这是不符合人眼对手机屏幕亮度的感知习惯的(人眼对于亮度的变化感知习惯近似于幂函数),
为了解决这个问题,通常就是加一个gamma曲线校正,最常见的gamma曲线就是二次函数
Gamma 曲线(最常用)这个我听不懂,你简单说
一句话:Gamma 曲线就是让 PWM 占空比按“平方”增长,而不是按“直线”增长。
对比
线性(现在没曲线的情况):
亮度值 128 → 占空比 50%
亮度值 64 → 占空比 25%
亮度值 192 → 占空比 75%
每加一点亮度,占空比加一样多。
Gamma 2.0(平方曲线):
亮度值 128 → 占空比 = (128/255)^2 = 25%
亮度值 64 → 占空比 = (64/255)^2 = 6.3%
亮度值 192 → 占空比 = (192/255)^2 = 56.6%
低亮度段占空比被压得更小,高亮度段被拉得更大。
为什么这样人眼才觉得均匀
人眼对暗部特别敏感:
- 占空比 1% → 2%,人眼觉得“亮了一倍”。
- 占空比 50% → 51%,人眼觉得“几乎没变”。
所以如果占空比线性增长,人眼就会觉得:
- 低段:变化太快,一下就亮了。
- 高段:变化太慢,几乎没感觉。
Gamma 曲线把低段的占空比压小、高段拉大,正好抵消人眼的这种特性,所以看起来才是均匀变亮。
代码长这样
duty = (bl_lvl * bl_lvl) / bl_max_level;
就是你之前加的那行。它做的就是平方,也就是 gamma 2.0。
一句话
Gamma 曲线 = 让占空比按平方增长,补偿人眼“暗部敏感、亮部迟钝”的特性,让亮度变化看起来均匀。

浙公网安备 33010602011771号