8266+DS3231时钟之显示TM1638的使用【四】下

今天继续8266+DS3231时钟项目的显示部分功能的详解。这个时钟系列的前的四篇分别是:
《8266+DS3231时钟之显示TM1638的使用【四】上》
《8266+DS3231时钟之开发个时钟遇到的N个坑【一】》
《8266+ds3231时钟之arduino官网发布的DS3231库的分析【二】》
《8266+DS3231时钟之DS3231具体实现及代码【三】》
有兴趣的可以去看看,如果觉得对你有帮助,请点个赞。

一、8266与TM638的数据接口

在上一篇中, 我们完成了基于TM1638数码管显示电路的搭建,这篇重点详细分析TM1638对应的驱动实现和具体应用。8266NodeMCU控制TM1638芯片主要通过三个DigitalPin,分别连接TM638的STB、CLK、DIO三个脚,当然TM1638的VDD取5V和GND应与8266NodeMCU接在同一个VDD和GND上。这样算起来共5个连接与8266相连。具体看下图:

在这里插入图片描述

二、TM1638数据传输的定义再强调

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由于驱动程序 是严格按上面的时序要求设计的,所以如果不明白上面的时序是无法明白驱动的含义的。

三、驱动程序详解

驱动程序 是针对本设计的具体硬件架构设计的。如果你的硬件设计不一样,那么在理解了下面的案例 后也可以写出适合自已的驱动。
下面把驱动的完整程序展示出来。所有的函数的功能,作用和实现的细节都已在程序中进行了非常详细的注释。由于本驱动比较简单 ,因此直接定义在了tm1638.h里了,不是很规范,但实用就好。

驱动定义了一个TM1638类。所有的显示实现,初始化等都做为该 类的成员函数。
在定义对象时,需要传递8266上对应DIO,STB,CLK的pin值给对象。
void WriteData(unsigned char data);对应的是上图5的写数据定义,在阅读时要根据图5来理解。
unsigned char ReadData(void); 对应的是上图6的读数据定义。
基它的依次去对应,相信对驱动的理解就简单很多了。

/*
数据命令字(见TM1638数据手册)
B7  B6  B5  B4  B3  B2  B1  B0    字节     说明
0    1   0   0   0   0   1   0     42     读键扫数据
0    1   0   0   0   0   0   0     40     自动地址增加显示模式
0    1   0   0   0   1   0   0     44     指定地址显示模式
寄存器地址命令:基地址为C0 ,偏移地址为00H - 0FH 具体见数据手册图2和7.2 地址命令设置
显示控制命令: 88H ,89H ,8AH,8BH,8CH,8DH,8EH,8FH分别对应显示亮度为十六分之一,二,四,十,十一,十二,十三,十四 。 80H显示关,
*/

#include <Arduino.h>

class TM1638 {
    
    
    private:
        const byte DATA_COMMAND_WRITE_AUTO = 0x40;  //数据命令字:数据写到显示寄存器,自动地址增加。
        const byte DATA_COMMAND_WRITE_ADDR = 0x44; //数据命令字:数据写到固定地址
        const byte DATA_COMMAND_READ_KEY = 0x42;   //数据命令字:读键值
        const byte BASE_ADDR = 0XC0; //地址命令:设置地址到显示寄存器第一个地址0xC0。在此基础上的偏移地址为00H-0FH
        const byte DISP_COMMAND_HIGH_LIGHT = 0x8F; //显示控制:设开,并最亮
        const byte DISP_COMMAND_CLOSE = 0x80;       //关闭显示
        int CLK,DIO,STB;

    public:
        //亮度的八个级别
        unsigned char Light[8]={
    
    0x88,0x89,0x8A,0x8B,0x8C,0x8D,0x8E,0x8F};
         //共阴数码管显示代码 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F。
        unsigned char tab[16]={
    
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
        //共阴4位管,小时个位管的第8位即第b7位是秒的显示位。所以秒闪的程序要刷新小时个位管
         //例入,秒闪灯亮时,即B7位要置1,如原来0的显示码是3F,把B7位置1后就变成了B7,这样再刷新小时个位时秒灯就亮了
        unsigned char tab_second[16]={
    
    0xBF,0x86,0xDB,0xCF,0xE6,0xED,0xFD,0x87,0xFF,0xEF,0xF7,0xFC,0xB9,0xDE,0xF9,0xF1};
        TM1638(const int clk, const int dio , const int stb);  //构造函数,初始化显示
        void WriteCommand(unsigned char  command);  //向TM1638写入指令
        void WriteData(unsigned char data);   //向TM1638写入数据
        void WriteAddrData(unsigned char relative_addr, unsigned char data); //relative_addr相对于0xC0基地址的相对地址
        unsigned char ReadData(void);  //读取TM1638扫描键数据
        unsigned char ReadKey(void);   //直接返回键值
        bool SelfTest(void);           //TM1638及数码管自检函数
        void Disp8(const unsigned char message[]); //数码管显示刷新函数
        //void SecondBlink(int blink_flag);
        void SecondBlink(int blink_flag,int showtime5); //秒灯闪烁函数

};

//最基础的写数据函数。从每个data字节数的最低位B0写起,最后是B7位.由DIO脚输出数据时序。由每个CLK时钟完成有效输出。
void TM1638::WriteData(unsigned char data){
    
        
    unsigned char i;
    pinMode(DIO,OUTPUT);
    for (i=0;i<8;i++){
    
    
        digitalWrite(CLK,LOW);     //时钟置低
        if (data&0x01){
    
                 //判断data传送位是0或1
            digitalWrite(DIO,HIGH);  //如果是1,转化成DIO脚输出高电平
        }else {
    
    
            digitalWrite(DIO,LOW);    //如果是0,转化成DIO脚输出低电平
        }
        data>>=1;
        digitalWrite(CLK,HIGH);  //时钟置高,,完成一个bit的传送
    }
}

//向TM1638传送命令字,命令字本身也是按数据传送,只是用STB脚的一次拉低拉高做为命令字的区分。
void TM1638::WriteCommand(unsigned char command){
    
    
    digitalWrite(STB,LOW );
    WriteData(command);
    digitalWrite(STB,HIGH);  //完成一次命令传送
}


//显示数据写入指定地址的寄存器。在一个STB的低到高时序中,先传送的数据为寄存器地址,再传送需写入该寄存器的显示数据
void TM1638::WriteAddrData(unsigned char relative_addr, unsigned char data){
    
    
    WriteCommand(DATA_COMMAND_WRITE_ADDR);
    digitalWrite(STB,LOW);    //=======开始传送
    WriteData(BASE_ADDR|relative_addr);   //指定地址模式下,发送地址0xc0|相对地址。即基地址C0H+偏移量relative_addr
    WriteData(data);
    digitalWrite(STB,HIGH);  //===============因为固定地址模式下每次发送完地址和数据需要一个STB的上升沿区隔

};

//最基础的读数据函数,以时序模拟的方式实现。每次读到的是数据字节的最低位B0位,所以连续读时bit位要右移。
 unsigned char TM1638::ReadData(void){
    
    
    unsigned char i,result=0;
    pinMode(DIO,INPUT);

    for (i=0;i<8;i++){
    
       
        result>>=1;            //先压入一个0bit,由于是读到低位这节,所以要右移
        digitalWrite(CLK,LOW);   //CLK 拉低
        if (digitalRead(DIO)==HIGH)  //如果读到的是高电平
            result|=0x80;           //高电平,则把当前bit置一,否则默认置0.
        digitalWrite(CLK,HIGH);     //完成一个bit的读取
    }
    return result;
 };

//键值读取函数
byte TM1638::ReadKey(void){
    
    
    unsigned char c[4],i,key_value=0;
        digitalWrite(STB,LOW);   //=====================
	WriteData(DATA_COMMAND_READ_KEY);		           //发送读键数据 命令
	for(i=0;i<4;i++){
    
                 //开始读键值
        c[i]=ReadData();       //一次读八位,共读四个字节
    }
        digitalWrite(STB,HIGH);			//==================		          
//4个字节读键数据的每个bit含义如下。由于本程序电路只有8个键,都连接在K3脚上,因此只有8个键值
//程序内思路最终将四个字节合并成一个字节。,方便后继处理。
//  B7   B6[K1]   B5[k2]   B4[k3]   B3   B2[k1]   B1[k2]   B0[k3]   对应存储数组
//        ks2      ks2      ks2           ks1      ks1      ks1      c[0]
//        ks4      ks4      ks4           ks3      ks3      ks3      c[1]
//        ks6      ks6      ks6           ks5      ks5      ks5      c[2]
//        ks8      ks8      ks8           ks7      ks7      ks7      c[3]
/连接K3的八键按下后,再通过移位相加后就变成一个字节/
//  ks8   ks6      ks4      ks2     ks7    ks5     ks3      ks1
//由于在硬件上电路板设计成如下的对应关系///
//  键8    键7      键6      键5     键4     键3     键2      键1
//所以最终下面 if((0x01<<i)==key_value)比较下来以的,i值就是第几个键值被按到
	for(i=0;i<4;i++){
    
    
        key_value|=c[i]<<i;  //连接K3的八键按下后,再通过移位相加后就变成一个字节如下/
                             //  B7    B6       B5       B4      B3    B2       B1       B0
                             //  ks8   ks6      ks4      ks2     ks7    ks5     ks3      ks1
    }	

    for(i=0;i<8;i++)        {
    
    
        if((0x01<<i)==key_value)   //第i位键值位Bit为1,则取得键值为i+1
        break;
    }
	return i+1;   //键值为1,2,,,8

};

//对象初始化
TM1638::TM1638(const int clk, const int dio , const int stb){
    
    
    unsigned char i;
    CLK=clk;
    DIO=dio;
    STB=stb;
    pinMode(clk,OUTPUT);
    pinMode(dio,OUTPUT);
    pinMode(stb,OUTPUT);
    //显示一次
    WriteCommand(DISP_COMMAND_HIGH_LIGHT);//亮度最亮
    WriteCommand(DATA_COMMAND_WRITE_AUTO); //采用自动地址加1显示模式
    digitalWrite(STB,LOW); //写地址和写数据时STB要保持 LOW
    WriteData(BASE_ADDR);   //设置基地址
    for (i=0;i<16;i++){
    
    
        WriteData(0x00);
    }
    digitalWrite(STB,HIGH);
    //       
};

//自检程序
bool TM1638:: SelfTest(void){
    
               //自增地址显示模式
    //
    for (int j=0;j<16;j++){
    
    
        if (j<8) {
    
                              //调亮度控制。先暗到亮再亮到暗
            WriteCommand(Light[j]);
        }else{
    
     
            WriteCommand(Light[15-j]);
        }
        WriteCommand(DATA_COMMAND_WRITE_AUTO);  //自增地址显示模式

        digitalWrite(STB,LOW);  //=============stb low 

        WriteData(BASE_ADDR);    
        for (int i=0;i<16;i++){
    
       //显示0123456789ABCDEF
            WriteData(tab[j]);
        }

        digitalWrite(STB,HIGH);   //==============HIGH

        delay(300);
    }
};

//显示电路上的月日时分秒温度
//unsigned char message[10]={0,0,0,0,0,0,0,0,0,0};   该结构在主程序里定义
void TM1638::Disp8(const unsigned char message[]){
    
        //固定地址显示模式
    unsigned char  t1,t2,t3;
    //显示时间,message的前八位
    for (int i=0;i<8;i++){
    
           
        WriteAddrData(i*2,tab[message[i]]);
    }
    //显示温度,message的第九位和第十位
    t1=tab[message[8]];
    t2=tab[message[9]];
    for (int j=0;j<8;j++){
    
      //对应写入显示寄存器的大循环
        t3=0;
         //处理把行向表示的字符处理成列向显示的字符
        switch (t1&0x01){
    
    
            case 0:
                if (t2&0x01==1){
    
    
                    t3=t3|0x01; //先把t2的最低位压入b1位,对应SEG10位
                    t3=t3<<1;  //右移一位
                    t3=t3&0x02; //再把t1的最低位压入B0位,对应SEG9位                    
                }else{
    
    
                    t3=t3&0x00; //先把t2的最低位压入b1位,对应SEG10位
                    t3=t3<<1;  //右移一位
                    t3=t3&0x02; //再把t1的最低位压入B0位,对应SEG9位
                }
                break;
            case 1:
                if (t2&0x01==1){
    
    
                    t3=t3|0x01; //先把t2的最低位压入b1位,对应SEG10位
                    t3=t3<<1;  //右移一位
                    t3=t3|0x01; //再把t1的最低位压入B0位,对应SEG9位                    
                }else{
    
    
                    t3=t3&0x00; //先把t2的最低位压入b1位,对应SEG10位
                    t3=t3<<1;  //右移一位
                    t3=t3|0x01; //再把t1的最低位压入B0位,对应SEG9位 
                }
                break;
        }
        WriteAddrData(j*2+1,t3);
        t1=t1>>1; //整体降一位
        t2=t2>>1;
        
        
    }

};
//秒灯闪烁程序,需要配合主程序的设用,blink_flag是控制灯亮与灭的控制标志。
void TM1638::SecondBlink(int blink_flag,int showtime5){
    
    
     switch (blink_flag){
    
    
        case 0:
            
            WriteAddrData(10,tab[showtime5]);  //秒灯在地址0x0A            
           
            break;

        case 1: 
            
            //即B7位要置1,如原来0的显示码是3F,把B7位置1后就变成了B7,这样再显示时秒灯就亮了
            WriteAddrData(10,tab_second[showtime5]);
            break;
    } 
};

四、具体应用

在完成了TM638的驱动后,我们设计的时钟就有了显示模块。因此在主程序实现上就可以通过创建TM1638的对象来操作。以下程序为主程序 里的摘录的与TM1638设用相关的部分。如果读者在调试时注意根据自已的环境配置TM1638对象相应的各个参数。

#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <Blinker.h>
#include <tm1638.h>
#include <Wire.h>
#include <DS3231.h>
#include <ticker.h>

//pin don't in  0(D3),2(D4),6-11,
//时钟管脚定义
#define SCL 5    //d1      I2C-SCL
#define SDA 4    //d2       I2C-SDA   
#define INT_SQW 14   // D5    // 中断及方波1HZ

//TM1638管脚定义
#define STB 0  //D3  TM1638 STB 
#define CLK 12  // D6  TM638 DIO
#define DIO 13  // D7  TM1638 CLK
Ticker secondTicker;  
//Ticker o_CountDown; //倒计时对象,60秒计时
/// 系统变量/

unsigned char showtime[10]={
    
    0,0,0,0,0,0,0,0,0,0};   //the time  show on led  now.最后两位是温度
int SecondInterruptMode=0;//0显示时间,1显示配网提示。

DS3231 RTC;      //创建DT3231时钟
int secondFlag=0;  //秒闪标志用于控制闪灯是亮还是灭的标志
unsigned char keynum=0; //读到的键盘值

TM1638  TM1638(CLK,DIO,STB);   //建立TM638对象

enum Mode {
    
         //杖举型,用于判定按键一处于什么模式,使+-键进行对应作用
  VOLUME=1,
  MUSIC=2
  } ; 
Mode NowMode=VOLUME;

//resetup等等按键的中断处理程序
ICACHE_RAM_ATTR void keyProcessing(unsigned char &num){
    
    
  
  switch (num){
    
       //按键值为1-8时
    case 1:    
    //按键处理程序。。。。   
      break;
    case 2:    
    //按键处理程序。。。。  
       break;
    case  .....................................
    ........
    case 8:
    //按键处理程序。。。。  
      break; 
  }

}

///时间函数///

void GetTime(){
    
        //给显示变量赋值
     
      showtime[0]=RTClib::now().month()/10;     //月 
      showtime[1]=RTClib::now().month()%10;     
      showtime[2]=RTClib::now().day()/10;      //日
      showtime[3]=RTClib::now().day()%10;
      showtime[4]=RTClib::now().hour()/10;      //时
      showtime[5]=RTClib::now().hour()%10;
      showtime[6]=RTClib::now().minute()/10;    //分
      showtime[7]=RTClib::now().minute()%10;
      showtime[8]=int(RTC.getTemperature())/10;  //温度
      showtime[9]=int(RTC.getTemperature())%10; 
}

 ICACHE_RAM_ATTR void second_interrupt(int mode){
    
     //时钟每秒中断
  if (mode==0){
    
            //模式0 用于秒闪灯工作以及显示时间。模式1用于显示其它
     //秒灯闪
    switch (secondFlag){
    
    
      case 0:  //秒灯灭
        TM1638.SecondBlink(1,showtime[5]); //只重显示小时个位
        secondFlag=1;
        break;
      case 1:  //秒灯亮
        TM1638.SecondBlink(0,showtime[5]);
        secondFlag=0;
        break;
    }
    
    //显示时间
    if ( showtime[7]!=RTClib::now().minute()%10 ) {
    
       //只要比较,分不同就重新显示

      GetTime();
      TM1638.Disp8(showtime);
     
    }  
  }
}

主启动程序和主调用程序 

void setup()
{
    
    
  Serial.begin(BAUDRATE);   
 
  // 显示器初始化//
   TM1638.SelfTest();
 

   /设置闹钟中断输出设置秒闪功能///
   pinMode(INT_SQW,INPUT_PULLUP); 
   interrupts();
   attachInterrupt(digitalPinToInterrupt(INT_SQW),alarm_interrupt,FALLING);   //8266经过实践发现,只支持CHANGE RISING FALLING 
   secondTicker.attach(1,second_interrupt,SecondInterruptMode);//每秒触发一次带参数的中断程序

     
}

void loop()
{
    
    
  
    keynum=TM1638.ReadKey();
    if (keynum==9) keynum=0;
    while(keynum==TM1638.ReadKey());  //等待按键释放
    if ((keynum>0)&&(keynum<9)){
    
    
      keyProcessing(keynum);    
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_45499326/article/details/113363101