前几天差不多把清翔的视频教程写完了,然后玩了两天,这次准备把以前买的TM1638芯片驱动的数码管按键模块用在51单片机上。
这个模块我在pdd上买的,用了2块的优惠券后付款7元包邮,价格还行吧,对比了下这个价格几乎就是最低价了。TM1638芯片都要1块多,再加上PCB,焊接,LED,电阻,数码管,还有运费,已经比较划算了。
买模块店家有送显示程序,但是我还是想自己看一下数据手册,趁热打铁,记录下学习过程
一、模块图片
上面的LED全亮是我刚才写的测试程序
最上面是8个LED,下方是8个8段数码管 ,再下方是芯片,芯片右侧是二极管(非发光的普通二极管),左侧是VCC,GND以及3条数据线总共5条线。最下方是8个按键
我的这个模块数码管是共阴极的接法,其实这个芯片可以接共阴极也可以接共阳极数码管,不过我感觉共阳极的接法用起来比较麻烦。
二、TM1638
2.1 管脚定义
带k的都是可以按键扫描的,SEG和GR是数码管段和位,STB,CLK,DIO是与数据相关的引脚
数据手册有说,不管芯片连接的是共阳极数码管还是共阴极数码管,SEG都必须接阳极,GR接阴极,不能反过来,这也是跟他们是P管开漏输出和N管开漏输出有关。
P管开漏输出和N管开漏输出有什么区别,我们不需要关心,这是电路设计才需要考虑的
这一段告诉我们如果要读取数值,需要在上升沿读才稳定。
2.2 按键扫描
一般按键扫描是最多接3*8=24个按键,可能店家为了节省成本,我这里只接了1*8=8个按键。
数据手册推荐按键接法
K1接8个,K2接8个,K3接8个按键。
实际我的模块上只用了K3的8个按键
虽然只接了K3,但是芯片还是考虑全部,所以读取的时候会发送4个字节。
读取方式:先发送读取按键命令,DIO就会按顺序输出4个字节,数据输出也是从低位到高位
输出数据结构
每个字节都是KS(偶)K1 K2 K3 KS(偶-1)K1 K2 K3 的结构,如果按下对应的按键,那么对应的位置就是1.比如我按下了K3和KS4的按键,那么在读取的第2个字节就是1001 0001,在程序里面就可以根据读到的数值判断出按下了哪个键,然后去处理什么事情。
2.3 命令
在管脚定义图里面说了,在STB为低的第一个字节视为命令
2.3.1 显示控制命令
显示控制命令的高4位固定是8,BIT3控制是否打开显示,剩下的控制亮度。这个芯片总共有8档亮度可调.比如我想打开显示,亮度1/16,那么就可以发送0x88(1000 1000)
2.3.2 数据命令
高四位固定是4,低四位设置具体功能。比如我想设置普通模式,自动增加地址,写数据到显示寄存器,就可以发送0x40(0100 0000),如果我想读取按键数据,就可以发送0x42(0100 0010)
2.3.3 显示地址设置
这个命令的高四位固定是C,低四位设置具体数值,来设置16个显示寄存器地址。其实这个命令的低四位就是对应地址的低四位。上电默认地址是00H。
显示寄存器地址
他这个表官方的就是没有对齐,有点难受。写数据的时候,在某个地址先发送低位再发送高位数据。SEG10后面的X直接写0就行。
根据我的原理图,
SEG1到8控制段选,GR控制位置。
板载的8个红色LED接在SEG9上,所以要想亮就需要把SEG9拉高
2.4 时序图
Twait要求大于1us,但是51单片机1个机器周期1.085us大于1us,所以在51单片机里面可以不考虑这个延时。
驱动共阳极数码管就是没有驱动共阴极方便。
2.5 地址增加模式数据流程
2.6 地址固定模式数据流程
2.7 读按键数据流程
Command1就是发送读按键命令,这个命令由2.3.2 数据命令里面的一个位控制
三、编程
知道理论之后,就可以开启编程了。
3.1 地址自动增加模式
#include <reg52.h>
typedef unsigned char uchar;
typedef unsigned char ucahr;
typedef unsigned int uint;
sbit TM1638STB = P1^0;
sbit TM1638CLK = P1^1;
sbit TM1638DIO = P1^2;
void TM1638_WByte(ucahr cmd);
void main()
{
TM1638STB = 0;
TM1638_WByte(0x88);//1000 1000显示开,脉冲宽度为1/16
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0x40);//普通模式,地址自动增加,写显示数据
TM1638_WByte(0x3f);//数字0------------------------此时地址为0x00
TM1638_WByte(0x00);//让SEG9为0,这样上方LED就不会亮
TM1638_WByte(0x06);//数字1------------------------此时地址为0x01
TM1638_WByte(0x00);//让SEG9为0,这样上方LED就不会亮
TM1638_WByte(0x5b);//数字2------------------------此时地址为0x02
TM1638STB = 1;
while(1);
}
//TM1638写一个字节(只负责送数据到线上)
void TM1638_WByte(ucahr cmd)
{
uchar i;
for (i = 0; i < 8; i++)
{
TM1638CLK = 0;
TM1638DIO = cmd & 0x01;
cmd >>= 1;
TM1638CLK = 1;
}
}
先写一个简单的程序测试一下。数码管前3个显示123,上方8个LED全不亮。在这里我没有设置显示地址,是因为默认地址就是0x00,也就是第一个数码管,当写入一个命令,后面跟一个显示数据后,地址会自动增加1个(0x01),但是这个时候地址就增加到了SEG9和SEG10这一块了,其中SEG9是控制LED的,其余没用全为0就行了,如果再继续写入一个数据,才会到0x02,这个是和第二个数码管显示内容相关的数据,以此类推。
如果使用的是固定地址模式,那么每次每次发送显示数据前都要指明显示地址。
如果想让数码管模块在单片机按下复位键还能正常显示,那么就需要一个初始化函数,也就是清屏函数,如下:
void TM1638_clear()
{
uchar i;
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0x40);
for (i = 0; i < 16; i ++)
TM1638_WByte(0x00);//在所有16个地址中写入0x00
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0xc0);//设置显示地址为0x00
TM1638STB = 1;
}
void main()
{
TM1638_clear();
TM1638STB = 0;
TM1638_WByte(0x88);//1000 1000显示开,脉冲宽度为1/16
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0x40);//普通模式,地址自动增加,写显示数据
TM1638_WByte(0x3f);//数字0------------------------此时地址为0x00
TM1638_WByte(0x00);//让SEG9为0,这样上方LED就不会亮
TM1638_WByte(0x06);//数字1------------------------此时地址为0x01
TM1638_WByte(0x00);//让SEG9为0,这样上方LED就不会亮
TM1638_WByte(0x5b);//数字2------------------------此时地址为0x02
TM1638STB = 1;
while(1);
}
写一个清屏函数,在主函数调用一下就可以了
写一个命令还需要自己先把STB拉低,写完再拉高,比较麻烦,封装一下
void TM1638_Wcmd(ucahr cmd)
{
TM1638STB = 0;
TM1638_WByte(cmd);
TM1638STB = 1;
}
3.2 现象
显示出了123,但是我没有设置地址,这就是地址增加模式。肉眼看到的是和LED一样的红色,但是拍出来颜色编程黄白色了。
3.3 固定地址模式
void main()
{
TM1638_clear();
TM1638STB = 0;
TM1638_WByte(0x88);//1000 1000显示开,脉冲宽度为1/16
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0x44);//普通模式,地址固定,写显示数据
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0xc0);//设置显示地址为0x00
TM1638_WByte(0x3f);//数字0------------------------此时地址为0x00
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0xc2);//设置显示地址为0x02
TM1638_WByte(0x06);//数字1------------------------此时地址为0x02
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0xc4);//设置显示地址为0x04
TM1638_WByte(0x5b);//数字2------------------------此时地址为0x04
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0xc8);//设置显示地址为0x08
TM1638_WByte(0x4f);//数字3------------------------此时地址为0x08
TM1638STB = 1;
while(1);
}
固定地址模式,就是在每次发送显示数据之前都要发送显示位置地址
3.4 现象
我在代码里面跳过了第4个数码管的显示地址,所以第四个数码管没有显示。
经过对比发现,如果用地址自动增加模式,我也可以实现发一次地址写一个数据,相当于拥有固定地址模式可以实现的功能,也可以指定一个起始地址然后连续发送数据,可见地址自动增加模式似乎更有优势。
3.5 按键扫描
我这个按键扫描读取到的数值和数据手册不一样,不知道为什么,我实际读取到的值与对应按键关系如下表
按键 \数据 | byte1 | byte2 | byte3 | byte4 |
---|---|---|---|---|
S1 | 01 | 00 | 00 | 00 |
S2 | 00 | 01 | 00 | 00 |
S3 | 00 | 00 | 01 | 00 |
S4 | 00 | 00 | 00 | 01 |
S5 | 10 | 00 | 00 | 00 |
S6 | 00 | 10 | 00 | 00 |
S7 | 00 | 00 | 10 | 00 |
S8 | 00 | 00 | 00 | 10 |
我就按我的情况来编程了。我判断键值的想法是先把读取到的4个字节数据存放在一个数组里面,然后判断数组里面的数是否有0x01或者0x10,如果有这两个任何一个,说明有按键被按下了,再看如果有的是0x01,那么说明前4个按键里面的某个按键被按下了,直接跳出数组循环,当前循环次数就是按键序号。如果检测到0x10,说明后4个按键被按下,当前循环次数+4就是对应的按键序号。
读取数据并判断键值的函数
uchar TM1638_RByte()
{
uchar temp,i;
temp = 0;
TM1638DIO = 1;//释放数据线
for (i = 0; i < 8; i++)
{
temp >>= 1;
TM1638CLK = 0;
TM1638CLK = 1;
if (TM1638DIO) temp |= 0x80;
}
return temp;
}
uchar TM1638_keyscan()
{
uchar i,temp[4];
TM1638STB = 0;
TM1638_WByte(0x42);
for (i = 0; i < 4; i++)
temp[i] = TM1638_RByte();
TM1638STB = 1;
for (i = 0; i < 4; i++)
{
if (temp[i] == 0x01 || temp[i] == 0x10)
return ((temp[i] == 0x01) ? i:(4+i));
}
return 8;
}
如果有按键被按下,返回值哪里用了三元运算符。
完整代码如下。我的程序写的是按下S1让LED8翻转,按下S2让蜂鸣器翻转。
在main函数里面我对按键进行了消抖
#include <reg52.h>
typedef unsigned char uchar;
typedef unsigned char ucahr;
typedef unsigned int uint;
sbit TM1638STB = P1^0;
sbit TM1638CLK = P1^1;
sbit TM1638DIO = P1^2;
sbit LED8 = P1^7;
sbit beep = P2^3;
uchar code du[23];
void TM1638_clear();
void TM1638_WByte(ucahr cmd);
void TM1638_Wcmd(ucahr cmd);
uchar TM1638_RByte();
uchar TM1638_keyscan();
void delay(unsigned int i);
void main()
{
uchar key_val;
TM1638_clear();
TM1638_Wcmd(0x8A);//亮度4/16
TM1638STB = 0;
TM1638_WByte(0X40);//普通模式,地址自动增加,写显示数据
TM1638_WByte(du[0]);//在默认的0x00地址写数字0
TM1638STB = 1;
while(1)
{
key_val = TM1638_keyscan();
delay(20);
if (TM1638_keyscan() != key_val)
key_val = 8;
if (key_val != 8)
{
switch (key_val)
{
case 0: LED8 = !LED8; key_val = 8;break;
case 1: beep = !beep; key_val = 8;break;
}
}
delay(50);
}
}
//TM1638写一个字节(只负责送数据到线上)
void TM1638_WByte(ucahr cmd)
{
uchar i;
for (i = 0; i < 8; i++)
{
TM1638CLK = 0;
TM1638DIO = cmd & 0x01;
cmd >>= 1;
TM1638CLK = 1;
}
}
void TM1638_Wcmd(ucahr cmd)
{
TM1638STB = 0;
TM1638_WByte(cmd);
TM1638STB = 1;
}
uchar TM1638_RByte()
{
uchar temp,i;
temp = 0;
TM1638DIO = 1;//释放数据线
for (i = 0; i < 8; i++)
{
temp >>= 1;
TM1638CLK = 0;
TM1638CLK = 1;
if (TM1638DIO) temp |= 0x80;
}
return temp;
}
uchar TM1638_keyscan()
{
uchar i,temp[4];
TM1638STB = 0;
TM1638_WByte(0x42);
for (i = 0; i < 4; i++)
temp[i] = TM1638_RByte();
TM1638STB = 1;
for (i = 0; i < 4; i++)
{
if (temp[i] == 0x01 || temp[i] == 0x10)
return ((temp[i] == 0x01) ? i:(4+i));
}
return 8;
}
void TM1638_clear()
{
uchar i;
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0x40);
for (i = 0; i < 16; i ++)
TM1638_WByte(0x00);//在所有16个地址中写入0x00
TM1638STB = 1;
TM1638STB = 0;
TM1638_WByte(0xc0);//设置显示地址为0x00
TM1638STB = 1;
}
void delay(unsigned int i)
{
unsigned int j;
for (; i > 0; i--)
for (j = 114; j > 0; j--);
}
uchar code du[]={
//0 1 2 3 4 5 6 7 8
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F,
//9 A(10) B(11) C(12) D(13) E(14) F(15) H(16) L(17)
0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x76, 0x38,
//n(18) u(19) -(20) 熄灭(21) .(22)
0x37, 0x3E, 0x40, 0x00, 0x80};
3.6 LED控制
控制LED,只需要 在对应地址让SEG9=0,比如我想让第2个LED亮,那么我就先把地址设置为0x03,然后发送数据0x01,这样就可以了。
TM1638模块差不多都说完了,我没有逻辑分析仪,示波器之类的工具,在分析获取到的数据的时候费点时间,我是把接收到的4个字节数据放在数组里面,然后把数组4个字节通过串口发送到电脑来看我到底接受到了什么信息,然后根据键值的特点来写的程序,我还不清楚为什么我收到的数据跟手册里写的不一样,不过还是用起来了。
|
在自己使用时,还可以把显示函数封装起来,以及LED控制封装,用起来更方便。