准备设计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/m³,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