单片机直接扫描:硬件设备简单,但会耗费大量的单片机CPU时间
专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可,如TM1620、TM1640
TM1620是一种LED(发光二极管显示器)驱动控制专用IC,内部集成有MCU数字接口、数据锁存器、LED驱动等电路。多用于数码管或LED显示设备。
工作温度:-0.5V ~ +7.0V
符号 | 管脚名称 | 管脚号 | 说明 |
---|---|---|---|
DIN | 数据输入 | 18 | 在时钟上升沿输入串行数据,从低位开始。 |
CLK | 时钟输入 | 19 | 在上升沿读取串行数据,下降沿输出数据。 |
STB | 片选输入 | 20 | 在下降沿初始化串行接口,随后等待接收指令。STB为低后的第一个字节作为指令,当处理指令时,当前其它处理被终止。当STB为高时,CLK 被忽略。 |
SGE1 ~SGE8 | 输出(段) | 2 ~ 9 | 段输出,P管开漏输出 |
GRID1 ~ GRID4 | 输出(位) | 16 ~ 17 、13 ~ 14 | 位输出,N管开漏输出 |
SEG13/GRID6 ~ SEG14/GRID5 | 输出(段/位) | 10 ~ 11 | 段/位复用输出,只能选段或位输出 |
VDD | 逻辑电源 | 1 | 接电源正 |
GND | 逻辑地 | 12、15 | 接系统地 |
DIG1、DIG2连接到了TM1620的位选引脚,分别是GRID1和GRID2,因为数码管就两个,就靠这两个位来进行控制
SEG1 ~ SEG8是段选,就是控制 A ~ DP 8个段哪一个段亮,8个位组成一个字节
STB、CLK、DIN是TM1620接收STC15单片机控制信号的,通过厂商定义的串行协议进行通信
在进行通行前,要先将STB拉低,开启数据传输,CLK在低电平时,可以往DIN上放数据,当CLK在高电平时,TM1620就读取DIN上的数据,数据传输是从低位开始,一位一位地传,最高位结束
/*
* @name TM1620_Write_Byte
* @brief TM1620写入一个字节
* @param dat:待写入数据
* @retval None
*/
static void TM1620_Write_Byte(uint8_t dat)
{
uint8_t i = 0;
STB = 0;
Public.Delay_ms(1);
for(i=0;i<8;i++)
{
//CLK为低电平时往DIN上放数据
CLK = 0;
DIN = 0x01 & (dat>>i); //先取出最低位进行发送,然后dat右移,把最低位移出去,再取补上地低位进行发送
_nop_(); //延时1us
//CLK为高电平时TM1620读取DIN上的数据
CLK = 1;
_nop_(); //延时1us
}
}
通过上面写好的TM1620_Write_Byte()函数,就可以往TM1620写入数据了,那TM1620怎么识别STC15传输过来的数据是要干嘛的呢?
厂商对TM1620也做了设置,来识别传输过来的数据的目的
简单理解:调用TM1620_Write_Byte()函数往TM1620写入一个字节数据,共8位,而TM1620就是根据最高的两位B7、B6来区别为不同的指令,如上表所示,4种不同的指令分别表示了不同的命令,而这命令的说明也是只有高手才能一眼看出是什么作用的,新手想弄懂还得花点时间
B7、B6都为0的命令就是显示模式命令,这两位是固定的了,该模式下B5 ~ B2是没用的位,所以就剩下B1、B0来控制,是用来设置TM1620要控制多少位和多少段的,有4位10段,这说明有4个数码管,一个数码管里面有10个段要控制,用函数往TM1620写入0x00就表示设置显示模式为4位10段;
本次实验用的到是6位8段,但实战板数码管只有两个,就是由上面硬件电路图中的DIG1和DIG2位控制,其他的DIG位选引脚不接,8段就是SEG1 ~ SEG8段选全用上,写入0x02就是设置为6位8段
B7为0,B6为1就是数据命令模式,B5和B4没有用到,靠B3 ~ B0来设置为不同的数据命令;
主要是地址增加模式设置,数据模式设置没用到,测试模式是厂商内部测试用的,做实验也没用到
自动地址增加:后面会介绍到显示寄存器,就是往显示寄存器写入一个地址初值,然后写入数据(要显示的段),每次写入一个字节数据后,地址指针会从初值自动加一,跳到下一个地址,可以不用再设置地址,直接写入数据;就只设置一次地址,可以连续写数据;
固定地址:先写入地址,再写入数据,写下一个数据前,要先再写入一个地址,就每次写数据前都要先写地址
这个命令比较简单,就是用来设置数码管显示的亮度,B7为1,B6为0,B5和B4没用,用剩余位进行选择控制
亮度调整是用了PWM,是TM1620内部自动发出的PWM波,只要写入不同的命令就能控制输出不同的占空比,这不需要额外的控制线
注意B3是控制显示开关的,要数码管亮起来,B3必须为1,也就是设置脉冲宽度的命令中,B3都是1,如设置脉冲宽度为1/16,那写入的命令就是0x88;让数码管不亮,则B3位清0,写入的命令就是0x80
我觉得在看地址命令设置之前,要先看懂显示寄存器地址的说明,所以先把显示寄存器地址解释一下
对这个显示寄存器的简单理解就是:数码管的位选,选择哪一个数码管亮起
GRID1 ~ GRID6 共6个位选引脚,一个位选引脚控制一个数码管;那数码管有可能接到GRID1,也有可能接到GRID6,当然也可以6个数码管分别接完6个GRID引脚,那想指定某一个数码管亮起来该怎么设置?这就要通过往TM1620写入上表中的地址数据来进行选择某一位选引脚
先看表右边的位选,再水平着向左看,HL(低四位)和HU(高四位)是对段选SEG来说的,和位选GRID的地址设置没关系,先不用管。
GRID1对应的地址是01和00,也就是说可以往TM1620写入0x01或者0x00来选择是GRID1这个位选引脚,那为什么两个地址都是选择这个位选引脚呢?因为地址00对应了SEG1 ~ SEG8这8个段选,地址01对应了SEG13和SEG14这两个段选(表的上半部分),因为TM1620是可以控制9段或者10段的,所以复用了SEG13和SEG14这两个段选引脚;但本次实验中的数码管是8段的,分别接到了SEG1 ~ SEG8,而位选引脚接到了GRID1,所以位选就是GRID1,再根据段选,GRID1的地址就设置为0x00,没有用到SEG13和SEG14,所以不用管0x01;其他位选引脚同理
而图片开头的文字说明说12个字节单元也能理解了,就6个位选,每个位选对应两种不同的地址,6 * 2 = 12,所以是12个字节单元
这就相当于一步把位选和段选都确定了,因为确定位选引脚后,那也要确定段选引脚吧,段选选好后,后续再写入数据控制哪几个段亮起,就可以显示数字了
经过上面的显示寄存器地址解释,就可以理解这个地址命令设置是什么东西了;上面的位选地址00和01等的地址怎么来,就是在这里进行设置的
B7、B6固定都为1,B3 ~ B0不同的组合就表示不同的地址,这些地址在程序中最好先用宏定义写好,在后面想设置位选和段选时,就直接把这些地址写入到TM1620就行了;可以说这些地址就是为了设置显示寄存器而准备的
前面介绍的都是一些命令或地址,TM1620收到了这些命令会做出相应的设置,把这些命令都通过TM1620_Write_Byte()函数发送给TM1620即可,数码管就会亮起;那具体怎么个发送法呢,厂商也给出了通信的图示,如下面介绍到的
按照厂商给的这个数据传输图,首先在STB为低电平时,才开始往DIN上发送命令,每一步命令是设置什么的图上有说明,注意每次传输完一个命令后,STB要拉高,下次发送命令时要先再次拉低再发送命令;在发送段选数据的时候STB就不用再拉高了
注意:TM1620的数据手册上有这样一句话:芯片显示寄存器在上电瞬间其内部保存的值可能是随机不确定的,此时客户直接发送开屏命令,将有可能出现显示乱码。所以我司建议客户对显示寄存器进行一次上电清零操作,即上电后向12位显存地址(00H-0BH)中全部写入数据0x00
清零操作就可以设置为地址自动增加模式,然后写入位选初始地址是0x00,从GRID1开始,然后用循环不断写入段选数据0x00,进行清零操作,后续代码中有这一步
固定地址模式就是每次写入段选数据前都要先确定位选,而且写完段选数据后STB要拉高,写下一个位选地址时STB要再次拉低
main.c ->主函数文件,包含main函数等;
Public.c ->公共函数文件,包含Delay延时函数等;
Sys_init ->系统初始化函数,包含GPIO初始化函数等;
ADC.c ->ADC初始化,采集ADC值等;
NTC.c ->NTC外设函数,包含查表,获取环境温度等;
TM1620.c ->驱动IC初始化,协议,温度显示等函数;
每间隔 500ms 通过ADC获取一次PCB板温度,数码管显示PCB板温度,数码管亮度会自动变化。
主要是定义4种不同命令的枚举类型,将所有的十六进制的命令都写成能看懂的名称,不然在c文件里发送0x00根本不知道是什么命令,还得去看手册
#ifndef __TM1620_H_
#define __TM1620_H_
//显示模式枚举类型
typedef enum
{
Disp_Mode_GRID4_SEG10 = 0x00, //4位10段
Disp_Mode_GRID5_SEG9 = 0x01, //5位9段
Disp_Mode_GRID6_SEG8 = 0x02 //6位8段
}Disp_Mode_t;
//写数据地址模式枚举类型
typedef enum
{
Write_Data_Addr_Auto_Add = 0x40, //自动地址增加
Write_Data_Addr_Fix = 0x44 //固定地址
}Write_Data_Addr_Mode_t;
//辉度等级枚举类型
typedef enum
{
Brightness_level_0 = 0x80, //显示关
Brightness_level_1 = 0x88, //脉冲宽度为1/16
Brightness_level_2 = 0x89, //脉冲宽度为2/16
Brightness_level_3 = 0x8A, //脉冲宽度为4/16
Brightness_level_4 = 0x8B, //脉冲宽度为10/16
Brightness_level_5 = 0x8C, //脉冲宽度为11/16
Brightness_level_6 = 0x8D, //脉冲宽度为12/16
Brightness_level_7 = 0x8E, //脉冲宽度为13/16
Brightness_level_8 = 0x8F, //脉冲宽度为14/16
}Brightness_level_t;
//显示寄存器地址枚举类型 —> 位选GRID的地址
typedef enum
{
Disp_SFR_Addr_Num = (uint8_t)12,
Disp_SFR_Addr_00H = 0xC0,
Disp_SFR_Addr_01H = 0xC1,
Disp_SFR_Addr_02H = 0xC2,
Disp_SFR_Addr_03H = 0xC3,
Disp_SFR_Addr_04H = 0xC4,
Disp_SFR_Addr_05H = 0xC5,
Disp_SFR_Addr_06H = 0xC6,
Disp_SFR_Addr_07H = 0xC7,
Disp_SFR_Addr_08H = 0xC8,
Disp_SFR_Addr_09H = 0xC9,
Disp_SFR_Addr_0AH = 0xCA,
Disp_SFR_Addr_0BH = 0xCB,
}Disp_SFR_Addr_t;
//定义结构体类型
typedef struct
{
Brightness_level_t Brightness; //辉度变量
void (*TM1620_Init)(); //TM1620初始化
void (*Disp_Tempareture)(); //数码管显示温度
}TM1620_t;
/* extern variables-----------------------------------------------------------*/
extern TM1620_t idata TM1620;
/* extern function prototypes-------------------------------------------------*/
#endif
/********************************************************
End Of File
********************************************************/
实现三个函数,第一个是TM162通过串行通行发送一个字节的函数,要注意STB拉低后要适当延时,不然数据通信会出错;
第二个函数是TM1620的初始化,先设置为地址自动增加,然后循环写入0x00清零显示寄存器,然后再设置为固定地址模式,给左右两个数码管初始化为全亮;
第三个函数是读取温度值并显示,把浮点型的温度值先转为整型,再分别取出十位和个位,分别显示在左边和右边的数码管上
注意:TM1620_Write_Byte()函数只是在TM1620.c文件里使用,并不提供给外部使用,所以不用声明函数指针指向该函数
/* Includes ------------------------------------------------------------------*/
#include
/* Private define-------------------------------------------------------------*/
#define DIN P24
#define CLK P25
#define STB P26
//左右两个数码管显示寄存器地址宏定义
#define Disp_Position_Left Disp_SFR_Addr_00H
#define Disp_Position_Right Disp_SFR_Addr_02H
/* Private variables----------------------------------------------------------*/
uint8_t Disp_SEG[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; //数码管显示0 ~ 9
static void TM1620_Write_Byte(uint8_t );
static void TM1620_Init();
static void Disp_Tempareture();
/* Public variables-----------------------------------------------------------*/
TM1620_t idata TM1620 =
{
Brightness_level_3,
TM1620_Init,
Disp_Tempareture
};
/* Private function prototypes------------------------------------------------*/
/*
* @name TM1620_Write_Byte
* @brief TM1620写入一个字节
* @param dat:待写入数据
* @retval None
*/
static void TM1620_Write_Byte(uint8_t dat)
{
uint8_t i = 0;
STB = 0;
Public.Delay_ms(1); //需要加延时等待,不然数码管显示会出异常
for(i=0;i<8;i++)
{
//CLK为低电平时往DIN上放数据
CLK = 0;
DIN = 0x01 & (dat>>i); //先取出最低位进行发送,然后dat右移,把最低位移出去,再取补上地低位进行发送
_nop_(); //延时1us
//CLK为高电平时TM1620读取DIN上的数据
CLK = 1;
_nop_(); //延时1us
}
}
/*
* @name TM1620_Init
* @brief TM1620初始化
* @param None
* @retval None
*/
static void TM1620_Init()
{
uint8_t i = 0;
//设置显示模式:6位8段
TM1620_Write_Byte(Disp_Mode_GRID6_SEG8);
STB = 1;
//设置地址模式:自动地址增加
TM1620_Write_Byte(Write_Data_Addr_Auto_Add);
STB = 1;
//清除显示寄存器,厂商要求
TM1620_Write_Byte(Disp_SFR_Addr_00H);
for(i=0;i<Disp_SFR_Addr_Num;i++)
{
TM1620_Write_Byte(0x00);
}
STB = 1;
//显示默认辉度:level3
TM1620_Write_Byte(TM1620.Brightness);
STB = 1;
//设置显示模式:6位8段
TM1620_Write_Byte(Disp_Mode_GRID6_SEG8);
STB = 1;
//设置地址模式:固定地址
TM1620_Write_Byte(Write_Data_Addr_Fix);
STB = 1;
//写显示寄存器
//先设置位选,设置为左边的数码管,再写入段选的数据0xFF,数码管8个段全亮
TM1620_Write_Byte(Disp_Position_Left);
TM1620_Write_Byte(0xFF);
STB = 1;
//然后设置位选为右边的数码管,同样写入段选数据0xFF,8个段全亮
TM1620_Write_Byte(Disp_Position_Right);
TM1620_Write_Byte(0xFF);
STB = 1;
//显示辉度
TM1620_Write_Byte(TM1620.Brightness);
STB = 1;
}
/*
* @name Disp_Tempareture
* @brief 数码管显示温度
* @param None
* @retval None
*/
static void Disp_Tempareture()
{
uint8_t temp = 0;
//因为获取到的温度是浮点型,而数码管只有两位,不够显示,所以要转为无符号整型数据
temp = (uint8_t)NTC.fTemperature;
TM1620_Write_Byte(Disp_Position_Left); //选择左边的数码管
TM1620_Write_Byte(Disp_SEG[temp/10]); //取出温度值的十位作为下标去找段码表对应的段选数据
STB = 1;
TM1620_Write_Byte(Disp_Position_Right); //选择右边的数码管
TM1620_Write_Byte(Disp_SEG[temp%10]); //取出温度值的个位作为下标去找段码表对应的段选数据
STB = 1;
//显示辉度
TM1620_Write_Byte(TM1620.Brightness);
STB = 1;
}
/********************************************************
End Of File
********************************************************/
主函数中先预编译串口打印初始化信息,然后while循环内不断获取温度值,再调用TM1620的Disp_Tempareture()函数来在数码管上显示两位温度值;
用switch语句来切换数码管的辉度,数码管的亮度就会一节一节地从暗到亮
/*
* @name main
* @brief 主函数
* @param void
* @retval int
*/
int main(void)
{
//系统初始化
Hradware.Sys_Init();
//串口1发送初始化信息
#ifdef Monitor_Run_Code
printf("Initialization completed,system startup!\r\n\r\n");
#endif
//系统主循环
while(1)
{
NTC.Get_Temperature_Value();
Public.Delay_ms(500);
//数码管显示温度
TM1620.Disp_Tempareture();
//调整亮度
switch (TM1620.Brightness)
{
case Brightness_level_1:TM1620.Brightness = Brightness_level_2;break;
case Brightness_level_2:TM1620.Brightness = Brightness_level_3;break;
case Brightness_level_3:TM1620.Brightness = Brightness_level_4;break;
case Brightness_level_4:TM1620.Brightness = Brightness_level_5;break;
case Brightness_level_5:TM1620.Brightness = Brightness_level_6;break;
case Brightness_level_6:TM1620.Brightness = Brightness_level_7;break;
case Brightness_level_7:TM1620.Brightness = Brightness_level_8;break;
case Brightness_level_8:TM1620.Brightness = Brightness_level_1;break;
default:TM1620.Brightness = Brightness_level_1;break;
}
Public.Delay_ms(500);
}
}
/********************************************************
End Of File
********************************************************/
如果定义函数指针指向TM1620_Write_Byte()函数,那在TM1620初始化和显示温度的函数中全都用TM1620.TM1620_Write_Byte()的形式来调用该函数往TM1620写数据的话,烧录后数码管是没有亮的;
但只要把任意一个TM1620.TM1620_Write_Byte()改为TM1620_Write_Byte(),即本来通过函数指针调用的,改为直接用函数名调用,烧录后数码管居然是可以亮的;或者全部改为直接用函数名也是可以亮
找了几个小时也没发现问题出在哪,这种现象也是第一次遇见,按理说通过函数指针调用和直接用函数名调用是没有区别的,但为什么在这个实验中就出现问题,而且还只要改其中任意一个就行,改了之后数码管就显示了,说明两种调用方式都可以用,混着用也行,说明代码逻辑是没有问题的
另外我把教程的代码也全改为了用函数指针调用,也是不亮的,网上也没查到这个问题,只能留着以后再看看有什么解决办法了
TM1620.h
//定义结构体类型
typedef struct
{
Brightness_level_t Brightness;
void (*TM1620_Write_Byte)(uint8_t); //指向TM1620初始化函数的函数指针
void (*TM1620_Init)();
void (*Disp_Tempareture)();
}TM1620_t;
TM1620.c
TM1620_t idata TM1620 =
{
Brightness_level_3,
TM1620_Write_Byte,
TM1620_Init,
Disp_Tempareture
};
//函数指针调用方式,全部使用这种调用方式,则数码管不亮
TM1620.TM1620_Write_Byte();
//函数名调用方式,只要有一个函数名调用方式,则数码管能亮
TM1620_Write_Byte();
驱动数码管也可以用三极管,但这样就需要单片机不断的进行扫描才能显示,使用专用的IC,如TM1620,就能实现自动扫描并显示
项目的TM1620显示模式命令设置是6位8段
根据TM1620的手册将寄存器的值进行定义封装,32的库就类似这样,不用每次使用时都要去看手册配寄存器
TM1620也相当于一个小MCU,也有自己的寄存器,STC15MCU通过串行协议与TM1620进行通信,对TM1620内部寄存器进行配置,让TM1620自动工作
当写很多地址时,选用地址增加模式,当写一两个地址时,选用固定地址模式
TM1620写入字节函数是内部函数,不需要被其他函数调用,所以不用写在结构体里
单片机引脚P24、P25、P26设置为开漏输出,因为这是给一个信号,不是进行驱动,如果要驱动的话,就要设置为推挽输出,输出信号控制的话,开漏和准双向口都可以
显示寄存器地址:GRID是位选,SEG是段选,根据硬件电路,第一个数码管接到了GRID1上,并且用到了SEG1 ~ SEG8,并没有用到SEG13和SEG14,所以GRID1的地址就是00H,GRID2同理
GRID位选最多是6个,每一个位选可以选择SEG1 ~SEG8 或者SEG13 ~SEG14两种段选,该两种段选所对应的GRID位选寄存器的地址又是不同的,两种段选不能一起使用,所以便有6 x 2 = 12个位选的地址
TM1620会根据写入数据的首两位判断为不同的命令