今天继续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);
}
}