第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的时间精度,换取视觉上的灰度层次。我做了这么多年嵌入式,觉得最核心的就三点:PWM频率要够、子帧分配要合理、Gamma校正不能省。把这三点吃透了,单色OLED也能做出不错的灰度效果。