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

显示温度时,29:8C

源代码|固件,直接烧录即可
/*
* 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);
}