STM32F1开发指南笔记20—-数码管驱动芯片TM1640解析

以前在单片机学习中,驱动数码管是直接将数码管连接单片机,在单片机上需要编写很复杂的程序,并且对数码管需要实时扫描。在实际项目开发中,一般使用数码管专用控制芯片来驱动数码管,提高开发效率。

TM1640是一种LED(发光二极管显示器)驱动控制专用电路,内部集成有MCU数字接口、数据锁存器、LED驱动等电路。主要应用于电子产品LED显示屏驱动。采用SOP28封装形式。TM1640只是数码管驱动芯片的一种,还有很多其他的驱动芯片,具体芯片选择需要根据项目需求及预算来决定。
在这里插入图片描述

在这里插入图片描述
可驱动16位数码管。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
有共阴极与共阳极两种电路图,根据开发板上的数码管实际连接来决定使用哪个。
在这里插入图片描述
在这里插入图片描述
这两个引脚分别与单片机的两个GPIO相连。

在这里插入图片描述
8位数码管
在这里插入图片描述
这八个共阴极的LED灯相当于一个数码管,可以当成一个数码管来处理。

驱动代码如下:

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "stm32f10x.h"
#include "sys.h"
#include "delay.h"
#include "rtc.h"
#include "TM1640.h"

int main (void)
{
    u8 c=0x01;
    RCC_Configuration(); //系统时钟初始化
    RTC_Config();       //RTC初始化
    TM1640_Init();      //TM1640初始化
   
    while(1)
    {
        if(RTC_Get()==0)    //读出RTC时间
        {
            TM1640_display(0,rday/10);  //天
            TM1640_display(1,rday%10+10);   //+10是为了显示后面的小数点
            TM1640_display(2,rhour/10); //时
            TM1640_display(3,rhour%10+10);
            TM1640_display(4,rmin/10);  //分
            TM1640_display(5,rmin%10+10);
            TM1640_display(6,rsec/10); //秒
            TM1640_display(7,rsec%10);

            TM1640_led(c); //与TM1640连接的8个LED全亮
            c<<=1; //数据左移 流水灯
            if(c==0x00)c=0x01; //8个灯显示完后重新开始
            delay_ms(125); //延时
        }
    }
}

TM1640.c函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include "TM1640.h"
#include "delay.h"

#define DEL  1   //宏定义 通信速率(默认为1,如不能通信可加大数值)

//地址模式的设置
//#define TM1640MEDO_ADD  0x40   //宏定义    自动加一模式
#define TM1640MEDO_ADD  0x44   //宏定义 固定地址模式(推荐)

//显示亮度的设置
//#define TM1640MEDO_DISPLAY  0x88   //宏定义 亮度  最小
//#define TM1640MEDO_DISPLAY  0x89   //宏定义 亮度
//#define TM1640MEDO_DISPLAY  0x8a   //宏定义 亮度
//#define TM1640MEDO_DISPLAY  0x8b   //宏定义 亮度
#define TM1640MEDO_DISPLAY  0x8c   //宏定义 亮度(推荐)
//#define TM1640MEDO_DISPLAY  0x8d   //宏定义 亮度
//#define TM1640MEDO_DISPLAY  0x8f   //宏定义 亮度 最大

#define TM1640MEDO_DISPLAY_OFF  0x80   //宏定义 亮度 关


//通信时序 启始(基础GPIO操作)(低层)
void TM1640_start()
{
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(1)); //接口输出高电平1 
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(1)); //接口输出高电平1
    delay_us(DEL);
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(0)); //接口输出0  
    delay_us(DEL);
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(0)); //接口输出0 
    delay_us(DEL);
}


//通信时序 结束(基础GPIO操作)(低层)
void TM1640_stop()
{
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(0)); //接口输出0  
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(1)); //接口输出高电平1
    delay_us(DEL);
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(1)); //接口输出高电平1 
    delay_us(DEL);
}


//写数据(低层)
void TM1640_write(u8 date)
{  
    u8 i;
    u8 aa;
    aa=date;
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(0)); //接口输出0  
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(0)); //接口输出0 
    for(i=0;i<8;i++)
    {
        GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(0)); //接口输出0 
        delay_us(DEL);

        if(aa&0x01)
        {
            GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(1)); //接口输出高电平1 
            delay_us(DEL);
        }
        else
        {
            GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(0)); //接口输出0  
            delay_us(DEL);
        }
        GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(1)); //接口输出高电平1
        delay_us(DEL);
        aa=aa>>1;
   }
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(0)); //接口输出0  
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(0)); //接口输出0 
}


//TM1640接口初始化
void TM1640_Init(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure;  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,ENABLE);      
    GPIO_InitStructure.GPIO_Pin = TM1640_DIN | TM1640_SCLK; //选择端口号(0~15或all)                        
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //选择IO接口工作方式      
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置IO接口速度(2/10/50MHz)    
    GPIO_Init(TM1640_GPIOPORT, &GPIO_InitStructure);

    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_DIN,(BitAction)(1)); //接口输出高电平1 
    GPIO_WriteBit(TM1640_GPIOPORT,TM1640_SCLK,(BitAction)(1)); //接口输出高电平1
    TM1640_start();
    TM1640_write(TM1640MEDO_ADD); //设置数据,0x40,0x44分别对应地址自动加一和固定地址模式
    TM1640_stop();
    TM1640_start();
    TM1640_write(TM1640MEDO_DISPLAY); //控制显示,开显示,0x88,  0x89,  0x8a,  0x8b,  0x8c,  0x8d,  0x8e,  0x8f分别对应脉冲宽度为:
                                      //------------------1/16,  2/16,  4/16,  10/16, 11/16, 12/16, 13/16, 14/16     //0x80关显示
    TM1640_stop(); 
               
}


//固定地址模式的显示输出8个LED控制
void TM1640_led(u8 date)
{
   TM1640_start();
   TM1640_write(TM1640_LEDPORT);            //传显示数据对应的地址
   TM1640_write(date);  //传1BYTE显示数据
   TM1640_stop();
}


//固定地址模式的显示输出
void TM1640_display(u8 address,u8 date)
{
    const u8 buff[21]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0xbf,0x86,0xdb,0xcf,0xe6,0xed,0xfd,0x87,0xff,0xef,0x00};//数字0~9及0~9加点显示段码表
    //---------------   0    1    2    3    4    5    6    7    8    9    0.   1.   2.   3.   4.   5.   6.   7.   8.   9.   无  
   TM1640_start();
   TM1640_write(0xC0+address);           //传显示数据对应的地址
   TM1640_write(buff[date]);                 //传1BYTE显示数据
   TM1640_stop();
}


//地址自动加一模式的显示输出
void TM1640_display_add(u8 address,u8 date)
{    
    u8 i;
    const u8 buff[21]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0xbf,0x86,0xdb,0xcf,0xe6,0xed,0xfd,0x87,0xff,0xef,0x00};//数字0~9及0~9加点显示段码表
    //---------------   0    1    2    3    4    5    6    7    8    9    0.   1.   2.   3.   4.   5.   6.   7.   8.   9.   无  
  TM1640_start();
   TM1640_write(0xC0+address);           //设置起始地址
   for(i=0;i<16;i++)
   {
      TM1640_write(buff[date]);
   }
   TM1640_stop();
}

TM1640.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef __TM1640_H
#define __TM1640_H   
#include "sys.h"

#define TM1640_GPIOPORT GPIOA   //定义IO接口
#define TM1640_DIN  GPIO_Pin_12 //定义IO接口
#define TM1640_SCLK GPIO_Pin_11 //定义IO接口
#define TM1640_LEDPORT  0xC8    //定义IO接口

void TM1640_Init(void);//初始化
void TM1640_led(u8 date);//
void TM1640_display(u8 address,u8 date);//
void TM1640_display_add(u8 address,u8 date);//
                           
#endif

rtc.c函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/*
    //时间读写与设置说明//
1,在mani函数开头放入RTC_Config();就可以使能时钟了。
在RTC_Config();函数中自带判断是不是首次使用RTC
2,使用 RTC_Get();读出时间。读出的数据存放在:
年 ryear (16位)
月 rmon     (以下都是8位)
日 rday
时 rhour
分 rmin
秒 rsec
周 rweek

3,使用 RTC_Set(4位年,2位月,2位日,2位时,2位分,2位秒); 写入时间。例如:RTC_Get(2017,08,06,21,34,00);

其他函数都是帮助如上3个函数的,不需要调用。
注意要使用RTC_Get和RTC_Set的返回值,为0时表示读写正确。

*/


#include "sys.h"
#include "rtc.h"


//以下2条全局变量--用于RTC时间的读取
u16 ryear; //4位年
u8 rmon,rday,rhour,rmin,rsec,rweek;//2位月日时分秒周


//首次启用RTC的设置
void RTC_First_Config(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//启用PWR和BKP的时钟(from APB1)
    PWR_BackupAccessCmd(ENABLE);//后备域解锁
    BKP_DeInit();//备份寄存器模块复位
    RCC_LSEConfig(RCC_LSE_ON);//外部32.768KHZ晶振开启  
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);//等待稳定    
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//RTC时钟源配置成LSE(外部低速晶振32.768KHZ)    
    RCC_RTCCLKCmd(ENABLE);//RTC开启    
    RTC_WaitForSynchro();//开启后需要等待APB1时钟与RTC时钟同步,才能读写寄存器    
    RTC_WaitForLastTask();//读写寄存器前,要确定上一个操作已经结束
    RTC_SetPrescaler(32767);//设置RTC分频器,使RTC时钟为1Hz,RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1)  
    RTC_WaitForLastTask();//等待寄存器写入完成
    //当不使用RTC秒中断,可以屏蔽下面2条
//    RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能秒中断  
//    RTC_WaitForLastTask();//等待写入完成
}


//实时时钟初始化
void RTC_Config(void)
{
    //在BKP的后备寄存器1中,存了一个特殊字符0xA5A5
    //第一次上电或后备电源掉电后,该寄存器数据丢失,表明RTC数据丢失,需要重新配置
    if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5){//判断寄存数据是否丢失      
        RTC_First_Config();//重新配置RTC        
        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//配置完成后,向后备寄存器中写特殊字符0xA5A5
    }else{
        //若后备寄存器没有掉电,则无需重新配置RTC
        //这里我们可以利用RCC_GetFlagStatus()函数查看本次复位类型
        if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET){
            //这是上电复位
        }
        else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET){
            //这是外部RST管脚复位
        }      
        RCC_ClearFlag();//清除RCC中复位标志

        //虽然RTC模块不需要重新配置,且掉电后依靠后备电池依然运行
        //但是每次上电后,还是要使能RTCCLK
        RCC_RTCCLKCmd(ENABLE);//使能RTCCLK        
        RTC_WaitForSynchro();//等待RTC时钟与APB1时钟同步

        //当不使用RTC秒中断,可以屏蔽下面2条
//        RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能秒中断        
//        RTC_WaitForLastTask();//等待操作完成
    }
    #ifdef RTCClockOutput_Enable  
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
        PWR_BackupAccessCmd(ENABLE);  
        BKP_TamperPinCmd(DISABLE);  
        BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock);
    #endif
}


//RTC时钟1秒触发中断函数(名称固定不可修改)
void RTC_IRQHandler(void)
{
    if (RTC_GetITStatus(RTC_IT_SEC) != RESET){

    }
    RTC_ClearITPendingBit(RTC_IT_SEC);
    RTC_WaitForLastTask();
}


//闹钟中断处理(启用时必须调高其优先级)
void RTCAlarm_IRQHandler(void)
{  
    if(RTC_GetITStatus(RTC_IT_ALR) != RESET){
   
    }
    RTC_ClearITPendingBit(RTC_IT_ALR);
    RTC_WaitForLastTask();
}

//判断是否是闰年函数
//月份   1  2  3  4  5  6  7  8  9  10 11 12
//闰年   31 29 31 30 31 30 31 31 30 31 30 31
//非闰年 31 28 31 30 31 30 31 31 30 31 30 31
//输入:年份
//输出:该年份是不是闰年.1,是.0,不是
u8 Is_Leap_Year(u16 year)
{                    
    if(year%4==0){ //必须能被4整除
        if(year%100==0){       
            if(year%400==0)return 1;//如果以00结尾,还要能被400整除          
            else return 0;  
        }else return 1;  
    }else return 0;
}                          
//设置时钟
//把输入的时钟转换为秒钟
//以1970年1月1日为基准
//1970~2099年为合法年份

//月份数据表                                                                      
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表  
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};//平年的月份日期表

//写入当前时间(1970~2099年有效)
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
    u16 t;
    u32 seccount=0;
    if(syear<2000||syear>2099)return 1;//syear范围1970-2099,此处设置范围为2000-2099      
    for(t=1970;t<syear;t++){ //把所有年份的秒钟相加
        if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
        else seccount+=31536000;                    //平年的秒钟数
    }
    smon-=1;
    for(t=0;t<smon;t++){         //把前面月份的秒钟数相加
        seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
        if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数        
    }
    seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加
    seccount+=(u32)hour*3600;//小时秒钟数
    seccount+=(u32)min*60;      //分钟秒钟数
    seccount+=sec;//最后的秒钟加上去
    RTC_First_Config(); //重新初始化时钟
    BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//配置完成后,向后备寄存器中写特殊字符0xA5A5
    RTC_SetCounter(seccount);//把换算好的计数器值写入
    RTC_WaitForLastTask(); //等待写入完成
    return 0; //返回值:0,成功;其他:错误代码.    
}


//读出当前时间值 ,返回值:0,成功;其他:错误代码.
u8 RTC_Get(void)
{
    static u16 daycnt=0;
    u32 timecount=0;
    u32 temp=0;
    u16 temp1=0;
    timecount=RTC_GetCounter();    
    temp=timecount/86400;   //得到天数(秒钟数对应的)
    if(daycnt!=temp){//超过一天了
        daycnt=temp;
        temp1=1970;  //从1970年开始
        while(temp>=365){
             if(Is_Leap_Year(temp1)){//是闰年
                 if(temp>=366)temp-=366;//闰年的秒钟数
                 else {temp1++;break;}
             }
             else temp-=365;       //平年
             temp1++;
        }  
        ryear=temp1;//得到年份
        temp1=0;
        while(temp>=28){//超过了一个月
            if(Is_Leap_Year(ryear)&&temp1==1){//当年是不是闰年/2月份
                if(temp>=29)temp-=29;//闰年的秒钟数
                else break;
            }else{
                if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
                else break;
            }
            temp1++;
        }
        rmon=temp1+1;//得到月份
        rday=temp+1;  //得到日期
    }
    temp=timecount%86400;     //得到秒钟数      
    rhour=temp/3600;     //小时
    rmin=(temp%3600)/60; //分钟    
    rsec=(temp%3600)%60; //秒钟
    rweek=RTC_Get_Week(ryear,rmon,rday);//获取星期  
    return 0;
}    


//按年月日计算星期(只允许1901-2099年)//已由RTC_Get调用
u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{    
    u16 temp2;
    u8 yearH,yearL;
    yearH=year/100;    
    yearL=year%100;
    // 如果为21世纪,年份数加100
    if (yearH>19)yearL+=100;
    // 所过闰年数只算1900年之后的
    temp2=yearL+yearL/4;
    temp2=temp2%7;
    temp2=temp2+day+table_week[month-1];
    if (yearL%4==0&&month<3)temp2--;
    return(temp2%7); //返回星期值
}

rtc.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef __RTC_H
#define __RTC_H  
#include "sys.h"

//全局变量的声明,在rtc.c文件中定义
//以下2条是使用extern语句声明全局变量
//注意:这里不能给变量赋值
extern u16 ryear;
extern u8 rmon,rday,rhour,rmin,rsec,rweek;

void RTC_First_Config(void);//首次启用RTC的设置
void RTC_Config(void);//实时时钟初始化
u8 Is_Leap_Year(u16 year);//判断是否是闰年函数                    
u8 RTC_Get(void);//读出当前时间值   
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);//写入当前时间
u8 RTC_Get_Week(u16 year,u8 month,u8 day);//按年月日计算星期

#endif