跳到主要内容

CIE1931 PWM LED亮度线性控制

0x00 背景

这篇文章大部分内容是参考一篇国外极客写的文章,然后加上我们的理解以及其他文献资料的补充来帮助读者理解。在这里不得不佩服原作者的分享精神以及理论结合实际的能力,因为他的文章确实帮助我们解决了在线性调LED亮度中遇到的问题。

0x01 文章里出现的英文单词解释

lightness = LED 亮度 luminance = LED 的照度(单位:流明),即单位面积上通过的光量

0x02 用PWM控制LED亮度遇到的问题

其实用PWM控制LED的亮度并不是你想的那么简单。下图描绘出PWM是一种通过调整占空比的方式工作的方波:

1

通过调整PWM高电平的占空比,我们可以控制LED的亮度(lightness)。大部分单片机都带PWM外设,或者你用软件模拟一个PWM也行。

然而当我们用下面的代码线性控制PWM输出时,LED的亮度(lightness)在我们人眼视觉的上看却不是线性的变化:

void loop() {
byte i = 0;
while (1) {
analogWrite(2, i);
delay(10);
i++;
}
}

可以看到,从暗到亮,线性效果并不是那么好,在前一段亮度增加的非常快,后期大部分时间都是在全亮的状态,如下图:

2

这背后的原因就是人眼对光的感知并不是线性的,而是呈log函数形式,那么解决问题的方法就来了

0x03 如何让PWM占空比和亮度(lightness)呈线性

为了解决问题,我们必须让PWM的输出符合人眼的视觉感知曲线。下面的CIE 1931 lightness 公式即描绘了我们人眼是如何感知光的亮度:

3

将公式转换一下得到:

4

分析一下上面的公式,L就是我们人眼感知的亮度,Y就是光线的照度流明,那么怎么将L、Y以及PWM联系起来呢?通过查阅相关文献,我知道了luminance和PWM是呈正比的关系,其实也很好解释,PWM为高电平时LED导通,导通后就有光通过,而luminance表征的正是光通过的量。参考文献见下图:

5

然后根据上面的公式,我们只需要将L作为输入,线性的给L赋值,就可以得到对应的Y(同PWM)。

0x04 在单片机中实现上述理论推导

如果在单片机中实现上面的公式会很慢,应为涉及到除法及开立方运算。所以需要我们生成一个保存运算结果的表格供单片机查询即可。下面的Python 脚本程序即提供了自动生成L->PWM表格的功能。Python 脚本如何使用请自行学习。

INPUT_SIZE = 255       # Input integer size
OUTPUT_SIZE = 255 # Output integer size
INT_TYPE = 'const unsigned char'
TABLE_NAME = 'cie';

def cie1931(L):
L = L*100.0
if L <= 8:
return (L/902.3)
else:
return ((L+16.0)/116.0)**3

x = range(0,int(INPUT_SIZE+1))
y = [round(cie1931(float(L)/INPUT_SIZE)*OUTPUT_SIZE) for L in x]

f = open('cie1931.h', 'w')
f.write('// CIE1931 correction table\n')
f.write('// Automatically generated\n\n')

f.write('%s %s[%d] = {\n' % (INT_TYPE, TABLE_NAME, INPUT_SIZE+1))
f.write('\t')
for i,L in enumerate(y):
f.write('%d, ' % int(L))
if i % 10 == 9:
f.write('\n\t')
f.write('\n};\n\n')
f.close()

在电脑上执行上面的Python脚本代码后,会在本地生成一个.h头文件,里面包含一个Lightness 和 PWM关系的一维数组表格。 脚本中 INPUT_SIZE = 255 表示将LED的亮度(lightness)等分成256阶,即256灰阶度,OUTPUT_SIZE = 255 表示你用的是一个8-bit 的PWM。下面举个栗子: 比如你的单片机用的是10-bit PWM外设,而LED的亮度你希望等分成32阶,那么你需要将INPUT_SIZE设置成31,OUTPUT_SIZE 设置成1023。最后运行脚本你会生成一个包含32个元素的一维数组,元素值域在0~1023,形如:

const unsigned char cie[32] = {0, 4, 7, ... , 940, 1023};

将生成数组应用到单片机程序中,用如下代码测试:

#include "cie1931.h"
void loop() {
byte i = 0;
while (1) {
analogWrite(2, cie[i]);
delay(10);
i++;
}
}

最后,可以看到LED的亮度在我们人眼的观察下,最终呈现线性的变化!

6