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

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

TM1637四位数码管显示时间|温度-源码|固件,VCC=3V3,GND,CLK-IO12,DIO-IO13。支持串口发送温度显示温度(temp 33.3),清除温度notemp。支持时间显示。支持WIFI自动联网尝试(预置3个,huawei,xiaomi,iphone 密码123456)可以串口配置,自动保存。

知识点:联网、配网与记忆,温度传感器DHT11,数码管显示,串口调试

20261009_200719_605_mobile.jpg

显示温度时,29:8C

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 <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 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();
  }
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_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 < 0.0f || t > 99.9f) {            // 非法:显示 "----"
    seg[0] = seg[1] = seg[2] = seg[3] = SEG_DASH;
    return;
  }
  int ip   = (int)t;
  int frac = (int)(t * 10.0f) % 10;
  int t0 = ip / 10, t1 = ip % 10;
  seg[0] = DIGIT[t0];
  seg[1] = DIGIT[t1] | 0x80;   // 小数点 -> 99.9 的 "."
  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;
}

// ---------------- 串口命令 ----------------
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 {
    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);
  Serial.setTxTimeoutMs(0);
  delay(1000);
  Serial.println("=== TM1637 Clock ===");

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

  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();
}

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();

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

  // ---- 模式切换:每 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) {
    int hh, mm, ss;
    getClock(hh, mm, ss);
    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);     // 温度:不显示冒号
  }

  delay(100);
}

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

请先登录后继续阅读。