// sketch.ino
// ATEN Code Jam – 方案C:OLED趨勢圖 + KY-040旋鈕模式切換
// MCU : STM32F103C8 BluePill @72MHz
// 框架 : Arduino (STMicroelectronics:stm32)
// 元件 : DHT22(PA0) | DS1307 RTC(I2C 0x68) | LCD1602(I2C 0x27)
// SSD1306 OLED(I2C 0x3C) | KY-040 Encoder(PA1/PA2/PA3)
// 排程 : millis() 非阻塞;禁止 delay() 影響主迴圈時序
#include <Arduino.h>
#include <string.h>
#include <stdio.h>
// STM32duino 2.x 已預定義 Serial1 = USART1 (TX=PA9, RX=PA10),不需重複宣告
// ====================================================================
// 腳位 / I2C 位址 / 圖表常數
// ====================================================================
#define PIN_DHT22 PA0
#define PIN_SCL PB6
#define PIN_SDA PB7
#define PIN_ENC_CLK PA1
#define PIN_ENC_DT PA2
#define PIN_ENC_SW PA3
#define ADDR_LCD 0x27 // PCF8574 I2C 擴充板
#define ADDR_RTC 0x68 // DS1307
#define ADDR_OLED 0x3C // SSD1306
#define HIST_SIZE 128 // 趨勢緩衝點數(= OLED 寬度)
#define GRAPH_TOP 14 // 趨勢圖頂端像素 Y
#define GRAPH_BOT 53 // 趨勢圖底端像素 Y
// ====================================================================
// 資料結構 / 全域狀態
// ====================================================================
struct DateTime {
uint8_t sec, min, hour;
uint8_t day, month;
uint16_t year;
};
static struct {
DateTime dt;
float temp;
float humid;
bool rtc_ok;
bool dht_ok;
uint8_t mode; // 0=溫度趨勢圖 1=濕度趨勢圖
} g;
static float g_th[HIST_SIZE]; // 溫度歷史(ring buffer)
static float g_hh[HIST_SIZE]; // 濕度歷史
static uint8_t g_head = 0; // 下次寫入索引
static uint8_t g_count = 0; // 有效點數
static uint8_t oled_buf[1024]; // SSD1306 幀緩衝(128×64/8)
// ====================================================================
// 5×8 ASCII 字型(0x20 space → 0x5A 'Z')
// ====================================================================
static const uint8_t F5X8[][5] = {
{0x00,0x00,0x00,0x00,0x00}, // 20 ' '
{0x00,0x00,0x5F,0x00,0x00}, // 21 !
{0x00,0x07,0x00,0x07,0x00}, // 22 "
{0x14,0x7F,0x14,0x7F,0x14}, // 23 #
{0x24,0x2A,0x7F,0x2A,0x12}, // 24 $
{0x23,0x13,0x08,0x64,0x62}, // 25 %
{0x36,0x49,0x55,0x22,0x50}, // 26 &
{0x00,0x05,0x03,0x00,0x00}, // 27 '
{0x00,0x1C,0x22,0x41,0x00}, // 28 (
{0x00,0x41,0x22,0x1C,0x00}, // 29 )
{0x14,0x08,0x3E,0x08,0x14}, // 2A *
{0x08,0x08,0x3E,0x08,0x08}, // 2B +
{0x00,0x50,0x30,0x00,0x00}, // 2C ,
{0x08,0x08,0x08,0x08,0x08}, // 2D -
{0x00,0x60,0x60,0x00,0x00}, // 2E .
{0x20,0x10,0x08,0x04,0x02}, // 2F /
{0x3E,0x51,0x49,0x45,0x3E}, // 30 0
{0x00,0x42,0x7F,0x40,0x00}, // 31 1
{0x42,0x61,0x51,0x49,0x46}, // 32 2
{0x21,0x41,0x45,0x4B,0x31}, // 33 3
{0x18,0x14,0x12,0x7F,0x10}, // 34 4
{0x27,0x45,0x45,0x45,0x39}, // 35 5
{0x3C,0x4A,0x49,0x49,0x30}, // 36 6
{0x01,0x71,0x09,0x05,0x03}, // 37 7
{0x36,0x49,0x49,0x49,0x36}, // 38 8
{0x06,0x49,0x49,0x29,0x1E}, // 39 9
{0x00,0x36,0x36,0x00,0x00}, // 3A :
{0x00,0x56,0x36,0x00,0x00}, // 3B ;
{0x08,0x14,0x22,0x41,0x00}, // 3C <
{0x14,0x14,0x14,0x14,0x14}, // 3D =
{0x00,0x41,0x22,0x14,0x08}, // 3E >
{0x02,0x01,0x51,0x09,0x06}, // 3F ?
{0x32,0x49,0x79,0x41,0x3E}, // 40 @
{0x7E,0x11,0x11,0x11,0x7E}, // 41 A
{0x7F,0x49,0x49,0x49,0x36}, // 42 B
{0x3E,0x41,0x41,0x41,0x22}, // 43 C
{0x7F,0x41,0x41,0x22,0x1C}, // 44 D
{0x7F,0x49,0x49,0x49,0x41}, // 45 E
{0x7F,0x09,0x09,0x09,0x01}, // 46 F
{0x3E,0x41,0x49,0x49,0x7A}, // 47 G
{0x7F,0x08,0x08,0x08,0x7F}, // 48 H
{0x00,0x41,0x7F,0x41,0x00}, // 49 I
{0x20,0x40,0x41,0x3F,0x01}, // 4A J
{0x7F,0x08,0x14,0x22,0x41}, // 4B K
{0x7F,0x40,0x40,0x40,0x40}, // 4C L
{0x7F,0x02,0x0C,0x02,0x7F}, // 4D M
{0x7F,0x04,0x08,0x10,0x7F}, // 4E N
{0x3E,0x41,0x41,0x41,0x3E}, // 4F O
{0x7F,0x09,0x09,0x09,0x06}, // 50 P
{0x3E,0x41,0x51,0x21,0x5E}, // 51 Q
{0x7F,0x09,0x19,0x29,0x46}, // 52 R
{0x46,0x49,0x49,0x49,0x31}, // 53 S
{0x01,0x01,0x7F,0x01,0x01}, // 54 T
{0x3F,0x40,0x40,0x40,0x3F}, // 55 U
{0x1F,0x20,0x40,0x20,0x1F}, // 56 V
{0x3F,0x40,0x38,0x40,0x3F}, // 57 W
{0x63,0x14,0x08,0x14,0x63}, // 58 X
{0x07,0x08,0x70,0x08,0x07}, // 59 Y
{0x61,0x51,0x49,0x45,0x43}, // 5A Z
};
// ====================================================================
// I2C Bit-Bang Driver (SDA=PB7, SCL=PB6)
// Open-drain 模擬:INPUT = 釋放(pull-up 拉高)/ OUTPUT+LOW = 拉低
// 避免 Wokwi STM32 OUTPUT_OPEN_DRAIN 行為不一致的問題
// ====================================================================
static inline void sda_hi() { pinMode(PIN_SDA, INPUT); }
static inline void sda_lo() { pinMode(PIN_SDA, OUTPUT); digitalWrite(PIN_SDA, LOW); }
static inline void scl_hi() { pinMode(PIN_SCL, INPUT); }
static inline void scl_lo() { pinMode(PIN_SCL, OUTPUT); digitalWrite(PIN_SCL, LOW); }
static void i2c_init() {
sda_hi(); scl_hi();
delayMicroseconds(5);
}
static void i2c_start() {
sda_hi(); delayMicroseconds(2);
scl_hi(); delayMicroseconds(5);
sda_lo(); delayMicroseconds(5); // START:SCL 高時 SDA 下降
scl_lo(); delayMicroseconds(5);
}
static void i2c_stop() {
sda_lo(); delayMicroseconds(2);
scl_hi(); delayMicroseconds(5);
sda_hi(); delayMicroseconds(5); // STOP:SCL 高時 SDA 上升
}
// 回傳 true = 收到 ACK
static bool i2c_write(uint8_t b) {
for (int8_t i = 7; i >= 0; i--) {
if ((b >> i) & 1) sda_hi(); else sda_lo();
delayMicroseconds(2);
scl_hi(); delayMicroseconds(5);
scl_lo(); delayMicroseconds(5);
}
sda_hi(); // 釋放 SDA 讓 Slave 拉低(ACK)
delayMicroseconds(2);
scl_hi(); delayMicroseconds(3);
bool ack = (digitalRead(PIN_SDA) == LOW);
scl_lo(); delayMicroseconds(5);
return ack;
}
// send_ack=true → 回 ACK(繼續讀),false → NACK(最後 byte)
static uint8_t i2c_read(bool send_ack) {
uint8_t val = 0;
sda_hi(); // 釋放 SDA 讓 Slave 驅動
for (int8_t i = 7; i >= 0; i--) {
scl_hi(); delayMicroseconds(5);
if (digitalRead(PIN_SDA)) val |= (1 << i);
scl_lo(); delayMicroseconds(5);
}
if (send_ack) sda_lo(); else sda_hi();
delayMicroseconds(2);
scl_hi(); delayMicroseconds(5);
scl_lo(); delayMicroseconds(5);
sda_hi();
return val;
}
// ====================================================================
// DHT22 Driver (PA0, 單線協定)
// ====================================================================
static bool dht22_read(float *out_t, float *out_h) {
uint8_t data[5] = {0};
uint32_t n, t0;
// 起始:拉低 2ms → 釋放 → 等感測器回應
pinMode(PIN_DHT22, OUTPUT);
digitalWrite(PIN_DHT22, LOW);
delay(2);
digitalWrite(PIN_DHT22, HIGH);
pinMode(PIN_DHT22, INPUT_PULLUP);
delayMicroseconds(30);
// 終止用計次上限(n<1000≈1ms),與 micros()/SysTick 無關
// bit 判斷仍用 micros() 差值,確保在正常協定時序下值正確
// 等感測器拉低(~20-40µs)
for (n = 0; digitalRead(PIN_DHT22) == HIGH; n++) { delayMicroseconds(1); if (n >= 1000) return false; }
// 感測器低電平 ~80µs
for (n = 0; digitalRead(PIN_DHT22) == LOW; n++) { delayMicroseconds(1); if (n >= 1000) return false; }
// 感測器高電平 ~80µs
for (n = 0; digitalRead(PIN_DHT22) == HIGH; n++) { delayMicroseconds(1); if (n >= 1000) return false; }
// 讀取 40 bits:高電平脈寬 >50µs = bit1, <50µs = bit0(micros 計時)
for (int i = 0; i < 40; i++) {
for (n = 0; digitalRead(PIN_DHT22) == LOW; n++) { delayMicroseconds(1); if (n >= 1000) return false; }
t0 = micros();
for (n = 0; digitalRead(PIN_DHT22) == HIGH; n++) { delayMicroseconds(1); if (n >= 1000) return false; }
data[i/8] = (data[i/8] << 1) | ((micros() - t0 > 50) ? 1 : 0);
}
// Checksum:前 4 bytes 加總 LSB == data[4]
if (((data[0]+data[1]+data[2]+data[3]) & 0xFF) != data[4]) return false;
// 拒絕全零資料(micros() 凍結時 bit 判斷全為 0,checksum 也剛好通過)
if (data[0] == 0 && data[1] == 0 && data[2] == 0 && data[3] == 0) return false;
uint16_t rh = ((uint16_t)data[0]<<8)|data[1];
uint16_t rt = ((uint16_t)data[2]<<8)|data[3];
*out_h = rh * 0.1f;
// 溫度最高位為負號 bit
*out_t = (rt & 0x8000) ? -(float)(rt & 0x7FFF) * 0.1f : (float)rt * 0.1f;
return true;
}
// ====================================================================
// DS1307 RTC Driver (I2C 0x68)
// 暫存器 0x00~0x06:sec(BCD) min hour weekday day month year
// ====================================================================
static uint8_t bcd2d(uint8_t b) { return (b>>4)*10 + (b&0x0F); }
static uint8_t d2bcd(uint8_t d) { return ((d/10)<<4)|(d%10); }
static bool rtc_read(DateTime *dt) {
// 指定起始暫存器
i2c_start();
if (!i2c_write(ADDR_RTC<<1)) { i2c_stop(); return false; }
i2c_write(0x00);
i2c_stop();
// 連續讀取 7 bytes
i2c_start();
if (!i2c_write((ADDR_RTC<<1)|1)) { i2c_stop(); return false; }
uint8_t s = i2c_read(true);
uint8_t mn = i2c_read(true);
uint8_t h = i2c_read(true);
i2c_read(true); // 星期,略過
uint8_t d = i2c_read(true);
uint8_t mo = i2c_read(true);
uint8_t y = i2c_read(false); // 最後一個 byte 回 NACK
i2c_stop();
// sec 暫存器 bit7 = CH(Clock Halt),若為 1 表示振盪器未啟動
if (s & 0x80) return false;
dt->sec = bcd2d(s & 0x7F);
dt->min = bcd2d(mn);
dt->hour = bcd2d(h & 0x3F);
dt->day = bcd2d(d);
dt->month = bcd2d(mo);
dt->year = 2000 + bcd2d(y);
return true;
}
// 解析編譯時間戳取得日期時間
// __DATE__ 格式:"Mmm DD YYYY"(例如 "May 18 2026")
// __TIME__ 格式:"HH:MM:SS"(例如 "14:30:00")
static void parse_compile_datetime(uint8_t *dd, uint8_t *mo, uint16_t *yr,
uint8_t *hr, uint8_t *mn, uint8_t *sc) {
const char *date = __DATE__;
const char *time = __TIME__;
const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
uint8_t mi;
for (mi = 0; mi < 12; mi++) {
if (date[0]==months[mi*3] && date[1]==months[mi*3+1] && date[2]==months[mi*3+2]) break;
}
*mo = mi + 1;
*dd = (date[4]==' ') ? (uint8_t)(date[5]-'0')
: (uint8_t)((date[4]-'0')*10 + (date[5]-'0'));
*yr = (uint16_t)((date[7]-'0')*1000 + (date[8]-'0')*100
+ (date[9]-'0')*10 + (date[10]-'0'));
*hr = (uint8_t)((time[0]-'0')*10 + (time[1]-'0'));
*mn = (uint8_t)((time[3]-'0')*10 + (time[4]-'0'));
*sc = (uint8_t)((time[6]-'0')*10 + (time[7]-'0'));
}
// 若 DS1307 振盪器已停止(CH=1)則用編譯時間戳初始化,否則保持現有時間
static void rtc_ensure_running() {
delay(100);
// 讀取秒暫存器:bit7 = CH(Clock Halt),1 表示振盪器停止
i2c_start();
if (!i2c_write(ADDR_RTC<<1)) { i2c_stop(); return; }
i2c_write(0x00);
i2c_stop();
i2c_start();
if (!i2c_write((ADDR_RTC<<1)|1)) { i2c_stop(); return; }
uint8_t sec_reg = i2c_read(false);
i2c_stop();
if (!(sec_reg & 0x80)) return; // CH=0:振盪器已在計時,不覆蓋
// CH=1:用編譯時間戳寫入初始時間
uint8_t dd, mo, hr, mn, sc;
uint16_t yr;
parse_compile_datetime(&dd, &mo, &yr, &hr, &mn, &sc);
i2c_start();
i2c_write(ADDR_RTC<<1);
i2c_write(0x00); // 起始暫存器 0x00
i2c_write(d2bcd(sc)); // sec(bit7=0 → CH cleared,啟動振盪器)
i2c_write(d2bcd(mn));
i2c_write(d2bcd(hr));
i2c_write(1); // 星期(顯示未使用)
i2c_write(d2bcd(dd));
i2c_write(d2bcd(mo));
i2c_write(d2bcd(yr % 100));
i2c_stop();
}
// ====================================================================
// PCF8574 + HD44780 LCD Driver (I2C 0x27)
// PCF8574 接線:P0=RS P1=RW P2=EN P3=BL P4=D4 P5=D5 P6=D6 P7=D7
// ====================================================================
#define LCD_BL 0x08
#define LCD_EN 0x04
#define LCD_RS 0x01
// 送出 4-bit nibble(高4位元),rs=0 指令 / rs=1 資料
static void lcd_nibble(uint8_t nib, uint8_t rs) {
uint8_t b = (nib<<4) | LCD_BL | (rs ? LCD_RS : 0);
i2c_start(); i2c_write(ADDR_LCD<<1); i2c_write(b | LCD_EN); i2c_stop();
delayMicroseconds(1);
i2c_start(); i2c_write(ADDR_LCD<<1); i2c_write(b); i2c_stop();
delayMicroseconds(50);
}
static void lcd_byte(uint8_t v, uint8_t rs) {
lcd_nibble(v >> 4, rs);
lcd_nibble(v & 0x0F, rs);
}
static void lcd_cmd(uint8_t c) { lcd_byte(c, 0); delayMicroseconds(40); }
static void lcd_char(uint8_t c) { lcd_byte(c, 1); delayMicroseconds(40); }
static void lcd_init() {
delay(60);
// 4-bit 初始化序列(HD44780 規格)
lcd_nibble(0x03, 0); delayMicroseconds(5000);
lcd_nibble(0x03, 0); delayMicroseconds(200);
lcd_nibble(0x03, 0); delayMicroseconds(200);
lcd_nibble(0x02, 0); // 切換至 4-bit 模式
lcd_cmd(0x28); // Function set: 4-bit, 2行, 5×8
lcd_cmd(0x08); // 顯示關
lcd_cmd(0x01); delay(2); // Clear display
lcd_cmd(0x06); // Entry mode: 游標右移
lcd_cmd(0x0C); // 顯示開, 游標隱藏
}
static void lcd_goto(uint8_t col, uint8_t row) {
lcd_cmd(row ? (0xC0 + col) : (0x80 + col));
}
static void lcd_print(const char *s) {
while (*s) lcd_char((uint8_t)*s++);
}
// ====================================================================
// SSD1306 OLED Driver (I2C 0x3C, 128×64 monochrome)
// ====================================================================
static void oled_cmd(uint8_t c) {
i2c_start();
i2c_write(ADDR_OLED<<1);
i2c_write(0x00); // Co=0, D/C#=0 → command byte
i2c_write(c);
i2c_stop();
}
static void oled_init() {
delay(100);
// SSD1306 初始化序列
const uint8_t seq[] = {
0xAE, // Display OFF
0xD5, 0x80, // Set Display Clock Divide
0xA8, 0x3F, // Multiplex ratio 1/64
0xD3, 0x00, // Display offset 0
0x40, // Start line 0
0x8D, 0x14, // Charge pump ON
0x20, 0x00, // Horizontal addressing mode
0xA1, // Segment remap (左右正向)
0xC8, // COM scan direction (上下正向)
0xDA, 0x12, // COM pins hardware config
0x81, 0xCF, // Contrast
0xD9, 0xF1, // Pre-charge period
0xDB, 0x40, // VCOMH deselect level
0xA4, // Entire display ON (follow RAM)
0xA6, // Normal display (非反轉)
0xAF // Display ON
};
for (uint8_t i = 0; i < sizeof(seq); i++) oled_cmd(seq[i]);
memset(oled_buf, 0, sizeof(oled_buf));
}
// 設定 / 清除幀緩衝中的像素
static void oled_px(uint8_t x, uint8_t y, bool on) {
if (x >= 128 || y >= 64) return;
uint16_t idx = (uint16_t)(y >> 3) * 128 + x;
uint8_t bit = y & 0x07;
if (on) oled_buf[idx] |= (1 << bit);
else oled_buf[idx] &= ~(1 << bit);
}
// 將幀緩衝傳輸到 OLED(1024 bytes,約 100ms @ 100kHz I2C)
static void oled_flush() {
oled_cmd(0x21); oled_cmd(0); oled_cmd(127); // 欄位範圍 0~127
oled_cmd(0x22); oled_cmd(0); oled_cmd(7); // 頁面範圍 0~7
i2c_start();
i2c_write(ADDR_OLED<<1);
i2c_write(0x40); // Co=0, D/C#=1 → data stream
for (uint16_t i = 0; i < 1024; i++) i2c_write(oled_buf[i]);
i2c_stop();
}
// 繪製 5×8 字元(自動轉大寫)
static void oled_putc(uint8_t x, uint8_t y, char c) {
if (c >= 'a' && c <= 'z') c -= 32;
if (c < 0x20 || c > 0x5A) c = ' ';
const uint8_t *glyph = F5X8[c - 0x20];
for (uint8_t cx = 0; cx < 5; cx++)
for (uint8_t cy = 0; cy < 8; cy++)
oled_px(x + cx, y + cy, (glyph[cx] >> cy) & 1);
}
// 繪製字串(字元寬6px)
static void oled_puts(uint8_t x, uint8_t y, const char *s) {
while (*s && x < 123) { oled_putc(x, y, *s++); x += 6; }
}
// 水平分隔線
static void oled_hline(uint8_t y) {
for (uint8_t x = 0; x < 128; x++) oled_px(x, y, true);
}
// ====================================================================
// KY-040 旋轉編碼器 (CLK=PA1, DT=PA2, SW=PA3)
// ====================================================================
static uint8_t enc_prev_clk;
static uint8_t enc_prev_sw;
static uint32_t enc_sw_ms;
static void enc_init() {
pinMode(PIN_ENC_CLK, INPUT_PULLUP);
pinMode(PIN_ENC_DT, INPUT_PULLUP);
pinMode(PIN_ENC_SW, INPUT_PULLUP);
enc_prev_clk = digitalRead(PIN_ENC_CLK);
enc_prev_sw = digitalRead(PIN_ENC_SW);
}
// 偵測旋轉方向:CLK 下降沿時判斷 DT 狀態
// 回傳 +1(順時針) / -1(逆時針) / 0(無變化)
static int8_t enc_dir() {
uint8_t clk = digitalRead(PIN_ENC_CLK);
uint8_t dt = digitalRead(PIN_ENC_DT);
int8_t dir = 0;
if (clk != enc_prev_clk && clk == LOW)
dir = (dt == HIGH) ? +1 : -1;
enc_prev_clk = clk;
return dir;
}
// 按鍵事件(消抖 50ms)
static bool enc_clicked() {
uint8_t sw = digitalRead(PIN_ENC_SW);
bool pressed = (sw == LOW && enc_prev_sw == HIGH &&
millis() - enc_sw_ms > 50);
if (pressed) enc_sw_ms = millis();
enc_prev_sw = sw;
return pressed;
}
// ====================================================================
// 趨勢 Ring Buffer
// ====================================================================
static void hist_push(float t, float h) {
g_th[g_head] = t;
g_hh[g_head] = h;
g_head = (g_head + 1) % HIST_SIZE;
if (g_count < HIST_SIZE) g_count++;
}
// 將數值對映到 OLED Y 座標(vmin→GRAPH_BOT, vmax→GRAPH_TOP)
static uint8_t val_to_y(float v, float vmin, float vmax) {
if (v < vmin) v = vmin;
if (v > vmax) v = vmax;
float ratio = (v - vmin) / (vmax - vmin);
return (uint8_t)(GRAPH_BOT - ratio * (GRAPH_BOT - GRAPH_TOP));
}
// 繪製折線趨勢圖
static void draw_graph(bool is_temp) {
float vmin = 0.0f;
float vmax = is_temp ? 50.0f : 100.0f;
int16_t prev_y = -1;
for (uint8_t i = 0; i < g_count; i++) {
uint8_t bi = (uint8_t)((g_head - g_count + i + HIST_SIZE) % HIST_SIZE);
float v = is_temp ? g_th[bi] : g_hh[bi];
uint8_t x = (uint8_t)(128 - g_count + i);
uint8_t y = val_to_y(v, vmin, vmax);
if (prev_y < 0) {
oled_px(x, y, true);
} else {
// 相鄰點間畫垂直連線,避免折線斷裂
uint8_t y1 = (y < (uint8_t)prev_y) ? y : (uint8_t)prev_y;
uint8_t y2 = (y < (uint8_t)prev_y) ? (uint8_t)prev_y : y;
for (uint8_t yy = y1; yy <= y2; yy++) oled_px(x, yy, true);
}
prev_y = (int16_t)y;
}
}
// ====================================================================
// 任務:OLED 更新(每 1000ms)
// 佈局:標題(y=0~7) | 分隔線(y=12) | 趨勢圖(y=14~53) | 分隔線(y=55) | 統計(y=56~63)
// ====================================================================
static void task_oled() {
bool is_t = (g.mode == 0);
memset(oled_buf, 0, sizeof(oled_buf));
char buf[24];
// --- 標題列(顯示模式名稱與當前數值)---
// dtostrf() 避免 newlib-nano 不支援 snprintf %f 的問題
char fv[8];
if (is_t) {
if (g.dht_ok) { dtostrf(g.temp, 5, 1, fv); snprintf(buf, sizeof(buf), "TEMP%sC", fv); }
else snprintf(buf, sizeof(buf), "TEMP ---.-C");
} else {
if (g.dht_ok) { dtostrf(g.humid, 5, 1, fv); snprintf(buf, sizeof(buf), "HUMD%s%%", fv); }
else snprintf(buf, sizeof(buf), "HUMD ---.-%% ");
}
oled_puts(0, 0, buf);
// 右上角:RTC/DHT 狀態指示
oled_puts(78, 0, g.rtc_ok ? "RTC:OK" : "RTC:ER");
// --- 分隔線 ---
oled_hline(12);
// --- 趨勢折線圖 ---
if (g_count > 1) draw_graph(is_t);
// --- 底部分隔線 + 統計(最小/最大)---
oled_hline(55);
if (g_count > 0) {
float mn = 9999.0f, mx = -9999.0f;
for (uint8_t i = 0; i < g_count; i++) {
uint8_t bi = (g_head - g_count + i + HIST_SIZE) % HIST_SIZE;
float v = is_t ? g_th[bi] : g_hh[bi];
if (v < mn) mn = v;
if (v > mx) mx = v;
}
char ms[8], xs[8];
dtostrf(mn, 4, 1, ms); dtostrf(mx, 4, 1, xs);
snprintf(buf, sizeof(buf), "LO:%s HI:%s", ms, xs);
oled_puts(0, 56, buf);
} else {
oled_puts(0, 56, "COLLECTING...");
}
oled_flush();
}
// ====================================================================
// 任務:LCD 更新(每 1000ms,2×16 字元)
// Row0: "HH:MM YYYY/MM/DD" (16 chars)
// Row1: "T:XX.XC H:XX.X%" (15 chars)
// ====================================================================
static void task_lcd() {
char r0[17], r1[17];
if (g.rtc_ok)
snprintf(r0, sizeof(r0), "%02u:%02u %04u/%02u/%02u",
g.dt.hour, g.dt.min, g.dt.year, g.dt.month, g.dt.day);
else
snprintf(r0, sizeof(r0), "--:-- ----/--/--");
if (g.dht_ok) {
char ts[7], hs[7];
dtostrf(g.temp, 4, 1, ts); dtostrf(g.humid, 4, 1, hs);
snprintf(r1, sizeof(r1), "T:%s\xdf" "C H:%s%%", ts, hs);
} else {
snprintf(r1, sizeof(r1), "T:---.- H:---.-%");
}
lcd_goto(0, 0); lcd_print(r0);
lcd_goto(0, 1); lcd_print(r1);
}
// ====================================================================
// 任務:Serial 輸出(符合題目格式)
// 正常:YYYY/MM/DD HH:MM:SS | XXX.XC | XXX.X%
// 異常:對應欄位以 - 顯示
// ====================================================================
static void task_serial() {
char buf[48];
if (g.rtc_ok)
snprintf(buf, sizeof(buf), "%04u/%02u/%02u %02u:%02u:%02u",
g.dt.year, g.dt.month, g.dt.day,
g.dt.hour, g.dt.min, g.dt.sec);
else
snprintf(buf, sizeof(buf), "----/--/-- --:--:--");
Serial1.print(buf);
Serial1.print(" | ");
if (g.dht_ok) {
char ts[8], hs[8];
dtostrf(g.temp, 5, 1, ts); dtostrf(g.humid, 5, 1, hs);
snprintf(buf, sizeof(buf), "%s\xdf" "C | %s%%", ts, hs);
Serial1.println(buf);
} else {
Serial1.println(" -C | -%");
}
}
// ====================================================================
// setup / loop (millis() 非阻塞排程)
// ====================================================================
void setup() {
Serial1.begin(115200);
i2c_init();
rtc_ensure_running();
lcd_init();
oled_init();
enc_init();
memset(&g, 0, sizeof(g));
}
void loop() {
uint32_t now = millis();
static uint32_t t_rtc = 0; // RTC 上次取樣
static uint32_t t_dht = 0; // DHT22 上次取樣
static uint32_t t_disp = 0; // LCD + Serial 上次更新
static uint32_t t_enc = 0; // 編碼器上次輪詢
// --- 編碼器輪詢(每 50ms)---
if (now - t_enc >= 50) {
t_enc = now;
int8_t d = enc_dir();
if (d != 0) g.mode = (g.mode + 1) % 2; // 旋轉:切換溫/濕圖
if (enc_clicked()) { g_head = 0; g_count = 0; } // 按下:重置歷史
}
// --- RTC 取樣(每 1000ms)---
if (now - t_rtc >= 1000) {
t_rtc = now;
g.rtc_ok = rtc_read(&g.dt);
}
// --- DHT22 取樣(每 5000ms,間隔 ≥2s 規格)---
if (now - t_dht >= 5000) {
t_dht = now;
float t, h;
bool ok = dht22_read(&t, &h);
if (ok) { g.temp = t; g.humid = h; }
g.dht_ok = ok;
}
// --- LCD + Serial 更新(每 1000ms)---
if (now - t_disp >= 1000) {
t_disp = now;
if (g.dht_ok) hist_push(g.temp, g.humid); // 每秒記錄一筆
task_lcd();
task_serial();
}
}