準備設計arduino uno r3為主控的環境監測系統,通過傳感器采集TVOC(總揮發性有機物)、HCHO(甲醛)和eCO2(等效二氧化碳)數據,并顯示在LCD屏幕上,同時支持數據記錄到SD卡,以及通過旋轉編碼器進行交互。
最終呈現效果:
結合RTC時間戳將數據記錄至SD卡,并通過LCD顯示屏和旋轉編碼器實現用戶交互。系統具備三屏數據顯示、智能SD卡管理、時間設置和數據記錄控制等核心功能。
代碼結構分析:
- 包含的庫:Wire(I2C通信)、LiquidCrystal_I2C(I2C LCD控制)、SoftwareSerial(軟串口,用于與傳感器通信)、SdFat(SD卡操作)、RTClib(實時時鐘)。
- 引腳定義:TVOC傳感器使用軟串口(RX, TX)、SD卡片選、旋轉編碼器(CLK, DT, SW)、記錄按鈕、LED。
- 全局對象:軟串口對象、LCD對象、SD卡對象、RTC對象。
- 全局變量:用于數據解析、傳感器數據存儲、顯示控制、編碼器狀態、記錄狀態、硬件狀態標志等。
- 函數:
- setup():初始化系統,包括串口、LCD、RTC、傳感器、SD卡、編碼器、記錄按鈕等。
- initLCD():初始化LCD,嘗試多個I2C地址。
- initSDCard():初始化SD卡,并檢查其功能。
- checkSDFunctional():檢查SD卡功能(寫一個測試文件并讀取驗證)。
- ensureDataFile():確保數據文件存在,并寫入表頭。
- loop():主循環,包括硬件狀態檢查、接收數據、處理編碼器和按鈕、更新顯示、定期檢查SD卡狀態。
- checkHardwareStatus():檢查硬件狀態(RTC、SD卡)。
- checkSDStatus():檢查SD卡狀態(物理存在和功能)。
- updateTimeDisplay():更新當前時間顯示。
- handleRecordButton():處理記錄按鈕的按下事件(切換記錄狀態)。
- handleEncoder():處理旋轉編碼器的旋轉和按鈕事件(切換屏幕、進入時間設置模式)。
- enterSetMode():進入時間設置模式。
- exitTimeSetMode():退出時間設置模式,更新RTC時間。
- receiveData():從TVOC傳感器接收數據。
- processData():處理接收到的傳感器數據,驗證校驗和,并存儲到結構體。
- logSensorData():將傳感器數據記錄到SD卡。
- loadLastRecord():從SD卡加載最后一條記錄。
- parseLastRecord():解析最后一條記錄。
- displaySDStatus():在LCD上顯示SD卡狀態(使用自定義字符)。
- updateDisplay():根據當前屏幕索引更新顯示內容。
- displayTVOCHCHO():顯示TVOC和HCHO數據。
- displayECO2():顯示eCO2數據。
- displayLastRecord():顯示最后記錄的數據和記錄狀態。
- displaySetItem():在設置模式下顯示當前設置項。
- handleSetMode():處理設置模式下的編碼器旋轉事件。
- adjustTimeValue():調整時間值(根據設置項)。
- daysInMonth():計算某年某月的天數。
功能概述:
該設備通過軟串口與TVOC傳感器通信,獲取TVOC、HCHO和eCO2數據。這些數據會顯示在LCD屏幕上,用戶可以通過旋轉編碼器切換顯示屏幕(三個屏幕:TVOC+HCHO、eCO2、最后記錄)。同時,設備支持將數據記錄到SD卡(記錄狀態由記錄按鈕控制)。設備還包含一個實時時鐘(RTC)用于時間戳。旋轉編碼器長按可以進入時間設置模式,調整年、月、日、時、分、秒。
詳細分析:
-
初始化(setup):
- 初始化串口(用于調試)。
- 初始化板載LED(用于指示狀態)。
- 初始化LCD(嘗試多個I2C地址)。
- 初始化RTC(如果失敗則顯示錯誤,如果RTC未運行則使用編譯時間設置)。
- 初始化TVOC傳感器的軟串口。
- 初始化SD卡(并確保數據文件存在)。
- 初始化編碼器引腳(上拉輸入)。
- 初始化記錄按鈕引腳(上拉輸入)。
- 創建自定義字符(SD卡圖標)。
- 顯示初始化完成信息。
-
主循環(loop):
- 檢查硬件狀態(每5秒檢查一次RTC和SD卡)。
- 接收傳感器數據(通過軟串口,按照特定幀格式解析)。
- 處理編碼器事件(旋轉和按鈕,包括短按切換屏幕,長按進入時間設置模式)。
- 處理記錄按鈕(按下切換記錄狀態,并切換到記錄狀態屏幕)。
- 每500ms更新顯示(包括時間和傳感器數據)。
- 每3秒檢查SD卡狀態(物理存在和功能狀態)。
-
數據記錄:
- 當記錄使能(recordingEnabled為true)且傳感器數據有效且RTC可用時,將數據寫入SD卡。
- 數據文件為CSV格式,包含UNIX時間戳、日期時間、TVOC、HCHO、CO2。
- 每次記錄后更新最后一條記錄(lastRecord結構體)。
-
顯示:
- 三個屏幕:
Screen0: TVOC和HCHO的數值(第一行TVOC,第二行HCHO)。
Screen1: eCO2的數值和日期時間(第一行eCO2,第二行日期和時間)。
Screen2: 最后記錄的數據(包括TVOC、eCO2、記錄狀態、SD卡狀態)。 - 在LCD右上角顯示SD卡狀態(自定義圖標:正常為SD圖標,物理存在但功能異常為'!',不存在為'X')。
- 三個屏幕:
-
時間設置模式:
- 長按編碼器按鈕進入時間設置模式。
- 通過旋轉編碼器調整當前設置項(年、月、日、時、分、秒)。
- 每按一次按鈕切換一個設置項,設置完所有項后退出設置模式并更新RTC時間。
-
SD卡狀態管理:
- 定期檢查SD卡物理存在(通過嘗試初始化)和功能狀態(通過讀寫測試)。
- 狀態變化時更新顯示。
-
錯誤處理:
- 初始化失敗時在串口輸出錯誤信息,并在LCD上顯示(如RTC錯誤)。
- SD卡寫入失敗時標記為功能異常,并點亮LED指示錯誤。
修復和增強:
代碼中有一些修復和增強,例如SD卡狀態檢測、時間設置等。
需要單獨考慮:
- 使用條件判斷來確保硬件可用性(如lcdInitialized, rtcAvailable等)。
- 使用volatile關鍵字修飾編碼器相關變量(因為它們在中斷服務函數中修改,但本代碼中并未使用中斷,而是在主循環中查詢,所以實際上可以不用volatile,但保留也無妨)。
- 使用狀態機思想處理編碼器旋轉和按鈕事件。
- 記錄按鈕和編碼器按鈕都做了防抖處理。
- 在記錄數據時,如果打開文件失敗,會將sdCardFunctional置為false,然后在下一次檢查時嘗試恢復。
代碼實現?
🛠? ?硬件架構?
- ?核心控制器?:Arduino開發板
- ?傳感器模塊?:TVOC傳感器(軟串口通信)
- ?存儲模塊?:SD卡(SPI接口)
- ?顯示模塊?:I2C LCD1602液晶屏
- ?用戶輸入?:旋轉編碼器(CLK/DT/SW引腳) + 記錄按鈕
- ?時間模塊?:DS1307 RTC時鐘
- ?狀態指示?:LED指示燈
🧩 ?代碼結構分析?
🔌 ?1. 初始化設置(setup())??
void setup() {// 串口調試初始化Serial.begin(9600); // 硬件初始化鏈initLCD(); // LCD顯示初始化[11](@ref)initRTC(); // 實時時鐘初始化[6](@ref)initSDCard(); // SD卡系統初始化[10](@ref)initSensors(); // 傳感器通信初始化// 用戶輸入設備初始化pinMode(ENC_CLK, INPUT_PULLUP); // 編碼器CLK引腳[9](@ref)pinMode(RECORD_BTN, INPUT_PULLUP); // 記錄按鈕// 自定義字符創建(SD圖標)lcd.createChar(0, sdIcon); // 創建SD卡圖標[11](@ref)
}
?關鍵點?:
- 采用模塊化初始化策略,各硬件獨立初始化
- LCD支持多地址自動探測?(0x27/0x3F)
- RTC首次啟動時自動注入編譯時間?
🔁 ?2. 主循環邏輯(loop())??
void loop() {checkHardwareStatus(); // 硬件健康監測(5秒間隔)receiveData(); // 傳感器數據采集handleEncoder(); // 編碼器事件處理[9](@ref)handleRecordButton(); // 記錄按鈕邏輯if(needDisplayUpdate()) { // 500ms顯示刷新updateTimeDisplay(); // 更新時間顯示[6](@ref)updateDisplay(); // 刷新LCD內容}checkSDStatus(); // SD卡狀態監測(3秒間隔)[10](@ref)
}
?核心機制?:
- ?分層式任務調度?:硬件監控、數據采集、用戶交互分離
- ?節流機制?:顯示刷新(500ms)、SD檢測(3s)避免資源爭用
- ?狀態機驅動?:通過currentScreen管理三屏顯示
📡 ?3. 傳感器數據處理?
void processData() {byte checksum = 0;for(int i=0; i<8; i++) checksum += rawData[i];if(checksum != rawData[8]) { // 校驗和驗證Serial.println("TVOC checksum error!");return;}// 數據解析(大端序)currentData.tvoc = (rawData[2] << 8) | rawData[3]; currentData.hcho = (rawData[4] << 8) | rawData[5];currentData.eco2 = (rawData[6] << 8) | rawData[7];logSensorData(); // 有效數據記錄
}
?協議特性?:
- ?幀結構?:0x2C頭 + 8字節數據 + 1字節校驗和
- ?錯誤處理?:校驗失敗自動丟棄數據包
- ?數據映射?:TVOC/HCHO單位μg/m3,eCO?單位ppm
💾 ?4. SD卡高級管理?
void checkSDStatus() {// 物理存在檢測bool physicalPresent = SD.begin(SD_CS_PIN); if(physicalPresent != sdCardPresent) { // 狀態變化檢測if(sdCardPresent) {sdCardFunctional = checkSDFunctional(); // 功能測試if(sdCardFunctional) ensureDataFile(); // 文件系統驗證[10](@ref)}}// 自動恢復機制if(sdCardPresent && !sdCardFunctional) {sdCardFunctional = checkSDFunctional(); // 定期重試}
}
?創新設計?:
- ?雙狀態檢測?:物理存在(sdCardPresent) + 功能狀態(sdCardFunctional)
- ?智能恢復?:定期嘗試重新掛載失效SD卡
- ?文件保障?:自動創建CSV文件并寫入表頭
- ?圖標化顯示?:自定義SD狀態字符(正常/異常/缺失)
? ?5. 時間管理系統?
void handleSetMode() {if(encTurned) {int delta = (digitalRead(ENC_DT) != lastClkState) ? -1 : 1;adjustTimeValue(delta); // 時間值調整switch(setIndex) { // 多級設置菜單[9](@ref)case 0: newTime = DateTime(newTime.year()+delta, ...); break;case 1: // 月份(帶天數邊界檢查)uint8_t newMonth = constrain(month+delta, 1, 12);uint8_t newDay = min(day, daysInMonth(newMonth, year));...}}
}
?交互特性?:
- ?長按觸發?:編碼器按鈕長按>1秒進入設置模式
- ?循環設置?:年→月→日→時→分→秒→保存
- ?智能邊界?:自動計算每月天數(含閏年)
📊 ?6. 數據顯示系統?
void updateDisplay() {switch(currentScreen) {case 0: // TVOC+HCHO同屏顯示lcd.print("TVOC:"); lcd.print(currentData.tvoc); lcd.print("HCHO:"); lcd.print(currentData.hcho);break;case 1: // eCO2與日期時間lcd.print("eCO2:"); lcd.print(currentData.eco2);snprintf(dateBuffer, "%02d%02d%02d", now.year%100, now.month, now.day);break;case 2: // 最后記錄與狀態lcd.print("TV:"); lcd.print(lastRecord.tvoc);lcd.print("CO2:"); lcd.print(lastRecord.eco2);lcd.print(recordingEnabled ? "ON" : "OFF");displaySDStatus(); // 右下角狀態圖標[11](@ref)}
}
?顯示優化?:
- ?多屏切換?:編碼器短按循環切換三個界面
- ?動態更新?:時間顯示每秒刷新,數據每500ms更新
- ?狀態集成?:SD圖標(0)/警告(!)/缺失(X)直觀指示
?? ?系統創新設計?
-
?SD卡智能恢復系統?
- 實現物理檢測→功能驗證→自動恢復的全鏈路管理
- 采用雙狀態機模型(prevSdCardPresent/sdCardPresent)
- 文件操作增加寫后驗證(創建測試文件校驗完整性)
-
?時間設置容錯機制?
void adjustTimeValue(int delta) {case 1: // 月份調整uint8_t newDay = min(day, daysInMonth(newMonth, year));
- 自動計算當月最大天數(含閏年判斷)
- 防止設置無效日期(如2月30日)
-
?數據記錄優化?
void logSensorData() {if(!recordingEnabled || !sdCardFunctional) return;File dataFile = SD.open("sensor.csv", FILE_WRITE);dataFile.print(unixTime); // UNIX時間戳[6](@ref)dataFile.print(currentData.tvoc); ...
- 雙時間戳存儲:人類可讀時間+UNIX時間戳
- CSV格式標準化:兼容Excel/LibreOffice分析
-
?LCD顯示優化?
- ?自定義字符?:8×5像素SD圖標設計
- ?空間復用?:15×0位置顯示狀態圖標
- ?多屏布局?:科學分配16×2顯示空間
📝 ?改進建議?
-
?增加傳感器異常處理?
// 在processData()中增加: if(currentData.tvoc > 30000) { // 異常值判定Serial.println("Sensor out of range!");runSelfTest(); // 觸發自檢 }
-
?實現數據緩存機制?
- SD卡不可用時啟用RAM緩存
- 恢復后自動寫入緩存數據
-
?添加低功耗模式?
void enterSleepMode() {if(noInteraction(5 * 60 * 1000)) { // 5分鐘無操作lcd.noBacklight();setCpuFrequency(10); // ESP32特有節能} }
-
?優化時間設置?
// 在displaySetItem()中: lcd.print("▲▼"); // 增加操作提示
🔍 ?關鍵引用說明?
- ?LCD初始化?:支持I2C地址自動探測
- ?RTC時間設置?:編譯時間注入機制
- ?SD卡操作?:CSV文件創建與表頭寫入
- ?編碼器控制?:旋轉檢測與按鈕處理
- ?數據顯示?:多屏切換與自定義字符
該設計實現了環境數據的采集→處理→存儲→顯示全鏈路管理,通過創新性的狀態管理和錯誤恢復機制,顯著提升了系統的可靠性和用戶體驗。
代碼修正1
時間設置功能的設計存在以下問題,導致LCD顯示內容在時間設置期間會被覆蓋:
-
?主顯示循環沖突?:
updateDisplay()
函數每500毫秒運行一次,而該函數并不檢查timeSetMode
狀態。因此即使在時間設置模式下,它仍會根據當前屏幕設置(0/1/2)覆蓋顯示內容。 -
?顯示刷新邏輯?:
displaySetItem()
僅在旋轉編碼器時才被調用(通過handleSetMode()
),沒有獨立的周期性刷新機制。當沒有編碼器操作時,主顯示循環會覆蓋時間設置界面。
解決方案
在updateDisplay()
函數開頭添加時間設置模式的專屬顯示邏輯:
void updateDisplay() {if (!lcdInitialized) return;// ============ 添加的代碼 - 時間設置模式優先 ============if (timeSetMode) {displaySetItem();return; // 進入設置模式后跳過常規顯示}// =================================================// ... 其余原有代碼保持不變 ...
}
具體修改說明
-
?優先級控制?:
if (timeSetMode) {displaySetItem();return; }
- 首先檢查是否處于時間設置模式
- 當
timeSetMode=true
時立即顯示設置界面 return
語句確保退出函數,防止常規內容覆蓋設置界面
完整測試代碼
https://download.csdn.net/download/Medlar_CN/91477067