先解答之前一个思考题:如果不把引脚配置为输出而写高电平,连接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。