零零碎碎隔段时间捣鼓一下，历时几个月，终于把网络时钟制作齐了我想要的功能。外观看图。这个闹钟功能比较全面了，采用了D1 mini开发板（esp8266）、LCD1602显示屏、DS1302时钟芯片、DHT11温湿度模块、无源蜂鸣器等配件，Arduino开发，实现的功能列表如下。显示日期、星期、时间和温湿度，外带一个Wifi小图标。可以联网自动对时。断电不影响走时（DS1302）。可设置一路闹钟，通过web页面设置，响铃为两只老虎旋律。可web页面配置wifi，没有设置wifi时自动开启wifi热点，以供手机链接时钟进行配置。两个按钮，一个重启模块，一个清楚WiFi配置信息。下面是进行WiFi设置和闹钟设置的两个网页。程序代码如下：/* ---By www.yizu.org--- Connect D1 mini I2C_LCD 5V VCC GND GND D2 SDA D1 SCL D1 mini DS1302 3V3 VCC GND GND D6 CLK D7 DAT D8 RST */ #include <ESP8266WiFi.h> #include <EEPROM.h> #include <WiFiUdp.h> #include <NTPClient.h> #include <LiquidCrystal_I2C.h> #include <TimeLib.h> #include <Arduino.h> #include <DS1302.h> #include <dht11.h> #include <ESP8266WebServer.h> #include <FS.h> LiquidCrystal_I2C lcd(0x27, 16, 2); int32_t t_xd=0; //定义web服务 ESP8266WebServer server(80); //设置NTPClient #define NTP_OFFSET 60 * 60 * 8 // 时区偏移量 #define NTP_ADDRESS "ntp1.aliyun.com" // NTP服务器 WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, NTP_ADDRESS, NTP_OFFSET); //设置时钟模块 DS1302 rtc(12,13,15); //设置校对时间间隔（单位秒） #define CALIBRATION_INTERVAL 60 unsigned long t_stamp=0; unsigned long t_stamp2=0; //设置蜂鸣器 #define BUZZER_PIN 14 //设置D5脚 #define ALARM_INTERVAL 300 //一个节拍的时间/毫秒 unsigned long b_stamp = 0; unsigned long b_stamp2 = 0; unsigned long duration = 0;//闹铃持续时间 boolean isAlarm=0;//蜂鸣器发声标记 uint8_t tonesTag=0; uint8_t tonesLength=33;//长度 //两只老虎音乐 char toneNotes[] = "cdeccdecefgefggagfecgagfecdgcdgc "; //'c', 'd', 'e', 'f', 'g', 'a', 'b', 'C' float beats[] = { 1,1,1,1,1,1,1,1,1,1,2,1,1,2,0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1,1,1,2,1,1,2,4 }; bool ringing=false; //闹钟设置 bool alarm_on=false; int alarm_hour=0; int alarm_min=0; //设置AP账号密码 const char* ssid = "myAP000"; const char* password = "12345678"; //设置温湿度传感器 #define DHT11PIN 2//定义dht11引脚 dht11 myDht; int tem=0;//温度 int hum=0;//湿度 //显示字符 #if defined(ARDUINO) && ARDUINO >= 100 #define printByte(args) write(args); #else #define printByte(args) print(args,BYTE); #endif //要显示的特殊字符 //uint8_t nian[8] = {0x08,0x0f,0x12,0x0f,0x0a,0x1f,0x02,0x02,};//年 //uint8_t yue[8] = {0x0f,0x09,0x0f,0x09,0x0f,0x09,0x0b,0x11,};//月 //uint8_t ri[8] = {0x1F,0x11,0x11,0x1F,0x11,0x11,0x1F,0x00,};//日 uint8_t dian[8] = {0x02,0x05,0x02,0x00,0x00,0x00,0x00,0x00,};//点 uint8_t dianC[8] ={0x00,0x17,0x08,0x08,0x08,0x08,0x07,0x00,};//温度单位 uint8_t wificon[8] ={0x00,0x0E,0x11,0x04,0x0A,0x00,0x04,0x00,};//WIFI链接 uint8_t wificon2[8]={0x00,0x11,0x0A,0x04,0x0A,0x11,0x00,0x00,};//断网 const char * days[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"} ; //连接WIFI void connectToWiFi() { //读取eeprom EEPROM.begin(68); //读取闹铃设置 if (EEPROM.read(66) == 1){ alarm_on = true; } else{ alarm_on = false; } alarm_hour = EEPROM.read(64); alarm_min = EEPROM.read(65); //读取闹铃设置结束 byte len=EEPROM.read(0); if(len>0&&len<30){ char ssid2[len+1]; for(int i=0;i<len;i++){ ssid2[i]=EEPROM.read(i+1); } ssid2[len]='\0'; Serial.printf("ssid:%s\n",ssid2); byte len2=EEPROM.read(32); if(len2>0&&len2<30){ char passwd2[len2+1]; for (int i = 0; i < len2; i++){ passwd2[i] = EEPROM.read(i + 32+1); } passwd2[len2]='\0'; EEPROM.end(); Serial.printf("password:%s\n",passwd2); //Serial.printf("password Lenth:%d\n",len2); WiFi.mode(WIFI_STA); WiFi.begin(ssid2,passwd2); Serial.println(""); Serial.print("Connected to WiFi at "); Serial.print(WiFi.localIP()); Serial.println(""); unsigned int count = 0; while ((count < 10) & (WiFi.status() != WL_CONNECTED)){ delay(500); Serial.print("."); count++; } }else{ WiFi.mode(WIFI_STA); WiFi.begin(ssid2); Serial.println(""); Serial.print("Connected to WiFi at "); Serial.print(WiFi.localIP()); Serial.println(""); unsigned int count = 0; while ((count < 10) & (WiFi.status() != WL_CONNECTED)){ delay(500); Serial.print("."); count++; } } }else{ WiFi.mode(WIFI_AP); WiFi.softAP(ssid, password); Serial.println(""); Serial.print("AP IP address:"); Serial.println(WiFi.softAPIP()); } } //显示WIFI连接状态 void showWifiStatus() { lcd.setCursor(11, 0); if(WiFi.status() == WL_CONNECTED) { lcd.write(byte(5)); } else { lcd.write(byte(6)); } } //校对时间 void correctionTime() { time_t tnow=now(); if(t_stamp==0||tnow-t_stamp>=5)//每10秒校对系统时钟 { Serial.println(""); Serial.print(rtc.getTimeStr()); Serial.print(" Correction the time."); if(WiFi.status() == WL_CONNECTED&&(t_stamp2==0||tnow-t_stamp2>=CALIBRATION_INTERVAL)) { if(timeClient.update()) { unsigned long t=timeClient.getEpochTime(); //设置系统时间 setTime(t); Serial.println(""); Serial.print("SysTime=NTP:"); Serial.printf("%02d:%02d:%02d",hour(t), minute(t), second(t)); //设置RTC时钟 if(rtc.peek(0)==88) { rtc.writeProtect(false); rtc.setDOW(weekday(t)+1); rtc.setTime(hour(t), minute(t), second(t)); rtc.setDate(day(t), month(t), year(t)); rtc.writeProtect(true); } } else { if(rtc.peek(0)==88) { //设置系统时间 Time t=rtc.getTime(); setTime(t.hour,t.min,t.sec,t.date,t.mon,t.year); Serial.println(""); Serial.print("SysTime=RTC:"); Serial.printf("%02d:%02d:%02d",t.hour,t.min,t.sec); } } t_stamp2=now(); } else { if(rtc.peek(0)==88) { //设置系统时间 Time t=rtc.getTime(); setTime(t.hour,t.min,t.sec,t.date,t.mon,t.year); Serial.println(""); Serial.print("SysTime=RTC:"); Serial.printf("%02d:%02d:%02d",t.hour,t.min,t.sec); } } t_stamp=now(); tempAndHun();//读取温度 } } void tempAndHun() { //-------------------------- Serial.println("\n"); int chk = myDht.read(DHT11PIN); Serial.print("Read sensor: "); switch (chk) { case DHTLIB_OK: Serial.println("OK"); break; case DHTLIB_ERROR_CHECKSUM: Serial.println("Checksum error"); break; case DHTLIB_ERROR_TIMEOUT: Serial.println("Time out error"); break; default: Serial.println("Unknown error"); break; } tem=myDht.temperature; hum=myDht.humidity; Serial.print("Humidity (%): "); Serial.println(hum); Serial.print("Temperature (oC): "); Serial.println(tem); } //音频频率 unsigned int toneFrequency(char note) { char names[] = { 'c', 'd', 'e', 'f', 'g', 'a', 'b', 'C' }; int tones[] = { 261, 294, 329, 349, 392, 440, 493, 523 }; for (int i = 0; i < 8; i++) { if (names[i] == note) { return tones[i]; } } } //驱动蜂鸣器 void alarm() { unsigned long currentMillis =millis(); if(b_stamp2==0) { b_stamp2=currentMillis; } if(currentMillis-b_stamp2<duration) { if(!isAlarm) { if(b_stamp==0) { tone(BUZZER_PIN,toneFrequency(toneNotes[tonesTag])); isAlarm=true; b_stamp=currentMillis; } else if(currentMillis-b_stamp>0.2*ALARM_INTERVAL) { if(toneNotes[tonesTag]!=' ') { tone(BUZZER_PIN,toneFrequency(toneNotes[tonesTag])); } isAlarm=true; b_stamp=currentMillis; } } else { if(currentMillis-b_stamp>beats[tonesTag]*ALARM_INTERVAL) { noTone(BUZZER_PIN); isAlarm=false; b_stamp=currentMillis; if(tonesTag<tonesLength-1) { tonesTag++; } else { tonesTag=0; } } } } else { b_stamp2=0; duration=0; noTone(BUZZER_PIN); } } void handleRouter() { String path = server.uri(); if(path=="/"){ path="/index.html"; } if (SPIFFS.exists(path)){ File file=SPIFFS.open(path,"r"); String contentType="text/html"; if(path.substring(path.lastIndexOf('.'))==".css"){ contentType="text/css"; } server.streamFile(file,contentType); file.close(); } else{ server.send(404, "text/plain", "File Not Found!\n\n"); } } void jsonRouter() { if(server.hasArg("nz")){ EEPROM.begin(68); // 4的倍数 String json = "{\"alarm\":\""; json+=EEPROM.read(66); json+="\",\"hour\":\""; json+=EEPROM.read(64); json+="\",\"min\":\""; json+=EEPROM.read(65); json+="\"}"; EEPROM.end(); server.send(200, "text/plain",json); } else{ server.send(404, "text/plain", "File Not Found!\n\n"); } } void postRouter() { EEPROM.begin(68);//4的倍数 if(server.hasArg("ssid")){ String ssid2 = server.arg("ssid"); Serial.printf("Write ssid:%s\n",ssid2.c_str()); EEPROM.write(0,ssid2.length()); for(int i=0;i<ssid2.length();i++){ EEPROM.write(i+1,ssid2[i]); } } if(server.hasArg("passwd")){ String passwd2 = server.arg("passwd"); passwd2.trim(); Serial.printf("Write password:%s\n",passwd2.c_str()); if(passwd2.length()>0){ EEPROM.write(32, passwd2.length()); for (int i = 0; i < passwd2.length(); i++) { EEPROM.write(i + 32 + 1, passwd2[i]); } }else{ EEPROM.write(32,0); } } //post?alarm=on&hour=03&min=02 if(server.hasArg("hour")){ duration==0; String h=server.arg("hour"); EEPROM.write(64,byte(h.toInt())); if (server.hasArg("min")){ String m = server.arg("min"); EEPROM.write(65, byte(m.toInt())); } if (server.hasArg("alarm")){ EEPROM.write(66, 1); } else{ EEPROM.write(66, 0); } } EEPROM.end(); server.send(200, "text/plain", "设置成功，即将重启时钟！\n\n"); delay(200); ESP.restart(); } void setup() { Serial.begin(115200); lcd.init();// 初始化lcd lcd.backlight(); lcd.setCursor(0, 0); lcd.print("Starting..."); lcd.setCursor(0, 1); lcd.print("Please wait."); //联网 connectToWiFi(); //初始化NTPClient timeClient.begin(); //开启时钟 if(rtc.peek(0)!=88) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("System reseted!"); rtc.writeProtect(false); rtc.halt(false); rtc.poke(0,88);//写入启动标记 rtc.writeProtect(true); delay(1000); } //对时 correctionTime(); //读取温湿度 tempAndHun(); lcd.clear(); pinMode(BUZZER_PIN,OUTPUT); //开启web服务 SPIFFS.begin(); server.on("/",handleRouter); server.on("/post",postRouter); server.on("/json",jsonRouter); server.onNotFound(handleRouter); server.begin(); Serial.println("HTTP server started"); pinMode(16,INPUT); } void loop() { correctionTime(); // 打印到屏幕 //lcd.createChar(1, nian); //lcd.createChar(2, yue); //lcd.createChar(3, ri); lcd.createChar(4, dianC); lcd.createChar(5, wificon); lcd.createChar(6, wificon2); time_t t=now(); lcd.setCursor(0, 0); //lcd.printf("%04d",year(t)); //lcd.write(byte(1)); //lcd.printf("%02d",month(t)); //lcd.write(byte(2)); //lcd.printf("%02d",day(t)); //lcd.write(byte(3)); lcd.printf("%04d-%02d-%02d",year(t),month(t),day(t)); lcd.setCursor(9, 1); lcd.print(days[weekday(t)-1]); lcd.setCursor(0, 1); lcd.printf("%02d:%02d:%02d",hour(t),minute(t),second(t)); lcd.setCursor(13, 0); lcd.print(tem);//显示温度 lcd.write(byte(4)); lcd.setCursor(13, 1); lcd.print(hum);//显示湿度 lcd.print("%"); showWifiStatus(); server.handleClient(); //长按下按钮2，清除wifi设置 if(digitalRead(16)==LOW){ if(t_xd==0){ t_xd=millis(); duration=0; }else{ if(millis()-t_xd>2000){ EEPROM.begin(64); EEPROM.write(0, 0); EEPROM.end(); Serial.println("EEPROM is clear."); delay(200); ESP.reset(); } } }else{ if(t_xd!=0) t_xd=0; } //闹钟 if(hour(t)==alarm_hour&&minute(t)==alarm_min){ if(duration==0&&!ringing&&alarm_on){ duration = 50000;//设置持续时间 ringing=true; } } else{ ringing=false; } alarm(); }零敲碎打的，一点一点加上的功能，所以代码比较乱，但用也是一个比较完整的Arduino开发实例，用到的知识点还是比较多的，用来学习不错。完整代码打包下载。
这里的esp8266选用D1 mini开发板，DS1302加I2C转接模块。首先上效果图。接着上代码。/* ---By www.yizu.org--- Connect D1 mini I2C_LCD 5V VCC GND GND D2 SDA D1 SCL D1 mini DS1302 3V3 VCC GND GND D6 CLK D7 DAT D8 RST */ #include <ESP8266WiFi.h> #include <WiFiUdp.h> #include <NTPClient.h> #include <LiquidCrystal_I2C.h> #include <TimeLib.h> #include <Arduino.h> #include <DS1302.h> LiquidCrystal_I2C lcd(0x27, 16, 2); //设置NTPClient #define NTP_OFFSET 60 * 60 * 8 // 时区偏移量 #define NTP_ADDRESS "ntp1.aliyun.com" // NTP服务器 WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, NTP_ADDRESS, NTP_OFFSET); //设置时钟模块 DS1302 rtc(15,13,12); //设置校对时间间隔（单位秒） #define CALIBRATION_INTERVAL 60 unsigned long t_stamp=0; unsigned long t_stamp2=0; //设置WIFI账号密码 const char* ssid = "wifi"; const char* password = "123456789"; //显示字符 #if defined(ARDUINO) && ARDUINO >= 100 #define printByte(args) write(args); #else #define printByte(args) print(args,BYTE); #endif //要显示的特殊字符 //uint8_t nian[8] = {0x08,0x0f,0x12,0x0f,0x0a,0x1f,0x02,0x02,};//年 //uint8_t yue[8] = {0x0f,0x09,0x0f,0x09,0x0f,0x09,0x0b,0x11,};//月 //uint8_t ri[8] = {0x1F,0x11,0x11,0x1F,0x11,0x11,0x1F,0x00,};//日 uint8_t dian[8] = {0x02,0x05,0x02,0x00,0x00,0x00,0x00,0x00,};//点 uint8_t dianC[8] ={0x00,0x17,0x08,0x08,0x08,0x08,0x07,0x00,};//温度单位 uint8_t wificon[8] ={0x00,0x0E,0x11,0x04,0x0A,0x00,0x04,0x00,};//WIFI链接 uint8_t wificon2[8]={0x00,0x11,0x0A,0x04,0x0A,0x11,0x00,0x00,};//断网 const char * days[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"} ; //连接WIFI void connectToWiFi() { Serial.println(""); Serial.print("Connecting to "); Serial.print(ssid); WiFi.begin(ssid, password); Serial.println(""); Serial.print("Connected to WiFi at "); Serial.print(WiFi.localIP()); Serial.println(""); unsigned int count = 0; while ((count < 10 ) & (WiFi.status() != WL_CONNECTED)) { delay(500); Serial.print("."); count ++; } } //显示WIFI连接状态 void showWifiStatus() { lcd.setCursor(11, 0); if(WiFi.status() == WL_CONNECTED) { lcd.write(byte(5)); } else { lcd.write(byte(6)); } } //校对时间 void correctionTime() { time_t tnow=now(); if(t_stamp==0||tnow-t_stamp>=10)//每10秒校对系统时钟 { Serial.println(""); Serial.print(rtc.getTimeStr()); Serial.print(" Correction the time."); if(WiFi.status() == WL_CONNECTED&&(t_stamp2==0||tnow-t_stamp2>=CALIBRATION_INTERVAL)) { if(timeClient.update()) { unsigned long t=timeClient.getEpochTime(); //设置系统时间 setTime(t); Serial.println(""); Serial.print("SysTime=NTP:"); Serial.printf("%02d:%02d:%02d",hour(t), minute(t), second(t)); //设置RTC时钟 if(rtc.peek(0)==88) { rtc.writeProtect(false); rtc.setDOW(weekday(t)+1); rtc.setTime(hour(t), minute(t), second(t)); rtc.setDate(day(t), month(t), year(t)); rtc.writeProtect(true); } } else { if(rtc.peek(0)==88) { //设置系统时间 Time t=rtc.getTime(); setTime(t.hour,t.min,t.sec,t.date,t.mon,t.year); Serial.println(""); Serial.print("SysTime=RTC:"); Serial.printf("%02d:%02d:%02d",t.hour,t.min,t.sec); } } t_stamp2=now(); } else { if(rtc.peek(0)==88) { //设置系统时间 Time t=rtc.getTime(); setTime(t.hour,t.min,t.sec,t.date,t.mon,t.year); Serial.println(""); Serial.print("SysTime=RTC:"); Serial.printf("%02d:%02d:%02d",t.hour,t.min,t.sec); } } t_stamp=now(); } } void setup() { Serial.begin(115200); lcd.init();// 初始化lcd lcd.backlight(); lcd.setCursor(0, 0); lcd.print("Starting..."); lcd.setCursor(0, 1); lcd.print("Please wait."); //联网 connectToWiFi(); //初始化NTPClient timeClient.begin(); //开启时钟 if(rtc.peek(0)!=88) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("System reseted!"); rtc.writeProtect(false); rtc.halt(false); rtc.poke(0,88);//写入启动标记 rtc.writeProtect(true); delay(1000); } //对时 correctionTime(); lcd.clear(); } void loop() { correctionTime(); // 打印到屏幕 //lcd.createChar(1, nian); //lcd.createChar(2, yue); //lcd.createChar(3, ri); lcd.createChar(4, dianC); lcd.createChar(5, wificon); lcd.createChar(6, wificon2); time_t t=now(); lcd.setCursor(0, 0); //lcd.printf("%04d",year(t)); //lcd.write(byte(1)); //lcd.printf("%02d",month(t)); //lcd.write(byte(2)); //lcd.printf("%02d",day(t)); //lcd.write(byte(3)); lcd.printf("%04d-%02d-%02d",year(t),month(t),day(t)); lcd.setCursor(9, 1); lcd.print(days[weekday(t)-1]); lcd.setCursor(0, 1); lcd.printf("%02d:%02d:%02d",hour(t),minute(t),second(t)); lcd.setCursor(13, 0); lcd.print("30");//显示温度，暂未加入该功能 lcd.write(byte(4)); lcd.setCursor(13, 1); lcd.print("25%");//显示湿度，暂未加入该功能 showWifiStatus(); }DS1302模块的库下载自http://www.rinkydinkelectronics.com/library.php?id=5，其他库均可在Arduino IDE中搜索下载。此段代码可以做到没有网络，或者去掉DS1302时都可正常运行。代码中，每隔10秒系统与DS1302模块校对时间，每隔60秒与NTP服务器校对时间，可以按需修改即可。屏幕上显示了一个WIFI连接的图标，WIFI断开时会显示一个“X”。
这里用了两个esp8266设备，分别是NodeMCU和Wemos D1 mini两个模块，Arduino设备是Arduino UNO。设备如下图示。三个设备的引脚说明如图，后面连接屏幕要用到。显示设备Lcd1602，能显示两行，每行16个字，后面焊接了IIC接口转接模块(PCA8574)，如图。IIC接口有GND、VCC、SDA、SCL三个接口，分别根据上面的引脚图，找到想要的引脚连接（NodeMcu的SCL与SDA对应引脚D1 D2），由于这个屏幕是5v工作电压，VCC要接5V，NodeMCU没有标识5V引脚，要接Vin。下面是编写代码，都用Arduino IDE进行，要安装LiquidCrystal I2C库，代码如下。#include <Wire.h> #include <LiquidCrystal_I2C.h> // I2C地址, 一般为0x3F, 0x20或0x27 LiquidCrystal_I2C lcd(0x27, 16, 2); int i; char str[16]; void setup(){ lcd.init();//初始化显示屏 lcd.backlight(); // 打开背光 i = 0; strcpy (str,"#"); } void loop(){ lcd.setCursor(0, 0);//设置起始坐标 lcd.print("Hello World!"); lcd.setCursor(0, 1); if(i<16){ i++; lcd.print(str); strcat(str,"#"); }else{ strcpy(str,"#"); i=0; lcd.clear();//清屏 } delay(500); }I2C地址还可以通过写入arduino一段代码,通过串口监视器查询到。// I2C Scanner // Written by Nick Gammon // Date: 20th April 2011 #include <Wire.h> void setup() { Serial.begin (115200); // Leonardo: wait for serial port to connect while (!Serial) { } Serial.println (); Serial.println ("I2C scanner. Scanning ..."); byte count = 0; Wire.begin(); for (byte i = 8; i < 120; i++) { Wire.beginTransmission (i); if (Wire.endTransmission () == 0) { Serial.print ("Found address: "); Serial.print (i, DEC); Serial.print (" (0x"); Serial.print (i, HEX); Serial.println (")"); count++; delay (1); // maybe unneeded? } // end of good response } // end of for loop Serial.println ("Done."); Serial.print ("Found "); Serial.print (count, DEC); Serial.println (" device(s)."); } // end of setup void loop() {}效果如图。