第4章 灰度控制基础:PWM原理与模拟灰度

灰度控制,说白了就是让单色OLED显示出不同深浅的亮度。你想想看,一块单色屏只能亮或灭,那画面得多单调?我刚开始做OLED驱动时,也觉得能点亮像素就够了。直到有一次给客户做手表界面,对方说「你们这图标跟贴了块胶布似的」——嗯,从那以后我才认真研究灰度控制。

4.1 PWM原理与模拟灰度

PWM(脉冲宽度调制)是实现灰度最常用的方法。它的核心思路很简单:在一个固定周期内,控制高电平持续的时间比例。这个比例就叫占空比。

占空比 高电平时间 视觉亮度
0% 0 μs 全黑
25% 250 μs(周期1ms) 较暗
50% 500 μs 中等亮度
75% 750 μs 较亮
100% 1000 μs 全亮

人眼有视觉暂留效应。当PWM频率高于60Hz时,我们看到的就不是闪烁,而是稳定的亮度。我个人习惯把PWM频率设在100Hz以上,这样既省电又不会让用户觉得屏幕在抖。

关键公式:

灰度值 = 占空比 × 最大灰度级

实际亮度 ≈ 占空比 × 最大亮度

注意:这只是理想情况,实际还要考虑OLED的发光效率非线性。

4.2 单色OLED的灰度实现技巧

单色OLED的每个像素只有亮/灭两种状态。要实现灰度,就得靠「时间上的累积」。我常用的方法有两种:

  • 帧内PWM:在一个显示帧内,对每个像素独立控制点亮时间。比如16级灰度,就把一帧分成16个时间片。
  • 帧间PWM:多帧叠加。比如要显示50%灰度,就亮一帧、灭一帧交替。这种方法实现简单,但帧率会下降。

我在项目中遇到过一个问题:用帧内PWM时,如果时间片分配不均匀,画面会出现「滚动条」效应。后来我改用「分散式PWM」——把每个像素的亮灭时间打散到整个帧周期里,效果就好多了。

我的经验:

对于128×64的OLED,建议用帧内PWM。因为像素少,MCU负担轻。如果是大屏(比如256×128),可以考虑帧间PWM,减少每帧的数据量。

4.3 多级灰度(4级/16级)的帧率控制

灰度级数越多,需要的子帧就越多。4级灰度需要2个子帧,16级需要4个子帧。但子帧多了,帧率就会下降。

为什么会这样?因为每个子帧都要扫描一遍所有像素。假设你的OLED刷新率是100Hz,要实现16级灰度,实际每帧要拆成4个子帧,那么子帧刷新率就是400Hz。这对MCU的SPI时钟和内存带宽都是考验。

灰度级数 子帧数 子帧刷新率(目标帧率100Hz) SPI时钟需求(128×64)
2级(黑白) 1 100 Hz ~1 MHz
4级 2 200 Hz ~2 MHz
16级 4 400 Hz ~4 MHz
256级 8 800 Hz ~8 MHz

我曾经用STM32F103驱动一块128×64的OLED,想实现256级灰度。结果SPI时钟跑到12MHz,画面还是闪烁。后来发现是DMA传输跟不上。所以我的建议是:对于普通MCU,16级灰度已经够用,再高就要考虑硬件加速了。

避坑指南:

我曾经在16级灰度下把子帧时间设成平均分配(每个子帧占25%时间)。结果画面出现明显的「阶梯感」。后来改用「二进制权重分配」——子帧时间按1:2:4:8分配,灰度过渡就平滑多了。

4.4 灰度与亮度的非线性关系

这里有个很多人忽略的问题:OLED的亮度与PWM占空比不是线性关系。你设50%占空比,实际亮度可能只有最大亮度的30%。

为什么?因为OLED是电流驱动器件。在低电流下,发光效率会下降。我实测过一块SSD1306驱动的OLED,数据如下:

PWM占空比 理论亮度(归一化) 实测亮度(归一化) 误差
10% 0.10 0.06 -40%
25% 0.25 0.18 -28%
50% 0.50 0.42 -16%
75% 0.75 0.70 -7%
100% 1.00 1.00 0%

要解决这个问题,可以用Gamma校正。说白了就是建一个查找表,把输入的灰度值映射到实际的PWM占空比上。

// Gamma校正查找表(16级灰度)
// 实测Gamma值 ≈ 1.8
const uint8_t gamma_table[16] = {
    0,    // 灰度0 -> 占空比0%
    1,    // 灰度1 -> 占空比1%
    3,    // 灰度2 -> 占空比3%
    6,    // 灰度3 -> 占空比6%
    10,   // 灰度4 -> 占空比10%
    15,   // 灰度5 -> 占空比15%
    21,   // 灰度6 -> 占空比21%
    28,   // 灰度7 -> 占空比28%
    36,   // 灰度8 -> 占空比36%
    45,   // 灰度9 -> 占空比45%
    55,   // 灰度10 -> 占空比55%
    66,   // 灰度11 -> 占空比66%
    78,   // 灰度12 -> 占空比78%
    88,   // 灰度13 -> 占空比88%
    95,   // 灰度14 -> 占空比95%
    100   // 灰度15 -> 占空比100%
};

我的习惯:

每次换OLED型号,我都会先测一组Gamma数据。不同厂家的OLED,Gamma值可能差很多。有的1.6,有的2.2。不校正的话,画面暗部细节全丢了。

灰度控制核心知识体系 灰度控制基础 PWM原理 单色OLED实现技巧 多级灰度帧率控制 灰度与亮度非线性 占空比控制 频率选择(>60Hz) 帧内PWM 帧间PWM 子帧拆分 SPI时钟需求 Gamma校正 查找表(LUT) 核心要点 PWM是灰度控制的基础 → 单色OLED需时间累积 → 多级灰度需平衡帧率 → 非线性需Gamma校正

灰度控制这块,说白了就是「时间换空间」。用PWM的时间精度,换取视觉上的灰度层次。我做了这么多年嵌入式,觉得最核心的就三点:PWM频率要够、子帧分配要合理、Gamma校正不能省。把这三点吃透了,单色OLED也能做出不错的灰度效果。