基于STM32的TM1638的按键控制以及数码管和LED灯的动态扫描
目录
前言
趁着国庆这几天想着做个与硬件的控制,于是就需要交互,LCD屏幕可以用来显示数据,而输入我想到了以前用过的矩阵键盘,但矩阵键盘需要的io太多了。于是买了TM1638模块,在网上看了很多博文,唉,一言难尽啊,驱动都是一样的,TM1638是一种类似于iic的时序但又不是iic时序,它的好处就是用三个引脚同时控制 8位数码管 8位LED灯以及8个按键 。
LED灯和数码管的动态扫描容易实现,按键按理也简单,第一次用这个本着以跑起来为主的目的在网上看了看别人怎么做的,结果,按键方面的控制硬是没有一个可以跑的,,大多数文章基本类似(因为驱动已经写好了)。无语的是它功能确实厉害(3个引脚控制3种8位的外设),但按键方面确实我没有找到能直接抄过来就能跑的,最终我怀疑是不是我硬件的问题(在某宝买了2块,结果2块或多或少有几个led灯点不亮,led灯的测试电压是2v可以电亮,我用万用表测我的只有1.13v。。。。),于是我找了个esp32的例程成功把按键跑起来了。
附上ESP32的TM1638例程
TM1638 LED数码显示模块ARDUINO驱动代码_悟渔的博客-CSDN博客_tm1638按键怎么编码
关于按键控制的困惑及解决方案
第一困惑: 我看博客困在了那个 DIO 这个引脚位,注释写着是输入,但为什么并没有更改GPIO的输出模式呢?在stm32里推挽输出,设置输出状态0/1后再对它进行读无效。于是,我不得不去看使用文档,后续知道DIO写数据是输出模式,读数据是输入模式,于是乎在写读时需要更改GPIO的引脚状态。
/**重新设置引脚输入输出状态**/
void Set_Pin_Mode(GPIO_TypeDef * GPIOx,u16 PIN,u32 Mode){
GPIO_InitTypeDef s ={0};
s.Pin=PIN;
s.Mode=Mode;
s.Speed=GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOx,&s);
}
第二困惑:我第一困惑解决了,还是跑不起来,后面看上面的那个esp32例程里用了延时来适应高主频的MCU,比如我用STM32F103是72MHZ全速运行,所以不加延时也是跑不起来的。调用驱动里的读按键一直都是读出来KEY 为 1。
于是加延时
/***************************************************************************************/
/**
* TM1638读键扫数据函数
* 参数:无
* 返回值:读取的按键号,1~8
*/
unsigned char TM1638_ReadKey(void) //TM1638读键扫数据函数
{
unsigned char c[4],i,key_value=0;
_STB=0; //STB=0,开始写命令
TM1638_WriteData(0x42); //普通模式,地址自动增加,读键扫数据
for(i=0;i<4;i++) c[i]=TM1638_Read(); //读取键值
_STB=1; //STB=1,读键值结束
for(i=0;i<4;i++)
key_value|=c[i]<<i;
for(i=0;i<8;i++)
if((0x01<<i)==key_value)
break;
if(i == 8)return 0;
return i+1;
}
经过我的实践,得出:这个按键读出来则一定是被按下了,而不是受抖动,不足的是通过这个模块我只是知道这个按键被按下,而不知道它到底触发了多少次,在测试中,我按下未松开,我通过串口去打印发现按键被按下,前若干次打印出来是键值,后面则是未按下的值,断断续续地又打印出键值,我通过跑例程也是这样。
不知道是不是时序有问题,欢迎同行赐教。
关于按键控制判断只按下一次
尽管这样我还是想通过写算法知道它到底是被按下了一次还是被按下多次(若是考虑到那种按一次数值单位增减的需求),虽然读出来的按键不稳定,但经过我的实际测试,刚按下读出来的数必然是连续的键值,然后必定是连续的0,若还是按着不动,则有可能又是连续的键值(这种情况下一般是按了有几s之久)。所以我也给出我的算法,经过我测试按下按着不动不超过5s,一般被视为只按下了一次。如果它被按下时通过读的键值是固定那么就百分百判断只按下了一次。
void TM1638_KEY_SCAN(void){
u8 static key_state=0;
u8 temp=TM1638_ReadKey(); //实时读取
if(key_state ==0 && temp){
key_state=1;
rkey=temp;
}
else if(key_state == 1) { //不稳定状态 为 0 或 原缓存数值
rkey=0;
}
if(!temp)key_state=0;
}
测试按键被按下一次的测试代码(在定时器中断keytime每1s自加1,20ms判断一次,不加时间判断也可以)
if(keytime>20){
keytime=0;
TM1638_KEY_SCAN();
if(rkey)SMGDT[6]=rkey;
if(rkey== 1) ++a;
else if(rkey ==2 )--a;
}
若是不需要比较细节的这种需求,直接通过读取键值就行。不同的键值读出来不一样就很容易判断。
数码管和LED动态扫描
LED灯每20ms刷新一次,数码管刷新放定时器中断内每1s刷新一根数码管如此反复轮询。
定义缓存
u8 SMGDT[8]={10,10,10,10,10,10,10,10};
u8 LEDDT[8]={0,0,0,0,0,0,0,0};
动态扫描,使用直接改SMGDT 和LEDDT对应位的值就行,0-7 对应LED和数码管1-8
//数码管动态扫描
void smg_play(u8 ser,u8 val){
if(val == 10) {
TM1638_TubeOff(ser); //关闭该位数码管
return ;
}
TM1638_Tube(ser, val, 0);
}
//8位数码管显示
void scan_smg(void){
u8 static s=1;
smg_play(s,SMGDT[s-1]);
if(++s>8)s=1;
}
void scan_led(void){
for( int i= 0;i< 8;i++){
TM1638_Light(i+1, LEDDT[i]);
}
}
在定时器里刷新数码管
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim == &htim2){
scan_smg();
}
}
刷新led
if(ledtime>20){
ledtime=0;
scan_led();
}
关于驱动代码(HAL库加寄存器位端控制GPIO)
TM1638.C
#include "stm32f1xx_hal.h"
#include "tm1638.h" //tm1638模块实现头文件
#include "delay.h"
#define _STB PGout(2)
#define _CLK PGout(3)
#define _DIO PGout(4)
#define DIOIN PGin(4)
u8 rkey=0;
u8 SMGDT[8]={10,10,10,10,10,10,10,10};
u8 LEDDT[8]={0,0,0,0,0,0,0,0};
unsigned char TM1638_LED[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07, //共阴极数码管段码,不带小数点
0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71}; //0~F,1亮0灭
unsigned char TM1638_LED_P[]={0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87, //共阴极数码管段码,带小数点
0xFF,0xEF,0xF7,0xFC,0xB9,0xDE,0xF9,0xF1}; //0~F,1亮0灭
/***************************************************************************************/
/**
* TM1638写数据函数
* 参数:data:要写入的8位数据
* 返回值:无
*/
void TM1638_WriteData(unsigned char data) //TM1638写数据函数
{
Set_Pin_Mode(GPIOG,GPIO_PIN_4,GPIO_MODE_OUTPUT_PP);
unsigned char i;
for(i=0;i<8;i++)
{
_CLK=0; //CLK=0
if(data&0x01)
{
_DIO=1; //DIO=1
}
else
{
_DIO=0; //DIO=0
}
data>>=1;
_CLK=1; //CLK=1
}
}
/***************************************************************************************/
/***************************************************************************************/
/**
* TM1638指定地址写数据函数
* 参数1:addr:要写入数据的地址
* 参数2:data:要写入的8位数据
* 返回值:无
*/
void TM1638_WriteAddressData(unsigned char addr,unsigned char data) //TM1638指定地址写数据函数
{
_STB=0; //STB=0
TM1638_WriteData(addr); //地址
TM1638_WriteData(data); //数据
_STB=1; //STB=1
}
/***************************************************************************************/
/***************************************************************************************/
/**
* TM1638指定数码管序号与显示数字函数
* 参数1:serial:数码管序号,1-8
* 参数2:num:要显示的数字,0-F
* 参数3:point:是否带小数点,Point:带,NoPoint:不带
* 返回值:无
*/
void TM1638_Tube(unsigned char serial, unsigned char num, unsigned char point) //TM1638指定数码管序号与显示数字函数
{
_STB=0; //STB=0
TM1638_WriteData(0x44); //普通模式,固定地址,写数据到显示寄存器
_STB=1; //STB=1
_STB=0; //STB=0
TM1638_WriteData(0x88); //显示开,亮度第1级
_STB=1; //STB=1
if(point == 1) //带小数点
{
TM1638_WriteAddressData(0XC0+2*(serial-1),TM1638_LED_P[num]); //第serial个数码管显示num,带小数点
}
else if(point == 0) //不带小数点
{
TM1638_WriteAddressData(0XC0+2*(serial-1),TM1638_LED[num]); //第serial个数码管显示num,不带小数点
}
}
/***************************************************************************************/
/***************************************************************************************/
/**
* TM1638关闭指定数码管函数
* 参数:serial:数码管序号
* 返回值:无
*/
void TM1638_TubeOff(unsigned char serial) //TM1638关闭指定数码管函数
{
_STB=0; //STB=0
TM1638_WriteData(0x44); //普通模式,固定地址,写数据到显示寄存器
_STB=1; //STB=1
_STB=0; //STB=0
TM1638_WriteData(0x88); //显示开,亮度第1级
_STB=1; //STB=1
TM1638_WriteAddressData(0XC0+2*(serial-1),0x00); //第serial个数码管灭
}
/***************************************************************************************/
/***************************************************************************************/
/**
* TM1638指定LED灯序号num与亮灭state函数
* 参数1:num:LED灯序号
* 参数2:state:LED灯状态,LightOn:开,LightOff:关
* 返回值:无
*/
void TM1638_Light(unsigned char num, unsigned char state) //TM1638指定LED灯序号num与亮灭state函数
{
_STB=0; //STB=0
TM1638_WriteData(0x44); //普通模式,固定地址,写数据到显示寄存器
_STB=1; //STB=1
_STB=0; //STB=0
TM1638_WriteData(0x88); //显示开,亮度第1级
_STB=1; //STB=1
if(state == 1)
{
TM1638_WriteAddressData(0XC0+2*(num-1)+1,0X01); //第num个灯亮
}
else if(state == 0)
{
TM1638_WriteAddressData(0XC0+2*(num-1)+1,0X00); //第num个灯灭
}
}
/***************************************************************************************/
/***************************************************************************************/
/**
* TM1638读数据函数
* 参数:无
* 返回值:读取的8位数据
*/
unsigned char TM1638_Read(void) //读数据函数
{
unsigned char i,temp=0;
// _DIO=1;
Set_Pin_Mode(GPIOG,GPIO_PIN_4,GPIO_MODE_INPUT);
delay_us(1);
for(i=0;i<8;i++)
{
temp>>=1;
delay_us(1);
_CLK=0; //CLK=0
delay_us(1);
if( DIOIN == 1) //读取DIO值
temp|=0x80; //按位或:与0或不变、与1或置1
_CLK=1; //CLK=1
}
return temp;
}
/***************************************************************************************/
/***************************************************************************************/
/**
* TM1638读键扫数据函数
* 参数:无
* 返回值:读取的按键号,1~8
*/
unsigned char TM1638_ReadKey(void) //TM1638读键扫数据函数
{
unsigned char c[4],i,key_value=0;
_STB=0; //STB=0,开始写命令
TM1638_WriteData(0x42); //普通模式,地址自动增加,读键扫数据
for(i=0;i<4;i++) c[i]=TM1638_Read(); //读取键值
_STB=1; //STB=1,读键值结束
for(i=0;i<4;i++)
key_value|=c[i]<<i;
for(i=0;i<8;i++)
if((0x01<<i)==key_value)
break;
if(i == 8)return 0;
return i+1;
}
/***************************************************************************************/
/***************************************************************************************/
/**
* TM1638初始化函数
* 参数:无
* 返回值:无
*/
void TM1638_Init(void) //TM1638初始化函数
{
unsigned char i; //临时变量
//已在hal库自动初始化过
_STB=0; //STB=0
TM1638_WriteData(0x44); //普通模式,固定地址,写数据到显示寄存器
_STB=1; //STB=1
_STB=0; //STB=0
TM1638_WriteData(0x88); //显示开,亮度第1级
_STB=1; //STB=1
for(i=0;i<16;i++)
{
TM1638_WriteAddressData(0XC0+i,0X00); //全地址写入0X00
}
}
/**重新设置引脚输入输出状态**/
void Set_Pin_Mode(GPIO_TypeDef * GPIOx,u16 PIN,u32 Mode){
GPIO_InitTypeDef s ={0};
s.Pin=PIN;
s.Mode=Mode;
s.Speed=GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOx,&s);
}
/***************************************************************************************/
//数码管动态扫描
void smg_play(u8 ser,u8 val){
if(val == 10) {
TM1638_TubeOff(ser); //关闭该位数码管
return ;
}
TM1638_Tube(ser, val, 0);
}
//8位数码管显示
void scan_smg(void){
u8 static s=1;
smg_play(s,SMGDT[s-1]);
if(++s>8)s=1;
}
void scan_led(void){
for( int i= 0;i< 8;i++){
TM1638_Light(i+1, LEDDT[i]);
}
}
void TM1638_KEY_SCAN(void){
u8 static key_state=0;
u8 temp=TM1638_ReadKey(); //实时读取
if(key_state ==0 && temp){
key_state=1;
rkey=temp;
}
else if(key_state == 1) { //不稳定状态 为 0 或 原缓存数值
rkey=0;
}
if(!temp)key_state=0;
}
TM1638.h
#ifndef __TM1638_H_
#define __TM1638_H_
#include "reg.h"
void TM1638_Init(void); //TM1638初始化函数
void TM1638_WriteData(unsigned char data); //TM1638写数据函数
void TM1638_WriteAddressData(unsigned char addr,unsigned char data); //TM1638指定地址写数据函数
void TM1638_Tube(unsigned char serial, unsigned char num, unsigned char point); //TM1638指定数码管序号与显示数字函数
void TM1638_TubeOff(unsigned char serial); //TM1638关闭指定数码管函数
void TM1638_Light(unsigned char num, unsigned char state); //TM1638指定LED灯序号num与亮灭state函数
unsigned char TM1638_Read(void); //TM1638读数据函数
unsigned char TM1638_ReadKey(void); //TM1638读键扫数据函数
//动态扫描 抽象出应用层
void scan_smg(void);
extern u8 SMGDT[8];
void scan_led(void);
extern u8 LEDDT[8];
void Set_Pin_Mode(GPIO_TypeDef * GPIOx,u16 PIN,u32 Mode);
void TM1638_KEY_SCAN(void);
extern u8 rkey;
extern u8 _rkey;
#endif
寄存器位段
#ifndef REG_H
#define REG_H
#include "stm32f1xx_hal.h"
#ifndef __TYPEDEF_
#define __TYPEDEF_
typedef unsigned char u8;
typedef unsigned short u16;
typedef uint32_t u32;
typedef const uint32_t uc32;
typedef const uint16_t uc16;
typedef const uint8_t uc8;
typedef __IO uint32_t vu32;
typedef __IO uint16_t vu16;
typedef __IO uint8_t vu8;
typedef __I uint32_t vuc32;
typedef __I uint16_t vuc16;
typedef __I uint8_t vuc8;
#endif
//λ������,ʵ��51���Ƶ�GPIO���ƹ���
//����ʵ��˼��,�ο�<<CM3Ȩ��ָ��>>������(87ҳ~92ҳ).
//IO�ڲ����궨��
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO�ڵ�ַӳ��
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
//IO�ڲ���,ֻ�Ե�һ��IO��!
//ȷ��n��ֵС��16!
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //���
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //����
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //���
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //����
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //���
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //����
#define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //���
#define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //����
#define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //���
#define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //����
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //���
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //����
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //���
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //����
#define Right PEout(5)
#define BUFF PBout(8)
#define Left PBout(5)
#define LEDSTATE PEout(1)
#define ReadKEY0 PEin(4)
#define mKEY0 PEin(4)
#define mKEY1 PEin(3)
#define mKEY2 PFin(4)
#endif
效果展示
数码管和led展示
按键按一次自增减展示
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?