cityant 发表于 2017-6-25 11:27:35

基于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;
}
}

off-ice 发表于 2018-5-15 19:55:43

可以把时钟芯片改成1302吗?手头只有1302

cityant 发表于 2020-2-6 21:48:58

78678967 发表于 2020-2-5 17:58
电路板 外壳都有 这是一个简约版本没有乱七八糟的传感器
百度云盘
https://pan.baidu.com/s/1AL4osak8bHywR ...

真不错

78678967 发表于 2020-2-5 17:58:45

改良楼主的DS3231版本电子表

电路板 外壳都有 这是一个简约版本没有乱七八糟的传感器
百度云盘
https://pan.baidu.com/s/1AL4osak8bHywRHEVATyU8w

cityant 发表于 2019-6-28 09:43:06

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

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


cityant 发表于 2019-6-23 12:22:17

78678967 发表于 2019-6-22 21:43
如何使用这个功能?smartconfig配置WiFi

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

给你这链接看一下:https://blog.csdn.net/weixin_33824363/article/details/85900191

78678967 发表于 2019-6-30 21:08:26

78678967 发表于 2019-6-30 12:39
这个固件月份的十位有点小问题努力修改中
修改完成

darkorigin 发表于 2017-6-25 11:58:42

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

cityant 发表于 2017-6-25 17:44:41

感谢大神的指点,确实本时钟没有考虑校时的网络延时误差。因为就日常用也没考虑这么精确。如果有更精确的要求,可以依据资料自行优化:lol

PINKWALKMAN 发表于 2017-6-26 08:19:25

不错的帖子,之前有楼主也问有什么方式校准时钟时间最好,有用市电工频50HZ来弄的也有误差,我当时推荐的用GPS来做,问题就是GPS需要把天线拉倒屋外,但是时间准确度是最高的,一点误差没有。

老胖熊 发表于 2017-6-28 10:12:41

日常生活,这个精度足够了。

huangshan78 发表于 2017-9-7 11:16:41

感谢分享,就是没有看懂获取时间后怎么写入ds3231,请指教。

cityant 发表于 2017-9-9 13:02:50

huangshan78 发表于 2017-9-7 11:16
感谢分享,就是没有看懂获取时间后怎么写入ds3231,请指教。

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

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

上面的语句就是写DS3231的。

huangshan78 发表于 2017-9-20 17:55:00

十分感谢:)

504835618 发表于 2017-10-14 18:34:38

请问编译软件版本是多少

504835618 发表于 2017-11-5 10:16:17

<TimeLib>发个库文件谢谢

tzy81 发表于 2017-12-7 21:10:52

我把楼主的程序改了一下,改用lcd1602显示

lichun7667 发表于 2017-12-15 15:39:16

回头做一个试试,早就想做一个了只是一直没合适的方案

cityant 发表于 2017-12-21 19:10:25

504835618 发表于 2017-10-14 18:34
请问编译软件版本是多少

Arduino 1.6.8

TimeLib库:

zipcord 发表于 2017-12-28 16:26:02

其实可以加温度,日期显示,那个weather station 的就可以用

a463637283 发表于 2017-12-28 20:14:56

这个真的是太棒了 一直再找这个资料
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 基于ESP8266自动校时时钟