基于ESP8266自动校时时钟
玩单片机以来比较喜欢折腾时钟,可能是比较简单的缘故。之前用51做了一个,一直放在客厅里用着,用的是DS1302。时间一直跑快,虽然代码中有定期调整功能,但功能不太好用。
近期一直在学习使用ESP8266这个芯片,个人比较喜欢其既可以联网又能作为MCU使用。因此又把之前的闹钟捡起来使用此芯片做了一个小时钟玩玩。
废话不多少说了,直接上,欢迎感兴趣的小伙伴拍砖。
器件:
Max7219 LED屏一个
NodeMCU模块一个
DS3231时钟模块一个
线若干
连线:
NodeMCU DS3231 LED屏
D1 <-> SCL
D2 <-> SDA
D4 <-> CLK
D5 <-> CS
D6 <-> DIN
功能:
1、显示时间、日期、温度(从DS3231中读取);
2、定期、手动通过NTP校时(手动校时通过短按NodeMCU的Flash键,校时失败会在左下方显示一个点)
3、支持无DS3231模块时显示时间、日期;
无手动调整时间、日期功能。
代码:需要的库请看代码部分,自行下载
注:本人非码农,因此代码基本是网上扒来改一下,以能跑起来为原则,高手勿喷:L。
#include <LedControl.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <Time.h>
#include <TimeLib.h>
#include <pgmspace.h>
#include <Wire.h> // must be included here so that Arduino library object file references work
#include <RtcDS3231.h>
#define MX_CLOCK 2 // NodeMCU-D4 黑 CLK
#define MX_CS 14 // NodeMCU-D5 白 CS
#define MX_DIN 12 // NodeMCU-D6 灰 DataIn
#define KEY 0 //按键,GPIO0 NodeMCU Flash Key
// I2C For ESP8266, these default to SDA = GPIO04(NodeMCU-D2) and SCL = GPIO05(NodeMCU-D1).
LedControl lc=LedControl(MX_DIN, MX_CLOCK, MX_CS, 4);
unsigned long lastdisp = millis(); //LED屏刷新标志
unsigned char bn = 0; // 显示缓冲区加载指针变量;
byte clear_buf = {byte(0)};
byte disp = {byte(0)}; //显示缓冲区;
byte disp_buf = {byte(0)}; //待显缓冲区;
unsigned int k = 0; //用于“:”闪烁
unsigned int t = 0; //用于切换日期时间显示
RtcDS3231<TwoWire> Rtc(Wire);
char ssid[] = "XXX";//WIFI SSID
char pass[] = "xxxxx"; // WIFI password
unsigned int localPort = 2390; // UDP本地监听端口
const unsigned long NtpInterval = 3600*1000UL; //NTP自动校时间隔(毫秒)
//const unsigned long SeventyYears = 2208988800UL; //1900-1970的秒数
//const unsigned long ThirtyYears = 946684800UL; //1970-2000的秒数
const unsigned long Century = 3155673600UL; //1900.01.01-2000.01.01的秒数
const long TimeZoneOffset = +28800;//GMT +8
unsigned long lastNtpupdate = millis(); //NTP校时标志
unsigned int rtcstatus = 1; //检测RTC是否有效
unsigned int ntpstatus = 0; //检测NTP同步状态
unsigned int keys = 0; //按键检测结果
IPAddress timeServerIP; // 用于存放解析后的NTP server IP;
const char* ntpServerName = "cn.ntp.org.cn"; //NTP服务器域名:尽量不要直接填写IP,
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
// A UDP instance to let us send and receive packets over UDP
WiFiUDP udp;
//-----------------------------------数字字符表
unsigned char dp[]{
// 5*8字模
{0x7E,0x81,0x81,0x81,0x7E,'z'}, // -0-字符0
{0x00,0x41,0xFF,0x01,0x00,'z'}, // -1-字符1
{0x43,0x85,0x89,0x91,0x61,'z'}, // -2-字符2
{0x82,0x81,0x91,0xA9,0xC6,'z'}, // -3-字符3
{0x18,0x28,0x48,0xFF,0x08,'z'}, // -4-字符4
{0xF2,0x91,0x91,0x91,0x8E,'z'}, // -5-字符5
{0x7E,0x91,0x91,0x91,0x4E,'z'}, // -6-字符6
{0x80,0x8F,0x90,0xA0,0xC0,'z'}, // -7-字符7
{0x6E,0x91,0x91,0x91,0x6E,'z'}, // -8-字符8
{0x62,0x91,0x91,0x91,0x7E,'z'}, // -9-字符9
{0x66,0x66,'z'}, // -10- ":"时间分隔符
{0x18,0x18,0x18,'z'}, // -11- "-"日期分隔符
{0xC0,0xC0,0x3C,0x66,0x42,0x42,0x24,'z'},// -12- "oC"温度符号
{0x00,0x00,'z'}, // -13-2空列
{0x06,0x06,'z'}, // -14-"."小数点
{0x01,0x00,'z'}, // -15-首列带点,用于NTP校时不成功时提醒
};
byte connwifi{ //开机显示Connwifi;
0x7C,0x82,0x82,0x82,0x44,0x00,// -C-
0x1C,0x22,0x22,0x22,0x1C,0x00,// -o-
0x1E,0x20,0x20,0x1E,0x00, // -n-
0x7E,0x04,0x18,0x04,0x7E,0x00,// -W-
0x5E,0x00, // -i-
0x7E,0x50,0x50,0x50,0x00, // -F-
0x5E,0x00, // -i-
};
unsigned long sendNTPpacket(IPAddress& address) // send an NTP request to the time server at the given address
{
Serial.print("sending NTP packet...");
memset(packetBuffer, 0, NTP_PACKET_SIZE); // set all bytes in the buffer to 0
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer = 0b11100011; // LI, Version, Mode
packetBuffer = 0; // Stratum, or type of clock
packetBuffer = 6; // Polling Interval
packetBuffer = 0xEC;// Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer= 49;
packetBuffer= 0x4E;
packetBuffer= 49;
packetBuffer= 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
udp.beginPacket(address, 123); //NTP requests are to port 123
udp.write(packetBuffer, NTP_PACKET_SIZE);
udp.endPacket();
}
#define countof(a) (sizeof(a) / sizeof(a))
void printDateTime(const RtcDateTime& dt)
{
char datestring;
snprintf_P(datestring,
countof(datestring),
PSTR("%04u/%02u/%02u %02u:%02u:%02u"),
dt.Year(),
dt.Month(),
dt.Day(),
dt.Hour(),
dt.Minute(),
dt.Second() );
Serial.print(datestring);
}
void scankey()
{
if (digitalRead(KEY) == 0){
unsigned int Keytime = 0;
while(!digitalRead(KEY)){
Keytime ++;
delay(10);
}
if (Keytime >= 300) keys = 2;
if (Keytime >= 2 & Keytime < 300) keys = 1;
}
}
void putin_buf (unsigned char u){ //将字符装入显示缓冲区
unsigned char a = 0;//定义变量用于数据提取指针
do{
disp_buf = dp;//将二维数组中的一组数据放入显示缓冲区
a++;//换下一个提取字节
bn++; //换下一个显示缓冲字节
}
while(dp != 'z'); //当显示数据为'z'时结束循环
bn++;
disp_buf = 0; //显示一列的空位,便于字符区分
}
void Load_time(void){//时间组合与显示
RtcDateTime Rtctime;
if (rtcstatus == 1){
Rtctime = Rtc.GetDateTime(); //读取RTC日期时间数据
}
else{
Rtctime = now(); //RTC不可用时,读取系统日期时间;
}
bn = 0;//初始化填充指针
memcpy(disp_buf,clear_buf,32); //清显示缓冲
if (ntpstatus == 0){
putin_buf(15); //插入带点的空列,显示NTP校时异常;
}
else{
putin_buf(13); //在前面显示两列空格,让内容居中;
}
if(Rtctime.Hour()/10 != 0){
putin_buf(Rtctime.Hour()/10); //显示小时值(十位,为0时消隐)
putin_buf(Rtctime.Hour()%10); //显示小时值(个位)
}
else{
putin_buf(13); //十位为0时,插入2列空格保持显示居中
putin_buf(Rtctime.Hour()%10);//显示小时值(个位)
}
if(k == 1){
putin_buf(10); //显示冒号“:"
}
else{
putin_buf(13); //显示":"闪烁效果
}
if(Rtctime.Minute()/10 != 0){
putin_buf(Rtctime.Minute()/10); //显示分钟值(十位)
putin_buf(Rtctime.Minute()%10); //显示分钟值(个位)
}
else{
putin_buf(0);
putin_buf(Rtctime.Minute()%10); //显示分钟值(个位)
}
}
void Load_date(void){ //日期组合与显示
RtcDateTime Rtctime;
if (rtcstatus == 1){
Rtctime = Rtc.GetDateTime(); //读取RTC日期时间数据
}
else{
Rtctime = now(); //RTC不可用时,读取系统日期时间;
}
bn = 0; //初始化填充指针
memcpy(disp_buf,clear_buf,32); //清显示缓冲
if (ntpstatus == 0){
putin_buf(15); //插入带点的空列,显示NTP校时异常;
}
else{
putin_buf(13); //在前面显示两列空格,让内容居中;
}
if(Rtctime.Month()/10 != 0){
putin_buf(Rtctime.Month()/10); //显示月份值(十位)
putin_buf(Rtctime.Month()%10); //显示月份值(个位)
}
else{
putin_buf(13);//十位为0时,插入2列空格保持显示居中
putin_buf(Rtctime.Month()%10);//显示月份值(个位)
}
putin_buf(11); //显示日期分割符“-"
if(Rtctime.Day()/10 != 0){
putin_buf(Rtctime.Day()/10);//显示日期值(十位)
putin_buf(Rtctime.Day()%10);//显示日期值(个位)
}
else{
putin_buf(0);
putin_buf(Rtctime.Day()%10);//显示日期值(个位)
}
}
void Load_temp(void){ //温度组合与显示
float temp = 0;
if (rtcstatus == 1){
RtcTemperature Temperature = Rtc.GetTemperature(); //读取DS3231中的温度数据
temp = Temperature.AsFloat();
}
else{ //RTC不可用时,使用一个错误值代替;
temp = 99.0;
}
bn = 0; //初始化填充指针
memcpy(disp_buf,clear_buf,32); //清显示缓冲区
putin_buf(13); //在前面显示两列空格,让内容居中;
if((temp > 60.0)|(temp < -20.0)){
putin_buf(11); //超出温度范围,显示"--";
putin_buf(11);
}
else{
putin_buf(int(temp*10)/100); //显示温度值(十位)
putin_buf((int(temp*10)%100)/10);//显示温度值(个位)
putin_buf(14); //显示小数点“."
putin_buf(int(temp*10)%10); //显示温度值小数部分
}
putin_buf(12); //显示温度符号;
}
void Display() {
int num = 0;
for(int address=3; address>=0; address--){ //逐个扫描四个LED屏
for(int col=0;col<8;col++) { //每块LED屏逐列扫描;
lc.setColumn(address,col,disp); //使用列方式显示;
num ++;
}
}
}
void setup() {
Serial.begin(115200);
Serial.println();
//初始化LED屏
int devices=lc.getDeviceCount(); //获取LED屏的数量
for(int address=0; address<devices; address++) {
lc.shutdown(address,false); //初始化LED
lc.setIntensity(address,6); //设置LED亮度,0-15;
lc.clearDisplay(address); //清屏
}
memcpy(disp,connwifi,sizeof(connwifi)); //显示ConWiFi字样;
Display();
//连接WiFi
Serial.print("Connecting to ");
Serial.print(ssid);
WiFi.begin(ssid, pass);
unsigned int count = 0;
while ((count < 30 ) & (WiFi.status() != WL_CONNECTED)) {
delay(500);
Serial.print(".");
count ++;
}
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
//初始化UDP服务
Serial.print("Starting UDP. ");
udp.begin(localPort);
Serial.print("Local port: ");
Serial.println(udp.localPort());
//初始化RTC时钟,并设定本地系统时间
Rtc.Begin();
if (!Rtc.GetIsRunning())
{
Serial.println("RTC was not actively running, starting now");
Rtc.SetIsRunning(true);
if (!Rtc.GetIsRunning()){
Serial.println("RTC error!! Using System Time");
rtcstatus = 0; //设置RTC无效
RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__); //获取固件编译时间
setTime(compiled); //设定本地系统时间
}
}
else {
if (!Rtc.IsDateTimeValid())
{
Serial.println("RTC lost confidence in the DateTime!");
RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__); //获取固件编译时间
Rtc.SetDateTime(compiled); //设定RTC时间
}
Rtc.Enable32kHzPin(false);
Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone);
RtcDateTime rtctime = Rtc.GetDateTime();
setTime(rtctime); //获取RTC时间,设置本地系统时间
rtcstatus == 1; //设置RTC有效
Serial.print("Load RTC Time: "); printDateTime(rtctime);
Serial.println("\n");
}
}
void loop() {
scankey(); //按键扫描
if ((lastNtpupdate != millis() / NtpInterval) | (keys == 1)) { //依据计时或按键(短按)进入NTP更新
keys = 0;
WiFi.hostByName(ntpServerName, timeServerIP); //get a random server from the pool
sendNTPpacket(timeServerIP); // send an NTP packet to a time server
delay(1000); // wait to see if a reply is available
int cb = udp.parsePacket();
if (!cb) {
Serial.println("no packet yet\n"); //没有接收到NTP响应报文
ntpstatus = 0;
}
else { // 收到NTP响应报文
udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
//the timestamp starts at byte 40 of the received packet and is four bytes,
// or two words, long. First, esxtract the two words:
unsigned long highWord = word(packetBuffer, packetBuffer);
unsigned long lowWord = word(packetBuffer, packetBuffer);
unsigned long secsSince1900 = highWord << 16 | lowWord; //得到NTP时间,NTP返回的为自1900.01.01以来的秒数;
unsigned long realtime = secsSince1900 - Century + TimeZoneOffset; //转换为本地时间;系统时间与RTC一般是自2000.01.01开始;
setTime(realtime);//设置系统时间与NTP时间同步。
Rtc.SetDateTime(realtime); //RTC时钟与NTP时间同步
ntpstatus = 1;
Serial.println("Time Update!!");
Serial.print("Current time:"); printDateTime(realtime);
Serial.println("\n");
}
lastNtpupdate = millis() / NtpInterval;
}
if (millis() / 1000 != lastdisp){ //主显示程序,循环显示时间、日期、温度
lastdisp = millis() / 1000;
if (t <= 6){ //时间显示保持6秒
Load_time();
}
else if(t <= 8){ //日期显示保持2秒
Load_date();
}
else if(t <= 10){ //温度显示保持2秒
Load_temp();
}
else{
t = 0;
}
t ++;
memcpy(disp,disp_buf,sizeof(disp_buf)); //将待显缓冲区内容移入显示缓冲
Display();
k = 1 - k;
}
}
可以把时钟芯片改成1302吗?手头只有1302 78678967 发表于 2020-2-5 17:58
电路板 外壳都有 这是一个简约版本没有乱七八糟的传感器
百度云盘
https://pan.baidu.com/s/1AL4osak8bHywR ...
真不错
改良楼主的DS3231版本电子表
电路板 外壳都有 这是一个简约版本没有乱七八糟的传感器百度云盘
https://pan.baidu.com/s/1AL4osak8bHywRHEVATyU8w 78678967 发表于 2019-6-27 20:22
我觉得用8266 D1 mini 比较好焊接而且自带USB 和编译芯片价格和ESP-12差不多新手焊接不了贴片元器件 ...
:L本来想既然做板,就做的看起来利落点儿。如果还是弄几个模块插上去,其实跟拿个洞洞板连起来差不多。
如果感兴趣的多,等有空再画个使用分立器件的板子。这几天我先调调看看有没有其它问题。
78678967 发表于 2019-6-22 21:43
如何使用这个功能?smartconfig配置WiFi
smartconfig是个标准协议,原理及方法网上一搜一大把,玩这个还是要自己动动手。:L
给你这链接看一下:https://blog.csdn.net/weixin_33824363/article/details/85900191 78678967 发表于 2019-6-30 12:39
这个固件月份的十位有点小问题努力修改中
修改完成
顶起,不错的代码。
改进的地方就是可以更精确,因为网络授时实际上会有个时间差,一般情况下,在几十MS范围内,这个基本可以忽略
有时候网络情况不好,差个几秒也是完全可能的。
所以一般是要用发送到返回之间的时差来修正接收到的数据(这样更严谨一些)
当然,楼主的代码只要是网络正常是完全没问题的。 感谢大神的指点,确实本时钟没有考虑校时的网络延时误差。因为就日常用也没考虑这么精确。如果有更精确的要求,可以依据资料自行优化:lol 不错的帖子,之前有楼主也问有什么方式校准时钟时间最好,有用市电工频50HZ来弄的也有误差,我当时推荐的用GPS来做,问题就是GPS需要把天线拉倒屋外,但是时间准确度是最高的,一点误差没有。 日常生活,这个精度足够了。 感谢分享,就是没有看懂获取时间后怎么写入ds3231,请指教。 huangshan78 发表于 2017-9-7 11:16
感谢分享,就是没有看懂获取时间后怎么写入ds3231,请指教。
RtcDS3231.h库提供了设置接口;
Rtc.SetDateTime(realtime); //RTC时钟与NTP时间同步
上面的语句就是写DS3231的。 十分感谢:) 请问编译软件版本是多少 <TimeLib>发个库文件谢谢 我把楼主的程序改了一下,改用lcd1602显示 回头做一个试试,早就想做一个了只是一直没合适的方案 504835618 发表于 2017-10-14 18:34
请问编译软件版本是多少
Arduino 1.6.8
TimeLib库: 其实可以加温度,日期显示,那个weather station 的就可以用 这个真的是太棒了 一直再找这个资料