目录
系列文章目录
前言
之前做了一个WiFi定时器时钟,用八位数码管进行显示,但是定时器时钟的精度较低,需要频繁校时。
这次做一个LCD1602版本的WiFi时钟,同样通过ESP8266(01S)从网络获取时间,获取时间后,将时间写入DS1302时钟芯片,每一次成功获取网络时间后,会每隔24小时自动校时(长时间之后ESP8266模块可能会与网络断开连接,但是这不影响,如果校时超时20s,会发送指令重启ESP8266模块,重新连接WiFi和网络,并校时)。不校时的时候,通过DS1302时钟芯片读取时间。
有三个版本(都是用普中A2开发板):
①八位数据接口,汉字显示星期
②八位数据接口,滚动显示时分秒
③I2C通信四位数据接口,汉字显示星期
本文代码对应的是版本②。
三个版本用到的单片机都是:STC89C52RC。
用到的外设有:ESP8266(01S)、LCD1602、DS1302、独立按键。
效果查看/操作演示:B站搜索"甘腾胜"或"gantengsheng"查看。
源代码下载:B站对应视频的简介有工程文件下载链接。
一、效果展示
二、原理分析
1、如何获取网络时间
ESP8266(01S)模块的使用和串口通信,可以看一下我的另一篇博客:八位数码管WiFi定时器时钟
2、如何显示汉字
LCD1602显示汉字的原理,可以看一下我的另一篇博客:LCD1602多汉字动态扫描显示
这次只需要显示一个汉字,不需要扫描显示,简单很多,只需要用到6个自定义字符就行了。
3、滚动显示时间
隔一段时间向上移动一个像素就行了,变化一个数字需要移动8个像素(因为LCD1602每个区域是5*8的点阵),代码中是隔70ms移动一个像素,隔8*70ms=560ms完成一个数字的滚动,停顿一下,再等待进行下一次的滚动显示。
4、版本③的LCD1602的I2C通信
I2C的通信协议可以看一下其他博主的介绍,这里说明一下指令的问题。需要先发一个0x02的指令设置为四线模式。因为用了6T(双倍速)模式,相当于晶振翻倍了,相当于变成了22.1184MHz,I2C通信需要加延时才行了,不然会超过PCF8574T允许的最大通信速率,导致显示不正常。
三、各模块代码
1、延时
h文件
c
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
c文件
c
/**
* @brief 延时函数,延时xms毫秒
* @param xms 延时的时间,范围:0~65535
* @retval 无
*/
void Delay(unsigned int xms) //@11.0592MHz,6T(双倍速)模式
{
unsigned char i,j;
while(xms)
{
i=4;
j=146;
do
{
while(--j);
} while(--i);
xms--;
}
}
2、定时器0
h文件
c
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0_Init(void);
#endif
c文件
c
#include <REGX52.H>
/**
* @brief 定时器0初始化
* @param 无
* @retval 无
*/
void Timer0_Init(void)
{
TMOD&=0xF0; //设置定时器模式(高四位不变,低四位清零)
TMOD|=0x01; //设置定时器模式(通过低四位设为"定时器0工作方式1"的模式)
TL0=0x66; //设置定时初值,定时1ms,晶振@11.0592MHz
TH0=0xFC; //设置定时初值,定时1ms,晶振@11.0592MHz
TF0=0; //清除TF0标志
TR0=1; //定时器0开始计时
ET0=1; //打开定时器0中断允许
EA=1; //打开总中断
PT0=0; //当PT0=0时,定时器0为低优先级,当PT0=1时,定时器0为高优先级
}
/*定时器中断函数模板
void Timer0_Routine() interrupt 1 //定时器0中断函数
{
static unsigned int T0Count; //定义静态变量
TL0=0x66; //设置定时初值,定时1ms,晶振@11.0592MHz
TH0=0xFC; //设置定时初值,定时1ms,晶振@11.0592MHz
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
3、串口通信
h文件
c
#ifndef __UART_H__
#define __UART_H__
void UART_Init();
void UART_SendByte(unsigned char Byte);
void UART_SendString(char *String);
#endif
c文件
c
#include <REGX52.H>
/**
* @brief 串口初始化,115200bps@11.0592MHz(6T模式),误差:0.00%
* @param 无
* @retval 无
*/
void Uart_Init(void)
{
PCON|=0x80; //使能波特率倍速位SMOD,倍速后为115200bps
SCON =0x50; //8位数据,可变波特率
// AUXR&=0xBF; //定时器时钟12T模式(89C52芯片无需设置这个)
// AUXR&=0xFE; //串口1选择定时器1为波特率发生器(89C52芯片无需设置这个)
TMOD&=0x0F; //设置定时器模式
TMOD|=0x20; //设置定时器模式
TL1=0xFF; //设置定时初始值
TH1=0xFF; //设置定时重载值
ET1=0; //禁止定时器1中断
TR1=1; //定时器1开始计时
EA=1; //开启所有中断
ES=1; //开启串口中断
PS=1; //要设置串口中断的优先级比定时器的高,
//否则发送或接收数据的时候会被打断,影响数据发送和接收
}
/**
* @brief 串口发送一个字节数据
* @param Byte 要发送的一个字节数据
* @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
/**
* @brief 串口发送字符串
* @param String 要发送的字符串
* @retval 无
*/
void UART_SendString(char *String)
{
while(*String)
{
UART_SendByte(*String);
String++;
}
}
/*串口中断函数模板
void UART_Routine() interrupt 4
{
if(RI==1)
{
RI=0;
}
}
*/
4、DS1302
h文件
c
#ifndef __DS1302_H__
#define __DS1302_H__
//外部可调用的时间数组,索引0~6分别对应年、月、日、时、分、秒、星期
extern char DS1302_Time[];
void DS1302_Init(void);
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_SetTime(void);
void DS1302_ReadTime(void);
#endif
c文件
c
#include <REGX52.H>
//引脚定义
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
#define DS1302_WP 0x8E //写保护的地址
//DS1302写入时间的地址:年,月,日,时,分,秒,星期
unsigned char code DS1302_WriteAddress[7]={0x8c,0x88,0x86,0x84,0x82,0x80,0x8a,};
//DS1302读取时间的地址:年,月,日,时,分,秒,星期
unsigned char code DS1302_ReadAddress[7]={0x8d,0x89,0x87,0x85,0x83,0x81,0x8b,};
//时间数组:年,月,日,时,分,秒,星期
char DS1302_Time[]={25,1,10,18,12,53,5}; //时间的初始值
/**
* @brief DS1302初始化
* @param 无
* @retval 无
*/
void DS1302_Init(void)
{
DS1302_CE=0;
DS1302_SCLK=0;
}
/**
* @brief DS1302写一个字节
* @param Command 命令字/地址
* @param Data 要写入的数据
* @retval 无
*/
void DS1302_WriteByte(unsigned char Command,Data)
{
unsigned char i;
DS1302_CE=1;
for(i=0;i<8;i++) //循环8次,每次写1位,先写低位再写高位
{
DS1302_IO=Command&(0x01<<i);
DS1302_SCLK=1; //SCLK置1后立即置0,该时序操作需考虑时钟芯片是否可承受这个时钟的最快频率
DS1302_SCLK=0; //由于单片机没有这么快的频率,故可不加延时
}
for(i=0;i<8;i++)
{
DS1302_IO=Data&(0x01<<i);
DS1302_SCLK=1; //CLK由低到高产生一个上升沿,从而写入数据
DS1302_SCLK=0;
}
DS1302_CE=0;
}
/**
* @brief DS1302读一个字节
* @param Command 命令字/地址
* @retval Data 读出的数据
*/
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
for(i=0;i<8;i++)
{
DS1302_SCLK=1;
DS1302_SCLK=0; //要先1后0,否则全都是65
if(DS1302_IO){Data|=(0x01<<i);}
}
DS1302_CE=0;
DS1302_IO=0; //读取后将IO设置为0,否则读出的数据会出错
return Data;
}
/**
* @brief DS1302设置时间,调用之后,DS1302_Time数组的数字会被设置到DS1302中
* @param 无
* @retval 无
*/
void DS1302_SetTime(void)
{
unsigned char i;
DS1302_WriteByte(DS1302_WP,0x00); //设置前关闭写保护
for(i=0;i<7;i++) //依次写入:年,月,日,时,分,秒,星期
{
DS1302_WriteByte(DS1302_WriteAddress[i],DS1302_Time[i]/10*16+DS1302_Time[i]%10); //十进制转换为BCD码
}
DS1302_WriteByte(DS1302_WP,0x80); //设置后开启写保护
}
/**
* @brief DS1302读取时间,调用之后,DS1302中的数据会被读取到DS1302_Time数组中
* @param 无
* @retval 无
*/
void DS1302_ReadTime(void)
{
unsigned char Temp,i;
for(i=0;i<7;i++) //依次读取:年,月,日,时,分,秒,星期
{
Temp=DS1302_ReadByte(DS1302_ReadAddress[i]);
DS1302_Time[i]=Temp/16*10+Temp%16;//BCD码转换为十进制
}
}
5、LCD1602
h文件
c
#ifndef __LCD1602_H__
#define __LCD1602_H__
void LCD_WriteCommand(unsigned char Command);
void LCD_WriteData(unsigned char Data);
void LCD_SetCursor(unsigned char Line,unsigned char Column);
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_Clear(void);
void LCD_MoveLeft(void);
void LCD_MoveRight(void);
void LCD_ScrollNum(unsigned char Line,unsigned char Column,unsigned char Order,unsigned char Number,unsigned char Quantity,char Offset);
#endif
c文件
c
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//阴码(亮点为1),横向取模,高位在左
unsigned char code NumberTable[]={ //5*7数字字模(低5位)
0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00, //0
0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00, //1
0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00, //2
0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00, //3
0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00, //4
0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00, //5
0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00, //6
0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00, //7
0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00, //8
0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00, //9
};
//函数定义:
/**
* @brief LCD1602私有延时函数,11.0592MHz(6T)调用可延时40us
* @param 无
* @retval 无
*/
void LCD_Delay40us(void)
{
unsigned char i;
i=34;
while(--i);
}
/**
* @brief LCD1602延时函数,11.0592MHz(6T)调用可延时2ms
* @param 无
* @retval 无
*/
void LCD_Delay2ms(void)
{
unsigned char i, j;
i=8;
j=40;
do
{
while(--j);
}while(--i);
}
/**
* @brief LCD1602写指令
* @param Command 要写入的指令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay40us();
LCD_EN=0;
LCD_Delay40us();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay40us();
LCD_EN=0;
LCD_Delay40us();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38); //八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0C); //显示开,光标关,闪烁关
LCD_WriteCommand(0x06); //数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01); //光标复位,清屏
LCD_Delay2ms(); //清屏指令执行需要较长时间,需要较长的延时
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
/**
* @brief LCD1602的光标复位,清屏
* @param 无
* @retval 无
*/
void LCD_Clear(void)
{
LCD_WriteCommand(0x01);
LCD_Delay2ms();
}
/**
* @brief LCD1602的屏幕向左移动一个字符位,光标不动
* @param 无
* @retval 无
*/
void LCD_MoveLeft(void)
{
LCD_WriteCommand(0x18);
}
/**
* @brief LCD1602的屏幕向左移动一个字符位,光标不动
* @param 无
* @retval 无
*/
void LCD_MoveRight(void)
{
LCD_WriteCommand(0x1C);
}
/**
* @brief LCD1602向上滚动显示数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Order 所用到的CGRAM的自定义字符的序号,范围:0~7
* @param Number 要显示的数字,范围:0~9
* @param Quantity 这一位置所显示数字的总数量,例如,秒的个位,可以显示0~9这十个数字,显示的数字的总数量为10
* @param Offset 滚动显示的偏移量,范围:-7~0
* @retval 无
*/
void LCD_ScrollNum(unsigned char Line,unsigned char Column,unsigned char Order,unsigned char Number,unsigned char Quantity,char Offset)
{
unsigned char i,j,k;
LCD_SetCursor(Line,Column);
LCD_WriteData(Order);
k=8*Quantity;
j=(8*Number+Offset+k)%k;
LCD_WriteCommand(0x40+8*Order);
for(i=0;i<8;i++)
{
LCD_WriteData(NumberTable[(j+i)%k]);
}
}
6、独立按键
h文件
c
#ifndef __KEYSCAN_H__
#define __KEYSCAN_H__
unsigned char Key(void);
void Key_Tick(void);
#endif
c文件
c
#include <REGX52.H>
sbit Key1=P3^1;
sbit Key2=P3^0;
sbit Key3=P3^2;
sbit Key4=P3^3;
unsigned char KeyNumber;
/**
* @brief 获取独立按键键码
* @param 无
* @retval 按下按键的键码,范围:0,1~12,0表示无按键按下
*/
unsigned char Key(void)
{
unsigned char KeyTemp=0;
KeyTemp=KeyNumber;
KeyNumber=0; //主程序中获取键码值之后键码值清零,在下一次定时器扫描按键之前再次获取键码值,一定会返回0
return KeyTemp;
}
/**
* @brief 获取当前按键的状态,无消抖及松手检测
* @param 无
* @retval 按下的按键,范围:0~4,无按键按下时返回值为0
*/
unsigned char Key_GetState()
{
unsigned char KeyValue=0;
if(Key1==0){KeyValue=1;}
if(Key2==0){KeyValue=2;}
if(Key3==0){KeyValue=3;}
if(Key4==0){KeyValue=4;}
return KeyValue;
}
/**
* @brief 按键驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Key_Tick(void)
{
static unsigned char NowState,LastState;
static unsigned int KeyCount;
LastState=NowState; //按键状态更新
NowState=Key_GetState(); //获取当前按键状态
//如果上个时间点按键未按下,这个时间点按键按下,则是按下瞬间
if(LastState==0)
{
switch(NowState)
{
case 1:KeyNumber=1;break;
case 2:KeyNumber=2;break;
case 3:KeyNumber=3;break;
case 4:KeyNumber=4;break;
default:break;
}
}
//如果上个时间点按键按下,这个时间点按键按下,则是一直按住按键
if(LastState && NowState)
{
KeyCount++;
if(KeyCount>=10) //按下超过200ms才被检测为长按(定时器中断函数中每隔20ms检测一次按键)
{
if(LastState==1 && NowState==1){KeyNumber=5;}
if(LastState==2 && NowState==2){KeyNumber=6;}
if(LastState==3 && NowState==3){KeyNumber=7;}
if(LastState==4 && NowState==4){KeyNumber=8;}
}
}
else
{
KeyCount=0;
}
//如果上个时间点按键按下,这个时间点按键未按下,则是松手瞬间
if(NowState==0)
{
switch(LastState)
{
case 1:KeyNumber=9;break;
case 2:KeyNumber=10;break;
case 3:KeyNumber=11;break;
case 4:KeyNumber=12;break;
default:break;
}
}
}
四、主函数
main.c
c
/*
by甘腾胜@20250125
效果展示:可以在B站搜索"甘腾胜"或"gantengsheng"查看
单片机:STC89C52RC
晶振:6T@11.0592MHz
波特率:115200bps
外设:ESP8266(01S)模块、LCD1602、DS1302、独立按键
注意:
(1)ESP8266供电电压为3.3V,接5V会发热严重,RX和TX要交叉连接
(2)此版本不用更改ESP8266模块的默认波特率115200bps,但下载的时候需要勾选"使能6T(双倍速)模式"
(3)无需设置ESP8266模块默认的WiFi模式(AP模式),超时20s,运行时程序会自动设置为STA模式
(4)每隔24h会自动联网校时(如果校时超时(20s),会重启ESP8266模块,重新连接WiFi和网络并校时)
(5)串口中断的优先级要比定时器0的高,否则会影响通信
(6)代码末尾有月份、星期的英文,以及网站返回的时间数据的样例
强调1:ESP8266模块供电电压为3.3V,不能用5V
强调2:下载的时候需要勾选"使能6T(双倍速)模式"
操作说明:
K1 K2 K3 K4
【K4】手动联网校时
*/
#include <REGX52.H> //包含头文件
#include "Delay.h"
#include "UART.h"
#include "Timer0.h"
#include "KeyScan.h"
#include "DS1302.h"
#include "LCD1602.h"
//预设三个WiFi账号,如果连接不上,超时20s会连接下一个,连接WiFi成功(账号和密码保存到了Flash)后,下次上电自动连接
/*例:(设置第一个预设账号)
如果WiFi账号是:abc
如果WiFi密码是:12345678
则应改成:char code WiFi1[]="AT+CWJAP=\"abc\",\"12345678\"\r\n";
*/
char code WiFi1[]="AT+CWJAP=\"ganpan\",\"01234567\"\r\n"; //发送的字符串中如果有双引号,需要用反斜杠转义
char code WiFi2[]="AT+CWJAP=\"wulou\",\"199019911992\"\r\n";
char code WiFi3[]="AT+CWJAP=\"GTS\",\"01234567\"\r\n";
unsigned char KeyNum; //存储获得的键码值
char Judge[5]; //用来判断是不是我们想要保存的字符串
char TimeBuffer[25]; //用来存储接收到的时间的字符型的信息
bit OKFlag=0; //接收到了ESP8266返回的OK文本的标志,1:接收到了,0:未接收到
bit ReadyFlag=0; //ESP8266准备好了的标志,1:准备好了,0:未准备好
bit WiFiGotIPFlag=0; //ESP8266连接WiFi且获取了IP的标志,1:已获取IP,0:未获取IP
bit WiFiDisconnectFlag=0; //未能连接WiFi的标志,1:未能连接,0:无
bit GetTimeFlag=0; //从网络获取时间的标志,1:获取,0:不获取
bit GotTimeFlag=0; //从网络获取了时间的标志,1:已获取,0:未获取到
char Time[7]; //存储接收到的字符型的时间数据转化之后的十进制数据,索引0~6分别对应年、月、日、时、分、秒、星期
bit ShowOKFlag; //成功获取网络时间后显示"OK"的标志,1:显示,0:不显示
unsigned int T0Count1,T0Count2,T0Count3,T0Count4,T0Count5,T0Count6; //定时器计数的变量
unsigned int ProofTimeCount; //定时器中隔一段时间自动校时的计数
bit TimeOutFlag; //连接WiFi超时的标志,1:超时,2:未超时
bit TimeOutCountFlag=1; //启动超时计数的标志,1:启动,2:不启动
bit ReadTimeFlag=1; //从DS1302时钟芯片读取时间的标志,1:读取,2:不读取
char Offset1,Offset2,Offset3,Offset4,Offset5,Offset6; //数字向上滚动的偏移量
//时十位,时个位,分十位,分个位,秒十位,秒个位
unsigned char LastHour_10,LastHour_1,LastMinute_10,LastMinute_1,LastSecond_10,LastSecond_1;
bit ShowTimeFlag; //显示时间的标志,1:显示,2:不显示(用来控制上电获取到网络时间后再显示时间)
/**
* @brief ESP8266初始化
* @param 无
* @retval 无
*/
void ESP8266_Init(void)
{
LCD_Clear(); //LCD清屏
LCD_ShowString(1,1,"ESP8266"); //第一行显示"ESP8266",表示等待ESP8266准备好
Delay(100); //适当延时,延时0.1s
//退出透传模式(如果ESP8266不断电,只让单片机复位的话,ESP8266就还处于透传模式,发送AT指令会无效)
UART_SendString("+++");
Delay(1000); //退出透传模式要1s之后才能发AT指令
if(ReadyFlag) //如果上电直接返回"ready"
{
ReadyFlag=0;
LCD_ShowString(2,1,"ready"); //LCD第二行显示"ready",表示ESP8266已准备好
Delay(500);
}
else //如果不返回"ready",则重启一下ESP8266模块
{
UART_SendString("AT+RST\r\n"); //复位
while(!ReadyFlag); //等待ESP8266返回"ready"
ReadyFlag=0;
LCD_ShowString(2,1,"ready");
Delay(500);
}
LCD_Clear();
LCD_ShowString(1,1,"WIFI"); //LCD第一行显示"WIFI",表示等待ESP8266连接WiFi
//WiFi账号密码保存在ESP8266的Flash中,掉电不丢失
//如果上次已经成功连接,则上电自动按上次的网络名称和密码连接
T0Count3=0; //超时的计数清零
TimeOutFlag=0; //超时的标志清零
while(!WiFiGotIPFlag && !WiFiDisconnectFlag && !TimeOutFlag);
if(TimeOutFlag) //如果是因为超时退出上面的while循环,说明ESP8266处于AP模式
{
UART_SendString("AT+CWMODE=1\r\n"); //发送AT指令设置为STA(Station)模式
while(!OKFlag); //等待ESP8266返回"OK"
OKFlag=0;
}
if(WiFiGotIPFlag) //如果成功获取了IP
{
WiFiGotIPFlag=0;
LCD_ShowString(2,1,"GOT IP"); //表示ESP8266已连接WiFi,并获取了IP
Delay(500);
}
else //如果WiFi不能连接
{ //WiFi账号密码是保存到ESP8266的flash里的,如果在else中连接成功,
//下次上电就可以直接连接WiFi并获得IP,就不会进入此else中
WiFiDisconnectFlag=0;
LCD_ShowString(2,1,"DISCONNECT"); //表示ESP8266不能连接WiFi
Delay(500);
LCD_ShowString(2,1,"CONNECTING1"); //表示ESP8266正在连接第一个预设的WiFi账号
T0Count3=0; //超时的计数清零
TimeOutFlag=0; //超时的标志清零
//如果WiFi连接不成功,就按下面的账号密码进行连接
UART_SendString(WiFi1);
while(!OKFlag && !WiFiGotIPFlag && !TimeOutFlag);
OKFlag=0;
WiFiGotIPFlag=0;
if(TimeOutFlag) //如果是因为超时退出上面的while循环,说明第一个预设的WiFi账号没连上
{
LCD_ShowString(2,1,"CONNECTING2"); //表示ESP8266正在连接第二个预设的WiFi账号
T0Count3=0; //超时的计数清零
TimeOutFlag=0; //超时的标志清零
//如果上面的WiFi连接不成功,超时(20s)了,就会尝试下面的账号密码进行连接
//超时的时间不能少于15s,否则会导致连接不成功
//如果上面的WiFi连接成功,就不会连接下面的WiFi账号
UART_SendString(WiFi2);
while(!OKFlag && !WiFiGotIPFlag && !TimeOutFlag);
OKFlag=0;
WiFiGotIPFlag=0;
if(TimeOutFlag) //如果是因为超时退出上面的while循环,说明第二个预设的WiFi账号没连上
{
LCD_ShowString(2,1,"CONNECTING3"); //第二位数码管显示"3",表示ESP8266正在连接第三个预设的WiFi账号
//如果上面的WiFi连接不成功,超时(20s)了,就会尝试下面的账号密码进行连接
//如果上面的WiFi连接成功,就不会连接下面的WiFi账号
UART_SendString(WiFi3);
while(!OKFlag && !WiFiGotIPFlag); //如果第三个WiFi账号连接不上,就会在此处陷入死循环
OKFlag=0;
WiFiGotIPFlag=0;
}
}
LCD_ShowString(2,1," ");
LCD_ShowString(2,1,"GOT IP");
//下面的处理不能少,如果是第一次连上WiFi,还会多返回一个"OK"(暂时未知原因,有大佬知道原因的,请私信告知我一下,谢谢!)
//如果不处理,会导致第一次连上WiFi后程序卡在数码管显示"5"
Delay(1500); //延时1.5s
OKFlag=0; //延时不能少于1s,要等ESP8266返回OK令OKFlag置1之后,再将OKFlag置零
}
LCD_Clear();
LCD_ShowString(1,1,"CIPSTART"); //表示ESP8266开始建立TCP连接
UART_SendString("AT+CIPSTART=\"TCP\",\"www.beijing-time.org\",80\r\n"); //建立 TCP 连接
while(!OKFlag);
OKFlag=0;
LCD_ShowString(2,1,"CONNECT OK"); //表示ESP8266已建立TCP连接
Delay(500);
LCD_Clear();
LCD_ShowString(1,1,"CIPMODE=1"); //表示ESP8266开始设置传输模式
UART_SendString("AT+CIPMODE=1\r\n"); //设置传输模式(0为普通模式,1为透传模式)
while(!OKFlag);
OKFlag=0;
LCD_ShowString(2,1,"OK"); //表示ESP8266已经设置传输模式为透传模式
Delay(500);
LCD_Clear();
LCD_ShowString(1,1,"CIPSEND"); //表示ESP8266开始发送数据
//开始发送数据(在透传模式时),当输入单独一包 +++ 时,返回普通 AT 指令模式,要至少间隔 1 秒再发下一条 AT 指令
UART_SendString("AT+CIPSEND\r\n");
while(!OKFlag );
OKFlag=0;
LCD_ShowString(2,1,"OK"); //表示ESP8266已经准备好了,可以发送数据了
Delay(500);
}
/**
* @brief 将接收到的时间数据(字符型)转换为十进制的数据,保存到时间数组Time中
* @param 无
* @retval 无
*/
void ConvertTime(void)
{
Time[0]=(TimeBuffer[14]-'0')*10+(TimeBuffer[15]-'0'); //年
if(TimeBuffer[8]=='J' && TimeBuffer[9]=='a'){Time[1]=1;} //月
else if(TimeBuffer[8]=='F'){Time[1]=2;}
else if(TimeBuffer[8]=='M' && TimeBuffer[9]=='a' && TimeBuffer[10]=='r'){Time[1]=3;}
else if(TimeBuffer[8]=='A' && TimeBuffer[9]=='p'){Time[1]=4;}
else if(TimeBuffer[8]=='M' && TimeBuffer[9]=='a' && TimeBuffer[10]=='y'){Time[1]=5;}
else if(TimeBuffer[8]=='J' && TimeBuffer[9]=='u' && TimeBuffer[10]=='n'){Time[1]=6;}
else if(TimeBuffer[8]=='J' && TimeBuffer[9]=='u' && TimeBuffer[10]=='l'){Time[1]=7;}
else if(TimeBuffer[8]=='A' && TimeBuffer[9]=='u'){Time[1]=8;}
else if(TimeBuffer[8]=='S'){Time[1]=9;}
else if(TimeBuffer[8]=='O'){Time[1]=10;}
else if(TimeBuffer[8]=='N'){Time[1]=11;}
else if(TimeBuffer[8]=='D'){Time[1]=12;}
Time[2]=(TimeBuffer[5]-'0')*10+(TimeBuffer[6]-'0'); //日
Time[3]=(TimeBuffer[17]-'0')*10+(TimeBuffer[18]-'0'); //时
Time[4]=(TimeBuffer[20]-'0')*10+(TimeBuffer[21]-'0'); //分
Time[5]=(TimeBuffer[23]-'0')*10+(TimeBuffer[24]-'0'); //秒
if(TimeBuffer[0]=='M'){Time[6]=1;} //星期
else if(TimeBuffer[0]=='T' && TimeBuffer[1]=='u'){Time[6]=2;}
else if(TimeBuffer[0]=='W'){Time[6]=3;}
else if(TimeBuffer[0]=='T' && TimeBuffer[1]=='h'){Time[6]=4;}
else if(TimeBuffer[0]=='F'){Time[6]=5;}
else if(TimeBuffer[0]=='S' && TimeBuffer[1]=='a'){Time[6]=6;}
else if(TimeBuffer[0]=='S' && TimeBuffer[1]=='u'){Time[6]=7;}
//返回的是GMT,北京时间比格林威治时间(Greenwich Mean Time简称GMT)早8小时。
Time[3]+=8; //UTC/GMT +8.00 (东八区)
if(Time[3]/24) //如果加8小时后是第二天
{
Time[3]%=24;
Time[6]++; //星期增加
if(Time[6]>7){Time[6]=1;}
Time[2]++;
if(Time[2]>=32) //大月
{
Time[2]=1;
Time[1]++;
if(Time[1]>12)
{
Time[1]=1;
Time[0]++;
Time[0]%=100;
}
}
else if(Time[2]==31) //小月
{
if(Time[1]==4 || Time[1]==6 || Time[1]==9 || Time[1]==11)
{
Time[2]=1;
Time[1]++;
}
}
else if(Time[2]==30) //闰年二月
{
if(Time[1]==2 && Time[0]%4==0)
{
Time[2]=1;
Time[1]++;
}
}
else if(Time[2]==29) //平年二月
{
if(Time[1]==2 && Time[0]%4)
{
Time[2]=1;
Time[1]++;
}
}
}
/*由于网络延迟、数据的处理等,导致处理后的时间慢一两秒,这里进行补偿,加多2秒*/
if(Time[3]<23 || Time[4]<59 || Time[5]<58) //如果加多2秒不会跳到第二天
{
Time[5]+=2;
if(Time[5]>=60)
{
Time[5]%=60;
Time[4]++;
if(Time[4]>=60)
{
Time[4]%=60;
Time[3]++;
}
}
}
}
/**
* @brief 更新显示时间
* @param 无
* @retval 无
*/
void ShowTime(void)
{
LCD_ShowChar(1,8,'-');
LCD_ShowChar(1,11,'-');
LCD_ShowChar(2,6,':');
LCD_ShowChar(2,9,':');
LCD_ShowString(1,1," ");
LCD_ShowString(2,1," ");
LCD_ShowChar(1,4,'2');
LCD_ShowChar(1,5,'0');
LCD_ShowNum(1,6,DS1302_Time[0],2); //年
LCD_ShowNum(1,9,DS1302_Time[1],2); //月
LCD_ShowNum(1,12,DS1302_Time[2],2); //日
LCD_ShowNum(2,13,DS1302_Time[6],1); //星期
LCD_ScrollNum(2,4,0,DS1302_Time[3]/10,3,Offset1); //时(十位)
if(DS1302_Time[3]/10==0)
{
LCD_ScrollNum(2,5,1,DS1302_Time[3]%10,4,Offset2); //时(个位)
}
else
{
LCD_ScrollNum(2,5,1,DS1302_Time[3]%10,10,Offset2); //时(个位)
}
LCD_ScrollNum(2,7,2,DS1302_Time[4]/10,6,Offset3); //分(十位)
LCD_ScrollNum(2,8,3,DS1302_Time[4]%10,10,Offset4); //分(个位)
LCD_ScrollNum(2,10,4,DS1302_Time[5]/10,6,Offset5); //秒(十位)
LCD_ScrollNum(2,11,5,DS1302_Time[5]%10,10,Offset6); //秒(个位)
if(ShowOKFlag)
{
LCD_ShowChar(1,16,'O');
LCD_ShowChar(2,16,'K');
}
else
{
LCD_ShowChar(1,16,20); //无显示
LCD_ShowChar(2,16,20); //无显示
}
}
void main()
{
unsigned char i;
P2_5=0; //防止开发板的蜂鸣器发声
LCD_Init(); //LCD1602初始化
Timer0_Init(); //定时器0初始化
DS1302_Init(); //DS1302初始化
UART_Init(); //串口初始化
ESP8266_Init(); //ESP8266初始化
WiFiGotIPFlag=0; //ESP8266初始化的时候,回显信息会让WiFiGotIPFlag置1
TimeOutCountFlag=0; //TimeOutCountFlag置0,不进行超时的计时
TimeOutFlag=0; //超时标志清零
LCD_Clear();
GetTimeFlag=1; //上电获取一次网络时间
while(1)
{
KeyNum=Key(); //获取键码值
if(KeyNum) //如果有按键按下
{
if(KeyNum==12) //如果按下K4(松手瞬间)
{
GetTimeFlag=1; //手动校时
}
}
if(WiFiGotIPFlag) //如果不小心触碰到ESP8266模块,导致接触不良而重启模块的话,获取IP后重新连接网络
{
TimeOutCountFlag=1; //启动超时的计时(防止出错卡在while循环)
LCD_Clear();
LCD_ShowString(1,1,"CIPSTART"); //表示ESP8266开始建立TCP连接
UART_SendString("AT+CIPSTART=\"TCP\",\"www.beijing-time.org\",80\r\n"); //建立 TCP 连接
while(!OKFlag && !TimeOutFlag);
OKFlag=0;
TimeOutFlag=0;
LCD_ShowString(2,1,"CONNECT OK"); //表示ESP8266已建立TCP连接
Delay(500);
LCD_Clear();
LCD_ShowString(1,1,"CIPMODE=1"); //表示ESP8266开始设置传输模式
UART_SendString("AT+CIPMODE=1\r\n"); //设置传输模式(0为普通模式,1为透传模式)
while(!OKFlag && !TimeOutFlag);
OKFlag=0;
TimeOutFlag=0;
LCD_ShowString(2,1,"OK"); //表示ESP8266已经设置传输模式为透传模式
Delay(500);
LCD_Clear();
LCD_ShowString(1,1,"CIPSEND"); //表示ESP8266开始发送数据
//开始发送数据(在透传模式时),当输入单独一包 +++ 时,返回普通 AT 指令模式,要至少间隔 1 秒再发下一条 AT 指令
UART_SendString("AT+CIPSEND\r\n");
while(!OKFlag && !TimeOutFlag);
OKFlag=0;
TimeOutFlag=0;
LCD_ShowString(2,1,"OK"); //表示ESP8266已经准备好了,可以发送数据了
Delay(500);
WiFiGotIPFlag=0; //要放在最后,否则回显信息又会让WiFiGotIPFlag置1
TimeOutCountFlag=0; //停止超时的计时
GetTimeFlag=1;
}
if(GetTimeFlag) //从网络获取时间
{
TimeOutFlag=0; //超时的标志清零
TimeOutCountFlag=1; //启动超时的计时(如果获取时间超时,则重启ESP8266模块)
GetTimeFlag=0;
//透传模式下,向"www.beijing-time.org"随便发送点什么,就会返回时间信息
UART_SendString("T\r\n");
}
if(TimeOutFlag) //如果获取时间超时了(可能是ESP8266没接收到指令或者网络断开了)
{
TimeOutFlag=0; //超时的标志清零
TimeOutCountFlag=0; //停止超时的计时
UART_SendString("+++"); //退出透传模式
Delay(1000); //退出透传模式要1s后才能发AT指令
UART_SendString("AT+RST\r\n"); //重启一下模块
}
if(GotTimeFlag) //如果获取了时间
{
GotTimeFlag=0;
TimeOutFlag=0; //超时的标志清零
TimeOutCountFlag=0; //停止超时的计时
ConvertTime();
for(i=0;i<7;i++){DS1302_Time[i]=Time[i];}
DS1302_SetTime(); //将获取到的网络时间写入DS1302时钟芯片
LastHour_10=DS1302_Time[3]/10; //成功获取时间后,更新变量的值
LastHour_1=DS1302_Time[3]%10;
LastMinute_10=DS1302_Time[4]/10;
LastMinute_1=DS1302_Time[4]%10;
LastSecond_10=DS1302_Time[5]/10;
LastSecond_1=DS1302_Time[5]%10;
ShowOKFlag=1; //校时后,1行16列显示"O",2行16列显示"K",显示2s
T0Count4=0; //显示"OK"2s的计数清零
T0Count2=0; //每次成功校对时间后,用于自动校时的计数清0
ProofTimeCount=0; //每次成功校对时间后,用于自动校时的计数清0
ShowTimeFlag=1;
}
if(ReadTimeFlag && ShowTimeFlag) //从DS1302芯片中读取时间
{
ReadTimeFlag=0;
DS1302_ReadTime(); //读取时间
if(LastHour_10 != DS1302_Time[3]/10){Offset1=-8;T0Count6=0;}
if(LastHour_1 != DS1302_Time[3]%10){Offset2=-8;T0Count6=0;}
if(LastMinute_10 != DS1302_Time[4]/10){Offset3=-8;T0Count6=0;}
if(LastMinute_1 != DS1302_Time[4]%10){Offset4=-8;T0Count6=0;}
if(LastSecond_10 != DS1302_Time[5]/10){Offset5=-8;T0Count6=0;}
if(LastSecond_1 != DS1302_Time[5]%10){Offset6=-8;T0Count6=0;}
LastHour_10=DS1302_Time[3]/10;
LastHour_1=DS1302_Time[3]%10;
LastMinute_10=DS1302_Time[4]/10;
LastMinute_1=DS1302_Time[4]%10;
LastSecond_10=DS1302_Time[5]/10;
LastSecond_1=DS1302_Time[5]%10;
ShowTime(); //更新显示时间
}
}
}
void Timer0_Routine() interrupt 1 //定时器0中断函数
{
//因使能了6T(双倍速)模式,所以定时器计算器中12T模式定时20ms对应的是6T模式的10ms
TL0=0x00; //设置定时初值,定时10ms,晶振@11.0592MHz
TH0=0xB8; //设置定时初值,定时10ms,晶振@11.0592MHz
T0Count1++;
T0Count2++;
if(TimeOutCountFlag){T0Count3++;} //TimeOutCountFlag为1才开始超时的计时
else{T0Count3=0;}
T0Count4++;
T0Count5++;
T0Count6++;
if(T0Count1>=2) //每隔20ms检测一次按键
{
T0Count1=0;
Key_Tick();
}
if(T0Count2>=6000) //1min,即60s
{
T0Count2=0;
ProofTimeCount++;
ProofTimeCount%=1440; //60*1440s=24h,每隔24小时自动联网校时
if(!ProofTimeCount){GetTimeFlag=1;}
}
if(T0Count3>=2000) //ESP8266连接WiFi的超时时间:20s
{
T0Count3=0;
TimeOutFlag=1;
}
if(T0Count4>=200) //如果从网络获取了时间,显示"OK"2秒钟
{
T0Count4=0;
ShowOKFlag=0;
}
if(T0Count5>=10) //每隔100ms从DS1302时钟芯片读取一次时间
{
T0Count5=0;
ReadTimeFlag=1;
}
if(T0Count6>=7) //每隔70ms滚动一个像素
{
T0Count6=0;
Offset1++;
Offset2++;
Offset3++;
Offset4++;
Offset5++;
Offset6++;
if(Offset1>0){Offset1=0;}
if(Offset2>0){Offset2=0;}
if(Offset3>0){Offset3=0;}
if(Offset4>0){Offset4=0;}
if(Offset5>0){Offset5=0;}
if(Offset6>0){Offset6=0;}
}
}
void UART_Routine() interrupt 4 //串口中断函数
{
static unsigned char i,j;
char TempChar; //缓存变量
static bit ReceiveTimeFlag=0; //开始保存时间数据的标志,1:开始保存,0:不保存
if(RI==1) //如果接收标志位为1,接收到了数据
{
RI=0; //接收标志位清0
TempChar=SBUF; //用缓存变量取出SBUF的数据
//如果接收到的字符是下面四个之一,则从数组Judge的索引0的位置开始保存接下来的字符
if(TempChar=='O' || TempChar=='D' || TempChar=='r' || TempChar=='I'){i=0;}
//如果不小心触碰到ESP8266模块,导致接触不良而重启模块的话,获取IP后令WiFiGotIPFlag置1,再在主函数中重新连接网络
//返回的时间数据里有"PI",会误使WiFiGotIPFlag置1,所以要有下面的处理
if(TempChar=='I'){Judge[1]='\0';}
Judge[i]=TempChar;
i++;
if(ReceiveTimeFlag) //开始接收包含时间信息的字符串
{
j++;
if(j>=4){TimeBuffer[j-4]=TempChar;}
if(j>=28){ReceiveTimeFlag=0;GotTimeFlag=1;}
}
//接收到"ready",注意,下面的if中Judge[1]和Judge[2]的字符不能和Judge[0]的重复
if(Judge[0]=='r' && Judge[1]=='e' && Judge[2]=='a'){Judge[1]='\0';ReadyFlag=1;}
//接收到"DISCONNECT"
if(Judge[0]=='I' && Judge[1]=='S' && Judge[2]=='C'){Judge[1]='\0';WiFiDisconnectFlag=1;}
//接收到"GOT IP"
if(Judge[0]=='I' && Judge[1]=='P'){Judge[1]='\0';WiFiGotIPFlag=1;}
//接收到"OK"
if(Judge[0]=='O' && Judge[1]=='K'){Judge[1]='\0';OKFlag=1;}
//接收到"Date: ",说明接下来的字符串包含时间信息
if(Judge[0]=='D' && Judge[1]=='a' && Judge[2]=='t'){Judge[1]='\0';ReceiveTimeFlag=1;j=0;}
i%=5; //Judge数组只有5个数据
}
}
/*月份和星期
January(一月)
February(二月)
March(三月)
April(四月)
May(五月)
June(六月)
July(七月)
August(八月)
September(九月)
October(十月)
November(十一月)
December(十二月)
Monday(星期一)
Tuesday(星期二)
Wednesday(星期三)
Thursday(星期四)
Friday(星期五)
Saturday(星期六)
Sunday(星期日)
*/
/*网站返回的时间数据(第四行)
HTTP/1.1 400 Bad Request
Content-Type: text/html; charset=us-ascii
Server: Microsoft-HTTPAPI/2.0
Date: Mon, 13 Jan 2025 08:27:07 GMT
Connection: close
Content-Length: 326
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Bad Request</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></HEAD>
<BODY><h2>Bad Request - Invalid Verb</h2>
<hr><p>HTTP Error 400. The request verb is invalid.</p>
</BODY></HTML>
*/
总结
LCD1602滚动显示时分秒的实现没用多少时间,因为之前做过一个32X8点阵屏的时钟,原理是差不多的,不过LCD1602由于硬件原因,会拖影现象。I2C版本的LCD1602的通信速率比较慢,如果星期的显示(耗时较长)实时更新,会导致走时的显示不流畅,即看起来会有卡顿的现象,所以星期的显示在检测到星期发生变化再进行更新显示。