回到顶部 回到顶部

基于Beetle ESP32-C3的互联网时钟 简单

头像 HonestQiao 2022.12.31 51 6

DFRobot出品的Beetle ESP32-C3,个头非常小巧,但是功能不弱,非常方便进行一些有趣的小玩意的开发。

这个项目,就是将Beetle ESP32-C3与OLED SSD1306结合,制作一个基于互联网的时钟。

 

通常,我们要制作一个能够稳定运行的时钟,往往需要RTC来确保时钟信息准确。

但这个基于Beetle ESP32-C3的互联网时钟,通过网络时间服务器进行对时,就算运行一万年(如果能够运行),显示的时间也不会有误差。

 

步骤1 硬件材料

要实现这个基于Beetle ESP32-C3的互联网时钟,所需要的材料非常少,具体如下:

材料清单

步骤2 硬件连线

硬件连线机器简单,实际上,只用将SPI接口的OLED直接插接到Beetle ESP32-C3母座上即可:

 

基于Beetle ESP32-C3的互联网时钟.drawio.png

 

image.png

 

image.png

 

 

 

步骤3 原理说明

基于Beetle ESP32-C3的互联网时钟,其时间信息,来源于通过网络获取的NTP时间。

 

网络时间协议NTP(Network Time Protocol)是TCP/IP协议族里面的一个应用层协议,用来使客户端和服务器之间进行时钟同步,提供高精准度的时间校正。NTP服务器从权威时钟源(例如原子钟、GPS)接收精确的协调世界时UTC,客户端再从服务器请求和接收时间。


NTP基于UDP报文进行传输,NTP就是用来使网络中的各个主机时钟同步的一种协议,他把主机的时钟同步到协调世界时UTC,其精度在LAN网络内可达1毫秒内,在WAN网络上可以达到几十毫秒内。

 

因此,只要在一定的时间内,进行NTP时间同步,那么设备上的时间,就基本上是没有误差的。

 

所以这个互联网时钟,就应用了NTP时间同步,因此就算运行一万年(如果能够运行),显示的时间也不会有误差。

步骤4 代码编写

代码
/* 基于NTP服务器的互联网时钟 
*/

#include <Arduino.h>
#include <stdlib.h>
#include <SPI.h>

// 时间库
#include <TimeLib.h>

// WiFi库
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiUdp.h>
#include <Wire.h>

// OLED库
#include <Adafruit_SSD1306.h>
#include <U8g2_for_Adafruit_GFX.h>

// OLED定义
// 屏幕大小定义
#define SCREEN_WIDTH 128  // 宽度
#define SCREEN_HEIGHT 64  // 高度

// 接口类型:1-I2C、2-SPI
#define OLED_TYPE   2

// Adafruit_SSD1306显示对象
#if OLED_TYPE == 1
// OLED I2C定义
#define OLED_RESET -1        // RESET
#define SCREEN_ADDRESS 0x3C  // I2C地址
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#endif

#if OLED_TYPE == 2
// SPI
#define OLED_CLK   0
#define OLED_MOSI  1
#define OLED_RESET 4
#define OLED_DC    6
#define OLED_CS    5
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT,(int)OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
#endif

// U8G2_FOR_ADAFRUIT_GFX显示对象
U8G2_FOR_ADAFRUIT_GFX u8g2_for_adafruit_gfx;

// WiFi定义
#define WIFI_SSID "OpenBSD"
#define WIFI_PASS "********"

WiFiMulti WiFiMulti;

// NTP定义
static const char ntpServerName[] = "ntp.aliyun.com";
const int timeZone = 8;  // 时区

// 通讯数据包定义
const int NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];

// UDP定义
WiFiUDP Udp;
unsigned int localPort = 8888;

// NTP方法和对象定义
time_t getNtpTime();

// 时间显示状态
time_t prevDisplay = 0;

/** 
 * showtime  显示时间 
**/
void showtime() {
  int Hour = hour();
  int Min = minute();
  int Sec = second();
  int HourHigh, HourLow, MinHigh, MinLow, SecHigh, SecLow;

  char buff[50];
  sprintf(buff, "%02d:%02d:%02d", Hour, Min, Sec);
  // Serial.println(buff);
  u8g2_for_adafruit_gfx.setFont(u8g2_font_fur20_tn);  // 中文字体
  u8g2_for_adafruit_gfx.setCursor(8, 52);  // start writing at this position
  u8g2_for_adafruit_gfx.print(buff);
}

/** 
 * getNtpTime  NTP对时 
**/
time_t getNtpTime() {
  IPAddress ntpServerIP;  // NTP 服务器地址

  while (Udp.parsePacket() > 0)
    ;  // 丢弃任何先前接收的数据包

  // 发起NTP请求
  Serial.println("Transmit NTP Request");
  WiFi.hostByName(ntpServerName, ntpServerIP);  // 获取IP
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);

  sendNTPpacket(ntpServerIP);                   // 发送数据包

  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // 读取数据包
      unsigned long secsSince1900;

      // 转换数据
      secsSince1900 = (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];

      // 返回数据
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");

  return 0;
}

/** 
 * sendNTPpacket  发送NTP数据包
**/
void sendNTPpacket(IPAddress &address) {
  // 清理buff
  memset(packetBuffer, 0, NTP_PACKET_SIZE);

  // 生成请求数据
  packetBuffer[0] = 0b11100011;  // LI, Version, Mode
  packetBuffer[1] = 0;           // Stratum, or type of clock
  packetBuffer[2] = 6;           // Polling Interval
  packetBuffer[3] = 0xEC;        // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;

  // 发送NTP数据包
  Udp.beginPacket(address, 123);
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

void setup() {
  // 串口初始化
  Serial.begin(115200);

  // 通讯初始化
  Wire.begin();

  // OLED初始化
  Serial.println(F("SSD1306 init:"));
#if OLED_TYPE == 1
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;  // 循环
  } else {
    Serial.println(F("SSD1306 allocation ok"));
  }
#endif

#if OLED_TYPE == 2
  if (!display.begin(SSD1306_SWITCHCAPVCC)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;  // 循环
  } else {
    Serial.println(F("SSD1306 allocation ok"));
  }
#endif

  // 显示默认 Adafruit splash screen
  display.display();
  delay(1000);

  // 关联u8g8到Adafruit GFX
  u8g2_for_adafruit_gfx.begin(display);

  // WiFi连接处理
  WiFiMulti.addAP(WIFI_SSID, WIFI_PASS);
  while (WiFiMulti.run() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }


  // 开启UDP
  Serial.println("Starting UDP");
  Udp.begin(localPort);
  Serial.print("Local port: ");
  Serial.println(localPort);
  Serial.println("waiting for sync");

  // 设置NTP对时
  setSyncProvider(getNtpTime);
  setSyncInterval(300);

  // 延时
  delay(500);
}

void loop() {
  // 显示时间
  if (timeStatus() != timeNotSet) {  // 检查更新最新时间
    if (now() != prevDisplay) {
      prevDisplay = now();

      // 清除显示缓存
      display.clearDisplay();

      // 画框
      display.drawRoundRect(0, 0, display.width() - 1, 16, 1, SSD1306_WHITE);                      // 状态栏
      display.drawRoundRect(0, 16, display.width() - 1, display.height() - 16, 1, SSD1306_WHITE);  // 内容区


      u8g2_for_adafruit_gfx.setFontDirection(0);        // 显示方向:从左到右(默认)
      u8g2_for_adafruit_gfx.setForegroundColor(WHITE);  // 应用Adafruit GFX颜色

      u8g2_for_adafruit_gfx.setFont(u8g2_font_unifont_t_chinese2);  // 中文字体
      u8g2_for_adafruit_gfx.setCursor(12, 12);                      // 显示坐标
      u8g2_for_adafruit_gfx.print("Net Clock@NTP");

      showtime();  // 显示时间

      // 显示
      display.display();
    }
  }

  // 延时
  delay(50);
}

要运行上面的代码,仅需要修改如下的部分,设置好WiFi连接,就能够正常运行:

// WiFi定义
#define WIFI_SSID "OpenBSD"
#define WIFI_PASS "********"
 

 

在上述代码中,使用了如下的模块:

* SPI:硬件库

* TimeLib:时间库

* WiFi:无线连接库

* WiFiMulti多WiFi配置连接库

* WiFiUdp:WiFi UDP通讯库

* Adafruit_SSD1306:SSD1306显示屏操作库

* U8g2_for_Adafruit_GFX:基于U8g2的字符显示库

 

另外,可以通过 OLED_TYPE 来配置使用I2C接口还是SPI接口。

 

代码中的网络时间处理部分,主要为getNtpTime()和sendNTPpacket(),用于向时间服务器发送时间同步信息,收到后在内存中记录,作为当前时间使用。

 

然后,在loop()中,根据校对的时间自动计时,并显示到屏幕上。

 

步骤5 实际效果

image.png

评论

user-avatar
  • rzyzzxw

    rzyzzxw2023.01.16

    高级

    0
    • 花生编程

      花生编程2023.01.11

      不错666

      0
      • 花生编程

        花生编程2023.01.11

        厉害厉害

        0
        • 三春牛-创客

          三春牛-创客2023.01.02

          OLED是不是容易烧屏啊?感觉用起来成本高,不如做个LCD的

          1
          • HonestQiao

            HonestQiao2023.01.06

            https://www.dfrobot.com.cn/goods-3628.html 用这个

        • 三春牛-创客

          三春牛-创客2023.01.02

          厉害厉害

          0