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号引脚上。
- #include <ee1/segment.h>
- #include <ee1/delay.h>
- int main()
- {
- segment_init(PIN_4, PIN_5);
- while ()
- for (uint8_t i = ; i != ; ++i)
- {
- segment_dec(i);
- segment_display(SEGMENT_DIGIT_R);
- delay();
- }
- }
根据上一篇教程的内容,你应该不难推断出 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 表示小数点,它们的定义是:
- const uint8_t segment_digit[] =
- {
- 0b00111111, //
- 0b00000110, //
- 0b01011011, //
- 0b01001111, //
- 0b01100110, //
- 0b01101101, //
- 0b01111101, //
- 0b00000111, //
- 0b01111111, //
- 0b01101111, //
- 0b01110111, // A
- 0b01111100, // B
- 0b00111001, // C
- 0b01011110, // D
- 0b01111001, // E
- 0b01110001 // F
- };
- 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。
- #include <ee1/segment.h>
- #include <ee1/delay.h>
- int main()
- {
- segment_init(PIN_4, PIN_5);
- while ()
- for (uint8_t i = ; i != ; ++i)
- {
- segment_dec(i);
- for (uint8_t j = ; j != ; ++j)
- {
- segment_display(SEGMENT_DIGIT_L);
- delay();
- segment_display(SEGMENT_DIGIT_R);
- delay();
- }
- }
- }
在这个程序中,数码管刷新间隔设置为2毫秒(1毫秒*2位)。实际上这一间隔在10毫秒以下即可,人眼感受不到差别。在这个程序中你甚至可以不写延时(相应地循环次数要增加很多倍),刷新间隔就很小,但其他应用中单片机除了驱动数码管还有别的工作,如果这些工作的运行时间是不固定的,就不能用它们替换掉延时,因为会导致数码管亮度不均衡。应该加入一个比运行时间长得多的延时来保证间隔大致相同,然而这一方案在其他工作运行时间达到10毫秒这一量级时就不能使用了。
在使用动态扫描来控制数码管时,你会发现一种从前所未有的麻烦,尤其是当与按键等输入相结合时,如作业第2题。以前使用的LED等设备,只需调用一次函数,其状态就能一直保持,直到再调用相应函数;而动态扫描是动态的,即你需要不断去控制它,这就是麻烦的根源所在。单片机当然有别的方案来解决这些问题,我们以后再讲。
今天的作业:
1. 保持1毫秒的延时,将显示切换的时间从0.5秒改为1秒(这有什么难的,不是吗?);
2. 实现以下功能:按下一个按键时,数码管显示的十六进制数字加1;保持按下1000毫秒后,每200毫秒数字加1;再加入另一个按键,但它的效果是数字减1。
AVR单片机教程——数码管的更多相关文章
- AVR单片机教程——旋转编码器
好久没写这个系列了.今天讲讲旋转编码器. 旋转编码器好像不是单片机玩家很常用的器件,但是我们的开发板上有,原因如下: 旋转编码器挺好用的.电位器能旋转的角度有限,旋转编码器可以无限圈旋转:旋转时不连续 ...
- AVR单片机教程——定时器中断
本文隶属于AVR单片机教程系列. 中断,是单片机的精华. 中断基础 当一个事件发生时,CPU会停止当前执行的代码,转而处理这个事件,这就是一个中断.触发中断的事件成为中断源,处理事件的函数称为中断 ...
- AVR单片机教程——蜂鸣器
本文隶属于AVR单片机教程系列. 引子 定时/计数器(简称定时器)是单片机编程中至关重要的一部分,再简单的单片机也会带有定时器. 也许你会觉得我们已经在delay函数中接触过定时器了,然而并不是, ...
- AVR单片机教程——PWM调光
本文隶属于AVR单片机教程系列. PWM 两位数码管的驱动方式是动态扫描,每一位都只有50%的时间是亮的,我们称这个数值为其占空比.让引脚输出高电平点亮LED,占空比就是100%. 在驱动数码管时 ...
- AVR单片机教程——小结
本文隶属于AVR单片机教程系列. 第一期挺让我失望的,是我太菜,没有把想讲的都讲出来.经常写了很多,然后一点一点删掉,最后就没多少了. 而且感觉难度不合适,处于很尴尬的位置.讲得简单,难的丢给库, ...
- AVR单片机教程——矩阵键盘
本文隶属于AVR单片机教程系列. 开发板上有4个按键,我们可以把每一个按键连接到一个单片机引脚上,来实现按键状态的检测.但是常见的键盘有104键,是每一个键分别连接到一个引脚上的吗?我没有考证过, ...
- AVR单片机教程——走向高层
本文隶属于AVR单片机教程系列. 在系列教程的最后一篇中,我将向你推荐3个可以深造的方向:RTOS.C++.事件驱动.掌握这些技术可以帮助你更快.更好地开发更大的项目. 本文涉及到许多概念性的内容 ...
- AVR单片机教程——数字输出
从上一篇教程中我们了解到,按键与开关的输入本质上就是数字信号的读取.这一篇教程要讲的是,控制LED的原理是数字信号的输出.数字IO是单片机编程之有别于桌面编程的各项内容中最简单.最基础的. 在讲数字信 ...
- AVR单片机教程——数字输入
我们已经学习了如何使用按键和拨动开关,不知你有没有好奇 button_down 和 switch_status 等函数是如何实现的.本篇教程带你一探究竟,让我们从按键的原理开始. 在原理图中,按键的符 ...
随机推荐
- cocos Uniforms值的赋值
cocos Uniforms值的赋值 举个void Renderer::drawBatchedTriangles()的渲染: //Start drawing verties in batch for( ...
- Fluent设置充分发展湍流入口(利用profile)
计算模型: 物性参数为: 密度:100kg/m3 粘度系数:0.003333kg/(m·s) 原视频下载地址: https://pan.baidu.com/s/1W3n_K-dZCVMF7M63wV2 ...
- js对象属性名以数字开头如何获取、js属性名以数字开头、missing ) after argument list
js对象中属性名以数字开头引发的报错 :missing ) after argument list var ChineseDistricts = { : { : '北京市', : '天津市', : ' ...
- GuavaCache简介(一)是轻量级的框架 少量数据,并且 过期时间相同 可以用 GuavaCache
还有一篇文章是讲解redis 如何删除过期数据的,参考:Redis的内存回收策略和内存上限(阿里) 划重点:在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清 ...
- asp.netCore3.0区域和路由配置变化
一.MVC 服务注册 ASP.NET Core 3.0 添加了用于注册内部的 MVC 方案的新选项Startup.ConfigureServices.三个新的顶级扩展方法与 MVC 方案上IServi ...
- Java 字符集编码
一.字符编码实例1.NioTest13_In.txt文件内容拷贝到NioTest13_Out.txt文件中 public class NioTest13 { public static void ma ...
- gcc 编译两个so其中soA依赖soB
有两个so,其中soB中调用soA: 那么我们打包soB的时候连接soA: 在打包test程序的时候连接soB,此时soB会自动查找依赖的soA: 如下测试 在编译之前指定环境变量:export LD ...
- 两款不错的js甘特图控件
dhtmlx:https://docs.dhtmlx.com/ jQuery.Gantt:http://taitems.github.io/jQuery.Gantt/
- django入门7之django template和xadmin常用技巧
django入门7之django template和xadmin常用技巧 <li {% ' == '/course' %}class="active"{% endif %}& ...
- nginx 安装 ssl 证书
nginx 安装 ssl 证书 关键词: pem 转 crt , 证书续期, nginx 部署 ssl 证书, 解决 SSL23_GET_SERVER_HELLO 错误. 之前免费申请的 1年的证书过 ...