关键词:ESP32 TM1637 四脚四位数码管 IIC通信 C语言
一、TM1637简单介绍
“TM1637 是一种带键盘扫描接口的LED(发光二极管显示器)驱动控制专用电路,内部集成有MCU数字接口、数据锁存器、LED 高压驱动、键盘扫描等电路。本产品性能优良,质量可靠。主要应用于电磁炉、 微波炉及小家电产品的显示屏驱动。采用DIP/SOP20的封装形式。”——天微电子的TM1637技术规格书
TM1637_TM(天微)_TM1637中文资料_PDF手册
技术规格书在链接网页的下方。下图是带有TM1637(器件U2)的四位数码管模块,含有四个引脚,除5V电源和地外,还有CLK和DIO两根线,分别在与ESP32的通信中扮演“时钟”和“数据传输”的角色。与不带驱动的数码管模块不同,如技术规格书中所说,TM1637这颗厚实的芯片里集成了一系列复杂的电路,我们不直接控制数码管的每个段选、位选信号,而是在与TM1637通信的过程中作相关配置,像显示数字、显示亮度如何如何。设置完成后,点亮数码管的后续工作交给TM1637来做。
TM1637有20个引脚,上图里的模块引出了四个。此外,还有2个键扫输入信号、8个段选信号、6个位选信号。我们只需要直接操作引出的四个脚,其余的信号引脚连接着数码管或其它元件。如果熟悉数码管的工作原理,即使不看其它细节也已明白,TM1637最多可以控制6位带小数点(8段)的数码管。
看下面的“键扫矩阵”,同样用到了SG1-SG8的段选信号,说明控制数码管显示和读取键扫描信息这两件事不能同时发生。SG1-SG8如果配合使用GRID1-GRID6,TM1637用做控制数码管显示;配合使用K1-K2,用作读键扫描。
键扫功能用来检测一组按键的状态,识别哪些按键被按下、哪些没有。由于我没测试这个功能,根据理解和猜测,接线时可以把16个按键和6位数码管都接好,然后在ESP32(主机)与TM1637通信时配置使用哪个功能就可以了。配置为控制数码管显示时,K1和K2可能会变成高阻态之类的,这样即使按下按键也不产生干扰。
二、接线
回到4脚数码管模块。VCC接5V电源,GND接地,CLK和DIO随便接两个GPIO就可以了。我使用的是ESP32 Devkit V1开发板,CLK接了D19,DIO接了D32。
三、程序与运行结果
3.1 TM1637的通信协议
“微处理器的数据通过两线总线接口和TM1637通信,在输入数据时当CLK是高电平时,DIO 上的信号必须保持不变;只有CLK上的时钟信号为低电平时,DIO上的信号才能改变。数据输入的开始条件是CLK 为高电平时,DIO 由高变低;结束条件是 CLK 为高时,DIO由低电平变为高电平。
TM1637的数据传输带有应答信号ACK,当传输数据正确时,会在第八个时钟的下降沿,芯片内部会产生一个应答信号ACK将DIO管脚拉低,在第九个时钟结束之后释放DIO口线。”——天微电子的TM1637技术规格书
根据规格书介绍看,TM1637使用IIC通信协议,低位(LSB)先行。初次接触通信协议时,可以简单将它理解为一种写信的格式——开头“敬爱的xx” “问候语”,“正文”,结尾“此致 敬礼/祝福语” “写信人和日期”。对于IIC来说,它的格式是:
“开始信号” - “(1字节数据)” “应答信号” “(1字节数据)” “应答信号”··· “(1字节数据)” “应答信号” - “停止信号”。
格式有几种变型,但大差不差,掌握一种,其它的自然明白。当然,IIC通信协议并不只有“格式”,还有很多特点,可以搜索了解相关知识,这里推荐b站“物联网小学妹”的视频,附上链接DAY3-1 IIC总线概述_哔哩哔哩_bilibili。
上面列出的不同颜色的信号,都有不同的两种,因为通信有写信方和收信方。比如,(1字节数据)分为写数据和读数据。也就是说,当两个设备通过IIC规则通信时,要写两套程序分别放在两个设备中,两套程序互相紧密配合,才能顺利传输数据。TM1637的程序已经被厂家写好了,它接收开始和结束信号,接收数据,发出应答。我们要做的是写一套程序与之配合:发出开始和结束信号,发出数据,接收应答。
TM1637根据你要实现的不同功能,又制订了三套不同的写信格式。如下图,以“写SRAM数据固定地址模式”格式为例。ESP32应该:
发出开始信号——设置数据——等待应答——发出停止信号——
发出开始信号——设置地址——等待应答——传输1字节数据——等待应答——发出停止信号——
发出开始信号——设置地址——等待应答——传输1字节数据——等待应答——发出停止信号——
······
发出开始信号——设置地址——等待应答——传输字节数据——等待应答——发出停止信号——
发出开始信号——控制显示——等待应答——发出停止信号。
其中所谓的设置数据(command1)、设置地址(command2)、控制显示(command3)本质上都是传输一字节数据,在这些数据里配置各种功能。比如当设置数据(command1)是0x44时,配置为“控制数码管显示-写SRAM固定地址”模式;是0x40时,是“控制数码管显示-写SRAM地址自动加一”模式。设置地址(command2)是0xc0时,将数据写入第1位数码管的显示寄存器;设置地址(command2)是0xc1时,将数据写入第2位数码管的显示寄存器······详细请见规格书里的表格。
3.2 发出开始信号
完整的程序在3.6,为方便理解各部分程序的执行过程,这里贴上用示波器检测的传输字节过程,与3.2、3.3、3.4、3.5节配合食用。
//IIC发出开始信号。在clk和dio都为高的情况下,将dio拉低的一刻,通信开始。
void IIC_Start()
{
gpio_set_level(clk,1);
gpio_set_level(dio,1);
esp_rom_delay_us(1); //延时1微秒
gpio_set_level(dio,0);
esp_rom_delay_us(2);
}
3.3 传输数据
//写一个字节
void IIC_Write_1_Byte(unsigned char Onebyte) //写入为8bit字节
{
gpio_set_level(clk,0); //确保时钟为低电平
for(int i=0;i<8;i++) //循环8次
{
if(Onebyte & 0x01) // 将写入的字节与0b0000_0001按位与操作。如果写入
{ //字节的最后一位是1,位与结果就是0b0000_0001,否则
gpio_set_level(dio,1); //是0b0000_0000。这样就能判断写入字节的最后一位了。
}
else
{
gpio_set_level(dio,0);
}
Onebyte = Onebyte>>1; // 将写入的字节右移一位。于是刚刚判断的那位溢出,
//倒数第二位变成了最后一位。高位补0。
esp_rom_delay_us(1); //延时为时钟周期低电平的宽度
gpio_set_level(clk,1); //时钟拉高,TM1637开始读取dio上的电平
esp_rom_delay_us(1); //延时为时钟周期高电平的宽度
gpio_set_level(clk,0); //时钟拉低,ESP32着手准备下一次的数据或循环结束
}
}
确保写一个字节前后都将clk置低电平(即gpio_set_level(clk,0)语句),这样能得到由0开始和结束、有头有尾的八个时钟“凸起”波形。
3.4 等待应答
//IIC等待应答
void IIC_Ack_Wait()
{
gpio_set_direction(dio,GPIO_MODE_INPUT); //将dio设为输入模式
gpio_set_pull_mode(dio,GPIO_PULLUP_ONLY);//将dio设为上拉
esp_rom_delay_us(5); //延时,等待dio稳定
int dio_ack_level = gpio_get_level(dio); //获取dio电平
if(dio_ack_level == 1) //开始判断
{
while(1)
{
ESP_LOGI("IIC","ACK FAILED"); //打印输出失败信息
vTaskDelay(pdMS_TO_TICKS(1000)); //延时1ms
}
}
gpio_set_level(clk,1);
esp_rom_delay_us(1);
gpio_set_level(clk,0); //制造第九个下降沿,
//TM1637释放dio线。
gpio_set_direction(dio,GPIO_MODE_OUTPUT);//使dio回到输出模式
}
TM1637发出的应答信号是将dio拉低,所以将esp32的dio引脚设为上拉输入模式。因此,如果没有应答信号,dio将一直是高电平。当判断dio是高电平时,进入每秒打印一个信息的死循环,下图是程序执行结果,程序执行时没有给数码管模块供电,模拟意外断开的情况。上面的黄色曲线是clk,下面的蓝色曲线是dio。
在实验时发现一些程序头几次执行,尤其是第一次执行时,花费的时间显著长。比如这里的gpio_set_direction(),见第八个clk下降沿到dio被拉高时的距离,达到100μs左右。原因未知。
你也可以在这写其它程序,比如直接发出停止信号,让这次IIC通讯结束。
值得一提的是,如果没有在应答函数里将esp32的dio设置为输入模式,仍然输出高电平,数码管模块依然能工作。因为在应答时,TM1637的程序是在写字节结束后(clk第八个下降沿时)无条件拉低dio,并且确实可以拉低到“低电平”的程度,见下图中两个高电平里凹下去的部分。但是在这个时间内,两个引脚都用作输出连在一起,不是良好的工作状态。类似的情况也可以在3.2的图1中看到,当gpio_set_derection()还没来得及将dio设为输入时,电平不能被彻底拉低。
3.5 发出结束信号
void IIC_Stop()
{
gpio_set_level(clk,0);
gpio_set_level(dio,0);
gpio_set_level(clk,1);
esp_rom_delay_us(1);
gpio_set_level(dio,1);
esp_rom_delay_us(2);
}
我的程序里IIC_Start(),IIC_Stop()尾部有2μs延时,是为衔接留出时间;IIC_Write_1_Byte()头尾没有延时,IIC_Ack_Wait()头部有为等待dio稳定下来的5μs延时。所以,在3.1列出的彩色流程中,只有等待应答流程和下一个流程之间没有延时,就像上面的图2那样:窄些的第九个时钟、TM1637释放dio线后,立刻进入下一流程。稳妥一点的话可以在这里延时几个μs,等待gpio_set_derection()将dio设回输出完毕。
3.6 完整程序
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "unistd.h"
#include "esp_log.h"
#define dio GPIO_NUM_32 //D32为数据管脚
#define clk GPIO_NUM_19 //D19为时钟管脚
//GPIO初始化
gpio_config_t io_config =
{
.pin_bit_mask = (1ULL<<clk|1ULL<<dio), //配置同时应用与D4和D19
.pull_up_en = GPIO_PULLUP_DISABLE, //禁用上拉
.pull_down_en = GPIO_PULLDOWN_DISABLE, //禁用下拉
.mode = GPIO_MODE_OUTPUT, //输出
.intr_type = GPIO_INTR_DISABLE, //禁用中断
};
//IIC发出开始信号。在clk和dio都为高的情况下,将dio拉低的一刻,通信开始。
void IIC_Start()
{
gpio_set_level(clk,1);
esp_rom_delay_us(1);
gpio_set_level(dio,1);
esp_rom_delay_us(1); //延时1微秒
gpio_set_level(dio,0);
esp_rom_delay_us(2);
}
//IIC结束
void IIC_Stop()
{
gpio_set_level(clk,0);
gpio_set_level(dio,0);
gpio_set_level(clk,1);
esp_rom_delay_us(1);
gpio_set_level(dio,1);
esp_rom_delay_us(2);
}
//IIC应答
void IIC_Ack_Wait()
{
gpio_set_direction(dio,GPIO_MODE_INPUT); //将dio设为输入模式
gpio_set_pull_mode(dio,GPIO_PULLUP_ONLY);//将dio设为上拉
esp_rom_delay_us(5); //延时,等待dio稳定
int dio_ack_level = gpio_get_level(dio); //获取dio电平
if(dio_ack_level == 1) //开始判断
{
while(1)
{
ESP_LOGI("IIC","ACK FAILED"); //打印输出失败信息
vTaskDelay(pdMS_TO_TICKS(1000)); //延时1ms
}
}
gpio_set_level(clk,1);
esp_rom_delay_us(1);
gpio_set_level(clk,0); //制造第九个下降沿,TM1637释放dio线。
gpio_set_direction(dio,GPIO_MODE_OUTPUT);//使dio回到输出模式
}
//IIC写一个字节
void IIC_Write_1_Byte(unsigned char Onebyte) //写入为8bit字节
{
gpio_set_level(clk,0); //确保时钟为低电平
for(int i=0;i<8;i++) //循环8次
{
if(Onebyte & 0x01) // 将写入的字节与0b0000_0001按位与操作。如果写入
{ //字节的最后一位是1,位与结果就是0b0000_0001,否则
gpio_set_level(dio,1); //是0b0000_0000。这样就能判断写入字节的最后一位了。
}
else
{
gpio_set_level(dio,0);
}
Onebyte = Onebyte>>1; // 将写入的字节右移一位。于是刚刚判断的那位溢出,
esp_rom_delay_us(1); //倒数第二位变成了最后一位。高位补0。
gpio_set_level(clk,1);
esp_rom_delay_us(1);
gpio_set_level(clk,0);
}
}
//-----------------------------------
//设置数据命令,本质上就是写一个固定字节
void command1(unsigned char Cmd)
{
IIC_Write_1_Byte(Cmd);
}
//设置地址命令,本质上就是写一个固定字节
void command2(unsigned char Adress)
{
IIC_Write_1_Byte(Adress);
}
//传输数据,即写一个字节
void data(unsigned char Data)
{
IIC_Write_1_Byte(Data);
}
//显示控制命令,本质上就是写一个固定字节
void command3(unsigned char Light)
{
IIC_Write_1_Byte(Light);
}
//-------------------------------------
void app_main(void)
{
gpio_config(&io_config);
IIC_Start();
command1(0x44);
IIC_Ack_Wait();
IIC_Stop();
IIC_Start();
command2(0xc0);
IIC_Ack_Wait();
data(0x4F);
IIC_Ack_Wait();
IIC_Stop();
IIC_Start();
command2(0xc1);
IIC_Ack_Wait();
data(0x5B);
IIC_Ack_Wait();
IIC_Stop();
IIC_Start();
command2(0xc2);
IIC_Ack_Wait();
data(0x3f);
IIC_Ack_Wait();
IIC_Stop();
IIC_Start();
command2(0xc3);
IIC_Ack_Wait();
data(0x3f);
IIC_Ack_Wait();
IIC_Stop();
//显示结果是“3200”。
IIC_Start();
command3(0x88);
IIC_Ack_Wait();
IIC_Stop();
//设置亮度为最暗。
}
/*
显示数据(data)
0 0x3F 数据命令设置 (数据手册中的command1)
1 0x06 0x40 写数据 自动地址增加
2 0x5B 0x42 读键扫 自动地址增加
3 0x4F 0x44 写数据 固定地址
4 0x66 0x46 读键扫 固定地址
5 0x6D
6 0x7D 地址命令设置 (数据手册中的command2)
7 0x07 0xc0 - 至多0xc6 (最多控制6位数码管)
8 0x7F
9 0x6F 显示命令设置 (数据手册中的command3)
A 0x77 0x80 - 0x87 关闭显示,不亮
b 0x7C 0x88 - 0x8f 开启显示,且数字越大越亮
C 0x39
d 0x5E
E 0x79
F 0x71
*/
四、其它注意点
1、延时函数可以用usleep(),但点开后会发现它是一层包装,会做一次条件判断,短时间调用esp_rom_delay_us(),长时间调用vTaskDelay()。所以直接用esp_rom_delay_us()就行,能省下一步条件判断。测得esp_rom_delay_us(x)的实际延时约为x+0.7μs。
2、由于gpio_set_direction()第一次运行的时间很长,所以如果用示波器观测波形,时基要拉长一些。另外,gpio_set_pull_mode()里的第二个参数是GPIO_PULLUP_ONLY而不是结构体配置中的GPIO_PULLUP_ENABLE。
3、所谓“自动地址加一模式”,就是只需要设置一次首地址(command2),之后连续传输数据即可。而固定地址模式需要设置一次显示寄存器地址、传输一次数据。麻烦些,但控制自由度高。
4、如果数码管模块成功显示后,不切断模块与ESP32的通讯(clk和dio),只切断模块的电源再快速接上,数码管不显示。然而,切断通讯后,短时间切断模块电源再接上,模块仍然可以显示。这意味着,如果在控制显示程序执行完毕后切断通讯,数码管的抗干扰能力提高,能容忍短时间断电。