公告 欢迎您,我的朋友,目前开放免费注册,注册登录后查看更多内容!开通会员送积分,相应文档随便看,积分充值登录后个人中心内按需充值!

🔑 TM1637四位数码管显示时间|气象温度-源码|固件

【源码固件】数码管显示当前时间和当地气象温度,串口支持更换城市、更新温度,每10秒切换一次温度显示,持续3秒;预置WIFI账号可修改可自动记忆;温度显示示例:18:8C,支持夜间高亮白天减亮度;注释详细

数码管支持时间、气象温度显示10秒轮回、运行稳定、接口获取天气数据;

针脚CLK=IO12,DIO=IO13,3V3/GND

知识点:配网联网、数码管显示、亮度调节、气象温度获取、串口调试与交互、防死机机制

20261016_080352_646_mobile.jpg

image.png

源码固件直接烧录

/*
* t_tm1637.cpp — TM1637 四位数码管 时钟 + 间歇温度显示
 *
* 烧录:platformio run -e tm1637 -t upload
 *
* 接线(本独立固件未编译 LCD/OLED,故 P1 上的 IO12/IO13 可作纯 GPIO 用):
*   TM1637 VCC -> 3.3V
*   TM1637 GND -> GND
*   TM1637 CLK -> P1-3  IO12 (PIN_TM1637_CLK)
*   TM1637 DIO -> P1-5  IO13 (PIN_TM1637_DIO)
 *
* ⚠️ 不要选 IO11 作 CLK/DIO:N8R8 模块上 GPIO11 复用为 VDD_SPI(给 Octal PSRAM 供电),
*    不是普通 GPIO,任何固件都驱动不了。
 *
* 显示逻辑:
- 常态显示时间 hh:mm,中间冒号按 1Hz 闪烁(充当“秒”指示,因为只显示到分)。
- 每 10 秒,若有温度值,切到温度显示约 3 秒(格式 99.9C,无冒号),然后回到时间。
- 温度暂无传感器:默认不切温度;串口发 "temp 25.3" 注入测试值,发 "notemp" 取消。
 *
* 串口命令(与 oled 一致,便于改 WiFi):
*   9IEDITWIFI:SSID,PASSWORD / 9IPROV / 9IRESETCONFIG
*   temp XX.X   (注入温度,启用间歇温度显示)
*   notemp      (清除温度,回到纯时间)
 */

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiUdp.h>
#include <HTTPClient.h>
#include <stdlib.h>
#include <NTPClient.h>
#include <time.h>
#include "pins.h"
#include "wifi_creds.h"
#include "prov_ap.h"

// ---------------- TM1637 引脚(见 pins.h)----------------
#define TM_CLK  PIN_TM1637_CLK
#define TM_DIO  PIN_TM1637_DIO

// ---------------- 显示参数 ----------------
#define TM_BRIGHTNESS   0x05      // 初始/默认亮度(0~7,越大越亮)
#define TM_BRIGHT_DAY    0x03     // 白天 08:00-18:00 较低亮度
#define TM_BRIGHT_NIGHT  0x07     // 夜间 18:00-08:00 较高亮度
#define COLON_DIGIT_IDX 1         // 中间冒号=第2位的 DP(0x80);绝大多数带时钟点模组如此
#define SWITCH_MS       10000UL   // 每 10 秒切到温度
#define TEMP_SHOW_MS    3000UL    // 温度显示持续 3 秒
#define COLON_BLINK_MS  500UL     // 冒号 1Hz 闪烁

// ---------------- 7 段编码(bit0=a,1=b,2=c,3=d,4=e,5=f,6=g;bit7=DP)----------------
static const uint8_t DIGIT[10] = {
  0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
#define SEG_BLANK  0x00
#define SEG_DASH   0x40   // 仅 g -> '-'
#define SEG_C      0x39   // a,d,e,f -> 'C'

// ---------------- TM1637 位控驱动(bit-bang,零依赖)----------------
class TM1637 {
public:
  TM1637(uint8_t clk, uint8_t dio) : m_clk(clk), m_dio(dio), m_bright(TM_BRIGHTNESS) {}
  void begin() {
    pinMode(m_clk, OUTPUT);
    pinMode(m_dio, OUTPUT);
    digitalWrite(m_clk, HIGH);
    digitalWrite(m_dio, HIGH);
    uint8_t z[4] = {0, 0, 0, 0};   // 上电先清屏
    display(z, false);
  }
  void display(const uint8_t seg[4], bool colon) {
    start();
    writeByte(0x40);               // 数据命令:自动地址递增
    stop();
    start();
    writeByte(0xC0);               // 起始地址 0
    for (int i = 0; i < 4; i++) {
      uint8_t s = seg[i];
      if (i == COLON_DIGIT_IDX && colon) s |= 0x80;   // 中间冒号(第2位 DP)
      writeByte(s);
    }
    stop();
    start();
    writeByte(0x88 | m_bright);    // 显示开 + 亮度
    stop();
  }
  void setBrightness(uint8_t b) {
    m_bright = b & 0x07;
    start();
    writeByte(0x88 | m_bright);   // 仅改亮度,不动显示内容
    stop();
  }
  uint8_t brightness() const { return m_bright; }
private:
  void start() {
    digitalWrite(m_clk, HIGH);
    digitalWrite(m_dio, HIGH);
    delayMicroseconds(2);
    digitalWrite(m_dio, LOW);
    delayMicroseconds(2);
    digitalWrite(m_clk, LOW);
  }
  void stop() {
    digitalWrite(m_clk, LOW);
    digitalWrite(m_dio, LOW);
    delayMicroseconds(2);
    digitalWrite(m_clk, HIGH);
    delayMicroseconds(2);
    digitalWrite(m_dio, HIGH);
  }
  // 返回从机 ACK(拉低=成功)
  bool writeByte(uint8_t b) {
    for (int i = 0; i < 8; i++) {
      digitalWrite(m_clk, LOW);
      digitalWrite(m_dio, (b & 0x01) ? HIGH : LOW);
      delayMicroseconds(2);
      digitalWrite(m_clk, HIGH);
      delayMicroseconds(2);
      b >>= 1;
    }
    // ACK:释放 DIO,第 9 个时钟读从机应答
    digitalWrite(m_clk, LOW);
    pinMode(m_dio, INPUT);
    delayMicroseconds(2);
    digitalWrite(m_clk, HIGH);
    delayMicroseconds(2);
    bool ack = (digitalRead(m_dio) == LOW);
    digitalWrite(m_clk, LOW);
    pinMode(m_dio, OUTPUT);
    return ack;
  }
  uint8_t m_clk, m_dio, m_bright;
};

static TM1637 g_tm(TM_CLK, TM_DIO);

// ---------------- WiFi / NTP ----------------
#define DEV_SEED_CREDS 1
static const char *PRESET_SSID[] = {"huawei", "xiaomi", "iphone"};
static const char *PRESET_PASS[] = {"123456", "123456", "123456"};

static WifiCreds creds;
static WiFiUDP ntpUDP;
static NTPClient timeClient(ntpUDP, "pool.ntp.org", 8 * 3600, 60000); // UTC+8

// ---------------- 温度(暂时无传感器,串口注入测试)----------------
static bool  g_tempAvail = false;
static float g_tempC    = 0.0f;

// ---------------- 状态机 ----------------
enum { MODE_TIME, MODE_TEMP };
static int            g_mode = MODE_TIME;
static unsigned long  g_lastSwitchMs = 0;
static unsigned long  g_tempEnterMs  = 0;
static uint8_t        g_curBright   = TM_BRIGHTNESS;

// ---------------- 显示缓冲(去重重绘,减少闪烁)----------------
static uint8_t g_lastSeg[4] = {0xFF, 0xFF, 0xFF, 0xFF};
static bool    g_lastColon  = false;

// ---------------- 构建段码 ----------------
static void buildTime(uint8_t seg[4], int hh, int mm, bool blankLeading) {
  int h0 = hh / 10, h1 = hh % 10;
  int m0 = mm / 10, m1 = mm % 10;
  seg[0] = (blankLeading && h0 == 0) ? SEG_BLANK : DIGIT[h0];
  seg[1] = DIGIT[h1];
  seg[2] = DIGIT[m0];
  seg[3] = DIGIT[m1];
}

static void buildTemp(uint8_t seg[4], float t) {
  if (t > 99.9f || t < -9.95f) {          // 超范围:显示 "----"
    seg[0] = seg[1] = seg[2] = seg[3] = SEG_DASH;
    return;
  }
  bool neg = (t < 0.0f);
  float at = neg ? -t : t;
  int t10 = (int)(at * 10.0f + 0.5f);     // 四舍五入到 0.1C,避免浮点截断少 1
  int ip   = t10 / 10;
  int frac = t10 % 10;
  if (neg) {
    seg[0] = SEG_DASH;                    // "-5.3C"
    seg[1] = DIGIT[ip] | 0x80;
  } else if (ip >= 10) {                  // "25.3C"
    seg[0] = DIGIT[ip / 10];
    seg[1] = DIGIT[ip % 10] | 0x80;
  } else {                                // " 5.3C" 整数十位留空
    seg[0] = SEG_BLANK;
    seg[1] = DIGIT[ip] | 0x80;
  }
  seg[2] = DIGIT[frac];
  seg[3] = SEG_C;                         // 'C'
}

// ---------------- 取时间(NTP 优先;未校时则板载自走时钟,从 08:00:00 起)----------------
static bool getClock(int &hh, int &mm, int &ss) {
  if (timeClient.isTimeSet()) {
    time_t t = timeClient.getEpochTime();
    struct tm *ti = localtime(&t);
    hh = ti->tm_hour; mm = ti->tm_min; ss = ti->tm_sec;
    return true;
  }
  unsigned long secs = 8UL * 3600UL + millis() / 1000UL;   // 自走时钟兜底
  hh = (secs / 3600) % 24;
  mm = (secs / 60) % 60;
  ss = secs % 60;
  return false;
}

// ---------------- 重绘(仅在内容变化时写屏,减少闪烁)----------------
static void render(const uint8_t seg[4], bool colon) {
  bool changed = (colon != g_lastColon);
  for (int i = 0; i < 4; i++)
    if (seg[i] != g_lastSeg[i]) { changed = true; break; }
  if (!changed) return;
  g_tm.display(seg, colon);
  for (int i = 0; i < 4; i++) g_lastSeg[i] = seg[i];
  g_lastColon = colon;
}

// ---------------- 联网温度(中国天气网 d1 实时接口,需 Referer)----------------
// 返回 var dataSK={...,"temp":"17.5",...},约 200B; cityname 为 GBK,固件只取 temp,不受影响
struct CityCode { const char *name; float lat; float lon; };
static const CityCode CITIES[] = {
  // 河南省(18 个省辖市)
  {"郑州",34.76f,113.65f},{"安阳",36.10f,114.39f},{"新乡",35.30f,113.93f},{"许昌",34.04f,113.85f},
  {"平顶山",33.74f,113.30f},{"信阳",32.13f,114.07f},{"南阳",32.99f,112.53f},{"开封",34.80f,114.34f},
  {"洛阳",34.62f,112.45f},{"商丘",34.41f,115.65f},{"焦作",35.24f,113.23f},{"鹤壁",35.90f,114.29f},
  {"濮阳",35.76f,115.03f},{"周口",33.63f,114.65f},{"漯河",33.58f,114.02f},{"驻马店",33.01f,114.02f},
  {"三门峡",34.78f,111.19f},{"济源",35.07f,112.59f},
  // 全国主要城市
  {"北京",39.90f,116.41f},{"上海",31.23f,121.47f},{"天津",39.13f,117.20f},{"重庆",29.56f,106.55f},
  {"石家庄",38.04f,114.51f},{"太原",37.87f,112.55f},{"西安",34.34f,108.94f},{"济南",36.65f,117.12f},
  {"青岛",36.07f,120.38f},{"合肥",31.82f,117.23f},{"南京",32.06f,118.80f},{"杭州",30.27f,120.15f},
  {"武汉",30.59f,114.31f},{"长沙",28.23f,112.94f},{"成都",30.57f,104.07f},{"广州",23.13f,113.26f},
  {"深圳",22.54f,114.06f},
};
#define N_CITIES  ((int)(sizeof(CITIES) / sizeof(CITIES[0])))

#define WEATHER_FETCH_MS 600000UL   // 每 10 分钟自动刷新
static int           g_cityIdx     = 0;      // 默认郑州
static String        g_customCode  = "";     // city <9位中国天气网代码> 直发的自定义城市
static unsigned long g_lastFetchMs = 0;
static String        g_cityLabel() { return g_customCode.length() ? String("自定义(") + g_customCode + ")" : String(CITIES[g_cityIdx].name); }

static bool fetchWeatherTemp() {
  if (WiFi.status() != WL_CONNECTED) return false;

  // 指定了中国天气网 9 位代码的城市:走 d1 实时接口(需 Referer)
  if (g_customCode.length()) {
    Serial.printf("[WTHR] %s -> d1 接口获取中...\n", g_cityLabel().c_str());
    HTTPClient http;
    String url = String("http://d1.weather.com.cn/sk_2d/") + g_customCode + ".html";
    if (!http.begin(url)) { Serial.println("[WTHR] d1 begin 失败(DNS/URL)"); return false; }
    http.addHeader("Referer", "http://www.weather.com.cn/");   // d1 接口必须带 Referer
    http.setUserAgent("Mozilla/5.0");
    int code = http.GET();
    bool ok = false;
    if (code == 200) {
      String body = http.getString();
      int p = body.indexOf("\"temp\":\"");   // 精确匹配,不会命中 "tempf"
      if (p >= 0) {
        g_tempC = body.substring(p + 8).toFloat();
        g_tempAvail = true; ok = true;
        Serial.printf("[WTHR] %s(d1) OK 实时温度 %.1fC\n", g_cityLabel().c_str(), g_tempC);
      } else {
        Serial.printf("[WTHR] d1 解析失败,body=%s\n", body.substring(0, 120).c_str());
      }
    }
    http.end();
    if (!ok && code != 200) Serial.printf("[WTHR] d1 获取失败 HTTP=%d(保留旧值)\n", code);
    return ok;
  }

  // 内置城市:Open-Meteo 主源(HTTP,免 key,干净 JSON)
  Serial.printf("[WTHR] %s -> Open-Meteo 获取中...\n", CITIES[g_cityIdx].name);
  HTTPClient http;
  http.setTimeout(10000);
  char url[128];
  snprintf(url, sizeof(url),
           "http://api.open-meteo.com/v1/forecast?latitude=%.4f&longitude=%.4f&current_weather=true",
           CITIES[g_cityIdx].lat, CITIES[g_cityIdx].lon);
  if (!http.begin(url)) { Serial.println("[WTHR] Open-Meteo begin 失败(DNS/URL)"); return false; }
  int code = http.GET();
  bool ok = false;
  if (code == 200) {
    String body = http.getString();
    const char *cw = strstr(body.c_str(), "\"current_weather\"");
    const char *pt = cw ? strstr(cw, "\"temperature\":") : nullptr;
    if (pt) {
      pt += 13;   // 跳过 "temperature":(14 字符,含引号+冒号)
      // 跳过冒号/空格/引号等非数值字符,定位到数字本身
      while (*pt && (*pt < '0' || *pt > '9') && *pt != '-' && *pt != '.') pt++;
      g_tempC = (float)atof(pt);
      g_tempAvail = true; ok = true;
      Serial.printf("[WTHR] %s Open-Meteo OK 实时温度 %.1fC\n", CITIES[g_cityIdx].name, g_tempC);
    } else {
      Serial.printf("[WTHR] Open-Meteo 解析失败,body=%s\n", body.substring(0, 120).c_str());
    }
  }
  http.end();
  if (!ok && code != 200) Serial.printf("[WTHR] Open-Meteo 获取失败 HTTP=%d(保留旧值)\n", code);
  return ok;
}

// ---------------- 串口命令 ----------------
static bool needRestart = false;
static void parseSerialLine(const String &line) {
  if (line.startsWith("9IEDITWIFI:")) {
    String body = line.substring(11);
    int comma = body.indexOf(',');
    if (comma < 0) { Serial.println("[PROV] 格式错:9IEDITWIFI:SSID,PASSWORD"); return; }
    String ssid = body.substring(0, comma);
    String pass = body.substring(comma + 1);
    ssid.trim(); pass.trim();
    if (ssid.length() == 0) { Serial.println("[PROV] SSID 空"); return; }
    creds.set(ssid.c_str(), pass.c_str());
    Serial.printf("[PROV] 已保存 WiFi: %s,即将重连\n", ssid.c_str());
    needRestart = true;
  } else if (line == "9IPROV") {
    Serial.println("[PROV] 收到 9IPROV,重启进入网页配网");
    needRestart = true;
  } else if (line == "9IRESETCONFIG") {
    creds.clearAll();
    Serial.println("[PROV] 已清空 WiFi 凭据,重启进入网页配网");
    needRestart = true;
  } else if (line.startsWith("temp ")) {
    float v = line.substring(5).toFloat();
    String arg = line.substring(5);
    arg.trim();
    if (arg.length() == 0 || (v == 0.0f && arg.indexOf('0') < 0)) {
      Serial.println("[TEMP] 解析失败,格式:temp 25.3");
    } else {
      g_tempC = v; g_tempAvail = true;
      g_mode = MODE_TEMP; g_tempEnterMs = millis();   // 立即切温度显示,便于串口确认
      Serial.printf("[TEMP] 已注入 %.1fC,立即显示 3 秒,之后每 10s 间歇切换\n", g_tempC);
    }
  } else if (line == "notemp") {
    g_tempAvail = false;
    Serial.println("[TEMP] 已清除温度,回到纯时间显示");
  } else if (line.startsWith("city ")) {
    String arg = line.substring(5);
    arg.trim();
    if (arg.length() == 0) { Serial.println("[WTHR] 格式:city 郑州 或 city 101180101(中国天气网9位代码)"); return; }
    bool found = false;
    for (int i = 0; i < N_CITIES; i++) {
      if (arg == CITIES[i].name) { g_cityIdx = i; g_customCode = ""; found = true; break; }
    }
    if (!found && arg.length() == 9) {          // 表外城市:直接发中国天气网 9 位代码,走 d1 接口
      bool digits = true;
      for (unsigned int k = 0; k < arg.length(); k++)
        if (!isDigit(arg[k])) { digits = false; break; }
      if (digits) { g_customCode = arg; found = true; }
    }
    if (found) {
      Serial.printf("[WTHR] 城市切换为 %s,立即获取\n", g_cityLabel().c_str());
      if (fetchWeatherTemp()) { g_lastFetchMs = millis(); g_mode = MODE_TEMP; g_tempEnterMs = millis(); }
    } else {
      Serial.printf("[WTHR] 未收录 \"%s\"。可用城市:\n", arg.c_str());
      for (int i = 0; i < N_CITIES; i++) Serial.printf("  %s\n", CITIES[i].name);
      Serial.println("  或发 city <中国天气网9位代码>(如 101180103)走 d1 接口");
    }
  } else {
    Serial.printf("[SERIAL] 未识别命令: \"%s\"\n", line.c_str());
  }
}

static void checkSerial() {
  static String buf;
  while (Serial.available()) {
    char c = (char)Serial.read();
    if (c == '\n' || c == '\r') {
      if (buf.length()) { parseSerialLine(buf); buf = ""; }
    } else buf += c;
  }
}

static bool attemptConnect(unsigned long timeoutMs) {
  WiFiMulti wm;
  for (int i = 0; i < creds.count(); i++)
    wm.addAP(creds.get(i)->ssid, creds.get(i)->pass);
  Serial.print("[WiFi] 连接中");
  unsigned long t = millis();
  int dots = 0;
  while (wm.run() != WL_CONNECTED && millis() - t < timeoutMs) {
    if ((dots++) % 10 == 0) Serial.print(".");
    delay(100);
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("[WiFi] 联网成功 SSID=%s IP=%s RSSI=%ddBm\n",
                  WiFi.SSID().c_str(), WiFi.localIP().toString().c_str(), WiFi.RSSI());
    return true;
  }
  Serial.println("[WiFi] 连接失败");
  return false;
}

void setup() {
  Serial.begin(115200);
  // ⚠️ 绝不能设 0:HWCDC(arduino-esp32 2.0.17) 的 write() 在"串口断开+发送缓冲满"时,
  //    tries=tx_timeout_ms=0 → 递减下溢成 0xFFFFFFFF → while(connected && to_send) 死循环,
  //    loopTask 被卡死(表现为运行几分钟后显示冻结/死掉,开串口则因缓冲被读空而不复现)。
  //    默认 100ms:满时阻塞约 100ms 后放弃退出,tries 正常归零。代价仅为打印偶发 100ms 延迟。
  Serial.setTxTimeoutMs(100);
  delay(1000);
  Serial.println("=== TM1637 Clock ===");

  g_tm.begin();
  Serial.printf("[TM1637] init CLK=GPIO%d DIO=GPIO%d\n", TM_CLK, TM_DIO);

  // ★ 上电 1 秒内先点亮 "----",不等 WiFi/NTP:避免"开机到首次显示"的黑屏窗口
  //   (此前 setup 要走完 连WiFi(最长20s)+NTP+拉天气 才开始第一次渲染,不开串口时
  //    看上去就像"不显示"。关串口监视器会触发 S3 USB-JTAG 复位重启,同样落在该窗口内)
  {
    uint8_t initSeg[4] = {SEG_DASH, SEG_DASH, SEG_DASH, SEG_DASH};
    g_tm.display(initSeg, false);
    for (int i = 0; i < 4; i++) g_lastSeg[i] = initSeg[i];
    g_lastColon = false;
  }

  creds.begin();
  Serial.printf("[WiFi] NVS 已有 %d 条凭据\n", creds.count());
  if (creds.empty()) {
#if DEV_SEED_CREDS
    Serial.println("[WiFi] NVS 为空,先用开发预设播种");
    for (int i = 0; i < 5; i++) creds.set(PRESET_SSID[i], PRESET_PASS[i]);
#else
    Serial.println("[PROV] NVS 为空,进入网页配网");
    startApProvisioning(creds, 180000);
    ESP.restart();
#endif
  }

  WiFi.mode(WIFI_STA);
  // 连不上也不强制造配网:时钟用板载自走时钟兜底,仍可显示
  if (!attemptConnect(20000)) {
    Serial.println("[WiFi] 未联网,时钟将使用板载自走时钟(08:00 起);串口发 9IEDITWIFI:SSID,PASS 配网");
  }

  timeClient.begin();
  for (int k = 0; k < 5 && !timeClient.update(); k++) delay(300);
  if (timeClient.isTimeSet())
    Serial.printf("[NTP] ok: %s\n", timeClient.getFormattedTime().c_str());
  else
    Serial.println("[NTP] 未校时,使用板载自走时钟(08:00 起)");

  g_lastSwitchMs = millis();

  // 不在 setup 里同步拉天气(会拖慢首次显示):
  // 开机 5 秒后由 loop 触发首次拉取;失败则 15s 后再试
  g_lastFetchMs = millis() - WEATHER_FETCH_MS + 5000UL;
}

void loop() {
  checkSerial();
  if (needRestart) { delay(200); ESP.restart(); }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[WiFi] reconnecting");
    if (!attemptConnect(20000)) Serial.println("[WiFi] reconnect failed");
  }

  timeClient.update();

  // ---- 定时刷新联网温度(每 10 分钟)----
  if (WiFi.status() == WL_CONNECTED && millis() - g_lastFetchMs >= WEATHER_FETCH_MS) {
    g_lastFetchMs = millis();
    fetchWeatherTemp();
  }

  unsigned long now = millis();
  uint8_t seg[4];

  // ---- 亮度按昼夜自动调整(08:00-18:00 低,其余高;仅变化时下发)----
  int hh, mm, ss;
  getClock(hh, mm, ss);
  uint8_t targetB = (hh >= 8 && hh < 18) ? TM_BRIGHT_DAY : TM_BRIGHT_NIGHT;
  if (targetB != g_curBright) { g_tm.setBrightness(targetB); g_curBright = targetB; }

  // ---- 模式切换:每 10s 若有温度则显示 3s ----
  if (!g_tempAvail) {
    g_mode = MODE_TIME;
    g_lastSwitchMs = now;            // 温度就绪后从“现在”重新计 10s
  } else {
    if (g_mode == MODE_TIME && now - g_lastSwitchMs >= SWITCH_MS) {
      g_mode = MODE_TEMP; g_tempEnterMs = now;
    } else if (g_mode == MODE_TEMP && now - g_tempEnterMs >= TEMP_SHOW_MS) {
      g_mode = MODE_TIME; g_lastSwitchMs = now;
    }
  }

  if (g_mode == MODE_TIME) {
    buildTime(seg, hh, mm, true);
    bool colon = ((now / COLON_BLINK_MS) % 2) == 0;   // 1Hz 闪烁 = 秒指示
    render(seg, colon);
  } else {
    buildTemp(seg, g_tempC);
    render(seg, false);     // 温度:不显示冒号
  }

  // ---- 板载 WS2812 心跳灯(IO48):每秒闪一下暗绿 = 固件活着 ----
  // 用途:不开串口时判断固件是否在跑——灯闪 = 固件正常(显示问题在数码管/供电);
  //       灯完全不闪 = 固件没跑(复位/卡死/供电不足)。确认无碍后可删掉这段。
  {
    static unsigned long lastBlinkMs = 0;
    static bool ledOn = false;
    if (now - lastBlinkMs >= 1000UL) {
      lastBlinkMs = now;
      ledOn = !ledOn;
      neopixelWrite(48, 0, ledOn ? 40 : 0, 0);   // 暗绿,每秒闪一下;确认无碍后可删
    }
  }

  delay(100);
}

该文档需要「VIP会员」身份

请先登录后继续阅读。