大纲
- 视觉残留
- 原理
- 生理基础
- 神经传导与处理
- 应用
- 与视觉暂留相关的现象
- 频闪融合
- 不好的实现
- 好的效果
- 延伸
在《51单片机编程学习笔记——动态数码管》一文中,我们看到如何使用动态数码管显示数字。但是基于动态数码管设计的特点,每次只能显示1个数字。这就不能让我们一次性显示多个数字,比如666就无法显示。
如果我们要让动态数码管“显示”多个数,就要结合我们人眼的视觉残留。
人眼的视觉残留又称视觉暂留,是指物体在快速运动时,当人眼所看到的影像消失后,人眼仍能继续保留其影像 0.1 - 0.4 秒左右的图像。
视觉残留
原理
生理基础
这一现象主要与视网膜上的感光细胞有关。感光细胞在受到光刺激后,会产生神经冲动并传递给大脑。当光刺激消失后,感光细胞并不会立即停止工作,而是会有一个短暂的 “余晖” 效应,使得神经冲动的发放和大脑对图像的感知会延续一小段时间。
神经传导与处理
从视网膜到大脑视觉中枢的神经传导过程也会对视觉残留产生影响。神经信号的传递和处理需要一定时间,当新的视觉信息快速更替时,大脑还来不及完全清除上一个图像的信息,就会出现视觉残留。
应用
- 电影:电影胶片以每秒 24 帧的速度播放,每帧画面在人眼中停留的时间非常短,但由于视觉残留,前一帧画面的影像还未在人眼中消失,下一帧画面就已经出现,这样就使得一系列静态的画面在人眼中形成了连续的动态影像。
- 动画:传统手绘动画通过绘制一系列略有差异的静态画面,然后以一定的速度播放这些画面,利用视觉残留让观众看到连贯的动画效果。在现代数字动画制作中,同样是基于视觉残留原理,通过计算机生成大量的帧,快速播放来呈现生动的动画。
- CRT 显示器:阴极射线管(CRT)显示器通过电子枪发射电子束来激发荧光粉发光,形成图像。由于荧光粉发光后会有短暂的余辉,利用视觉残留,使得快速扫描的电子束形成的一幅幅图像能够在人眼中融合成连续的画面。
与视觉暂留相关的现象
- 余晖效应:在黑暗环境中,如果快速移动一个发光的物体,人眼会看到物体移动的轨迹好像有一条 “尾巴”,这就是余晖效应,是视觉残留的一种直观表现。
- 频闪融合:当闪烁的光源频率达到一定程度时,人眼会感觉不到闪烁,而是看到一个稳定的光源,这也是基于视觉残留,使得闪烁的光在人眼中融合成了连续的光。
频闪融合
在《51单片机编程学习笔记——动态数码管》中,我们实现了display方法。通过它,我们可以指定在左起index(0~7)位置显示单个数字的能力。
#include <REG52.H>
#include <intrins.h>
void Delay1ms() //@11.0592MHz
{
unsigned char i, j;
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
void Delay(unsigned int millisecond) {
unsigned int i = 0;
for (i = 0; i < millisecond; i++) {
Delay1ms();
}
}
sbit P22 = P2^2;
sbit P23 = P2^3;
sbit P24 = P2^4;
void select_led_index(unsigned char index) {
// 如果index=0,则P22=1,P23=1,P24=1
// 如果index=1,则P22=0,P23=1,P24=1
// 如果index=2,则P22=1,P23=0,P24=1
// 如果index=3,则P22=0,P23=0,P24=1
// 如果index=4,则P22=1,P23=1,P24=0
// 如果index=5,则P22=0,P23=1,P24=0
// 如果index=6,则P22=1,P23=0,P24=0
// 如果index=7,则P22=0,P23=0,P24=0
P22 = (index & 0x01) ? 0 : 1;
P23 = (index & 0x02) ? 0 : 1;
P24 = (index & 0x04) ? 0 : 1;
}
void display(unsigned char index, unsigned char digit) {
select_led_index(index);
P0 = digit;
}
那么我们就可以基于display方法实现一个依次显示多个数字的方法show_number。
不好的实现
在第一版的show_number中,我们从右向左,依次算出每位数字,将其保存到show_digit数组中。比如我们要显示98762345,则show_digit保存的是[9,8,7,6,2,3,4,5]。
void show_number(unsigned long number) {
unsigned char i;
unsigned char show_digit[8] = {0};
unsigned char digit[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
for (i = 0; i < 8; i++) {
show_digit[7-i] = number % 10;
number /= 10;
}
for (i = 0; i < 8; i++) {
display(i, digit[show_digit[i]]);
}
}
在main函数的while循环中,我们
void main() {
unsigned int i = 0;
while(1) {
i = 98762345;
show_number(i);
}
}
可以看到,这个显示并不好,只有最后一位显示的比较明显,但是还是错误的——将第一位9显示到最后一位。
造成这个现象的原因是多种的,其中包括动态数码管的残影,以及视觉残留没有很好的形成。
回顾动态数码管的原理图,可以看到显示哪一位数字是由上侧的LED1~LED8引脚决定的;而显示什么数字,则是由P0决定。
通过对比不通电状态下的动态数码管,可以发现上述代码还是让各个位置都点亮了。只是除了最后一位很亮之外,其他都不太亮。
那我们先要解决一个问题,就是让各个位置都要同等程度亮起来。
根据之前人眼“视觉残留”的原理,前几位不太亮的根本原因是它们亮的时间太短了,没有给我们视觉神经足够的刺激,导致没有形成足够的“亮度”信号残留。
基于这样的分析,我们只要让每一位显示时间长一点就行了。(如果出现快速闪烁,那就是Delay的时间太长了,“视觉残留”没有很好的形成,神经系统已经察觉到了我们的“作弊行为”。)
void show_number(unsigned long number) {
unsigned char i;
unsigned char show_digit[8] = {0};
unsigned char digit[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
for (i = 0; i < 8; i++) {
show_digit[7-i] = number % 10;
number /= 10;
}
for (i = 0; i < 8; i++) {
display(i, digit[show_digit[i]]);
Delay(1); // 每位多显示1ms
}
}
可以看到,展示效果得到了很好的改善。但是还有部分重影,或者说:不该亮的地方还是有点亮。
下图是987这三个数的显示。可以看到组成数组7的,不该亮的几个二极管,还是有点亮。
要解决这个问题,就要消除动态数码管的残影。具体方法就是在现实下个数字前,让P0处于低电位,这样就可以消除让之前所有处于高电位的二极管熄灭。
好的效果
void show_number(unsigned long number) {
unsigned char i;
unsigned char show_digit[8] = {0};
unsigned char digit[10] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f};
for (i = 0; i < 8; i++) {
show_digit[7-i] = number % 10;
number /= 10;
}
for (i = 0; i < 8; i++) {
display(i, digit[show_digit[i]]);
Delay(1);
P0 = 0x00;
}
}
可以看到显示的987的对比度要好一些了。
延伸
如果我们希望动态数码管显示的数字一直在递增,一种比较容易想到的方法是
void main() {
unsigned long i = 0;
unsigned int j = 0;
while(1) {
for(i=0;i<99999999;i++) {
show_number(i);
}
}
}
这种实现会让最后一位变化的特别快。如果我们希望最后一位的变化可以被我们肉眼识别,一种比较容易想到的办法是
void main() {
unsigned long i = 0;
unsigned int j = 0;
while(1) {
for(i=0;i<99999999;i++) {
show_number(i);
Delay(10);
}
}
}
这个方案存在的问题是:如果Delay时间太长,视觉残留被打破,我们看到明显的闪烁;如果Delay时间太短,最后一位递增太快。
针对这个问题,我们可以使用下面这个方法。即多增加一层循环,让显示更加清晰(保持了高频刷新,视觉残留被加强了),同时最后一位递增速度变慢了。
void main() {
unsigned long i = 0;
unsigned int j = 0;
while(1) {
for(i=0;i<99999999;i++) {
for (j = 0; j < 10; j++) {
show_number(i);
}
}
}
}