1. 项目缘起为什么我们需要一个温湿度数据记录器最近在做一个环境监测相关的项目需要长时间、稳定地记录几个关键点的温度和湿度数据。一开始我尝试用现成的物联网传感器模块通过Wi-Fi上传数据到云端。想法很美好但现实很骨感监测点要么在信号不好的角落要么为了省电不能一直开着Wi-Fi更别提有些场景下网络本身就是个奢侈品。数据断断续续关键时段的数据丢失让人非常头疼。痛定思痛我决定自己动手做一个更可靠、更灵活的离线数据记录器。核心需求很明确第一数据必须可靠存储不能因为网络或电源问题丢失第二设备要足够便携和低功耗能部署在各种犄角旮旯第三数据要方便获取总不能每次都用读卡器去拔卡。基于这几点我选择了“BT SD”这个组合方案用SD卡作为本地大容量、非易失的存储介质确保数据万无一失用蓝牙BT作为近场、低功耗的数据传输通道方便我用手机或电脑随时读取数据而无需物理接触设备。这个方案听起来简单但实际做下来从芯片选型、电路设计、固件开发到数据格式处理每一步都有不少门道和坑。网上关于ESP32蓝牙或SD卡单独使用的教程很多但把两者结合起来做成一个稳定、实用的数据记录器的完整分享却不多。今天我就把自己从零搭建这个“温湿度数据记录器BT SD”的全过程、核心原理、踩过的坑以及优化心得毫无保留地分享出来。无论你是嵌入式新手想练手还是有类似需求的开发者寻找解决方案相信这篇长文都能给你带来实实在在的参考。2. 核心硬件选型与设计思路做一个数据记录器硬件是基石。选型决定了项目的性能、功耗、成本和开发难度。我的核心思路是主控要兼顾性能、功耗和无线功能传感器要稳定准确存储要可靠整体要低功耗。2.1 主控芯片为什么是ESP32在众多MCU中我最终选择了ESP32。这不是随大流而是经过仔细权衡的。首先功能集成度是决定性因素。ESP32内置了双核处理器、Wi-Fi和蓝牙包括经典蓝牙BT和低功耗蓝牙BLE。对于我这个项目蓝牙是关键需求。如果选用STM32这类纯MCU我需要额外增加一个蓝牙模块如HC-05、HM-10这不仅增加了电路复杂度和PCB面积也提高了功耗和成本。ESP32单芯片搞定计算和通信简洁高效。其次开发生态和成本。ESP32拥有极其丰富的Arduino核心和ESP-IDF开发框架支持社区资源海量遇到问题很容易找到解决方案。其成本也相当有竞争力一片ESP32-WROOM-32模组的价格远低于“STM32 独立蓝牙模块”的组合。最后功耗考量。虽然ESP32在全力运行时的功耗不算极低但它提供了非常灵活的功耗模式。对于数据记录器这种大部分时间在休眠定时醒来采集数据并存储的应用场景我们可以充分利用ESP32的深度睡眠Deep Sleep模式将功耗控制在微安级别用电池供电运行数周甚至数月不是问题。注意市面上ESP32子型号很多对于本项目选择最通用的ESP32-WROOM-32或ESP32-S模组即可。它们都包含了必要的Flash和PSRAM部分型号且引脚布局兼容便于开发。2.2 温湿度传感器DHT22 vs SHT3x温湿度传感器的选择直接关系到数据的质量。常见的有DHT11、DHT22AM2302、SHT30、SHT31等。DHT22性价比之王。数字输出精度和量程温度-40~80℃湿度0-100%RH对于绝大多数环境监测应用已经足够。它采用单总线通信只需要一个GPIO引脚电路简单。但其响应速度较慢约2秒一次通信协议对时序要求严格在代码不优化时可能偶尔读取失败。SHT3x系列如SHT30工业级精度响应速度快I2C通信稳定可靠。精度和长期稳定性优于DHT22但价格也高出数倍。对于我的项目监测环境变化而非高精度科学实验DHT22的精度完全满足要求。其更低的成本允许我在多个点位部署。关键在于要通过稳定的代码和适当的错误重试机制来保证DHT22的读取可靠性。如果预算充足且要求极高可靠性SHT3x是更省心的选择。2.3 存储方案SD卡与文件系统本地存储是数据记录器的“保险箱”。SD卡通过SPI接口连接是理想选择容量大从几百MB到32GB甚至更高价格低廉数据非易失。为什么不用ESP32内部的Flash内部Flash容量有限通常4MB或16MB且需要承担存储程序、文件系统、其他数据的任务频繁擦写会影响寿命。而SD卡专为数据存储设计可以单独管理存储空间近乎无限坏了也方便更换。关键设计点电平转换ESP32的GPIO是3.3V逻辑电平而大部分SD卡工作电压在3.3V。但一些老式或大容量SD卡可能对信号有更高要求。为了确保兼容性和稳定性强烈建议在SPI信号线MOSI, MISO, SCK, CS上串联一个100-330欧姆的电阻这能有效抑制信号反射。如果追求极致可靠可以使用专用的3.3V电平转换芯片。电源管理SD卡在写入时会有较高的瞬时电流几十到上百毫安。如果整个系统由电池通过LDO供电这个电流脉冲可能导致电源电压瞬间跌落引起MCU复位。解决方案是在SD卡的VCC引脚附近放置一个100μF以上的钽电容或电解电容进行储能缓冲。文件系统为了在SD卡上以文件形式存储数据我们需要一个文件系统。FAT32是最通用、兼容性最好的选择。几乎所有的电脑、手机和操作系统都能直接识别FAT32格式的SD卡。在代码中我们可以使用成熟的库如Arduino的SD库或ESP-IDF的wear_levellingFATFS来操作FAT32文件系统。2.4 电路设计要点与功耗优化整体的电路原理图并不复杂ESP32模组作为核心DHT22连接一个GPIO引脚如上拉电阻SD卡通过SPI接口GPIO23/MOSI, GPIO19/MISO, GPIO18/SCK, GPIO5/CS连接。但有几个细节决定了项目的成败电源路径设计为了极致低功耗在深度睡眠时我们需要切断传感器和SD卡等外设的供电。可以使用一个MOSFET如AO3400或低静态电流的电源开关芯片如TPS229xx系列由ESP32的一个GPIO控制仅在采集数据前才给外设上电。采集存储完成后立即关闭外设电源然后MCU进入深度睡眠。唤醒源ESP32可以从深度睡眠中被定时器Timer、外部引脚EXT0/EXT1等唤醒。对于定时记录我们使用内部RTC定时器作为唤醒源最为简单高效。在代码中设置好睡眠时间到点自动唤醒无需外部电路。上拉电阻I2C总线如果使用SHT3x、DHT22的数据线都需要上拉电阻通常4.7kΩ-10kΩ。虽然ESP32可以启用内部上拉但在长线连接或干扰环境下外部上拉电阻更可靠。3. 固件开发从采集、存储到蓝牙传输硬件搭好了接下来就是让设备“活”起来的软件部分。我的固件逻辑是一个典型的事件循环休眠 - 定时唤醒 - 初始化外设 - 采集传感器数据 - 写入SD卡 - 关闭外设 - 进入休眠。同时蓝牙作为随时可激活的服务在设备唤醒期间等待连接和指令。3.1 低功耗任务调度与深度睡眠实现实现长续航的关键是尽可能让ESP32处于深度睡眠状态。以下是核心代码逻辑框架基于Arduino框架#include esp_sleep.h // 定义唤醒间隔微秒例如300秒5分钟 #define uS_TO_S_FACTOR 1000000ULL #define TIME_TO_SLEEP 300 RTC_DATA_ATTR int bootCount 0; // 存储在RTC内存深度睡眠后保持 void setup() { Serial.begin(115200); delay(1000); // 给串口一点启动时间 // 增加启动次数 bootCount; Serial.printf(启动次数 #%d\n, bootCount); // 1. 使能外设电源通过控制MOSFET的GPIO pinMode(PERIPH_POWER_PIN, OUTPUT); digitalWrite(PERIPH_POWER_PIN, HIGH); delay(50); // 等待外设电源稳定 // 2. 初始化传感器和SD卡 initSensor(); if (!initSDCard()) { Serial.println(SD卡初始化失败); // 处理错误例如闪烁LED报警然后继续尝试或休眠 } // 3. 采集并记录数据 float temp, humi; if (readSensor(temp, humi)) { logDataToSD(temp, humi); } // 4. 初始化蓝牙服务在记录数据后避免影响关键任务 initBluetoothService(); // 5. 进入一个短暂的活动循环处理蓝牙连接等 // 这里可以设置一个“活动窗口”例如持续30秒等待蓝牙连接和指令 unsigned long activeWindowStart millis(); while (millis() - activeWindowStart 30000) { // 活动窗口30秒 handleBluetoothCommands(); // 处理蓝牙数据请求、配置更改等 delay(100); } // 6. 活动窗口结束关闭外设电源 digitalWrite(PERIPH_POWER_PIN, LOW); delay(10); // 7. 配置并进入深度睡眠 Serial.println(准备进入深度睡眠...); esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); delay(100); // 确保串口输出完成 esp_deep_sleep_start(); // 程序在此停止直到被定时器唤醒 } void loop() { // Deep Sleep模式下loop永远不会被执行 }关键点解析RTC_DATA_ATTR这个属性将变量存储在RTC慢速内存中ESP32深度睡眠时这部分内存仍然保持供电因此变量值得以保留。非常适合用来记录启动次数、存储一些需要跨睡眠周期的状态标志。外设电源管理通过一个GPIO控制MOSFET开关在不需要时彻底断开传感器和SD卡的供电这是降低平均功耗最有效的手段之一。实测中SD卡和DHT22在待机时仍有几十到几百微安的漏电流长期累积不容忽视。活动窗口Active Window这是平衡功耗与功能性的设计。设备唤醒后完成核心的数据记录任务然后开启一个固定时长如30秒的窗口。在这个窗口内蓝牙服务是激活的手机可以连接并读取历史数据或修改配置。窗口结束后无论是否有连接都强制进入睡眠。这避免了设备因蓝牙一直等待连接而无法休眠。3.2 SD卡数据存储的可靠性设计将数据写入SD卡看似简单但要做到稳定可靠防止数据损坏或丢失需要一些策略。1. 文件操作策略避免频繁打开关闭文件每次采集都打开、写入、关闭文件会大大增加SD卡损耗和写入时间。更好的做法是在设备启动时以追加模式“a”打开一个日志文件在整个活动窗口期间保持打开状态每次采集只需调用file.println()写入一行数据。在进入睡眠前再安全地关闭文件。文件命名与滚动日志文件不能无限增长。可以按日期命名文件例如LOG_20240515.csv。每次启动时检查当前日期如果日期变化就关闭旧文件创建以新日期命名的文件。这既方便管理也避免了单个文件过大。使用CSV格式存储为逗号分隔值CSV文件是最通用的选择。例如2024-05-15 14:30:05, 25.6, 48.2。第一列是时间戳第二列温度第三列湿度。电脑上的Excel、记事本手机上的文本编辑器都能直接打开处理。2. 错误处理与数据缓冲SD卡操作可能失败卡未插好、接触不良、文件系统损坏。代码必须有健壮的错误处理。bool logDataToSD(float temperature, float humidity) { File dataFile; // 尝试打开文件如果失败则尝试重新初始化SD卡 dataFile SD.open(/datalog.csv, FILE_APPEND); if (!dataFile) { Serial.println(打开文件失败尝试重新初始化SD卡...); SD.end(); delay(100); if (!SD.begin()) { Serial.println(SD卡初始化失败数据将丢失); // 此处可以添加将数据暂存到SPIFFS或RTC内存的应急逻辑 return false; } dataFile SD.open(/datalog.csv, FILE_APPEND); if (!dataFile) { Serial.println(再次打开文件失败); return false; } } // 构造数据行 String dataString getTimeString() , String(temperature, 1) , String(humidity, 1); // 写入数据 dataFile.println(dataString); dataFile.flush(); // 立即将数据从缓冲区写入物理卡防止丢失 Serial.println(数据已写入: dataString); dataFile.close(); return true; }file.flush()这个调用非常重要。它强制将文件缓冲区中的数据立即写入SD卡。否则数据可能还留在ESP32的内存缓冲区里如果此时突然断电或进入睡眠数据就会丢失。应急存储在SD.begin()失败时可以考虑将当前几条数据临时存储到ESP32的SPIFFS闪存文件系统或RTC内存中等下次启动SD卡恢复时再写入。这是一个提升可靠性的进阶技巧。3.3 蓝牙服务设计与数据交互协议蓝牙功能用于近场交互。我选择了蓝牙经典BT而非低功耗蓝牙BLE主要因为经典蓝牙的SPP串口协议使用起来就像有线串口一样简单在手机端如使用Serial Bluetooth Terminal APP和电脑端如Putty、Arduino串口监视器都可以直接连接并收发文本数据开发调试和用户交互都非常直观。初始化蓝牙串口#include BluetoothSerial.h BluetoothSerial SerialBT; void initBluetoothService() { if (!SerialBT.begin(ESP32-DataLogger)) { // 蓝牙设备名称 Serial.println(蓝牙初始化失败); // 可以闪烁LED报警但不应阻止主流程 } else { Serial.println(蓝牙已启动设备名: ESP32-DataLogger); Serial.println(请使用手机或电脑蓝牙连接并配对。); } }在活动窗口中处理蓝牙指令void handleBluetoothCommands() { // 检查蓝牙串口是否有数据到达 if (SerialBT.available()) { String command SerialBT.readStringUntil(\n); // 假设以换行符结束命令 command.trim(); // 去除首尾空白字符 if (command.equals(GET_DATA)) { // 读取SD卡中的最新N条数据并通过蓝牙发送 sendLatestDataViaBT(50); // 发送最新50条 } else if (command.equals(GET_STATUS)) { SerialBT.println(Status: OK, Battery: 3.8V, Records: 1205); } else if (command.startsWith(SET_INTERVAL:)) { // 解析并设置新的采样间隔例如命令: SET_INTERVAL:600 int newInterval command.substring(13).toInt(); if (newInterval 60) { // 最小间隔限制 setNewSleepInterval(newInterval); SerialBT.println(Interval set to: String(newInterval) s); } } else { SerialBT.println(Unknown command: command); SerialBT.println(Available: GET_DATA, GET_STATUS, SET_INTERVAL:xxx); } } }设计一个简单的文本协议使得交互清晰可控。例如GET_DATA设备通过蓝牙返回最新的若干行数据。GET_STATUS返回设备状态如电池电压、记录总数、采样间隔等。SET_INTERVAL:300将采样间隔设置为300秒。在手机端使用任何支持蓝牙SPP的串口APP连接设备后就可以发送这些文本命令并接收回复实现数据拉取和简单配置。这种方式比开发专用的APP要快捷得多。4. 实测中的挑战、优化与排坑记录理论很丰满实测中却遇到了不少问题。下面是我在开发和部署过程中遇到的主要挑战及解决方案。4.1 SD卡初始化失败与文件系统损坏这是最常见的问题。现象是SD.begin()返回false或者在写入过程中报错。排查与解决硬件连接首先用万用表检查所有SPI连线MOSI, MISO, SCK, CS是否连通有无虚焊。CS引脚必须选择一个固定的GPIO并且确保在代码中SD.begin(CS_PIN)使用的引脚号一致。电源问题用示波器观察SD卡VCC引脚在初始化瞬间的电压。如果看到明显的跌落低于3.0V说明电源驱动能力不足或去耦电容不够。增加一个100-220μF的电解电容靠近SD卡座是立竿见影的解决办法。卡兼容性与格式化不是所有SD卡都兼容性好。优先使用Class 10或UHS-I的知名品牌小容量卡如16GB、32GB。大容量卡如128GB以上默认可能是exFAT格式需要格式化为FAT32。在电脑上使用Windows自带的格式化工具时如果看不到FAT32选项对于大于32GB的卡可以使用第三方工具如SD Memory Card Formatter由SD协会官方提供或“guiformat”进行强制FAT32格式化。文件系统修复如果SD卡在异常断电后文件系统损坏可以将其插入电脑让操作系统尝试修复Windows的chkdsk macOS的磁盘工具。在代码中也可以增加更健壮的逻辑如果连续多次初始化失败可以尝试调用SD.format()进行格式化慎用会清空数据或者通过蓝牙通知用户需要检查SD卡。4.2 蓝牙连接不稳定或无法发现有时手机搜不到设备或者连接频繁断开。排查与解决名称与可见性确保蓝牙名称是唯一的避免冲突。ESP32的蓝牙默认是可发现的。如果不行检查代码中SerialBT.begin()是否成功。供电干扰当SD卡进行写入操作时产生的电流脉冲可能引起电源噪声干扰蓝牙射频电路。确保数字电源给ESP32内核和模拟/射频电源之间有良好的LC滤波。在ESP32模组的VDD3P3_RF引脚给射频供电附近按照数据手册推荐放置足够且靠近的滤波电容例如10μF 0.1μF。天线与距离ESP32-WROOM模组集成了PCB天线性能在空旷环境下不错但如果设备被放在金属盒内或角落信号会极大衰减。确保设备部署位置周围没有大面积金属屏蔽。对于要求传输距离的场景可以考虑使用带外部天线接口的ESP32模组如ESP32-WROOM-32UE并连接外置天线。配对与绑定经典蓝牙通常需要配对。第一次连接时手机端会提示输入配对码如1234。在代码中可以通过SerialBT.setPin(“1234”)来设置。确保配对流程顺利完成。4.3 数据记录的时间戳与同步问题设备离线运行它的内部时钟RTC在深度睡眠后依靠外部32.768kHz晶振维持但精度有限一天可能会有几秒到几十秒的误差。长时间运行后时间戳就不准了。解决方案定期蓝牙同步最简单的办法。在手机APP连接设备后发送一个包含当前标准时间的命令如SET_TIME:2024-05-15 14:30:00。设备收到后用这个时间校准自己的RTC。可以在每次成功连接后自动进行。记录相对时间如果绝对时间不是必须的可以只记录从启动开始的相对时间秒数或采样次数。后期处理数据时根据第一次成功同步的时间点进行换算。使用GPS或网络授时进阶如果设备偶尔能连接Wi-Fi可以在唤醒后通过NTP协议获取网络时间。或者对于户外应用可以增加一个GPS模块从中获取高精度的时间信息。但这会增加复杂度和功耗。4.4 功耗优化到极致最初的版本用2000mAh的锂电池只能坚持一周左右。经过以下优化续航提升到了近两个月。测量基线功耗首先让设备进入你设计的最深睡眠模式比如深度睡眠所有外设断电用万用表电流档串联测量整机电流。ESP32深度睡眠的理想电流在10μA左右。如果你的实测值是几百微安甚至毫安级说明有漏电。排查漏电路径GPIO状态检查所有未使用的GPIO引脚。最好将它们设置为INPUT_PULLUP或INPUT_PULLDOWN避免悬空引起漏电。对于控制外设电源的GPIO在睡眠时要确保输出正确的电平例如控制MOSFET关断的引脚应输出低电平。使能引脚一些传感器或模块有“使能”或“EN”引脚。确保在睡眠时将其拉低以关闭模块。电源芯片静态电流你使用的LDO或DC-DC芯片本身的静态电流是多少选择一款超低静态电流如几微安的电源管理芯片如TI的TPS系列。优化活动窗口这是功耗大头。缩短蓝牙活动窗口时间。我的最终方案是默认窗口为10秒。如果在这10秒内收到任何蓝牙指令则窗口自动延长30秒。如果一直没连接10秒后就睡眠。这大大降低了无连接时的待机功耗。降低工作频率在采集数据时可以将ESP32的CPU频率从240MHz降低到80MHz这能显著降低活动期间的功耗峰值。使用setCpuFrequencyMhz(80)即可。5. 进阶扩展与项目变体思路一个基础的数据记录器完成后可以根据不同应用场景进行扩展使其功能更强大。5.1 多传感器集成与数据融合除了温湿度你还可以轻松集成其他I2C或SPI传感器大气压传感器BMP280/BME280BME280本身就能测温、湿、压。用它可以替代DHT22获得气压数据用于海拔估算或天气趋势分析。光照传感器BH1750记录环境光强。空气质量传感器SGP30检测TVOC和eCO2。土壤湿度传感器用于农业灌溉监控。只需在代码中增加相应传感器的初始化、读取函数并在日志文件中增加数据列即可。注意功耗每增加一个传感器都要评估其工作电流和是否需要电源管理。5.2 添加无线传输LoRa/NB-IoT与云端同步蓝牙是近场方案。如果需要远程监控可以增加其他无线模块LoRa超远距离、低功耗适合将多个分散的记录器数据汇聚到一个网关。可以设计为记录器平时用SD卡存储每天在固定时间唤醒LoRa将压缩后的批量数据发送给远处的网关。NB-IoT直接使用蜂窝网络覆盖广但需要SIM卡和流量费用。适合城市内或移动网络覆盖良好的野外固定点监测。由于其功耗相对较高通常需要搭配更大的电池或太阳能板。在这种混合架构下SD卡的角色从“唯一存储”变成了“本地缓存与备份”。网络通畅时数据优先上传云端网络中断时数据安全地存储在本地SD卡待网络恢复后断点续传。5.3 设计一个简单的数据可视化前端数据最终是为了分析。你可以写一个简单的Python脚本定期比如每天通过蓝牙读取设备里的CSV文件并自动生成图表。import pandas as pd import matplotlib.pyplot as plt from datetime import datetime # ... (蓝牙通信代码用于获取CSV文件内容) # 假设已经将数据读入字符串 csv_data df pd.read_csv(pd.io.common.StringIO(csv_data), headerNone, names[timestamp, temperature, humidity]) df[timestamp] pd.to_datetime(df[timestamp]) df.set_index(timestamp, inplaceTrue) # 绘制最近24小时的数据 plt.figure(figsize(12, 6)) plt.subplot(2, 1, 1) plt.plot(df[temperature].last(24H), labelTemperature, colorred) plt.ylabel(°C) plt.legend() plt.subplot(2, 1, 2) plt.plot(df[humidity].last(24H), labelHumidity, colorblue) plt.ylabel(%RH) plt.legend() plt.tight_layout() plt.savefig(environment_plot.png) plt.show()这个脚本可以部署在树莓派或旧电脑上实现数据的自动拉取和可视化报告生成。5.4 外壳设计与环境部署对于长期部署一个合适的外壳至关重要。材料选择防水、防尘的塑料或金属外壳。对于温湿度传感器外壳需要留有通风孔以保证空气流通但又不能直接进雨水。可以使用防水透气膜Gore-Tex类材料覆盖通气孔。传感器放置传感器探头最好与主板有一定距离或通过排线引出以避免主板发热影响温度读数。电源对于户外长期部署太阳能板锂电池管理是终极方案。一块小型的6V 2W太阳能板配合一个简单的TP4056充电模块和18650电池可以实现近乎永续的运行。从最初被数据丢失问题困扰到亲手打造出这个稳定可靠的离线数据记录器整个过程充满了挑战和乐趣。这个项目麻雀虽小五脏俱全涵盖了嵌入式开发的多个核心环节低功耗设计、外设驱动、文件系统、无线通信、电源管理和可靠性设计。我最大的体会是可靠性来自于对每一个细节的较真——那个不起眼的滤波电容、那行flush()函数调用、那段错误重试的代码往往就是系统能否长期稳定运行的关键。如果你正在筹划类似的项目我的建议是先从最小系统开始让SD卡读写和蓝牙通信分别调通。然后逐步集成传感器实现低功耗循环。最后花大量时间去做长时间的压力测试和功耗测试模拟各种异常情况突然拔卡、断电、信号干扰完善你的错误处理逻辑。当你看到设备在角落里默默工作数周数据一条不落地保存下来时那种成就感绝对是使用现成产品无法比拟的。这个开源项目的所有硬件设计图和固件代码我都整理放在了GitHub上欢迎一起交流改进。