系列文章目录
前言
用彩色灯带DIY一个创意WiFi时钟,谁不喜欢呢?
所用单片机:STC8G1K08A
用到的外设:WS2812B彩色灯带(60个灯珠)、ESP8266(01S)、DS1302
效果查看/操作演示:B站搜索“甘腾胜”或“gantengsheng”查看。
源代码下载:B站对应视频的简介有工程文件下载链接。
一、效果展示
(1)通过灯珠显示ESP8266(01S)的状态
(2)成功连接网络后显示流水灯
(3)三原色显示时分秒
(4)12个较暗的白灯分别表示1~12
二、原理分析
1、用什么MCU
要实现的功能所用到的IO引脚不多,这款单片机很适合。P30、P31和ESP8266(01S)模块进行串口通信,P33用来控制WS2812B彩色灯带的显示,P55、P54、P32和DS1302通信,进行时间的读写,6个IO引脚刚好全部用完。
有8K的ROM空间,1024Byte的RAM空间。
2、ESP8266(01S)模块的使用
如何用AT指令和ESP8266(01S)模块的使用可以看一下我另一篇博客的简单说明。
基于51单片机和ESP8266(01S)、八位数码管、独立按键的定时器WiFi时钟
3、WS2812B彩色灯带的显示
WS2812B彩色灯带的驱动可以看一下我另一篇博客的简单说明
基于51单片机和WS2812B彩色灯带的流水灯
4、如何显示时钟
用的是60个灯珠的灯带,刚好对应平时挂钟上的60小格,通过12个较暗的白灯定位平时挂钟上的12个数字:1~12。利用红绿蓝三原色表示时分秒,这样时分、时秒、分秒、时分秒重合的时候可以看到叠加之后的颜色,时分重合显示青色,时秒重合显示紫色,分秒重合显示黄色,时分秒重合显示白色。
5、显示ESP8266模块的状态
通过灯珠显示ESP8266模块的状态,具体表示什么含义可以看一下代码的详细注释。如果不能获取网络时间,可以通过灯珠看出卡在哪个环节。
三、各模块代码
1、延时函数
h文件
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
c文件
/**
* @brief 延时函数,延时约xms毫秒
* @param xms 要延时的时间,范围:0~65535
* @retval 无
*/
void Delay(unsigned int xms) //1T@22.1184MHz(STC8F/STC8A/STC8G/STC8H)
{
unsigned char i,j;
while(xms--)
{
i=29;
j=183;
do
{
while(--j);
} while(--i);
}
}
2、定时器0
h文件
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0_Init(void);
#endif
c文件
#include <STC8G.H>
/**
* @brief 定时器0初始化
* @param 无
* @retval 无
*/
void Timer0_Init(void)
{
AUXR|=0x80; //定时器时钟1T模式
TMOD&=0xF0; //设置定时器模式(16位不自动重载)(高四位不变,低四位清零)
TMOD|=0x01; //设置定时器模式(16位不自动重载)(通过低四位设为“定时器0工作方式1”的模式)
TL0=0x9A; //设置定时初值,定时1ms,1T@22.1184MHz
TH0=0xA9; //设置定时初值,定时1ms,1T@22.1184MHz
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=0x9A; //设置定时初值,定时1ms,1T@22.1184MHz
TH0=0xA9; //设置定时初值,定时1ms,1T@22.1184MHz
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
3、串口通信
h文件
#ifndef __UART_H__
#define __UART_H__
void UART_Init();
void UART_SendByte(unsigned char Byte);
void UART_SendString(char *String);
#endif
c文件
#include <STC8G.H>
void UART_Init(void) //115200bps@22.1184MHz
{
SCON = 0x50; //8位数据,可变波特率
AUXR |= 0x40; //定时器时钟1T模式
AUXR &= 0xFE; //串口1选择定时器1为波特率发生器
TMOD &= 0x0F; //设置定时器模式(16位自动重装)
TL1 = 0xD0; //设置定时初始值
TH1 = 0xFF; //设置定时初始值
ET1 = 0; //禁止定时器中断
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文件
#ifndef __DS1302_H__
#define __DS1302_H__
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文件
#include <STC8G.H>
#include <INTRINS.H> //需要用空操作 _nop_(); 来延时
//引脚定义
sbit DS1302_CLK=P5^5;
sbit DS1302_DAT=P5^4;
sbit DS1302_RST=P3^2;
#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,2,2,18,11,23,7}; //时间的初始值
/**
* @brief DS1302初始化
* @param 无
* @retval 无
*/
void DS1302_Init(void)
{
DS1302_RST=0;
DS1302_CLK=0;
}
/**
* @brief DS1302写一个字节
* @param Command 命令字/地址
* @param Data 要写入的数据
* @retval 无
*/
void DS1302_WriteByte(unsigned char Command,Data)
{
unsigned char i;
DS1302_RST=1;
for(i=0;i<8;i++) //循环8次,每次写1位,先写低位再写高位
{
DS1302_DAT=Command&(0x01<<i);
DS1302_CLK=1; //SCLK置1后立即置0,该时序操作需考虑时钟芯片是否可承受这个时钟的最快频率
_nop_();_nop_(); //单片机速度较快,需要加延时,否则无法正确读写时间
DS1302_CLK=0;
_nop_();_nop_();
}
for(i=0;i<8;i++)
{
DS1302_DAT=Data&(0x01<<i);
DS1302_CLK=1; //CLK由低到高产生一个上升沿,从而写入数据
_nop_();_nop_();
DS1302_CLK=0;
_nop_();_nop_();
}
DS1302_RST=0;
}
/**
* @brief DS1302读一个字节
* @param Command 命令字/地址
* @retval Data 读出的数据
*/
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00;
DS1302_RST=1;
for(i=0;i<8;i++)
{
DS1302_DAT=Command&(0x01<<i);
DS1302_CLK=0;
_nop_();_nop_();
DS1302_CLK=1;
_nop_();_nop_();
}
for(i=0;i<8;i++)
{
DS1302_CLK=1;
_nop_();_nop_();
DS1302_CLK=0; //要先1后0,否则全都是65
_nop_();_nop_();
if(DS1302_DAT){Data|=(0x01<<i);}
}
DS1302_RST=0;
DS1302_DAT=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、WS2812B
h文件
#ifndef __WS2812B_H__
#define __WS2812B_H__
extern unsigned char xdata WS2812B_Buffer[][3];
extern unsigned char code Table0[];
extern unsigned char code Table1[];
extern unsigned char code Table2[];
void WS2812B_Clear(void);
void WS2812B_MoveInOrder(unsigned char *Array,unsigned int Offset);
void WS2812B_MoveInReverseOrder(unsigned char *Array,unsigned int Offset);
void WS2812B_SetBuffer(unsigned char Position,unsigned char R,unsigned char G,unsigned char B);
void WS2812B_SetRed(unsigned char Position,unsigned char R);
void WS2812B_SetGreen(unsigned char Position,unsigned char G);
void WS2812B_SetBlue(unsigned char Position,unsigned char B);
void WS2812B_WriteByte(unsigned char Byte);
void WS2812B_UpdateDisplay(void);
#endif
c文件
#include <STC8G.H> //包含寄存器定义的头文件
#include <INTRINS.H> //需要用空操作 _nop_(); 来延时
//引脚定义
sbit WS2812B_Din=P3^3;
//用3*60=180个字节作为WS2812B彩色灯带的显示缓存,共有60个灯珠,每个灯珠需要写入24Bit(3个字节)控制显示的颜色
//WS2812B芯片要求按G、R、B的顺序发送数据,并且每个字节要高位先发
//缓存数组中每三个字节为一组,每一组分别对应一个灯珠的G(绿)、R(红)、B(蓝)三原色
//第一组对应数据传输方向的第一个灯珠,第二组对应第二个灯珠,以此类推
//想更改灯带的显示,先更改此显示缓存中的数据,再通过函数WS2812B_UpdateDisplay将显示缓存的数据写入每个灯珠的WS2812B芯片内
unsigned char xdata WS2812B_Buffer[60][3];
//用来实现流水灯退出的效果
unsigned char code Table0[]={
0,0,0, //无显示
};
//通过查表的方法显示流水灯,三个为一组,一组对应一个灯珠的R、G、B的值
unsigned char code Table1[]={ //流星流水灯
255,0,0,127,0,0,63,0,0,31,0,0,15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,1,0,0, //红
0,255,0,0,127,0,0,63,0,0,31,0,0,15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,1,0, //绿
0,0,255,0,0,127,0,0,63,0,0,31,0,0,15,0,0,11,0,0,7,0,0,5,0,0,3,0,0,1, //蓝
255,255,0,127,127,0,63,63,0,31,31,0,15,15,0,11,11,0,7,7,0,5,5,0,3,3,0,1,1,0, //黄
255,0,255,127,0,127,63,0,63,31,0,31,15,0,15,11,0,11,7,0,7,5,0,5,3,0,3,1,0,1, //紫
0,255,255,0,127,127,0,63,63,0,31,31,0,15,15,0,11,11,0,7,7,0,5,5,0,3,3,0,1,1, //青
};
unsigned char code Table2[]={ //顺向逆向流星流水灯叠加
255,127,63,31,15,11,7,5,3,1,
};
/**
* @brief WS2812B彩带私有延时函数,1T@24.000MHz调用可延时约100us
* @param 无
* @retval 无
*/
void WS2812B_Delay100us(void)
{
unsigned char i,j;
i=3;
j=82;
do
{
while(--j);
}while(--i);
}
/**
* @brief WS2812B彩色灯带清空显示缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param 无
* @retval 无
*/
void WS2812B_Clear(void)
{
unsigned char i;
for(i=0;i<60;i++)
{
WS2812B_Buffer[i][0]=0;
WS2812B_Buffer[i][1]=0;
WS2812B_Buffer[i][2]=0;
}
}
/**
* @brief WS2812B彩色灯带按灯带数据传输方向移动显示数组Array中的数据(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param *Array Array为传递过来的指针(即内存地址),数组名就是数组的首地址
* @param Offset 偏移量,范围:0~Array数组数据总数/3-1
Array中的数据三个为一组,移动后第一个灯珠显示第Offset组的数据(第0组为数组Array中的前三个数据)
* @retval 无
*/
void WS2812B_MoveInOrder(unsigned char *Array,unsigned int Offset)
{ //Offset用unsigned int定义是为了方便拓展显示更长的流水灯
unsigned char i;
//缓存数组中的数据按数组索引增大的方向移动3个字节
for(i=0;i<60-1;i++)
{
WS2812B_Buffer[60-1-i][0]=WS2812B_Buffer[60-2-i][0];
WS2812B_Buffer[60-1-i][1]=WS2812B_Buffer[60-2-i][1];
WS2812B_Buffer[60-1-i][2]=WS2812B_Buffer[60-2-i][2];
}
//移动后,对第一个灯珠对应的三个字节进行赋值,从而实现流水灯的效果
Array+=Offset*3; //指针方面的知识不清楚的先去了解一下
WS2812B_Buffer[0][0]=*(Array+1); //WS2812B_Buffer中按G、R、B存放,
WS2812B_Buffer[0][1]=*Array; //Array数组中按R、G、B存放,
WS2812B_Buffer[0][2]=*(Array+2); //赋值的时候需要调一下顺序
}
/**
* @brief WS2812B彩色灯带按灯带数据传输方向的反方向移动显示数组Array中的数据(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param *Array Array为传递过来的指针(即内存地址),数组名就是数组的首地址
* @param Offset 偏移量,范围:0~Array数组数据总数/3-1
Array中的数据三个为一组,移动后最后一个灯珠显示第Offset组的数据(第0组为数组Array中的前三个数据)
* @retval 无
*/
void WS2812B_MoveInReverseOrder(unsigned char *Array,unsigned int Offset)
{
unsigned char i;
//缓存数组中的数据按数组索引减小的方向移动3个字节
for(i=0;i<60-1;i++)
{
WS2812B_Buffer[i][0]=WS2812B_Buffer[i+1][0];
WS2812B_Buffer[i][1]=WS2812B_Buffer[i+1][1];
WS2812B_Buffer[i][2]=WS2812B_Buffer[i+1][2];
}
//移动后,对最后一个灯珠对应的三个字节进行赋值,从而实现流水灯的效果
Array+=Offset*3;
WS2812B_Buffer[60-1][0]=*(Array+1); //WS2812B_Buffer中按G、R、B存放,
WS2812B_Buffer[60-1][1]=*Array; //Array数组中按R、G、B存放,
WS2812B_Buffer[60-1][2]=*(Array+2); //赋值的时候需要调一下顺序
}
/**
* @brief WS2812B彩色灯带设置一个灯珠的缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param Position 要设置的位置,范围:0~60-1,对应第1个到第60个灯珠
* @param R 红(Red),范围:0~255
* @param G 绿(Green),范围:0~255
* @param B 蓝(Blue),范围:0~255
* @retval 无
*/
void WS2812B_SetBuffer(unsigned char Position,unsigned char R,unsigned char G,unsigned char B)
{
WS2812B_Buffer[Position][0]=G;
WS2812B_Buffer[Position][1]=R;
WS2812B_Buffer[Position][2]=B;
}
/**
* @brief WS2812B彩色灯带设置一个灯珠的红色缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param Position 要设置的位置,范围:0~60-1,对应第1个到第60个灯珠
* @param R 红(Red),范围:0~255
* @retval 无
*/
void WS2812B_SetRed(unsigned char Position,unsigned char R)
{
WS2812B_Buffer[Position][1]=R;
}
/**
* @brief WS2812B彩色灯带设置一个灯珠的绿色缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param Position 要设置的位置,范围:0~60-1,对应第1个到第60个灯珠
* @param G 绿(Green),范围:0~255
* @retval 无
*/
void WS2812B_SetGreen(unsigned char Position,unsigned char G)
{
WS2812B_Buffer[Position][0]=G;
}
/**
* @brief WS2812B彩色灯带设置一个灯珠的蓝色缓存(需要调用函数WS2812B_UpdateDisplay才能更新灯带的显示)
* @param Position 要设置的位置,范围:0~60-1,对应第1个到第60个灯珠
* @param B 蓝(Blue),范围:0~255
* @retval 无
*/
void WS2812B_SetBlue(unsigned char Position,unsigned char B)
{
WS2812B_Buffer[Position][2]=B;
}
/**
* @brief WS2812B彩色灯带写入一个字节
* @brief 频率要求:1T@24.000MHz,如果要换其他频率,则需要调整“_nop_();”的数量
* @param Byte 要写入的字节
* @retval 无
*/
void WS2812B_WriteByte(unsigned char Byte)
{
unsigned char i;
EA=0; //关闭总中断(如果用到中断的话)(时序要求严格,不能被打断),并要求中断函数执行的时间不能太长
//时间太长,相当于发送了重置信号
for(i=0;i<8;i++)
{
if(Byte&(0x80>>i)) //写1(高位先发)
{
WS2812B_Din=1; //根据高电平的时长确定发送的是1还是0,跟DS18B20类似
//用空操作进行延时,单片机使用不同的频率,就需要不一样“_nop_();”的数量
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
WS2812B_Din=0; //经测试,数据线拉低后不用加延时
}
else //写0
{
WS2812B_Din=1;
_nop_();_nop_();_nop_();_nop_();_nop_();
WS2812B_Din=0; //经测试,数据线拉低后不用加延时
}
}
EA=1; //开启总中断
}
/**
* @brief WS2812B彩色灯带更新显示,将显示缓存数组WS2812B_Buffer的数据写入到灯珠的WS2812B芯片内
* @param 无
* @retval 无
*/
void WS2812B_UpdateDisplay(void)
{
unsigned char i;
for(i=0;i<60;i++) //连续写入显示缓存的3*60个字节
{
WS2812B_WriteByte(WS2812B_Buffer[i][0]); //G(绿)
WS2812B_WriteByte(WS2812B_Buffer[i][1]); //R(红)
WS2812B_WriteByte(WS2812B_Buffer[i][2]); //B(蓝)
}
WS2812B_Delay100us(); //Reset(重置)信号
}
四、主函数
main.c
/*
by甘腾胜@202500202
效果展示:可以在B站搜索“甘腾胜”或“gantengsheng”查看
单片机:STC8G1K08A
晶振:1T@23.1184MHz
波特率:115200bps
外设:WS2812B彩色灯带(60个灯珠)、ESP8266(01S)模块、DS1302
注意:
(1)ESP8266模块供电电压为3.3V,接5V会发热严重
(2)ESP8266模块与单片机的RX和TX要交叉连接
(3)无需设置ESP8266模块默认的WiFi模式(AP模式),超时20s,运行时程序会自动设置为STA模式(WiFi模式会保存到ESP8266芯片)
(3)无需修改ESP8266模块默认的波特率115200bps
(4)每隔12h会自动联网校时(如果校时超时(20s),会重启ESP8266模块,重新连接WiFi和网络并校时)
(5)串口中断的优先级要比定时器0的高,否则会影响串口通信
(6)代码末尾有月份、星期的英文,以及网站返回的时间数据的样例
(7)不校时的时候,利用时钟芯片DS1302走时,并从中读取时间用以显示
接线:
(1)ESP8266模块用3.3V供电,TX、RX分别接单片机的P30、P31
(2)WS2812B彩色灯带用5V供电,数据线接P33
(3)DS1302的CLK、DAT、RST分别接单片机的P55、P54、P32
*/
#include <STC8G.H>
#include "Delay.h"
#include "WS2812B.h"
#include "Timer0.h"
#include "UART.h"
#include "DS1302.h"
#define Duration 25 //流星流水灯延时的时长,更改数值可以改变移动的速度,数值越大,速度越慢
//预设三个WiFi账号,如果连接不上,超时20s会连接下一个,连接WiFi成功(账号和密码保存到了Flash)后,下次上电自动连接
/*例:(设置第一个预设账号)
如果WiFi账号是:abc
如果WiFi密码是:12345678
则应改成:char code WiFi1[]="AT+CWJAP=\"abc\",\"12345678\"\r\n";
*/
char code WiFi1[]="AT+CWJAP=\"gantengsheng\",\"01234567\"\r\n"; //发送的字符串中如果有双引号,需要用反斜杠转义
char code WiFi2[]="AT+CWJAP=\"ganpan\",\"01234567\"\r\n";
char code WiFi3[]="AT+CWJAP=\"GTS\",\"01234567\"\r\n";
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:WiFi连接失败,0:无
bit GetTimeFlag=0; //从网络获取时间的标志,1:获取,0:不获取
bit GotTimeFlag=0; //从网络获取了时间的标志,1:已获取,0:未获取到
char Time[7]; //存储接收到的字符型的时间数据转化之后的十进制数据,索引0~6分别对应年、月、日、时、分、秒、星期
unsigned int T0Count1,T0Count2,T0Count3,T0Count4; //定时器计数的变量
unsigned int ProofTimeCount; //定时器中隔一段时间自动校时的计数
bit TimeOutFlag; //连接WiFi超时的标志,1:超时,2:未超时
bit TimeOutCountFlag=1; //启动超时计数的标志,1:启动,2:不启动
bit ReadTimeFlag=1; //从DS1302时钟芯片读取时间的标志,1:读取,2:不读取
bit ShowTimeFlag; //显示时间的标志,1:显示,2:不显示(用来控制上电获取到网络时间后再显示时间)
bit ShowGotTimeFlag; //成功获取网络时间后12个白灯以最大亮度显示2s(表示获取了网络时间)的标志,1:显示,0:不显示
/**
* @brief ESP8266连接WiFi
* @param 无
* @retval 无
*/
void ESP8266_ConnectWiFi(void)
{
WS2812B_Clear(); //WS2812B彩色灯带清空显示
WS2812B_SetBuffer(0,255,0,0); //第1个灯显示红色,表示等待ESP8266准备好
WS2812B_UpdateDisplay(); //更新显示
Delay(100); //适当延时,延时0.1s
//退出透传模式(如果ESP8266不断电,只让单片机复位的话,ESP8266就还处于透传模式,发送AT指令会无效)
UART_SendString("+++");
Delay(1000); //退出透传模式要1s之后才能发AT指令
if(ReadyFlag) //如果上电直接返回“ready”
{
ReadyFlag=0;
WS2812B_SetBuffer(0,255,255,255); //第1个灯显示白色,表示ESP8266已准备好
WS2812B_UpdateDisplay();
Delay(500);
}
else //如果不返回“ready”,则重启一下ESP8266模块
{
UART_SendString("AT+RST\r\n"); //复位
while(!ReadyFlag); //等待ESP8266返回"ready"
ReadyFlag=0;
WS2812B_SetBuffer(0,255,255,255); //第1个灯显示白色,表示ESP8266已准备好
WS2812B_UpdateDisplay();
Delay(500);
}
WS2812B_SetBuffer(5,255,0,0); //第6个灯显示红色,表示等待ESP8266连接WiFi
WS2812B_UpdateDisplay();
//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;
WS2812B_SetBuffer(5,255,255,255); //第6个灯显示白色,表示ESP8266已连接WiFi,并获取了IP
WS2812B_UpdateDisplay();
Delay(500);
}
else //如果WiFi不能连接
{ //WiFi账号密码是保存到ESP8266的flash里的,如果在else中连接成功,
//下次上电就可以直接连接WiFi并获得IP,就不会进入此else中
WiFiDisconnectFlag=0;
WS2812B_SetBuffer(5,0,255,0); //第6个灯显示绿色,表示ESP8266连接WiFi失败
WS2812B_UpdateDisplay();
Delay(500);
WS2812B_SetBuffer(5,255,255,0); //第6个灯显示黄色,表示ESP8266正在连接第一个预设的WiFi账号
WS2812B_UpdateDisplay();
T0Count3=0; //超时的计数清零
TimeOutFlag=0; //超时的标志清零
//如果WiFi连接不成功,就按下面的账号密码进行连接
UART_SendString(WiFi1);
while(!OKFlag && !WiFiGotIPFlag && !TimeOutFlag);
OKFlag=0;
WiFiGotIPFlag=0;
if(TimeOutFlag) //如果是因为超时退出上面的while循环,说明第一个预设的WiFi账号没连上
{
WS2812B_SetBuffer(5,255,0,255); //第6个灯显示紫色,表示ESP8266正在连接第二个预设的WiFi账号
WS2812B_UpdateDisplay();
T0Count3=0; //超时的计数清零
TimeOutFlag=0; //超时的标志清零
//如果上面的WiFi连接不成功,超时(20s)了,就会尝试下面的账号密码进行连接
//超时的时间不能少于15s,否则会导致连接不成功
//如果上面的WiFi连接成功,就不会连接下面的WiFi账号
UART_SendString(WiFi2);
while(!OKFlag && !WiFiGotIPFlag && !TimeOutFlag);
OKFlag=0;
WiFiGotIPFlag=0;
if(TimeOutFlag) //如果是因为超时退出上面的while循环,说明第二个预设的WiFi账号没连上
{
WS2812B_SetBuffer(5,0,255,255); //第6个灯显示青色,表示ESP8266正在连接第三个预设的WiFi账号
WS2812B_UpdateDisplay();
//如果上面的WiFi连接不成功,超时(20s)了,就会尝试下面的账号密码进行连接
//如果上面的WiFi连接成功,就不会连接下面的WiFi账号
UART_SendString(WiFi3);
while(!OKFlag && !WiFiGotIPFlag); //如果第三个WiFi账号连接不上,就会在此处陷入死循环
OKFlag=0;
WiFiGotIPFlag=0;
}
}
WS2812B_SetBuffer(5,255,255,255); //第6个灯显示白色,表示ESP8266已连接WiFi,并获取了IP
WS2812B_UpdateDisplay();
//下面的处理不能少,如果是第一次连上WiFi,还会多返回一个“OK”(暂时未知原因,有大佬知道原因的,请私信告知我一下,谢谢!)
//如果不处理,会导致第一次连上WiFi后程序卡在数码管显示“5”
Delay(1500); //延时1.5s
OKFlag=0; //延时不能少于1s,要等ESP8266返回OK令OKFlag置1之后,再将OKFlag置零
T0Count3=0; //超时的计数清零
TimeOutFlag=0; //超时的标志清零
}
}
/**
* @brief ESP8266连接网络
* @param 无
* @retval 无
*/
void ESP8266_ConnectNetwork(void)
{
TimeOutCountFlag=1; //启动超时的计时(防止出错卡在while循环)
WS2812B_Clear();
WS2812B_SetBuffer(0,255,255,255); //已获取到IP
WS2812B_SetBuffer(5,255,255,255); //第1个和第6个灯显示白色
WS2812B_SetBuffer(10,255,0,0); //第11个灯显示红色,表示ESP8266开始建立TCP连接
WS2812B_UpdateDisplay();
UART_SendString("AT+CIPSTART=\"TCP\",\"www.beijing-time.org\",80\r\n"); //建立 TCP 连接
while(!OKFlag && !TimeOutFlag);
OKFlag=0;
TimeOutFlag=0;
WS2812B_SetBuffer(10,255,255,255); //第11个灯显示白色,表示ESP8266已建立TCP连接
WS2812B_UpdateDisplay();
Delay(500);
WS2812B_SetBuffer(15,255,0,0); //第16个灯显示红色,表示ESP8266开始设置传输模式
WS2812B_UpdateDisplay();
UART_SendString("AT+CIPMODE=1\r\n"); //设置传输模式(0为普通模式,1为透传模式)
while(!OKFlag && !TimeOutFlag);
OKFlag=0;
TimeOutFlag=0;
WS2812B_SetBuffer(15,255,255,255); //第16个灯显示白色,表示ESP8266已经设置传输模式为透传模式
WS2812B_UpdateDisplay();
Delay(500);
WS2812B_SetBuffer(20,255,0,0); //第21个灯显示红色,表示ESP8266准备发送数据
WS2812B_UpdateDisplay();
//开始发送数据(在透传模式时),当输入单独一包 +++ 时,返回普通 AT 指令模式,要至少间隔 1 秒再发下一条 AT 指令
UART_SendString("AT+CIPSEND\r\n");
while(!OKFlag && !TimeOutFlag);
OKFlag=0;
TimeOutFlag=0;
WS2812B_SetBuffer(20,255,255,255); //第21个灯显示白色,表示ESP8266可以发送数据了
WS2812B_UpdateDisplay();
Delay(500);
WiFiGotIPFlag=0; //要放在最后,否则回显信息又会让WiFiGotIPFlag置1
TimeOutCountFlag=0; //停止超时的计时
TimeOutFlag=0; //超时的标志清零
}
/**
* @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)
{
unsigned char i;
WS2812B_Clear();
for(i=0;i<12;i++)
{
if(ShowGotTimeFlag)
{
WS2812B_SetBuffer(5*i,255,255,255);
}
else
{
WS2812B_SetBuffer(5*i,5,5,5);
}
}
WS2812B_SetBuffer(5*(DS1302_Time[3]%12)+DS1302_Time[4]/12,0,0,255); //时,蓝色
WS2812B_SetGreen(DS1302_Time[4],255); //分,绿色
WS2812B_SetRed(DS1302_Time[5]%60,255); //秒,红色
WS2812B_UpdateDisplay();
}
/**
* @brief 联网成功后显示流水灯
* @param 无
* @retval 无
*/
void LED_Flow(void)
{
unsigned char i,j;
unsigned char Sum; //用来控制顺向和逆向流星流水灯的叠加显示,Sum表示顺向或逆向流星流水灯出现的个数总和-1
//流星流水灯(红、绿、蓝、黄、紫、青、白)
WS2812B_Clear();
for(j=0;j<2;j++) //正向,循环2次
{
for(i=0;i<10*6;i++) //每种颜色流星灯的长度是10个灯珠,总共有6种颜色
{
WS2812B_MoveInOrder(Table1,i);
WS2812B_UpdateDisplay();
Delay(Duration); //更改延时的时长可以改变流星灯的速度(在宏定义那里修改)
}
}
for(i=0;i<60;i++) //退出
{
WS2812B_MoveInOrder(Table0,0);
WS2812B_UpdateDisplay();
Delay(Duration);
}
for(j=0;j<2;j++) //逆向,循环2次
{
for(i=0;i<10*6;i++)
{
WS2812B_MoveInReverseOrder(Table1,i);
WS2812B_UpdateDisplay();
Delay(Duration);
}
}
for(i=0;i<60;i++) //退出
{
WS2812B_MoveInReverseOrder(Table0,0);
WS2812B_UpdateDisplay();
Delay(Duration);
}
//三原色流星流水灯顺向逆向叠加
WS2812B_Clear();
Sum=0;
for(j=0;j<60+10;j++) //顺红逆绿
{
WS2812B_Clear(); //清空缓存
for(i=0;i<=Sum;i++)
{
if(j<10) //流星灯进入过程
{
WS2812B_Buffer[Sum-i][1]=Table2[i]; //红
WS2812B_Buffer[60-1-(Sum-i)][0]=Table2[i]; //绿
}
else if(j-i<60) //完全进入及退出
{
WS2812B_Buffer[j-i][1]=Table2[i]; //红
WS2812B_Buffer[60-1-(j-i)][0]=Table2[i]; //绿
}
}
WS2812B_UpdateDisplay();
Sum++; //每经过一个Delay延时,流星灯进入的数量+1
if(Sum>9){Sum=9;} //流星灯长度为1~10,对应sum的0~9,流星灯完全进入后保持9不变
Delay(Duration);
}
Sum=0;
for(j=0;j<60+10;j++) //顺蓝逆红
{
WS2812B_Clear(); //清空缓存
for(i=0;i<=Sum;i++)
{
if(j<10) //流星灯进入过程
{
WS2812B_Buffer[Sum-i][2]=Table2[i]; //蓝
WS2812B_Buffer[60-1-(Sum-i)][1]=Table2[i]; //红
}
else if(j-i<60) //完全进入及退出
{
WS2812B_Buffer[j-i][2]=Table2[i]; //蓝
WS2812B_Buffer[60-1-(j-i)][1]=Table2[i]; //红
}
}
WS2812B_UpdateDisplay();
Sum++;
if(Sum>9){Sum=9;}
Delay(Duration);
}
Sum=0;
for(j=0;j<60+10;j++) //顺绿逆蓝
{
WS2812B_Clear();
for(i=0;i<=Sum;i++)
{
if(j<10)
{
WS2812B_Buffer[Sum-i][0]=Table2[i]; //绿
WS2812B_Buffer[60-1-(Sum-i)][2]=Table2[i]; //蓝
}
else if(j-i<60)
{
WS2812B_Buffer[j-i][0]=Table2[i]; //绿
WS2812B_Buffer[60-1-(j-i)][2]=Table2[i]; //蓝
}
}
WS2812B_UpdateDisplay();
Sum++;
if(Sum>9){Sum=9;}
Delay(Duration);
}
}
void main()
{
unsigned char i;
P3M1=0;P3M0=0; //P3口设置为上拉模式
P5M1=0;P5M0=0; //P5口设置为上拉模式
Timer0_Init(); //定时器0初始化
UART_Init(); //串口初始化
DS1302_Init(); //DS1302初始化
ESP8266_ConnectWiFi(); //ESP8266连接WiFi
ESP8266_ConnectNetwork(); //ESP8266连接网络
LED_Flow(); //联网成功后显示流水灯
GetTimeFlag=1; //上电获取一次网络时间
while(1)
{
if(WiFiGotIPFlag) //如果ESP8266模块重启,并获取了IP
{
ESP8266_ConnectNetwork(); //ESP8266连接网络
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时钟芯片
ShowGotTimeFlag=1; //成功校时后,12个白灯以最大亮度显示2s
T0Count4=0; //12个白灯以最大亮度显示2s的计数清零
T0Count2=0; //每次成功校对时间后,用于自动校时的计数清0
ProofTimeCount=0; //每次成功校对时间后,用于自动校时的计数清0
ShowTimeFlag=1;
}
if(ReadTimeFlag && ShowTimeFlag) //从DS1302芯片中读取时间
{
ReadTimeFlag=0;
DS1302_ReadTime(); //读取时间
ShowTime(); //更新显示时间
}
}
}
void Timer0_Routine() interrupt 1 //定时器0中断函数
{
TL0=0x9A; //设置定时初值,定时1ms,1T@22.1184MHz
TH0=0xA9; //设置定时初值,定时1ms,1T@22.1184MHz
T0Count1++;
T0Count2++;
if(TimeOutCountFlag){T0Count3++;} //TimeOutCountFlag为1才开始超时的计时
else{T0Count3=0;}
T0Count4++;
if(T0Count1>=100) //每隔100ms从DS1302时钟芯片读取一次时间
{
T0Count1=0;
ReadTimeFlag=1;
}
if(T0Count2>=60000) //1min,即60s
{
T0Count2=0;
ProofTimeCount++;
ProofTimeCount%=30; //1min*720=12h,每隔12小时自动联网校时
if(!ProofTimeCount){GetTimeFlag=1;}
}
if(T0Count3>=20000) //ESP8266连接WiFi的超时时间:20s
{
T0Count3=0;
TimeOutFlag=1;
}
if(T0Count4>=2000) //成功校时后,12个白灯以最大亮度显示2s
{
T0Count4=0;
ShowGotTimeFlag=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>
*/
总结
本来想做定时器时钟的,后来发现烧录程序的时候会调整芯片内的RC振荡,每次都有误差,就算补偿好了,下次烧录又会导致误差较大,所以就用了DS1302时钟芯片。
用了DS1302时钟芯片后,发现不能正常显示时间,经过较长时间的排查,才发现DS1302的CLK信号太快了,加了延时才解决。