Jerry @DOA&INPAC, SJTU

Working out everything from the first principles.

导航

AVR单片机教程——数码管

本文隶属于AVR单片机教程系列。

 

先解答之前一个思考题:如果不把引脚配置为输出而写高电平,连接LED会怎样?

实验结果是,LED会亮,但相比于输出高电平的情况,亮度很低。这是为什么呢?

通过上一篇教程我们知道,引脚输入输出模式是由寄存器DDRx中DDxn位控制的,可以推断出 pin_mode 函数会改变一个引脚对应的DDxn值,输入为0,输出为1,而其复位后的值为0,即输入,因此如果不把引脚配置为输出,它的模式就是输入。类似地 pin_write 函数会改变PORTxn,其值为函数的第二个参数。

所以不配置输出而写高电平的结果就是,这一引脚的DDxn为0,PORTxn为1,是带上拉电阻的输入模式。上拉电阻相当于VCC接电阻后再接在引脚上,外部电路是引脚接一个电阻再接一个LED到地,总体可以等效为LED被一个电阻限流后接在VCC和地之间,因此LED会亮。

这个电阻的阻值是上拉电阻和本来的限流电阻的阻值之和,上拉电阻是比较大的(根据datasheet P432 Figure30-164可以估算出上拉电阻约40kΩ),相比于限流电阻就是外电阻的输出高电平的情况,LED上的电流小很多,因此亮度也相应低了。

这是一个模拟电路的问题,只用数字电路的分析方法是解决不了的。这个简答的问题也反映了单片机相关知识的综合性。

 

今天来讲数码管,就是开发板左边那两个日字。

早期数码管也成为辉光灯,依靠气体放电发光,现在一般指7段数码管(因为日字有7段),其本质是许多LED。我们的开发板上是2位数码管,也有1位、3位、4位的;除了7段数码管还有米字数码管,不过比较少见。注意7段数码管其实有8段,每个数字右下角有一个小数点。还有一种4位时钟数码管还有中间两点,但有两个小数点不能点亮。

数码管的8段从上面一段开始顺时针依次为A、B、C、D、E、F,中间为G,小数点称为DP。

各种LED数码管的原理都相同,内部电路连接略有不同。1位数码管有10个引脚,而内部有8个LED,是怎样连接的呢?在数字输出那一篇中提到过共阳和共阴,数码管也是这样的:8个LED的正极(或负极)一起连接到两个引脚上,负极(或正极)分别连接到一个引脚,称为共阳(或共阴)数码管。开发板上的2位数码管中,每一位都是共阴连接的。

8个正极与单片机之间的连接有些复杂,现在无法讲解,不过可以理解为8个由单片机控制的、相互独立的输出引脚,每个分别串联了电阻后连接数码管的正极。左右两位的负极连接到数码管右侧的排针,使用时要把它们连接到单片机引脚上。

如果要让数码管的特定几段亮,应该如何配置9个引脚的电平呢?答案是要亮的段的正极为高电平,其他为低电平,负极也是低电平。另外,如果负极是高电平,则LED正负极之间电压为0或小于0,是不会亮的,这一事实在驱动多位数码管时很重要。注意到负极低电平亮而高电平不亮,与此前使用过的LED不同,写程序的时候不要想当然。

根据手册,我们可以用 segment_dec 等函数设置数码管显示的数据,用 segment_display 来让数码管显示。把显示数据分成设置数据与控制显示,分别由两个函数来完成,往高深讲是将数据与视图分离,而更贴近本篇教程主题的原因是,我们后面将会看到,多位数码管无法仅靠一个函数显示多位数字(至少在顺序控制流下)。

作为数码管的第一个程序,我们先点亮右边的数码管,让它循环显示0~9。左右两位的负极分别连接到单片机4和5号引脚上。

 1 #include <ee1/segment.h>
 2 #include <ee1/delay.h>
 3 
 4 int main()
 5 {
 6     segment_init(PIN_4, PIN_5);
 7     while (1)
 8         for (uint8_t i = 0; i != 10; ++i)
 9         {
10             segment_dec(i);
11             segment_display(SEGMENT_DIGIT_R);
12             delay(500);
13         }
14 }

根据上一篇教程的内容,你应该不难推断出 segment_display 对数码管负极做的工作吧。它把要显示的一位的负极所连接的引脚的电平拉低,另一位拉高,这是比较显然的。segment_display 对数码管正极做的工作是根据数据配置数码管8个正极的电平,这里的数据是由 segment_dec 设置的。数据中每一个bit和数码管中一段的亮暗相对应,最低位对应A段而最高位对应DP段(这只是我设计开发板时的连接方式)。那么,显示数字1需要点亮B和C段,其显示数据就是0b00000110,即数字1的段码。segment_dec 等函数根据输入参数,给一个大小为2的数组写入了两个值,分别对应两位数码管要显示的数据。

这里插一句,0b前缀表示二进制数,这是GNU扩展,不属于C标准,C++也从C++14才开始支持这种表示法。按理来说应该尽可能少用编译器扩展,但这个特性实在是太好用了,所以就拿来用了。

库中已经声明了一些段码。segment_digit 为0~9、A~F的段码,segment_dot 表示小数点,它们的定义是:

 1 const uint8_t segment_digit[] =
 2 {
 3     0b00111111, // 0
 4     0b00000110, // 1
 5     0b01011011, // 2
 6     0b01001111, // 3
 7     0b01100110, // 4
 8     0b01101101, // 5
 9     0b01111101, // 6
10     0b00000111, // 7
11     0b01111111, // 8
12     0b01101111, // 9
13     0b01110111, // A
14     0b01111100, // B
15     0b00111001, // C
16     0b01011110, // D
17     0b01111001, // E
18     0b01110001  // F
19 };
20 const uint8_t segment_dot = 0b10000000;

这些段码是根据字符形状手动输入的。你可以用相同的方法创造更多字符,也可以在网上找一些工具来完成。

如果(我是说如果)共阴数码管的A~G、DP段分别连接单片机PA0~PA7,负极接低电平,DDRA全为1,那么用一个段码给PORTA赋值,数码管上就能显示对应的数字或字母,或其他图案。这里留个思考题,对于共阳数码管,应该在什么地方作些改动?

以上就是驱动一位数码管的方法。

 

现在我们来看如何驱动多位数码管。开发板上是2位数码管,实际上多位数码管的驱动方法都是一样的,在此以共阴2位数码管为例。

前面提到共阴多位数码管的每一位都是共阴连接的,那么不同位之间是如何连接的呢?有一些2位数码管的两位之间没有任何联系,整个器件至少有18个引脚,这个数字已经有些大了。而有一种电路连接可以做到在1位数码管的基础上,每增加一位,只需多一个引脚,见下图:

增加的这个引脚就是新的一位的负极,而其每一段的正极与第一位是共用的。对于不同位上的同一段,不难发现它们是共阳连接的。位是共阴的,段是共阳的,处于习惯上的考虑,这样的数码管称为共阴数码管。

然而。这样节省引脚的接法是有代价的,就是你在一个时刻只能独立控制8个LED,即一位的8段(根据对称性,你也可以控制2位的同一段,但这样更受限制)。你无法做到的是点亮第1位的A段和第2位的B段,同时其他段都不亮。为什么呢?点亮第1位的A段,需要使共阳A段为高电平而共阴第1位为低电平,同理B段为高电平,第2位为低电平,结果就是第1位的B段和第2为的A段也被同时点亮了。在这样的方案下,两位数码管只能显示同一个数字。

我们需要一些别的办法。根据前一部分的内容,分别显示两位中的任意一位都是可以做到的。如果用户想看到两个数字,程序可以在第1秒显示左边的数字,第2秒显示右边的数字,用户至少是可以理解的,尽管这种方法相当糟糕。为什么是1秒而不是10秒呢?1秒的情况下用户平均等待0.5秒可以看到两个数字的信息,10秒的话就是5秒等待,用户不愿意。

可是现在用户连0.5秒都不愿意等了,怎么办?一定要把等待时间压缩到0吗?用户是人,人眼的刷新率不太高,只要每秒有24幅图像,它(其实是大脑)就认为这些图像是连续的。所以只要这一等待时间比人眼获取图像的间隔要小,用户就不会再有怨言了。

现在你来做用户。你能看到两个数字,两个数字呈现的时间间隔是一段很短的时间,短到你分辨不出它到底有没有一段间隔。这意味着什么?两个数字同时显示了出来。在此背后,数码管的控制器每隔一小段时间切换一个数字来显示,这种驱动方式成为动态扫描

我们来写个程序验证一下这种方案。在前一个程序的基础上,我们让左边一位显示0。

 1 #include <ee1/segment.h>
 2 #include <ee1/delay.h>
 3 
 4 int main()
 5 {
 6     segment_init(PIN_4, PIN_5);
 7     while (1)
 8     for (uint8_t i = 0; i != 10; ++i)
 9     {
10         segment_dec(i);
11         for (uint8_t j = 0; j != 250; ++j)
12         {
13             segment_display(SEGMENT_DIGIT_L);
14             delay(1);
15             segment_display(SEGMENT_DIGIT_R);
16             delay(1);
17         }
18     }
19 }

在这个程序中,数码管刷新间隔设置为2毫秒(1毫秒*2位)。实际上这一间隔在10毫秒以下即可,人眼感受不到差别。在这个程序中你甚至可以不写延时(相应地循环次数要增加很多倍),刷新间隔就很小,但其他应用中单片机除了驱动数码管还有别的工作,如果这些工作的运行时间是不固定的,就不能用它们替换掉延时,因为会导致数码管亮度不均衡。应该加入一个比运行时间长得多的延时来保证间隔大致相同,然而这一方案在其他工作运行时间达到10毫秒这一量级时就不能使用了。

在使用动态扫描来控制数码管时,你会发现一种从前所未有的麻烦,尤其是当与按键等输入相结合时,如作业第2题。以前使用的LED等设备,只需调用一次函数,其状态就能一直保持,直到再调用相应函数;而动态扫描是动态的,即你需要不断去控制它,这就是麻烦的根源所在。单片机当然有别的方案来解决这些问题,我们以后再讲。

 

今天的作业:

1. 保持1毫秒的延时,将显示切换的时间从0.5秒改为1秒(这有什么难的,不是吗?);

2. 实现以下功能:按下一个按键时,数码管显示的十六进制数字加1;保持按下1000毫秒后,每200毫秒数字加1;再加入另一个按键,但它的效果是数字减1。

posted on 2019-10-14 00:19  Jerry_SJTU  阅读(1624)  评论(0编辑  收藏  举报