极客工坊

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 32799|回复: 193

基于ESP8266自动校时时钟

  [复制链接]
发表于 2017-6-25 11:27:35 | 显示全部楼层 |阅读模式
玩单片机以来比较喜欢折腾时钟,可能是比较简单的缘故。
之前用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模块时显示时间、日期;
无手动调整时间、日期功能。

代码:需要的库请看代码部分,自行下载
注:本人非码农,因此代码基本是网上扒来改一下,以能跑起来为原则,高手勿喷
  1. #include <LedControl.h>
  2. #include <ESP8266WiFi.h>
  3. #include <WiFiUdp.h>
  4. #include <Time.h>
  5. #include <TimeLib.h>
  6. #include <pgmspace.h>
  7. #include <Wire.h> // must be included here so that Arduino library object file references work
  8. #include <RtcDS3231.h>

  9. #define MX_CLOCK        2    // NodeMCU-D4   黑   CLK
  10. #define MX_CS           14   // NodeMCU-D5   白   CS
  11. #define MX_DIN          12   // NodeMCU-D6   灰   DataIn  
  12. #define KEY 0   //按键,GPIO0 NodeMCU Flash Key
  13. // I2C For ESP8266, these default to SDA = GPIO04(NodeMCU-D2) and SCL = GPIO05(NodeMCU-D1).

  14. LedControl lc=LedControl(MX_DIN, MX_CLOCK, MX_CS, 4);
  15. unsigned long lastdisp = millis();    //LED屏刷新标志
  16. unsigned char bn = 0;   // 显示缓冲区加载指针变量;
  17. byte clear_buf[32] = {byte(0)};
  18. byte disp[32] = {byte(0)};    //显示缓冲区;
  19. byte disp_buf[32] = {byte(0)};    //待显缓冲区;
  20. unsigned int k = 0;   //用于“:”闪烁
  21. unsigned int t = 0;   //用于切换日期时间显示


  22. RtcDS3231<TwoWire> Rtc(Wire);
  23. char ssid[] = "XXX";  //  WIFI SSID
  24. char pass[] = "xxxxx";       // WIFI password
  25. unsigned int localPort = 2390;      // UDP本地监听端口
  26. const unsigned long NtpInterval = 3600*1000UL;   //NTP自动校时间隔(毫秒)
  27. //const unsigned long SeventyYears = 2208988800UL;      //1900-1970的秒数
  28. //const unsigned long ThirtyYears = 946684800UL;       //1970-2000的秒数
  29. const unsigned long Century = 3155673600UL;        //1900.01.01-2000.01.01的秒数
  30. const long TimeZoneOffset = +28800;  //GMT +8
  31. unsigned long lastNtpupdate = millis();    //NTP校时标志
  32. unsigned int rtcstatus = 1;   //检测RTC是否有效
  33. unsigned int ntpstatus = 0;   //检测NTP同步状态
  34. unsigned int keys = 0;    //按键检测结果
  35. IPAddress timeServerIP; // 用于存放解析后的NTP server IP;
  36. const char* ntpServerName = "cn.ntp.org.cn";    //NTP服务器域名:尽量不要直接填写IP,
  37. const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
  38. byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets

  39. // A UDP instance to let us send and receive packets over UDP
  40. WiFiUDP udp;

  41. //-----------------------------------数字字符表
  42. unsigned char dp[][8]{
  43. // 5*8字模
  44. {0x7E,0x81,0x81,0x81,0x7E,'z'},      // -0-  字符0
  45. {0x00,0x41,0xFF,0x01,0x00,'z'},      // -1-  字符1
  46. {0x43,0x85,0x89,0x91,0x61,'z'},      // -2-  字符2
  47. {0x82,0x81,0x91,0xA9,0xC6,'z'},      // -3-  字符3
  48. {0x18,0x28,0x48,0xFF,0x08,'z'},      // -4-  字符4
  49. {0xF2,0x91,0x91,0x91,0x8E,'z'},      // -5-  字符5
  50. {0x7E,0x91,0x91,0x91,0x4E,'z'},      // -6-  字符6
  51. {0x80,0x8F,0x90,0xA0,0xC0,'z'},      // -7-  字符7
  52. {0x6E,0x91,0x91,0x91,0x6E,'z'},      // -8-  字符8
  53. {0x62,0x91,0x91,0x91,0x7E,'z'},      // -9-  字符9
  54. {0x66,0x66,'z'},                     // -10- ":"时间分隔符
  55. {0x18,0x18,0x18,'z'},                // -11- "-"日期分隔符
  56. {0xC0,0xC0,0x3C,0x66,0x42,0x42,0x24,'z'},  // -12- "oC"温度符号
  57. {0x00,0x00,'z'},                     // -13-  2空列
  58. {0x06,0x06,'z'},                     // -14-  "."小数点
  59. {0x01,0x00,'z'},                     // -15-  首列带点,用于NTP校时不成功时提醒
  60. };

  61. byte connwifi[32]{        //开机显示Connwifi;
  62. 0x7C,0x82,0x82,0x82,0x44,0x00,  // -C-
  63. 0x1C,0x22,0x22,0x22,0x1C,0x00,  // -o-
  64. 0x1E,0x20,0x20,0x1E,0x00,       // -n-
  65. 0x7E,0x04,0x18,0x04,0x7E,0x00,  // -W-
  66. 0x5E,0x00,                      // -i-
  67. 0x7E,0x50,0x50,0x50,0x00,       // -F-
  68. 0x5E,0x00,                           // -i-
  69. };

  70. unsigned long sendNTPpacket(IPAddress& address)   // send an NTP request to the time server at the given address
  71. {
  72.     Serial.print("sending NTP packet...");

  73.     memset(packetBuffer, 0, NTP_PACKET_SIZE);    // set all bytes in the buffer to 0
  74.     // Initialize values needed to form NTP request
  75.     // (see URL above for details on the packets)
  76.     packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  77.     packetBuffer[1] = 0;     // Stratum, or type of clock
  78.     packetBuffer[2] = 6;     // Polling Interval
  79.     packetBuffer[3] = 0xEC;  // Peer Clock Precision
  80.     // 8 bytes of zero for Root Delay & Root Dispersion
  81.     packetBuffer[12]  = 49;
  82.     packetBuffer[13]  = 0x4E;
  83.     packetBuffer[14]  = 49;
  84.     packetBuffer[15]  = 52;
  85.   
  86.     // all NTP fields have been given values, now
  87.     // you can send a packet requesting a timestamp:
  88.     udp.beginPacket(address, 123); //NTP requests are to port 123
  89.     udp.write(packetBuffer, NTP_PACKET_SIZE);
  90.     udp.endPacket();
  91. }

  92. #define countof(a) (sizeof(a) / sizeof(a[0]))

  93. void printDateTime(const RtcDateTime& dt)
  94. {
  95.     char datestring[20];

  96.     snprintf_P(datestring,
  97.             countof(datestring),
  98.             PSTR("%04u/%02u/%02u %02u:%02u:%02u"),
  99.             dt.Year(),
  100.             dt.Month(),
  101.             dt.Day(),
  102.             dt.Hour(),
  103.             dt.Minute(),
  104.             dt.Second() );
  105.     Serial.print(datestring);
  106. }

  107. void scankey()
  108. {
  109.   if (digitalRead(KEY) == 0){
  110.     unsigned int Keytime = 0;
  111.     while(!digitalRead(KEY)){
  112.       Keytime ++;
  113.       delay(10);
  114.       }
  115.     if (Keytime >= 300) keys = 2;
  116.     if (Keytime >= 2 & Keytime < 300) keys = 1;
  117.     }
  118.   }

  119. void putin_buf (unsigned char u){   //将字符装入显示缓冲区
  120.   
  121.   unsigned char a = 0;  //定义变量用于数据提取指针
  122.   do{   
  123.     disp_buf[bn] = dp[u][a];  //将二维数组中的一组数据放入显示缓冲区
  124.     a++;  //换下一个提取字节
  125.     bn++; //换下一个显示缓冲字节
  126.   }
  127.   while(dp[u][a] != 'z'); //当显示数据为'z'时结束循环
  128.   bn++;
  129.   disp_buf[bn] = 0; //显示一列的空位,便于字符区分
  130. }

  131. void Load_time(void){  //时间组合与显示

  132.   RtcDateTime Rtctime;
  133.   if (rtcstatus == 1){
  134.     Rtctime = Rtc.GetDateTime();   //读取RTC日期时间数据
  135.   }
  136.   else{
  137.     Rtctime = now();   //RTC不可用时,读取系统日期时间;
  138.   }
  139.   
  140.   bn = 0;  //初始化填充指针
  141.   memcpy(disp_buf,clear_buf,32);    //清显示缓冲
  142.   if (ntpstatus == 0){
  143.       putin_buf(15);    //插入带点的空列,显示NTP校时异常;
  144.   }
  145.   else{
  146.       putin_buf(13);     //在前面显示两列空格,让内容居中;
  147.   }
  148.   if(Rtctime.Hour()/10 != 0){
  149.     putin_buf(Rtctime.Hour()/10);   //显示小时值(十位,为0时消隐)
  150.     putin_buf(Rtctime.Hour()%10);   //显示小时值(个位)  
  151.   }
  152.   else{
  153.     putin_buf(13);   //  十位为0时,插入2列空格保持显示居中
  154.     putin_buf(Rtctime.Hour()%10);  //显示小时值(个位)   
  155.   }
  156.   if(k == 1){
  157.     putin_buf(10);    //显示冒号“:"
  158.   }
  159.   else{
  160.     putin_buf(13);    //显示":"闪烁效果
  161.   }
  162.   if(Rtctime.Minute()/10 != 0){
  163.     putin_buf(Rtctime.Minute()/10);   //显示分钟值(十位)
  164.     putin_buf(Rtctime.Minute()%10);   //显示分钟值(个位)  
  165.   }
  166.   else{
  167.     putin_buf(0);
  168.     putin_buf(Rtctime.Minute()%10);   //显示分钟值(个位)   
  169.   }
  170. }

  171. void Load_date(void){   //日期组合与显示

  172.   RtcDateTime Rtctime;
  173.   if (rtcstatus == 1){
  174.     Rtctime = Rtc.GetDateTime();   //读取RTC日期时间数据
  175.   }
  176.   else{
  177.     Rtctime = now();   //RTC不可用时,读取系统日期时间;
  178.   }
  179.   
  180.   bn = 0;   //初始化填充指针
  181.   memcpy(disp_buf,clear_buf,32);    //清显示缓冲
  182.   if (ntpstatus == 0){
  183.       putin_buf(15);    //插入带点的空列,显示NTP校时异常;
  184.   }
  185.   else{
  186.       putin_buf(13);     //在前面显示两列空格,让内容居中;
  187.   }
  188.   if(Rtctime.Month()/10 != 0){
  189.     putin_buf(Rtctime.Month()/10);   //显示月份值(十位)
  190.     putin_buf(Rtctime.Month()%10);   //显示月份值(个位)  
  191.   }
  192.   else{
  193.     putin_buf(13);  //  十位为0时,插入2列空格保持显示居中
  194.     putin_buf(Rtctime.Month()%10);  //显示月份值(个位)   
  195.   }
  196.   putin_buf(11);    //显示日期分割符“-"

  197.   if(Rtctime.Day()/10 != 0){
  198.     putin_buf(Rtctime.Day()/10);  //显示日期值(十位)
  199.     putin_buf(Rtctime.Day()%10);  //显示日期值(个位)  
  200.   }
  201.   else{
  202.     putin_buf(0);
  203.     putin_buf(Rtctime.Day()%10);  //显示日期值(个位)   
  204.   }
  205. }

  206. void Load_temp(void){   //温度组合与显示
  207.   
  208.   float temp = 0;
  209.   if (rtcstatus == 1){
  210.     RtcTemperature Temperature = Rtc.GetTemperature();    //  读取DS3231中的温度数据
  211.     temp = Temperature.AsFloat();
  212.   }
  213.   else{   //RTC不可用时,使用一个错误值代替;
  214.     temp = 99.0;
  215.   }
  216.   
  217.   bn = 0; //初始化填充指针
  218.   memcpy(disp_buf,clear_buf,32);    //清显示缓冲区
  219.   putin_buf(13);    //在前面显示两列空格,让内容居中;
  220.   if((temp > 60.0)|(temp < -20.0)){
  221.     putin_buf(11);   //超出温度范围,显示"--";
  222.     putin_buf(11);
  223.   }
  224.   else{
  225.     putin_buf(int(temp*10)/100);     //显示温度值(十位)
  226.     putin_buf((int(temp*10)%100)/10);//显示温度值(个位)
  227.     putin_buf(14);                   //显示小数点“."
  228.     putin_buf(int(temp*10)%10);     //显示温度值小数部分
  229.   }
  230.   putin_buf(12);                  //显示温度符号;
  231. }

  232. void Display() {

  233.   int num = 0;
  234.   for(int address=3; address>=0; address--){    //逐个扫描四个LED屏
  235.     for(int col=0;col<8;col++) {    //每块LED屏逐列扫描;
  236.       lc.setColumn(address,col,disp[num]);    //使用列方式显示;
  237.       num ++;
  238.     }
  239.   }
  240. }

  241. void setup() {
  242.   
  243.   Serial.begin(115200);
  244.   Serial.println();
  245.   
  246.   //初始化LED屏
  247.   int devices=lc.getDeviceCount();    //获取LED屏的数量
  248.   for(int address=0; address<devices; address++) {
  249.       lc.shutdown(address,false);   //初始化LED
  250.       lc.setIntensity(address,6);   //设置LED亮度,0-15;
  251.       lc.clearDisplay(address);   //清屏
  252.   }
  253.   memcpy(disp,connwifi,sizeof(connwifi));   //显示ConWiFi字样;
  254.   Display();
  255.       
  256.   //连接WiFi
  257.   Serial.print("Connecting to ");
  258.   Serial.print(ssid);
  259.   WiFi.begin(ssid, pass);
  260.   unsigned int count = 0;
  261.   while ((count < 30 ) & (WiFi.status() != WL_CONNECTED)) {
  262.       delay(500);
  263.       Serial.print(".");
  264.       count ++;
  265.   }
  266.   Serial.println("WiFi connected");
  267.   Serial.print("IP address: ");
  268.   Serial.println(WiFi.localIP());
  269.   
  270.   //初始化UDP服务
  271.   Serial.print("Starting UDP. ");
  272.   udp.begin(localPort);
  273.   Serial.print("Local port: ");
  274.   Serial.println(udp.localPort());

  275.   //初始化RTC时钟,并设定本地系统时间
  276.   Rtc.Begin();
  277.   if (!Rtc.GetIsRunning())
  278.   {
  279.       Serial.println("RTC was not actively running, starting now");
  280.       Rtc.SetIsRunning(true);
  281.       if (!Rtc.GetIsRunning()){
  282.           Serial.println("RTC error!! Using System Time");
  283.           rtcstatus = 0;   //设置RTC无效
  284.           RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__);   //获取固件编译时间
  285.           setTime(compiled);    //设定本地系统时间
  286.       }
  287.   }
  288.   else {
  289.       if (!Rtc.IsDateTimeValid())
  290.       {
  291.           Serial.println("RTC lost confidence in the DateTime!");
  292.           RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__);   //获取固件编译时间
  293.           Rtc.SetDateTime(compiled);    //设定RTC时间
  294.       }
  295.       
  296.       Rtc.Enable32kHzPin(false);
  297.       Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone);
  298.       RtcDateTime rtctime = Rtc.GetDateTime();
  299.       setTime(rtctime);   //获取RTC时间,设置本地系统时间
  300.       rtcstatus == 1;   //设置RTC有效
  301.       Serial.print("Load RTC Time: "); printDateTime(rtctime);
  302.       Serial.println("\n");
  303.   }
  304. }

  305. void loop() {

  306.   scankey();    //  按键扫描
  307.   if ((lastNtpupdate != millis() / NtpInterval) | (keys == 1)) {    //依据计时或按键(短按)进入NTP更新

  308.       keys = 0;
  309.       WiFi.hostByName(ntpServerName, timeServerIP);     //get a random server from the pool
  310.       sendNTPpacket(timeServerIP);    // send an NTP packet to a time server
  311.       delay(1000);      // wait to see if a reply is available
  312.       int cb = udp.parsePacket();
  313.       if (!cb) {
  314.           Serial.println("no packet yet\n");    //没有接收到NTP响应报文
  315.           ntpstatus = 0;
  316.       }
  317.       else {        // 收到NTP响应报文
  318.           udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
  319.           //the timestamp starts at byte 40 of the received packet and is four bytes,
  320.           // or two words, long. First, esxtract the two words:
  321.           unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
  322.           unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
  323.           unsigned long secsSince1900 = highWord << 16 | lowWord;     //  得到NTP时间,NTP返回的为自1900.01.01以来的秒数;
  324.           unsigned long realtime = secsSince1900 - Century + TimeZoneOffset;    //  转换为本地时间;系统时间与RTC一般是自2000.01.01开始;
  325.           setTime(realtime);  //  设置系统时间与NTP时间同步。
  326.           Rtc.SetDateTime(realtime);    //  RTC时钟与NTP时间同步
  327.           ntpstatus = 1;
  328.           Serial.println("Time Update!!");
  329.           Serial.print("Current time:"); printDateTime(realtime);
  330.           Serial.println("\n");
  331.       }
  332.       lastNtpupdate = millis() / NtpInterval;
  333.   }

  334.   if (millis() / 1000 != lastdisp){   //主显示程序,循环显示时间、日期、温度
  335.       lastdisp = millis() / 1000;
  336.       if (t <= 6){    //时间显示保持6秒
  337.         Load_time();
  338.       }
  339.       else if(t <= 8){    //日期显示保持2秒
  340.         Load_date();
  341.       }
  342.       else if(t <= 10){   //温度显示保持2秒
  343.         Load_temp();
  344.       }
  345.       else{
  346.         t = 0;
  347.       }
  348.       t ++;
  349.       memcpy(disp,disp_buf,sizeof(disp_buf));   //将待显缓冲区内容移入显示缓冲
  350.       Display();
  351.       k = 1 - k;
  352.   }
  353. }
复制代码

全部器件

全部器件

NodeMCU和DS3231

NodeMCU和DS3231

Max7219 LED屏

Max7219 LED屏
回复

使用道具 举报

 楼主| 发表于 2019-6-28 09:43:06 | 显示全部楼层
78678967 发表于 2019-6-27 20:22
我觉得用8266 D1 mini 比较好焊接而且自带USB 和编译芯片价格和ESP-12差不多新手焊接不了贴片元器件 ...

本来想既然做板,就做的看起来利落点儿。如果还是弄几个模块插上去,其实跟拿个洞洞板连起来差不多。
如果感兴趣的多,等有空再画个使用分立器件的板子。这几天我先调调看看有没有其它问题。


回复 支持 1 反对 0

使用道具 举报

 楼主| 发表于 2019-6-23 12:22:17 | 显示全部楼层
78678967 发表于 2019-6-22 21:43
如何使用这个功能?smartconfig配置WiFi

smartconfig是个标准协议,原理及方法网上一搜一大把,玩这个还是要自己动动手。

给你这链接看一下:https://blog.csdn.net/weixin_33824363/article/details/85900191
回复 支持 1 反对 0

使用道具 举报

发表于 2019-6-30 21:08:26 | 显示全部楼层
78678967 发表于 2019-6-30 12:39
这个固件月份的十位有点小问题努力修改中

修改完成

NTPkndskffds.zip

1.61 MB, 下载次数: 106

回复 支持 1 反对 0

使用道具 举报

发表于 2017-6-25 11:58:42 | 显示全部楼层
顶起,不错的代码。
改进的地方就是可以更精确,因为网络授时实际上会有个时间差,一般情况下,在几十MS范围内,这个基本可以忽略
有时候网络情况不好,差个几秒也是完全可能的。
所以一般是要用发送到返回之间的时差来修正接收到的数据(这样更严谨一些)
当然,楼主的代码只要是网络正常是完全没问题的。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2017-6-25 17:44:41 | 显示全部楼层
感谢大神的指点,确实本时钟没有考虑校时的网络延时误差。因为就日常用也没考虑这么精确。如果有更精确的要求,可以依据资料自行优化
回复 支持 反对

使用道具 举报

发表于 2017-6-26 08:19:25 | 显示全部楼层
不错的帖子,之前有楼主也问有什么方式校准时钟时间最好,有用市电工频50HZ来弄的也有误差,我当时推荐的用GPS来做,问题就是GPS需要把天线拉倒屋外,但是时间准确度是最高的,一点误差没有。
回复 支持 反对

使用道具 举报

发表于 2017-6-28 10:12:41 | 显示全部楼层
日常生活,这个精度足够了。
回复 支持 反对

使用道具 举报

发表于 2017-9-7 11:16:41 | 显示全部楼层
感谢分享,就是没有看懂获取时间后怎么写入ds3231,请指教。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2017-9-9 13:02:50 | 显示全部楼层
huangshan78 发表于 2017-9-7 11:16
感谢分享,就是没有看懂获取时间后怎么写入ds3231,请指教。

RtcDS3231.h库提供了设置接口;

Rtc.SetDateTime(realtime);    //  RTC时钟与NTP时间同步

上面的语句就是写DS3231的。
回复 支持 反对

使用道具 举报

发表于 2017-10-14 18:34:38 | 显示全部楼层
请问编译软件版本是多少
回复 支持 反对

使用道具 举报

发表于 2017-11-5 10:16:17 | 显示全部楼层
<TimeLib>发个库文件谢谢
回复 支持 反对

使用道具 举报

发表于 2017-12-7 21:10:52 | 显示全部楼层
我把楼主的程序改了一下,改用lcd1602显示
IMG_20171207_150550.jpg
IMG_20171205_210603.jpg
回复 支持 反对

使用道具 举报

发表于 2017-12-15 15:39:16 | 显示全部楼层
回头做一个试试,早就想做一个了只是一直没合适的方案
回复 支持 反对

使用道具 举报

 楼主| 发表于 2017-12-21 19:10:25 | 显示全部楼层
504835618 发表于 2017-10-14 18:34
请问编译软件版本是多少

Arduino 1.6.8

TimeLib库:

Time-master.zip

27.62 KB, 下载次数: 174

回复 支持 反对

使用道具 举报

发表于 2017-12-28 16:26:02 | 显示全部楼层
其实可以加温度,日期显示,那个weather station 的就可以用
回复 支持 反对

使用道具 举报

发表于 2017-12-28 20:14:56 | 显示全部楼层
这个真的是太棒了 一直再找这个资料
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|联系我们|极客工坊 ( 浙ICP备09023225号 )

GMT+8, 2019-12-13 06:28 , Processed in 0.092983 second(s), 28 queries .

Powered by Discuz! X3.4 Licensed

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表