ESP32驱动四脚四位数码管模块(TM1637)

关键词: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节配合食用。

图1 传输一个字节的实际过程
//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设为输入时,电平不能被彻底拉低。

图2

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),只切断模块的电源再快速接上,数码管不显示。然而,切断通讯后,短时间切断模块电源再接上,模块仍然可以显示。这意味着,如果在控制显示程序执行完毕后切断通讯,数码管的抗干扰能力提高,能容忍短时间断电。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值