动态扫描数码管
前面我们学习了如何使用一位LED显示数字,很简单是吧?
现在我们加点难度。一位数码管只能显示一位数字,现在我们要显示8位数字(或者显示时间)。
那么我们就需要8位数码管,如果按照1位数码管的硬件接法,8位数码管就需要64根IO。
相当于1个LED使用1根IO口控制。
大家觉得可行吗?当然可行,我们芯片有100根管脚,80多根IO。
但是你只打算用芯片控制8位数码管吗?肯定不是嘛!这样的方案肯定是非常浪费IO的。
那怎么办嗯?要解决这个问题,要用到一个原理
,两个芯片
。
一个原理
不知道大家是否了解过以前的胶片电影,一张一张的画片,连续播放就能看到活生生的,会动的人。
这是为什么呢?
原理是“视觉暂留”。
科学实验证明,人眼在某个视像消失后,仍可使该物像在视网膜上滞留0.1-0.4秒左右。电影胶片以每秒24格画面匀速转动,一系列静态画面就会因视觉暂留作用而造成一种连续的视觉印象,产生逼真的动感。
我们在数码管上能不能用这个原理呢?
8个数码管都用同样的IO控制亮灭,轮流显示。
只要同一个LED的点亮间隔不大于0.4秒(实际要比这个小),那么我们就会一直看到这个数码管是亮着的。(和真正一直亮会有什么差别?)
这样我们就只要8根IO了?
如何选择8个数码管该点亮哪个?
前面我们用共阴极数码管,阴极是接到地线的。
我们可以用IO口控制阴极,只有对应的IO是低电平,这个数码管才有亮。如果阴极是高电平,数码管就不会亮。
如此,我们需要8+8根IO就够了。省去了48根IO,太有成就了。
两个芯片
我们都是高兴太早,通常IO口还是不够, 使用16根IO也是很浪费的。
那这么办呢?利用数字电路,有两个芯片能帮上我们的忙。
74HC138和74HC595。
138是三八译码器,595是8位串行输入、并行输出的位移缓存器。
74是一系列数字功能芯片,注意中间字母的区别。我们选用的是HC类型,HC表示是CMOS电平,或者简单说就是3.3V电压。
- 三八译码器
三八译码器是什么?我们从数据手册一探究竟。
打开数据手册,标题:SNx4HC138 3-Line To 8-Line Decoders/Demultiplexers
翻译为中文就是:SNx4HC138 3线转8线译码器/多路分配器,怎么转呢?往下看。
我们选用的型号是SN74HC138PWR。型号这些数字和字母都是什么意思呢?
SN74是芯片系列。
HC是芯片种类。
138是芯片具体型号。
PW是封装,TSSOP16。
R包装形式,编带。
138的功能:用3根线的电平,选择8根线中的一根线输出低电平,其他输出高电平。
芯片电气信号
真值表如下:
左边是输入,右边是输出。
ENABLE信号通常我们输出H-L-L,也就是默认使能,不进行控制。
C/B/A,38译码器3根输入线,一共有8种组合。
输出信号8根,根据3根输入线的状态,选择其中1根输出低电平,其他线输出高电平。
因此,选中的数码管是低电平,那么就只能用共阴极数码管。
- 595功能
打开595手册
标题:8-Bit Shift Registers With 3-State Output Registers
意思:8位移位寄存器,具有3态输出。
我们选用的型号是:SN74HC595PWR, 名称含义与138类似。
芯片电气信号
时序图
14脚SER输入,11 脚SRCLK上升沿,从14脚输入1位数据。8次之后,就有一个BYTE的数据保存在595中。当时钟继续输出,数据将从9脚输出,因此,可以通过多个595串联实现更多的移位位数。两个595就可以组成16位移位寄存器。
12脚RCLK上升沿,保存在595中的8位数据,从595的8个并行输出引脚输出(OE需要低电平)
10脚SRCLR是复位脚,低电平有效 ,上电后输出高即可。
更多细节可参考:https://baike.baidu.com/item/74HC595/9886491
我们用三八译码器控制刷管的共阴极,595控制数码管的正极。三八译码器决定哪个数码管亮,595决定亮的内容。如此,我们就只需要7个IO口就搞定了。
硬件原理
节省IO是一种共识,所以要用8位数码管时,我们不需要用8位单独的数码管组成。
而是用2个内部连接好信号的4位数码管。如下图:
这种数码管内部已经将共用的信号连在一起。同样,也有共阴极和共阳极数码管之分。
内部连接信号如下:
电路图根据前面分析的原理设计,如下图:
调试
第一步
静态显示,38译码器设定一个固定输出,选中一个数码管,控制595输出,让数码管显示不同数字。
- 初始化硬件
``` /* 595_SDI--- ADC-TPX---PB0---数据输入 595_LCLK---ADC-TPY---PB1---数据锁存---上升沿锁存 595_SCLK---TP-S0---PC5---数据移位---上升沿移位 595_RST---TP-S1---PC4---芯片复位--低电平复位
A138_A0---FSMC_D2---PD0
A138_A1---FSMC_D1---PD15
A138_A2---FSMC_D0---PD14
/ void seg_init(void) { / GPIOD Periph clock enable */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
/* 38译码器输入0 ,选中第4个数码管*/
GPIO_ResetBits(GPIOD, GPIO_Pin_0|GPIO_Pin_14|GPIO_Pin_15);
/* Configure PD0 and PD2 in output pushpull mode */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_14|GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_ResetBits(GPIOB, GPIO_Pin_0|GPIO_Pin_1);
/* Configure PD0 and PD2 in output pushpull mode */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_ResetBits(GPIOC, GPIO_Pin_4|GPIO_Pin_5);
/* Configure PD0 and PD2 in output pushpull mode */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
/* 拉高复位信号 */
GPIO_SetBits(GPIOC, GPIO_Pin_4);
} ```
- 138驱动
/*
选择数码管,控制138选中对应数码管
pos参数就是位置
*/
void seg_select(uint8_t pos)
{
if (pos == 1) {
GPIO_SetBits(GPIOD, GPIO_Pin_14);
GPIO_ResetBits(GPIOD, GPIO_Pin_0|GPIO_Pin_15);
} else if (pos == 2) {
GPIO_SetBits(GPIOD, GPIO_Pin_0|GPIO_Pin_14);
GPIO_ResetBits(GPIOD, GPIO_Pin_15);
} else if(pos == 3) {
GPIO_SetBits(GPIOD, GPIO_Pin_15|GPIO_Pin_14);
GPIO_ResetBits(GPIOD, GPIO_Pin_0);
} else if(pos == 4) {
GPIO_SetBits(GPIOD, GPIO_Pin_0|GPIO_Pin_15|GPIO_Pin_14);
} else if (pos == 5) {
GPIO_ResetBits(GPIOD, GPIO_Pin_0|GPIO_Pin_15|GPIO_Pin_14);
} else if(pos == 6) {
GPIO_ResetBits(GPIOD, GPIO_Pin_15|GPIO_Pin_14);
GPIO_SetBits(GPIOD, GPIO_Pin_0);
} else if(pos == 7) {
GPIO_ResetBits(GPIOD, GPIO_Pin_0|GPIO_Pin_14);
GPIO_SetBits(GPIOD, GPIO_Pin_15);
} else if(pos == 8) {
GPIO_ResetBits(GPIOD, GPIO_Pin_14);
GPIO_SetBits(GPIOD, GPIO_Pin_0|GPIO_Pin_15);
}
}
- 595驱动
``` / 输出一位数码管显示数据 / void seg_display_1seg(uint8_t segbit) { uint8_t tmp; uint8_t cnt = 0;
tmp = segbit;
cnt = 0;
/* 拉低 595_LCLK*/
GPIO_ResetBits(GPIOB, GPIO_Pin_1);
while(1) {
/* 拉低 595_SCLK*/
GPIO_ResetBits(GPIOC, GPIO_Pin_5);
/* 将数据从 SDI发出去*/
if((tmp & 0x80)== 0x00)//注意操作符的优先级
{
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
} else {
GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
tmp = tmp<<1; //移位
delay(100);
/* 拉高 595_SCLK 移位数据 */
GPIO_SetBits(GPIOC, GPIO_Pin_5);
delay(100);
cnt++;
if(cnt >= 8)
break;
}
GPIO_SetBits(GPIOB, GPIO_Pin_1);
delay(100);
} ```
- 应用
在main中初始化数码管,138固定输出值,调用595驱动函数输出各种数字。
``` seg_init();
/*
第一步,调试595和138功能
在第1个数码管显示0-9
*/
seg_select(1);
seg_display_1seg(0x3f);
seg_display_1seg(0x06);
seg_display_1seg(0x5b);
seg_display_1seg(0x4f);
seg_display_1seg(0x66);
seg_display_1seg(0x6d);
seg_display_1seg(0x7d);
seg_display_1seg(0x07);
seg_display_1seg(0x7f);
seg_display_1seg(0x67);
seg_display_1seg(0x3f|0x80);
```
输出数字对应的数码管段值,列入一个数组,索引就是数字,比如显示数字1,输出的数码管段值就是SegTab1, 也就是0x06。
uint8_t SegTab[10]={0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x67};
单步运行看效果。
第二步
固定显示,595输出固定值,调试38译码器,让数字在数码管上轮流显示相同的数字。
代码和第一步类似。
经过第一第二步调试后,138和595驱动就完成了。
第三步
第一第二步只是实现了单个数码管显示,属于静态显示。
前面讲原理时讲过,8位数码管使用动态显示方法。需要将38译码器和595配合,才能动态刷8位数据,我们现在尝试固定显示12345678。
动态刷新是一个循环,因此放在while循环中实现。
代码如下:
seg_select(1);
seg_display_1seg(0x3f);
seg_select(2);
seg_display_1seg(0x06);
seg_select(3);
seg_display_1seg(0x5b);
seg_select(4);
seg_display_1seg(0x4f);
seg_select(5);
seg_display_1seg(0x66);
seg_select(6);
seg_display_1seg(0x6d);
seg_select(7);
seg_display_1seg(0x7d);
seg_select(8);
seg_display_1seg(0x07);
单步运行,看效果。发现一个问题,在调用138切换数码管时,会将前面显示的内容显示到下一个位置。
比如,数码管1显示1,调用数码管138切换显示位置到数码管2,这时,数码管2会显示1。这是个问题。
我们全速运行程序看看效果。显示的内容并不是87654321,而是76543218,而且有重影,影子隐隐约约是我们要的效果:87654321。
如何解决这个问题呢?
方法是:在切换显示位置前,将显示内容清零,也就是将595的输出内容输出为0。
增加一个seg_clear函数实现这个功能。实现如下:
void seg_clear(void)
{
uint8_t cnt = 0;
cnt = 0;
/* 拉低 595_LCLK*/
GPIO_ResetBits(GPIOB, GPIO_Pin_1);
while(1) {
/* 拉低 595_SCLK*/
GPIO_ResetBits(GPIOC, GPIO_Pin_5);
/* 将数据从 SDI*/
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
delay(100);
/* 拉高 595_SCLK 移位数据 */
GPIO_SetBits(GPIOC, GPIO_Pin_5);
delay(100);
cnt++;
if(cnt >= 8)
break;
}
GPIO_SetBits(GPIOB, GPIO_Pin_1);
delay(100);
}
在前面的测试函数中所有seg_select函数之前都添加本函数。
编译下载全速运行,效果正常。
第四步
经过第三步调试,8位数码管的功能已经实现了。
那,驱动算完成了吗?没有。为什么?
先介绍一个很重要的概念:时间片。
什么是时间片呢?拿LED和8位数码管进行对比。
LED,只要将IO置位,就能点亮,之后如果不改变LED状态,不需要再管它。
8位数码管呢?因为我们用动态扫描方法,不能仅仅将8位数码管输出一次内容之后就不管了,要一直刷新。
前面原理也说过,每个数码管刷一次的时间间隔不能小于24ms。
这种需要定时操作的,我们通常就说这个功能需要时间片。
好,了解了时间片。那么应用程序要如何使用呢?
应用程序只是想在数码管上显示一些数字而已,数码管怎么显示的,它是不管的。
为了显示数字,让应用程序间隔24ms就调用你的程序刷新显示,这明显不合理,专业术语叫强耦合,本来不相关的。
讲到这,不知道大家是否明白。不了解也没关系,后面再慢慢理解。
总之,矛盾就是:应用只是想显示一数字,数码管驱动要时间片维持显示。
怎么实现呢?用缓冲。缓冲就是一个组数,这个数组是应用和驱动之间的联系。
应用程序将要显示的内容放到缓冲。驱动将缓冲中的内容显示到数码管。
如此,就达到了最简单的模块分离。
程序设计中有一个理论:生产者和消费者。
数码管驱动和应用虽然不是真正的生产者和消费者,但是使用缓冲的逻辑是相似的。
- 有8位数码管,就定义包含8个空间的数组。
/* 动态扫描 添加缓冲功能 */
/* 8位数码管的显示内容 */
char BufIndex = 0;
/* 缓冲,保存的是对应数码管段值 */
char Seg8DisBuf[8]={0x7f,0x07,0x7d,0x6d,0x66,0x4f,0x5b,0x06};
- 定义一个函数,用于动态刷新数码管。这个函数最好放在定时或者RTOS的定时任务中执行。现在我们还没学会,可以放在main函数中的while运行。
``` / 动态刷新 定时调用本函数, 本函数对应用层屏蔽,意思是:应用层不知道我是通过动态刷新实现8位数码管功能。 / void seg_display_task(void ) { seg_clear(); seg_select(BufIndex+1); seg_display_1seg(Seg8DisBuf[BufIndex]);
BufIndex++;
if(BufIndex >=8)
BufIndex = 0;
} ```
- 定义一个函数,给应用程序调用,改变数码管缓冲的值。
/*
segbit 数码管段值,为1的bit点亮
seg 数码管位置,1~8
*/
void seg_fill_disbuf(uint8_t segbit, uint8_t seg)
{
Seg8DisBuf[seg-1] = segbit;
return;
}
- 在main函数中调用数码管刷新功能。
``` /-----------------驱动--------------/ / 使用显示缓冲方法,要改变显示内容, 调用函数seg_fill_disbuf改变Seg8DisBuf中的内容即可 / seg_display_task();
/*-----------------应用-----------------*/
cnt++;
if(cnt >= 1000) {
cnt=0;
disnum ++;
if(disnum > 9)
disnum = 0;
seg_fill_disbuf(SegTab[disnum], 1);
}
/*----------------------------------*/
delay(1000);
```
驱动是数码管的内容,while循环最后delay 1000,也就是刷新间隔。现在没定时器,暂时定一个值,数码管不闪烁即可。
应用就是延时1000次个delay(1000)后,改变数码管1显示的数字,从0显示到9。
编译下载看效果。
end