/**
* @file sketch.ino
* @brief ATEN Code Jam - STM32F103C8 環境監控韌體(硬體 I2C1 版)
* @details MCU: STM32F103C8 BluePill @72MHz,Arduino 框架。
* 元件:DHT22(PA0) | DS1307 RTC(I2C 0x68) | LCD2004(I2C 0x27) |
* SSD1306 OLED(I2C 0x3C) | LED OK(PA1) | LED ERR(PA2) | Buzzer(PA3)。
* 排程:millis() 非阻塞;禁止 delay() 影響主迴圈時序。
*
* @par 分層架構
* - Layer 1:I2C HAL(MCU-specific,改 HAL_MCU_* define 切換)
* - Layer 2:Device Drivers(DHT22 / DS1307 / SSD1306 / HD44780+PCF8574)
* - Layer 3:Application Tasks(task_* / hist_push / draw_graph)
* - Layer 4:Main(setup / loop)
*/
#include <Arduino.h>
#include <string.h>
#include <stdio.h>
// ====================================================================
// 腳位 / I2C 位址
// ====================================================================
#define PIN_DHT22 PA0
#define DHTLIB_TIMEOUT 1000
#define PIN_LED_OK PA1
#define PIN_LED_ERR PA2
#define PIN_BUZZER PA3
#define PIN_BTN PA4 ///< Demo 按鈕:每按一次循環切換錯誤注入狀態
#define ADDR_LCD 0x27
#define ADDR_RTC 0x68
#define ADDR_OLED 0x3C
#define HIST_SIZE 128
#define GRAPH_TOP 14
#define GRAPH_BOT 53
#define TEMP_ALERT_HIGH 35.0f
#define HUMID_ALERT_HIGH 80.0f
// I2C 硬體設定(APB1=36MHz, Fast-Mode 400kHz)
#define I2C_APB1_MHZ (36)
#define I2C_CCR_FAST_400KHZ (30)
#define I2C_TRISE_FAST_400KHZ (12)
#define I2C_SWRST_HOLD_US (10)
// DHT22 時序
#define DHT22_START_LOW_MS (18)
#define DHT22_PULLUP_US (40)
#define DHT22_BIT_THRESH_US (40)
// RTC
#define RTC_YEAR_BASE (2000)
// OLED 尺寸
#define OLED_WIDTH (128)
#define OLED_HEIGHT (64)
#define OLED_PAGES (8)
#define OLED_MAX_TEXT_X (123)
// 圖表量程與統計初始值
#define GRAPH_TEMP_MAX (50.0f)
#define GRAPH_HUMID_MAX (100.0f)
#define STAT_INIT_MIN (9999.0f)
#define STAT_INIT_MAX (-9999.0f)
// 取消下一行註解以啟用 Unit Tests(Serial1 輸出 PASS/FAIL 結果)
// #define UTEST_ENABLE
// ====================================================================
// 資料結構 / 全域狀態
// ====================================================================
/** @brief 日期時間結構,對應 DS1307 寄存器格式 */
struct DateTime {
uint8_t sec; /**< 秒(0–59) */
uint8_t min; /**< 分(0–59) */
uint8_t hour; /**< 時(0–23) */
uint8_t day; /**< 日(1–31) */
uint8_t month; /**< 月(1–12) */
uint16_t year; /**< 年(完整西元年,如 2026) */
};
/** @brief 應用程式全域狀態(感測器讀值 + 告警旗標) */
static struct {
DateTime dt; /**< 最近一次 RTC 讀值 */
float temp; /**< 最近一次溫度(°C) */
float humid; /**< 最近一次濕度(%RH) */
bool rtc_ok; /**< RTC 讀取成功旗標 */
bool dht_ok; /**< DHT22 讀取成功旗標 */
bool alert; /**< 目前是否處於告警狀態 */
uint8_t mode; /**< OLED 模式:0=溫度趨勢,1=濕度趨勢 */
} g;
static float g_th[HIST_SIZE];
static float g_hh[HIST_SIZE];
static uint8_t g_head = 0;
static uint8_t g_count = 0;
static uint8_t oled_buf[1024];
/** @brief Demo 錯誤注入狀態:0=Normal, 1=DHT ERR, 2=RTC ERR, 3=Both ERR */
static uint8_t g_demo_state = 0;
/**
* @defgroup Layer1_I2cHal I2C HAL(Layer 1 — MCU-specific)
* @brief 硬體 I2C 抽象層。改 HAL_MCU_* define 切換目標 MCU;
* Layer 2 以上只呼叫 I2cHal_* 公開介面,換 MCU 完全不需修改。
* @{
*/
// ====================================================================
// [Layer 1] I2C HAL — 改下方 #define 選擇目標 MCU
// 公開介面:I2cHal_init / I2cHal_write / I2cHal_writeEx / I2cHal_read
// Layer 2(Device Drivers)只呼叫公開介面,換 MCU 時完全不需修改
// ====================================================================
#define HAL_MCU_STM32F103C8 1 /**< STM32F103C8 實作:1=啟用, 0=停用 */
// ─── STM32F103C8 私有實作區 ─────────────────────────────────────────
// PB6=SCL, PB7=SDA, 400 kHz Fast Mode, APB1=36MHz
#if HAL_MCU_STM32F103C8 == 1
#define HW_I2C_TIMEOUT 20000UL /**< I2C busy-wait 最大迴圈次數(逾時保護) */
// Forward declarations(Stm32_ 前綴,防多 MCU 共存時命名衝突)
static void Stm32_applyConfig();
static void Stm32_waitStop();
static void Stm32_recoverBus();
static bool Stm32_waitSr1(uint16_t flag);
static void Stm32_i2cInit();
static bool Stm32_i2cWrite(uint8_t addr, const uint8_t *buf, uint8_t len);
static bool Stm32_i2cWriteEx(uint8_t addr, uint8_t prefix, const uint8_t *buf, uint16_t len);
static bool Stm32_i2cRead(uint8_t addr, uint8_t *buf, uint8_t len);
/** @brief 套用 I2C1 暫存器設定(APB1=36MHz, Fast-Mode 400kHz)。消除 init/recover 重複碼。 */
static void Stm32_applyConfig() {
I2C1->CR2 = I2C_APB1_MHZ;
I2C1->CCR = I2C_CCR_FS | I2C_CCR_FAST_400KHZ;
I2C1->TRISE = I2C_TRISE_FAST_400KHZ;
I2C1->CR1 = I2C_CR1_PE | I2C_CR1_ACK;
}
/** @brief 發出 STOP 後輪詢等待 I2C BUSY bit 清除,最多等待 HW_I2C_TIMEOUT 次。 */
static void Stm32_waitStop() {
uint32_t n = HW_I2C_TIMEOUT;
while ((I2C1->SR2 & I2C_SR2_BUSY) && n--);
}
/** @brief 完整 I2C 軟體重置(SWRST pulse + 重設所有暫存器),用於 BUSY bit 卡死時恢復。 */
static void Stm32_recoverBus() {
I2C1->CR1 |= I2C_CR1_SWRST;
delayMicroseconds(I2C_SWRST_HOLD_US);
I2C1->CR1 = 0;
Stm32_applyConfig();
}
/**
* @brief 輪詢等待 I2C SR1 旗標;遇到錯誤或逾時則觸發 recoverBus。
* @param flag 目標旗標(如 I2C_SR1_SB, I2C_SR1_ADDR, I2C_SR1_TXE)
* @retval true 旗標已設立
* @retval false 偵測到 ARLO/BERR/AF 或等待逾時
*/
static bool Stm32_waitSr1(uint16_t flag) {
uint32_t n = HW_I2C_TIMEOUT;
while (n--) {
uint16_t sr1 = (uint16_t)I2C1->SR1;
if (sr1 & (uint16_t)(I2C_SR1_ARLO | I2C_SR1_BERR | I2C_SR1_AF)) {
I2C1->SR1 = sr1 & (uint16_t)~(I2C_SR1_ARLO | I2C_SR1_BERR | I2C_SR1_AF);
I2C1->CR1 |= I2C_CR1_STOP;
Stm32_waitStop();
return false;
}
if (sr1 & flag) return true;
}
// timeout:完整 recoverBus
Stm32_recoverBus();
return false;
}
/** @brief 初始化 I2C1(PB6=SCL, PB7=SDA,AF Open-Drain,400kHz Fast Mode)。 */
static void Stm32_i2cInit() {
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// PB6(SCL) / PB7(SDA):AF Open-Drain 50 MHz
GPIOB->CRL = (GPIOB->CRL & ~(0xFFUL << 24)) | (0xFFUL << 24);
I2C1->CR1 = I2C_CR1_SWRST;
I2C1->CR1 = 0;
Stm32_applyConfig();
}
/**
* @brief I2C 寫入(START + addr + buf + STOP)。
* @param addr 7-bit 裝置位址
* @param buf 傳送資料緩衝區
* @param len 傳送位元組數
* @retval true 傳送成功
* @retval false I2C 錯誤或逾時
*/
static bool Stm32_i2cWrite(uint8_t addr, const uint8_t *buf, uint8_t len) {
uint32_t n = HW_I2C_TIMEOUT;
while ((I2C1->SR2 & I2C_SR2_BUSY) && n--);
if (!n) { Stm32_recoverBus(); return false; }
I2C1->CR1 |= I2C_CR1_START;
if (!Stm32_waitSr1(I2C_SR1_SB)) return false;
I2C1->DR = (uint8_t)(addr << 1);
if (!Stm32_waitSr1(I2C_SR1_ADDR)) return false;
(void)I2C1->SR1; (void)I2C1->SR2;
for (uint8_t i = 0; i < len; i++) {
if (!Stm32_waitSr1(I2C_SR1_TXE)) return false;
I2C1->DR = buf[i];
}
if (!Stm32_waitSr1(I2C_SR1_BTF)) return false;
I2C1->CR1 |= I2C_CR1_STOP;
Stm32_waitStop();
return true;
}
/**
* @brief I2C 擴充寫入:在同一 frame 內先送 prefix byte,再送 buf,不需合併緩衝區。
* @details 用於 OLED Horizontal Mode flush:prefix=0x40(data stream),buf=1024-byte oled_buf。
* @param addr 7-bit 裝置位址
* @param prefix 控制位元組(如 SSD1306 的 0x00=command / 0x40=data)
* @param buf 資料緩衝區
* @param len buf 位元組數(最大 65535)
* @retval true 傳送成功
* @retval false buf 為 NULL、I2C 錯誤或逾時
*/
static bool Stm32_i2cWriteEx(uint8_t addr, uint8_t prefix, const uint8_t *buf, uint16_t len) {
if (NULL == buf) { // §2.4 pointer check
return false;
}
uint32_t n = HW_I2C_TIMEOUT;
while ((I2C1->SR2 & I2C_SR2_BUSY) && n--);
if (!n) { Stm32_recoverBus(); return false; }
I2C1->CR1 |= I2C_CR1_START;
if (!Stm32_waitSr1(I2C_SR1_SB)) return false;
I2C1->DR = (uint8_t)(addr << 1);
if (!Stm32_waitSr1(I2C_SR1_ADDR)) return false;
(void)I2C1->SR1; (void)I2C1->SR2;
// 先送 prefix(SSD1306 control byte,0x40 = data stream)
if (!Stm32_waitSr1(I2C_SR1_TXE)) return false;
I2C1->DR = prefix;
// 再連續送 frame buffer(同一 I2C frame,無需 STOP)
for (uint16_t i = 0; i < len; i++) {
if (!Stm32_waitSr1(I2C_SR1_TXE)) return false;
I2C1->DR = buf[i];
}
if (!Stm32_waitSr1(I2C_SR1_BTF)) return false;
I2C1->CR1 |= I2C_CR1_STOP;
Stm32_waitStop();
return true;
}
/**
* @brief I2C 讀取(START + addr|R + buf + NACK + STOP)。
* @param addr 7-bit 裝置位址
* @param buf 接收資料緩衝區
* @param len 接收位元組數(> 0)
* @retval true 接收成功
* @retval false len=0、I2C 錯誤或逾時
*/
static bool Stm32_i2cRead(uint8_t addr, uint8_t *buf, uint8_t len) {
if (0 == len) return false; // §2.5 argument check
uint32_t n = HW_I2C_TIMEOUT;
while ((I2C1->SR2 & I2C_SR2_BUSY) && n--);
if (!n) { Stm32_recoverBus(); return false; }
I2C1->CR1 |= I2C_CR1_ACK;
I2C1->CR1 |= I2C_CR1_START;
if (!Stm32_waitSr1(I2C_SR1_SB)) return false;
I2C1->DR = (uint8_t)((addr << 1) | 1);
if (!Stm32_waitSr1(I2C_SR1_ADDR)) {
I2C1->CR1 |= I2C_CR1_STOP;
Stm32_waitStop();
return false;
}
if (1 == len) {
I2C1->CR1 &= ~(uint16_t)I2C_CR1_ACK;
(void)I2C1->SR1; (void)I2C1->SR2;
I2C1->CR1 |= I2C_CR1_STOP;
if (!Stm32_waitSr1(I2C_SR1_RXNE)) return false;
buf[0] = (uint8_t)I2C1->DR;
return true;
}
(void)I2C1->SR1; (void)I2C1->SR2;
for (uint8_t i = 0; i < len - 1; i++) {
if (!Stm32_waitSr1(I2C_SR1_RXNE)) return false;
buf[i] = (uint8_t)I2C1->DR;
if (i == (uint8_t)(len - 2)) {
I2C1->CR1 &= ~(uint16_t)I2C_CR1_ACK;
I2C1->CR1 |= I2C_CR1_STOP;
}
}
if (!Stm32_waitSr1(I2C_SR1_RXNE)) return false;
buf[len - 1] = (uint8_t)I2C1->DR;
return true;
}
#endif // HAL_MCU_STM32F103C8
// ─── 未來 MCU 實作區(新增 #if HAL_MCU_xxx == 1 ... #endif 即可)────
// #if HAL_MCU_RP2040 == 1
// static void Rp2040_i2cInit() { ... }
// static bool Rp2040_i2cWrite(addr, buf, len) { ... }
// static bool Rp2040_i2cWriteEx(addr, prefix, buf, len) { ... }
// static bool Rp2040_i2cRead(addr, buf, len) { ... }
// #endif
// ─── HAL 公開介面(永遠存在;#if 內部選擇 MCU 實作)────────────────
/** @brief 初始化 I2C 硬體。須在所有 I2cHal_write/read 呼叫前執行。 */
static void I2cHal_init() {
#if HAL_MCU_STM32F103C8 == 1
Stm32_i2cInit();
#endif
}
/**
* @brief I2C 寫入(HAL 公開介面)。
* @param addr 7-bit 裝置位址
* @param buf 傳送緩衝區
* @param len 位元組數
* @retval true 成功;false 失敗
*/
static bool I2cHal_write(uint8_t addr, const uint8_t *buf, uint8_t len) {
#if HAL_MCU_STM32F103C8 == 1
return Stm32_i2cWrite(addr, buf, len);
#else
return false;
#endif
}
/**
* @brief I2C 擴充寫入(HAL 公開介面):prefix + buf 合一 frame。
* @param addr 7-bit 裝置位址
* @param prefix 控制位元組(先送)
* @param buf 資料緩衝區
* @param len buf 位元組數
* @retval true 成功;false 失敗
*/
static bool I2cHal_writeEx(uint8_t addr, uint8_t prefix,
const uint8_t *buf, uint16_t len) {
#if HAL_MCU_STM32F103C8 == 1
return Stm32_i2cWriteEx(addr, prefix, buf, len);
#else
return false;
#endif
}
/**
* @brief I2C 讀取(HAL 公開介面)。
* @param addr 7-bit 裝置位址
* @param buf 接收緩衝區
* @param len 接收位元組數
* @retval true 成功;false 失敗
*/
static bool I2cHal_read(uint8_t addr, uint8_t *buf, uint8_t len) {
#if HAL_MCU_STM32F103C8 == 1
return Stm32_i2cRead(addr, buf, len);
#else
return false;
#endif
}
/** @} */ // end of Layer1_I2cHal
/**
* @defgroup Layer2_DeviceDrivers Device Drivers(Layer 2 — MCU-agnostic)
* @brief 感測器/顯示器驅動層。透過 I2cHal_* 存取 I2C 裝置;
* DHT22 使用 Arduino GPIO API,換 Arduino-compatible MCU 無需修改。
* @{
*/
// ====================================================================
// [Layer 2] Device Drivers(MCU-agnostic,依賴 I2cHal_* 介面)
// DHT22 / DS1307 RTC / SSD1306 OLED / HD44780+PCF8574 LCD
// ====================================================================
// ─── DHT22 Driver (PA0, bit-bang, Arduino GPIO API) ─────────────────
/**
* @brief 讀取 DHT22 溫濕度感測器(1-Wire bit-bang,PIN_DHT22)。
* @param out_t 輸出溫度(°C),呼叫者需確保非 NULL
* @param out_h 輸出濕度(%RH),呼叫者需確保非 NULL
* @retval true 讀取並 checksum 驗證成功
* @retval false pointer 為 NULL、通訊逾時或 checksum 錯誤
*/
static bool dht22_read(float *out_t, float *out_h) {
if (NULL == out_t || NULL == out_h) { // §2.4 指標使用前驗證
return false;
}
uint8_t bits[5] = {0};
uint8_t cnt = 7;
uint8_t idx = 0;
// Start signal — delay(18) 為 DHT22 規格必要(≥1ms LOW),
// 是 DHTlib port 的刻意例外,非違反主迴圈時序規則。
pinMode(PIN_DHT22, OUTPUT);
digitalWrite(PIN_DHT22, LOW);
delay(DHT22_START_LOW_MS);
digitalWrite(PIN_DHT22, HIGH);
delayMicroseconds(DHT22_PULLUP_US);
pinMode(PIN_DHT22, INPUT_PULLUP);
// 從這裡開始保護時序,禁止中斷打斷 bit 解碼
noInterrupts();
uint32_t loopCnt;
// ACK LOW
loopCnt = DHTLIB_TIMEOUT;
while (digitalRead(PIN_DHT22) == HIGH) {
if (--loopCnt == 0) { interrupts(); return false; }
}
// ACK HIGH
loopCnt = DHTLIB_TIMEOUT;
while (digitalRead(PIN_DHT22) == LOW) {
if (--loopCnt == 0) { interrupts(); return false; }
}
// post-ACK LOW
loopCnt = DHTLIB_TIMEOUT;
while (digitalRead(PIN_DHT22) == HIGH) {
if (--loopCnt == 0) { interrupts(); return false; }
}
// 讀 40 bit
for (int i = 0; i < 40; i++) {
loopCnt = DHTLIB_TIMEOUT;
while (digitalRead(PIN_DHT22) == LOW) {
if (--loopCnt == 0) { interrupts(); return false; }
}
uint32_t t = micros();
loopCnt = DHTLIB_TIMEOUT;
while (digitalRead(PIN_DHT22) == HIGH) {
if (--loopCnt == 0) { interrupts(); return false; }
}
if ((micros() - t) > DHT22_BIT_THRESH_US) {
bits[idx] |= (1 << cnt);
}
if (cnt == 0) { cnt = 7; idx++; }
else { cnt--; }
}
interrupts();
if (bits[4] != ((bits[0] + bits[1] + bits[2] + bits[3]) & 0xFF)) return false;
uint16_t rh = ((uint16_t)bits[0] << 8) | bits[1];
uint16_t rt = ((uint16_t)bits[2] << 8) | bits[3];
*out_h = rh * 0.1f;
*out_t = (rt & 0x8000) ? -(float)(rt & 0x7FFF) * 0.1f : (float)rt * 0.1f;
return true;
}
// ====================================================================
// DS1307 RTC Driver (I2C 0x68)
// ====================================================================
/** @brief BCD 轉十進位。@param b BCD 編碼值。@return 十進位值。 */
static uint8_t bcd2d(uint8_t b) { return (b >> 4) * 10 + (b & 0x0F); }
/** @brief 十進位轉 BCD。@param d 十進位值(0–99)。@return BCD 編碼值。 */
static uint8_t d2bcd(uint8_t d) { return ((d / 10) << 4) | (d % 10); }
/**
* @brief 從 DS1307 讀取目前時間。
* @param dt 輸出 DateTime 結構指標(不可為 NULL)
* @retval true 讀取成功且振盪器正常(CH=0)
* @retval false dt 為 NULL、I2C 失敗或 CH bit=1(振盪器停止)
*/
static bool rtc_read(DateTime *dt) {
if (NULL == dt) { // §2.4 指標使用前驗證
return false;
}
uint8_t reg = 0x00;
if (!I2cHal_write(ADDR_RTC, ®, 1)) return false;
uint8_t buf[7];
if (!I2cHal_read(ADDR_RTC, buf, 7)) return false;
if (buf[0] & 0x80) return false; // CH bit = 振盪器停止
dt->sec = bcd2d(buf[0] & 0x7F);
dt->min = bcd2d(buf[1]);
dt->hour = bcd2d(buf[2] & 0x3F);
dt->day = bcd2d(buf[4]);
dt->month = bcd2d(buf[5]);
dt->year = RTC_YEAR_BASE + bcd2d(buf[6]);
return true;
}
/**
* @brief 解析 __DATE__ / __TIME__ 編譯時間戳,用於 RTC 初始化。
* @param dd 輸出日(1–31)
* @param mo 輸出月(1–12)
* @param yr 輸出年(完整西元年)
* @param hr 輸出時(0–23)
* @param mn 輸出分(0–59)
* @param sc 輸出秒(0–59)
*/
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'));
}
/**
* @brief 確認 DS1307 振盪器正常運行;若 CH=1(剛上電)則寫入編譯時間並啟動。
* @note 僅在 CH bit=1 時寫入,不覆蓋已在計時的 RTC。
*/
static void rtc_ensure_running() {
delay(100);
uint8_t reg = 0x00;
uint8_t sec_reg;
if (!I2cHal_write(ADDR_RTC, ®, 1)) return;
if (!I2cHal_read(ADDR_RTC, &sec_reg, 1)) return;
if (!(sec_reg & 0x80)) return; // CH=0:已在計時
uint8_t dd, mo, hr, mn, sc;
uint16_t yr;
parse_compile_datetime(&dd, &mo, &yr, &hr, &mn, &sc);
uint8_t init_buf[8] = {
0x00,
d2bcd(sc),
d2bcd(mn),
d2bcd(hr),
1,
d2bcd(dd),
d2bcd(mo),
d2bcd((uint8_t)(yr % 100))
};
if (!I2cHal_write(ADDR_RTC, init_buf, 8)) { // §2.3 檢查回傳值
return;
}
}
// ====================================================================
// SSD1306 OLED Driver (I2C 0x3C, 128x64, Horizontal Addressing Mode)
// ====================================================================
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
};
/** @brief 送出單一 SSD1306 命令位元組(control byte 0x00 + command)。@param c 命令值。 */
static void oled_cmd(uint8_t c) {
uint8_t buf[2] = { 0x00, c };
I2cHal_write(ADDR_OLED, buf, 2);
}
/**
* @brief 設定 oled_buf 中指定像素的亮滅狀態(不直接寫 I2C,須呼叫 oled_flush)。
* @param x 水平座標(0–127)
* @param y 垂直座標(0–63)
* @param on true=點亮,false=熄滅
*/
static void oled_px(uint8_t x, uint8_t y, bool on) {
if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return;
uint16_t idx = (uint16_t)(y >> 3) * OLED_WIDTH + x;
uint8_t bit = y & 0x07;
if (on) oled_buf[idx] |= (1 << bit);
else oled_buf[idx] &= ~(1 << bit);
}
/**
* @brief 將 oled_buf(1024 bytes)一次性輸出至 SSD1306 VRAM(Horizontal Addressing Mode)。
* @details 2 筆 I2C:① column/page range 命令包 ② I2cHal_writeEx 送 1024 bytes。
* 相較 Page Mode 16 筆,減少 87.5% I2C 交易量。
*/
static void oled_flush() {
// 命令包:0x00=command stream, 0x21 col 0-127, 0x22 page 0-7
static const uint8_t ADDR_SEQ[] = {
0x00, // control byte:command stream
0x21, 0x00, 0x7F, // Set Column Address: 0 - 127
0x22, 0x00, 0x07 // Set Page Address: 0 - 7
};
I2cHal_write(ADDR_OLED, ADDR_SEQ, sizeof(ADDR_SEQ));
// 資料包:0x40 = data stream control byte,oled_buf 1024 bytes 一次送完
I2cHal_writeEx(ADDR_OLED, 0x40, oled_buf, (uint16_t)sizeof(oled_buf));
}
/** @brief 在 oled_buf 指定位置繪製一個 5x8 字元(大寫英數字符)。@param x 左上角 X。@param y 左上角 Y。@param c ASCII 字元。 */
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);
}
/** @brief 在 oled_buf 指定位置繪製字串(每字元寬 6px,遇右邊界停止)。@param x 起始 X。@param y 起始 Y。@param s 字串(不可為 NULL)。 */
static void oled_puts(uint8_t x, uint8_t y, const char *s) {
if (NULL == s) { // §2.4 指標使用前驗證
return;
}
while (*s && x < OLED_MAX_TEXT_X) { oled_putc(x, y, *s++); x += 6; }
}
/** @brief 在 oled_buf 第 y 列畫水平分隔線(x=0 到 127)。@param y 像素列(0–63)。 */
static void oled_hline(uint8_t y) {
for (uint8_t x = 0; x < OLED_WIDTH; x++) oled_px(x, y, true);
}
/** @brief 切換 SSD1306 顯示反白模式(告警視覺提示)。@param inv true=反白,false=正常。 */
static void oled_invert(bool inv) {
oled_cmd(inv ? 0xA7 : 0xA6);
}
/** @brief 初始化 SSD1306(Horizontal Addressing Mode,128x64,Charge Pump ON)。 */
static void oled_init() {
delay(100);
static const uint8_t SEQ[] = {
0xAE, // 關閉顯示
0xD5, 0x80, // 振盪頻率
0xA8, 0x3F, // Mux ratio 64
0xD3, 0x00, // 顯示偏移 0
0x40, // 起始行 0
0x8D, 0x14, // Charge pump ON
0x20, 0x00, // Horizontal Addressing Mode(改自 Page Mode 0x02)
0xA1, // SEG remap
0xC8, // COM 掃描方向
0xDA, 0x12, // COM pins config
0x81, 0xCF, // 對比度
0xD9, 0xF1, // Pre-charge
0xDB, 0x40, // VCOMH
0xA4, // 從 RAM 顯示
0xA6, // 正常顯示
0xAF, // 開啟顯示
};
for (uint8_t i = 0; i < sizeof(SEQ); i++) oled_cmd(SEQ[i]);
memset(oled_buf, 0, sizeof(oled_buf));
}
/** @} */ // end of Layer2_DeviceDrivers
/**
* @defgroup Layer3_AppTasks Application Tasks(Layer 3 — 純邏輯)
* @brief 顯示更新、告警管理、Serial 輸出等任務函式;不直接存取硬體暫存器。
* @{
*/
// ====================================================================
// [Layer 3] Application Tasks(純邏輯,不碰硬體暫存器)
// task_oled / task_lcd / task_serial / task_alert
// hist_push / draw_graph / val_to_y
// ====================================================================
// ─── 趨勢 Ring Buffer 與圖表 ─────────────────────────────────────────
/**
* @brief 推入一筆溫濕度紀錄至環形歷史 buffer(HIST_SIZE=128 點)。
* @param t 溫度(°C)
* @param h 濕度(%RH)
*/
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++;
}
/**
* @brief 將感測器數值映射至 OLED 像素 Y 座標(GRAPH_TOP 到 GRAPH_BOT)。
* @param v 輸入值(超出範圍會被 clamp)
* @param vmin 量程下界
* @param vmax 量程上界
* @return 像素 Y 座標(GRAPH_TOP=最高,GRAPH_BOT=最低)
*/
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));
}
/** @brief 將歷史 buffer 繪製為折線圖至 oled_buf(連接相鄰點的垂直線段)。@param is_temp true=溫度圖,false=濕度圖。 */
static void draw_graph(bool is_temp) {
float vmax = is_temp ? GRAPH_TEMP_MAX : GRAPH_HUMID_MAX;
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)(OLED_WIDTH - g_count + i);
uint8_t y = val_to_y(v, 0.0f, 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;
}
}
/** @brief 更新 OLED 顯示(趨勢圖 + 統計),由 DHT22 事件觸發(非計時器)。 */
static void task_oled() {
bool is_t = (g.mode == 0);
memset(oled_buf, 0, sizeof(oled_buf));
char buf[24];
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);
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 = STAT_INIT_MIN, mx = STAT_INIT_MAX;
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_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();
}
/**
* @brief 告警管理任務(每 500ms):偵測溫濕度超限,控制 LED/Buzzer 閃爍及 OLED 反白。
* @details OLED 反白觸發條件:溫濕度告警(g.alert)或 DHT22 異常(!g.dht_ok)。
* SSD1306 為單色 OLED,反白(0xA7)為「紅色背景」的最佳近似。
* @param now 目前 millis() 時間戳(用於閃爍計時)
*/
static void task_alert(uint32_t now) {
bool new_alert = g.dht_ok &&
(g.temp > TEMP_ALERT_HIGH || g.humid > HUMID_ALERT_HIGH);
if (new_alert != g.alert) {
g.alert = new_alert;
}
// OLED 反白:溫濕度告警 或 DHT22 異常(!g.dht_ok)時觸發
static bool prev_invert = false;
bool need_invert = g.alert || !g.dht_ok;
if (need_invert != prev_invert) {
prev_invert = need_invert;
oled_invert(need_invert);
}
if (g.alert) {
static uint32_t t_flash = 0;
static uint8_t flash_state = 0;
if (now - t_flash >= 500) {
t_flash = now;
flash_state ^= 1;
digitalWrite(PIN_LED_ERR, flash_state ? HIGH : LOW);
digitalWrite(PIN_BUZZER, flash_state ? HIGH : LOW);
}
digitalWrite(PIN_LED_OK, LOW);
} else {
digitalWrite(PIN_LED_OK, HIGH);
digitalWrite(PIN_LED_ERR, LOW);
digitalWrite(PIN_BUZZER, LOW);
}
}
// ====================================================================
// PCF8574 + HD44780 LCD2004 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
static const uint8_t LCD_ROW_ADDR[] = {0x00, 0x40, 0x14, 0x54};
/** @brief 填入 PCF8574 nibble 封包(EN 高→低 + RS + BL 旗標)至 buf[0..1]。@param buf 2-byte 輸出緩衝。@param nib 4-bit 資料。@param rs 1=資料,0=命令。 */
static inline void lcd_fill_nibble(uint8_t *buf, uint8_t nib, uint8_t rs) {
uint8_t b = (nib << 4) | LCD_BL | (rs ? LCD_RS : 0);
buf[0] = b | LCD_EN;
buf[1] = b;
}
/** @brief 透過 I2C 送出單一 nibble(2-byte PCF8574 封包)。@param nib 4-bit 值。@param rs RS 旗標。 */
static void lcd_nibble_slow(uint8_t nib, uint8_t rs) {
uint8_t bytes[2];
lcd_fill_nibble(bytes, nib, rs);
I2cHal_write(ADDR_LCD, bytes, 2);
}
/** @brief 透過 I2C 送出完整 byte(高 nibble + 低 nibble,各 2 bytes,共 4 bytes)。@param v 8-bit 資料。@param rs RS 旗標。 */
static void lcd_byte_slow(uint8_t v, uint8_t rs) {
uint8_t bytes[4];
lcd_fill_nibble(&bytes[0], v >> 4, rs);
lcd_fill_nibble(&bytes[2], v & 0xF, rs);
I2cHal_write(ADDR_LCD, bytes, 4);
delayMicroseconds(40);
}
/** @brief 送出 LCD 命令(RS=0 的 lcd_byte_slow 包裝)。@param c 命令值。 */
static void lcd_cmd_slow(uint8_t c) { lcd_byte_slow(c, 0); }
/** @brief 初始化 HD44780 LCD(4-bit 模式,4 行 20 字,PCF8574 I2C 橋接)。 */
static void lcd_init() {
delay(60);
lcd_nibble_slow(0x03, 0); delay(5);
lcd_nibble_slow(0x03, 0); delayMicroseconds(200);
lcd_nibble_slow(0x03, 0); delayMicroseconds(200);
lcd_nibble_slow(0x02, 0);
lcd_cmd_slow(0x28);
lcd_cmd_slow(0x08);
lcd_byte_slow(0x01, 0); delay(2);
lcd_cmd_slow(0x06);
lcd_cmd_slow(0x0C);
}
/**
* @brief 寫入 LCD 指定行(DDRAM goto + 20 字元)。
* @details 每個字元各一筆 I2C(4 bytes nibble 封包);goto 後 delay(1ms) 確保 Wokwi 模擬時序。
* @param row 行號(0–3);超出範圍直接返回
* @param s 字串(長度不足 20 則補空白,NULL 視為全空白行)
*/
static void lcd_write_row(uint8_t row, const char *s) {
if (row >= 4) { // §2.5 驗證 row 範圍(LCD_ROW_ADDR 陣列大小為 4)
return;
}
uint8_t cmd_buf[4];
uint8_t cmd = 0x80 | LCD_ROW_ADDR[row];
lcd_fill_nibble(&cmd_buf[0], cmd >> 4, 0);
lcd_fill_nibble(&cmd_buf[2], cmd & 0xF, 0);
I2cHal_write(ADDR_LCD, cmd_buf, 4);
delay(1); // 確保 HD44780 在 Wokwi 模擬下完成 set DDRAM address
for (uint8_t i = 0; i < 20; i++) {
uint8_t c = (s && s[i]) ? (uint8_t)s[i] : ' ';
uint8_t char_buf[4];
lcd_fill_nibble(&char_buf[0], c >> 4, 1);
lcd_fill_nibble(&char_buf[2], c & 0xF, 1);
I2cHal_write(ADDR_LCD, char_buf, 4);
}
}
/** @brief LCD 顯示更新任務(每 1000ms):Row 0 時間每秒更新,Row 1–3 dirty detection。 */
static void task_lcd() {
char r0[21], r1[21], r2[21], r3[21];
if (g.rtc_ok)
snprintf(r0, sizeof(r0), "%02u:%02u:%02u %04u/%02u/%02u ",
g.dt.hour, g.dt.min, g.dt.sec,
g.dt.year, g.dt.month, g.dt.day);
else
snprintf(r0, sizeof(r0), "--:--:-- ----/--/-- ");
if (g.dht_ok) {
char ts[8];
dtostrf(g.temp, 6, 1, ts);
snprintf(r1, sizeof(r1), "Temp:%s\xdf" "C ", ts);
} else {
snprintf(r1, sizeof(r1), "Temp: ---.-\xdf" "C ");
}
if (g.dht_ok) {
char hs[8];
dtostrf(g.humid, 6, 1, hs);
snprintf(r2, sizeof(r2), "Humd:%s %% ", hs);
} else {
snprintf(r2, sizeof(r2), "Humd: ---.- %% ");
}
snprintf(r3, sizeof(r3), "RTC: %-4sDHT: %-4s ",
g.rtc_ok ? "OK" : "ERR",
g.dht_ok ? "OK" : "ERR");
// Row 0(時間)每秒都更新
lcd_write_row(0, r0);
// Row 1-3 只在內容變化時更新
static char prev_r1[21] = "", prev_r2[21] = "", prev_r3[21] = "";
if (memcmp(r1, prev_r1, 21) != 0) { lcd_write_row(1, r1); memcpy(prev_r1, r1, 21); }
if (memcmp(r2, prev_r2, 21) != 0) { lcd_write_row(2, r2); memcpy(prev_r2, r2, 21); }
if (memcmp(r3, prev_r3, 21) != 0) { lcd_write_row(3, r3); memcpy(prev_r3, r3, 21); }
}
/** @brief Serial 輸出任務(每 1000ms):輸出 "YYYY/MM/DD HH:MM:SS | T°C | H%" 格式。 */
static void task_serial() {
char buf[64];
char dt[20];
if (g.rtc_ok)
snprintf(dt, sizeof(dt), "%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(dt, sizeof(dt), "----/--/-- --:--:--");
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 | %s\xc2\xb0" "C | %s%%", dt, ts, hs);
} else {
snprintf(buf, sizeof(buf), "%s | ---.-\xc2\xb0" "C | ---.-%%", dt);
}
Serial1.println(buf);
}
// ====================================================================
// Unit Tests(§1.6 UTest_<ModuleName>_<functionName>)
// 預設關閉;取消 #define UTEST_ENABLE 啟用
// ====================================================================
#ifdef UTEST_ENABLE
static uint8_t nUtestPass = 0;
static uint8_t nUtestFail = 0;
static void UTest_assert(bool bCond, const char *pszName) {
if (bCond) {
nUtestPass++;
Serial1.print(F("PASS: "));
} else {
nUtestFail++;
Serial1.print(F("FAIL: "));
}
Serial1.println(pszName);
}
// ─── RTC Driver:BCD 轉換 ─────────────────────────────────────────────────
static void UTest_RtcDriver_bcd2d() {
UTest_assert(0 == bcd2d(0x00), "bcd2d(0x00)==0");
UTest_assert(9 == bcd2d(0x09), "bcd2d(0x09)==9");
UTest_assert(10 == bcd2d(0x10), "bcd2d(0x10)==10");
UTest_assert(59 == bcd2d(0x59), "bcd2d(0x59)==59");
UTest_assert(99 == bcd2d(0x99), "bcd2d(0x99)==99");
}
static void UTest_RtcDriver_d2bcd() {
UTest_assert(0x00 == d2bcd(0), "d2bcd(0)==0x00");
UTest_assert(0x09 == d2bcd(9), "d2bcd(9)==0x09");
UTest_assert(0x10 == d2bcd(10), "d2bcd(10)==0x10");
UTest_assert(0x59 == d2bcd(59), "d2bcd(59)==0x59");
// Round-trip:確保 bcd2d 與 d2bcd 互逆
UTest_assert(25 == bcd2d(d2bcd(25)), "round-trip 25");
UTest_assert(0 == bcd2d(d2bcd(0)), "round-trip 0");
UTest_assert(59 == bcd2d(d2bcd(59)), "round-trip 59");
}
// ─── RTC Driver:編譯時間解析 ────────────────────────────────────────────
static void UTest_RtcDriver_parseCompileDateTime() {
uint8_t nDd = 0, nMo = 0, nHr = 0, nMn = 0, nSc = 0;
uint16_t nYr = 0;
parse_compile_datetime(&nDd, &nMo, &nYr, &nHr, &nMn, &nSc);
UTest_assert(nMo >= 1 && nMo <= 12, "parseDT: month in [1,12]");
UTest_assert(nDd >= 1 && nDd <= 31, "parseDT: day in [1,31]");
UTest_assert(nYr >= 2020, "parseDT: year >= 2020");
UTest_assert(nHr <= 23, "parseDT: hour <= 23");
UTest_assert(nMn <= 59, "parseDT: min <= 59");
UTest_assert(nSc <= 59, "parseDT: sec <= 59");
}
// ─── OLED Driver:Y 座標映射 ─────────────────────────────────────────────
static void UTest_OledDriver_valToY() {
// 最小值 → 底部像素(GRAPH_BOT)
UTest_assert(GRAPH_BOT == val_to_y(0.0f, 0.0f, 50.0f), "valToY: min==GRAPH_BOT");
// 最大值 → 頂部像素(GRAPH_TOP)
UTest_assert(GRAPH_TOP == val_to_y(50.0f, 0.0f, 50.0f), "valToY: max==GRAPH_TOP");
// 下界 clamp:輸入小於 vmin 仍回傳 GRAPH_BOT
UTest_assert(GRAPH_BOT == val_to_y(-1.0f, 0.0f, 50.0f), "valToY: clamp_min");
// 上界 clamp:輸入大於 vmax 仍回傳 GRAPH_TOP
UTest_assert(GRAPH_TOP == val_to_y(51.0f, 0.0f, 50.0f), "valToY: clamp_max");
// 中點:允許 ±1 pixel 浮點誤差
uint8_t nMid = (uint8_t)((GRAPH_BOT + GRAPH_TOP) / 2);
uint8_t nResult = val_to_y(25.0f, 0.0f, 50.0f);
UTest_assert(nResult >= nMid - 1 && nResult <= nMid + 1, "valToY: midpoint +-1px");
}
// ─── Hist Ring Buffer ─────────────────────────────────────────────────────
static void UTest_Hist_push() {
// 儲存全域狀態,測試後還原
uint8_t nSaveHead = g_head;
uint8_t nSaveCount = g_count;
g_head = 0; g_count = 0;
// 空 buffer
UTest_assert(0 == g_count, "hist: init count==0");
// push 第 1 筆
hist_push(25.0f, 60.0f);
UTest_assert(1 == g_count, "hist: count after 1 push");
UTest_assert(1 == g_head, "hist: head after 1 push");
UTest_assert(25.0f == g_th[0], "hist: temp stored at [0]");
UTest_assert(60.0f == g_hh[0], "hist: humid stored at [0]");
// push 到滿(HIST_SIZE 筆),確認 count 上限與 head 回繞
g_head = 0; g_count = 0;
for (uint8_t i = 0; i < HIST_SIZE; i++) {
hist_push((float)i, (float)i);
}
UTest_assert(HIST_SIZE == g_count, "hist: count capped at HIST_SIZE");
UTest_assert(0 == g_head, "hist: head wraps to 0 after full");
// 再 push 1 筆,count 不得超過 HIST_SIZE
hist_push(99.0f, 99.0f);
UTest_assert(HIST_SIZE == g_count, "hist: count still HIST_SIZE after overflow");
// 還原全域狀態
g_head = nSaveHead;
g_count = nSaveCount;
}
// ─── 總入口 ───────────────────────────────────────────────────────────────
static void UTest_runAll() {
nUtestPass = 0;
nUtestFail = 0;
Serial1.println(F("=== UTEST START ==="));
UTest_RtcDriver_bcd2d();
UTest_RtcDriver_d2bcd();
UTest_RtcDriver_parseCompileDateTime();
UTest_OledDriver_valToY();
UTest_Hist_push();
Serial1.print(F("=== UTEST: "));
Serial1.print(nUtestPass);
Serial1.print(F(" passed, "));
Serial1.print(nUtestFail);
Serial1.println(F(" failed ==="));
}
#endif // UTEST_ENABLE
/** @} */ // end of Layer3_AppTasks
/**
* @defgroup Layer4_Main Main(Layer 4)
* @brief Arduino 入口點 setup() / loop(),協調所有層的初始化與週期性排程。
* @{
*/
// ====================================================================
// [Layer 4] Main — setup() / loop()
// ====================================================================
/** @brief 硬體初始化:Serial / GPIO / I2C / RTC / LCD / OLED,啟用 LED OK。 */
void setup() {
Serial1.begin(115200);
#ifdef UTEST_ENABLE
UTest_runAll(); // 硬體 init 前先跑純邏輯測試
#endif
pinMode(PIN_LED_OK, OUTPUT); digitalWrite(PIN_LED_OK, LOW);
pinMode(PIN_LED_ERR, OUTPUT); digitalWrite(PIN_LED_ERR, LOW);
pinMode(PIN_BUZZER, OUTPUT); digitalWrite(PIN_BUZZER, LOW);
pinMode(PIN_BTN, INPUT_PULLUP);
I2cHal_init();
rtc_ensure_running();
lcd_init();
oled_init();
oled_flush();
memset(&g, 0, sizeof(g));
digitalWrite(PIN_LED_OK, HIGH);
}
/** @brief 主迴圈:millis() 非阻塞排程(RTC 1s / DHT22 5s / LCD+Serial 1s / Alert 500ms / OLED 事件觸發)。 */
void loop() {
// delay(1); // 讓 Wokwi 模擬引擎推進模擬時間,避免 millis 停滯
uint32_t now = millis();
// Watchdog 計數器:每 10000 次 loop 印一行,確認 loop 還活著
static uint32_t loop_count = 0;
if (++loop_count % 1000000 == 0) {
Serial1.print(F("ALIVE millis="));
Serial1.println(now);
}
static uint32_t t_rtc = 0;
static uint32_t t_dht = 0;
static uint32_t t_disp = 0;
static uint32_t t_alert = 0;
static bool bOledDirty = true; // 啟動時強制首次渲染
// Demo 按鈕:每按一次循環切換錯誤注入狀態(50ms 去彈跳)
// 狀態序列:0=Normal → 1=DHT ERR → 2=RTC ERR → 3=Both ERR → 0=Normal
{
static uint32_t t_btn = 0;
static uint8_t btn_prev = HIGH;
if (now - t_btn >= 50) {
t_btn = now;
uint8_t btn_cur = (uint8_t)digitalRead(PIN_BTN);
if (btn_prev == HIGH && btn_cur == LOW) { // 下降沿觸發
g_demo_state = (g_demo_state + 1) & 0x03;
static const char * const DEMO_NAMES[] =
{"Normal", "DHT ERR", "RTC ERR", "Both ERR"};
Serial1.print(F("[DEMO] -> "));
Serial1.println(DEMO_NAMES[g_demo_state]);
bOledDirty = true;
}
btn_prev = btn_cur;
}
}
// RTC 取樣(每 1000ms,直接讀硬體 DS1307,確保時間準確)
if (now - t_rtc >= 1000) {
t_rtc = now;
g.rtc_ok = rtc_read(&g.dt);
if (g_demo_state == 2 || g_demo_state == 3) g.rtc_ok = false; // Demo 注入
}
// DHT22 取樣(每 5000ms);讀取完成(成功或失敗)都標記 OLED 需重繪
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; hist_push(t, h); }
g.dht_ok = ok;
if (g_demo_state == 1 || g_demo_state == 3) g.dht_ok = false; // Demo 注入
bOledDirty = true; // 事件觸發:DHT22 週期到,OLED 需更新
}
// I2C 互斥旗標:同一 loop 迭代只允許一個 I2C 任務,防止連續大量交易卡死 BUSY bit
bool bI2cBusy = false;
// LCD + Serial 更新(每 1000ms,I2C 優先權高)
if (!bI2cBusy && now - t_disp >= 1000) {
t_disp = now;
task_lcd();
task_serial();
bI2cBusy = true;
}
// OLED 更新:事件觸發(DHT22 新資料),非計時器輪詢
// Horizontal Mode flush = 2 筆 I2C(原 16 筆);與 LCD 互斥
if (!bI2cBusy && bOledDirty) {
bOledDirty = false;
task_oled();
bI2cBusy = true;
}
// 告警管理(每 500ms;閃爍 timer 內建 500ms,間隔對齊後 toggle 仍精準)
if (now - t_alert >= 500) {
t_alert = now;
task_alert(now);
}
delay(1);
}
/** @} */ // end of Layer4_Main
Loading
stm32-bluepill
stm32-bluepill
Loading
ssd1306
ssd1306