
上周帮朋友搭了一个室内CO2监测的小项目主控用的是ESP32-S3传感器是JW01系列的二氧化碳模块。本来以为是个十几分钟就能搞定的简单串口读取结果最耗时间的不是接线也不是代码而是供电。第一次上电传感器指示灯一亮一灭串口助手打印出来的数据全是乱码开发板还隔几秒就重启一次。折腾了一晚上最后才定位到是供电的锅。这个项目本身不大但踩坑过程很有代表性。ESP32-S3这类开发板看起来“USB一插就能用”真正接上传感器后电流需求一起来供电短板就会暴露得特别明显。这篇就把整个项目的设计思路、完整Arduino代码、以及所有供电相关的坑一次说清楚给想用ESP32系列驱动串口传感器做数据采集的朋友一个参考。1. 项目整体设计与元件选型思路1.1 为什么选择ESP32-S3做主控ESP32-S3是乐鑫推出的一款带WiFi和蓝牙功能的双核MCU相比经典ESP32它在AI加速、IO口数量、USB外设支持上都有升级。这次选它做主控主要看中三点第一IO资源够用。JW01只占一组UARTESP32-S3自带3个硬件串口除了接传感器之外还能同时挂OLED屏幕、按键、SD卡不会出现接口冲突。第二WiFi能力对后续扩展很有价值。CO2数据如果只在自己手机上看没什么意义接上WiFi之后可以上报到本地MQTT服务器或者写个简单的Web页面在浏览器里看实时曲线。ESP32-S3的双核240MHz跑这些任务绰绰有余。第三开发环境友好。Arduino IDE对ESP32-S3的支持已经非常完善装一下esp32板级支持包就能像写普通Arduino一样开发不需要碰ESP-IDF那套复杂工具链对快速验证传感器方案来说效率高很多。当然它也有缺点比如GPIO的3.3V逻辑电平和一些5V传感器对接时要多注意电平匹配。这个留到后面供电部分详细说。1.2 JW01二氧化碳传感器协议与接线设计JW01是深圳一家传感器厂商推出的红外CO2模块也有同系列的粉尘传感器版本型号不同输出协议会有一点差异。我这次用的是JW01-CO2型基于NDIR非色散红外原理测量CO2浓度。模块的关键参数整理如下参数说明工作电压5V DC测量范围0~5000ppm输出方式UART TTL串口主动上传默认波特率9600bps数据帧长度9字节预热时间约1~3分钟红外CO2传感器的工作原理比较简单内部有一个红外光源和一个红外探测器中间是气体采样腔。光源发出的红外光穿过气体腔CO2分子会对特定波长的红外光产生吸收探测器测到的光强变化量就和CO2浓度对应起来了。因为是物理光学测量方式不像电化学传感器那样容易受氧气浓度影响寿命和稳定性都更好。不过NDIR原理的传感器有个特点内部有一个加热元件来产生红外辐射探头启动时电流会比正常工作时高出不少这一点正是后面供电踩坑的直接原因。接线其实很简单总共4根线传感器的VCC接5V电源正极GND接电源负极同时也要接ESP32-S3的GNDTX接ESP32-S3的RX引脚RX接ESP32-S3的TX引脚我用的开发板上UART2的RX接到IO16TX接到IO17所以接线就是JW01 VCC - 5V电源正极 JW01 GND - 电源负极 ESP32-S3 GND JW01 TX - ESP32-S3 IO16 (UART2 RX) JW01 RX - ESP32-S3 IO17 (UART2 TX)1.3 数据流和整体方案架构整个系统的数据流是单向清晰的JW01模块定时主动向串口发送9字节数据帧ESP32-S3的UART2接收到完整帧后解析出CO2浓度值一方面通过USB串口打印到串口监视器另一方面保存到全局变量方便后续接OLED显示或WiFi上传。为什么让传感器主动上传而不是用查询方式因为JW01默认就是主动上传模式上电后每隔大约2秒自动发一帧数据不需要主机发指令。这样省了主控侧的状态机处理程序写起来更简单也降低了串口通信出错的概率。当然主动上传模式有一个小问题如果主控没有及时读取串口缓冲区数据会堆积读出来时可能已经丢了几帧。解决办法是提高读取频率或者利用available()判断缓冲长度后再集中读取代码部分会具体演示。2. 供电踩坑全过程从死机到稳定2.1 第一次接线直接USB供电引发的一串问题第一次做这个项目时我的想法很简单把ESP32-S3开发板用USB线插到电脑上从开发板的5V引脚引两根线给JW01供电同时把TX/RX接好串口通信不就有了吗实际现象完全出乎意料。上电的瞬间JW01模块上的指示灯闪了一下就灭了ESP32-S3开发板开始反复重启串口监视器里先是出现几行乱码然后又是熟悉的Boot信息。用手摸一下电源接口附近的芯片明显烫手。这是我做硬件项目以来特别典型的教训开发板的USB供电能力被严重低估了。很多USB口标称能输出500mA甚至更高但实际到开发板5V引脚上的可用电流远没有那么多。因为USB线有内阻开发板上的电源路径有保护二极管板载LDO自身也在消耗电流。更麻烦的是如果用电脑前置USB口或者扩展坞取电实际输出电压可能只剩4.6~4.8V电流稍大电压就往下掉。ESP32-S3本身在WiFi开启状态下峰值电流可以达到300~500mA。JW01的NDIR传感器上电瞬间也有一个明显的电流脉冲用来给红外光源加热。两个电流峰值叠加在一起USB口根本撑不住电压跌落导致后级稳压器输出异常MCU复位传感器也跟着断电重启于是出现了一闪一灭、反复重启的诡异现象。2.2 供电不足的判断方法供电不足引起的故障最典型的表现是三类设备反复重启没有任何规律传感器指示灯亮度闪烁或亮一下灭一下串口数据乱码、偶发丢帧、数值异常跳变这些现象很容易被误判成“传感器坏了”或者“代码写错了”。我当时第一反应就是用示波器抓串口波形折腾半天发现波形本身没问题后来用万用表一量传感器VCC引脚电压在4.2V~4.8V之间剧烈跳动真相才浮出水面。所以做这类项目如果出现以上现象第一步不是查代码而是拿万用表测供电。重点测两个位置传感器VCC对GND的电压工作过程中是否稳定在4.8V以上ESP32-S3开发板5V引脚对GND的电压接上负载后有没有明显跌落如果电压在传感器上电瞬间下拉超过0.3V基本可以确定是供电能力不足不是代码问题。2.3 外置5V电源与滤波电容方案解决供电问题最省事也最可靠的办法是把传感器供电和开发板供电拆开给JW01单独一个5V电源。我当时手边没有额外的高质量USB充电头直接用了一个5V/2A的电源适配器通过USB线引出5V接到一个面包板上再从面包板给JW01供电。同时把ESP32-S3的GND和这个外置电源的GND连在一起。为什么一定要共地因为串口通信是单端信号RX和TX引脚上的电平都是以各自的GND为参考的。如果传感器和主控各用各的电源两个GND之间存在压差传感器发出来的高电平可能是3.8V主控可能认为已经是无效电平了反过来也一样通信必然乱套。共地之后双方的参考电平一致通信才可能正常。供电回路里我又加了两颗电容5V正极 ------[ 100uF 电解电容 ]--- 地 ---[ 100nF 瓷片电容 ]--- 地100uF电解电容负责储能传感器上电瞬间需要大电流时由它先顶上避免电压瞬间被拉垮。100nF瓷片电容负责滤除高频噪声防止开关电源适配器带来的纹波干扰串口信号。实测这样改完之后JW01稳定运行指示灯不再闪烁串口数据连续正常。之后我又试了用18650锂电池加一个升降压模块输出固定5V的方案也可以正常工作。如果以后要做便携式CO2检测仪用这个方案供电更合适。2.4 串口电平匹配5V TX怼3.3V RX的隐患供电解决之后本以为万事大吉后来发现还有另一个隐形坑电平匹配。JW01是5V供电的模块它的TX输出高电平理论上是接近5V的而ESP32-S3的GPIO接口最大耐压是3.3V实际上很多引脚标称极限3.6V如果你直接把5V电平的信号接到3.3V的RX引脚上短时间可能没事长期运行就存在烧毁主控引脚的隐患。这里需要区分不同情况。有些串口传感器虽然供电是5V但内部已经把TX信号转换成了3.3V电平这种直接接问题不大。而我手里这个JW01没有标清楚保险起见按5V电平处理。正确做法有两个最省事的做法是串联一个1kΩ电阻在JW01的TX和ESP32-S3的RX之间。这个电阻不会影响串口信号传输但能限制故障电流万一出现异常高压电阻会把电流限制在安全范围内。更正规的做法是做了一个2kΩ与1kΩ的分压电路把5V电平分压到3.3V左右JW01 TX ----[ 2kΩ ]-------- ESP32-S3 RX | [ 1kΩ ] | GND分压计算非常简单5V × 1000 / (2000 1000) ≈ 1.67V这里写错了重新算。2kΩ和1kΩ串联总电阻3kΩ1kΩ上的电压 5V × 1k / 3k 1.67V这不够用。应该用分压比例算例如分压电阻用1kΩ在上、2kΩ在下5V × 2k / (1k2k) 3.33V。也就是说JW01 TX串一个1kΩ电阻到ESP32 RXRX引脚处再对地接一个2kΩ下拉电阻JW01 TX ----[ 1kΩ ]-------- ESP32-S3 RX | [ 2kΩ ] | GND这样RX处的电压约为 5V × 2k / (1k 2k) ≈ 3.33V既满足ESP32-S3高电平识别阈值又不会超过耐压上限。实测通信完全正常。顺带说一句ESP32-S3的TX输出3.3V电平接JW01的RX一般没问题因为大多数串口传感器说明书都注明支持3.3V TTL信号输入但如果遇到模块不兼容就需要加一个电平转换模块如逻辑电平转换板处理。JW01这块倒是没在这边出问题。3. 完整Arduino代码与数据处理3.1 Arduino开发环境配置代码部分用的是Arduino IDE版本2.x。需要的准备工作有两件第一在“开发板管理器”里安装esp32板级支持包。打开Arduino IDE后进入“文件 - 首选项 - 附加开发板管理器网址”填入乐鑫官方地址https://espressif.github.io/arduino-esp32/package_esp32_index.json然后在“工具 - 开发板 - 开发板管理器”里搜索esp32安装最新版支持包。这一步会下载几十MB的编译工具链耐心等待即可。第二在“工具 - 开发板”里选到“ESP32S3 Dev Module”然后注意几个关键配置项配置项推荐值Flash Size8MB或16MB按你板子实际大小选USB CDC On BootEnabled 或 Disabled 都可以PSRAM如果板载PSRAM就EnabledUpload Speed921600这些配置不影响本项目的串口通信逻辑但PSRAM和Flash Size选错了会导致编译或烧录失败。如果不确定对照自己开发板丝印上的型号来选就行。3.2 代码思路与完整源码JW01主动上传数据所以主程序只需要做一件事不断读取串口缓冲区找到完整的数据帧解析出CO2浓度。JW01的串口数据帧格式是9字节我手里的模块实际输出长这样字节0: 0xFF帧头 字节1: CO2浓度高8位 字节2: CO2浓度低8位 字节3~7: 保留字节通常为0x00 字节8: 校验字节CO2浓度值 (字节1 8) | 字节2单位ppm。比如收到了FF 01 2C 00 00 00 00 00 2D那么浓度就是0x012C 300ppm。完整的Arduino代码如下#include HardwareSerial.h // 使用UART2RX接IO16TX接IO17 #define JW01_RX_PIN 16 #define JW01_TX_PIN 17 HardwareSerial JW01Serial(2); uint16_t co2Value 0; // 存储解析出的CO2浓度单位ppm uint8_t frameBuffer[9] {0}; void setup() { // 串口监视器 Serial.begin(115200); // 与JW01通信的UART2波特率96008N1 JW01Serial.begin(9600, SERIAL_8N1, JW01_RX_PIN, JW01_TX_PIN); Serial.println(ESP32-S3 JW01 CO2 Sensor Test); Serial.println(Waiting for sensor data...); } void loop() { if (readJW01CO2(frameBuffer, co2Value)) { Serial.print(CO2: ); Serial.print(co2Value); Serial.println( ppm); } // 传感器约2秒上传一帧这里稍微等一下即可 delay(100); } /** * 尝试从串口缓冲中读取一个完整的9字节帧并解析CO2浓度。 * * 返回值: 成功解析返回true否则返回false */ bool readJW01CO2(uint8_t *buf, uint16_t *co2) { // 数据帧小于9字节就先不处理 if (JW01Serial.available() 9) { return false; } // 先读一个字节判断是否帧头0xFF uint8_t first JW01Serial.read(); if (first ! 0xFF) { // 不是帧头丢弃继续等待下一个字节 return false; } // 已经消费了第一个字节接下来再读8个字节 // 注意这里要用超时机制防止读不够8字节导致阻塞 uint8_t idx 0; uint32_t startTime millis(); while (idx 8) { if (JW01Serial.available() 0) { buf[idx] JW01Serial.read(); } else if (millis() - startTime 100) { // 超过100ms还没收满剩余字节说明帧不完整放弃 return false; } } // 把读到的8个字节放回frameBuffer的完整位置 // 这里直接用局部变量操作最终把9字节拼起来 uint8_t fullFrame[9]; fullFrame[0] 0xFF; for (int i 0; i 8; i) { fullFrame[i 1] buf[i]; } // 校验第9字节是前7个数据字节字节1~7的累加和低8位 // 不同批次模块校验算法可能不同如果校验一直不通过可以先把校验逻辑注释掉 uint8_t sum 0; for (int i 1; i 8; i) { sum fullFrame[i]; } if (sum ! fullFrame[8]) { // 校验失败丢弃这帧 return false; } // 解析浓度 *co2 (fullFrame[1] 8) | fullFrame[2]; return true; }这段代码有几个细节值得说明。第一为什么先读一个字节判断帧头再读剩余字节因为串口数据是持续的字节流如果用available() 9就直接读9个字节很可能读到的9个字节是从某一帧中间开始的帧头不在第一位解析必然失败。先读一个字节判断是不是0xFF能过滤掉大量错位数据。第二为什么读剩余字节时要加超时如果数据流中断只剩两三个字节在缓冲区里while循环会一直等下去导致程序卡死。加上millis()超时判断100ms内收不满就直接放弃保证主循环不会被阻塞。第三校验算法的兼容问题。我用的这块模块校验和是“字节1到字节7的累加和取低8位”但同一系列不同批次的产品可能会有差异。如果你跑起来发现所有数据都被校验失败丢掉就把校验那段直接注释掉只看解析结果是否合理。红外CO2传感器读数一般在400~1000ppm之间波动偶尔出现几千ppm也不用太惊讶可能是室内闷了太久或者刚呼了一口气。3.3 帧校验与容错处理串口通信的容错设计直接决定系统的长期稳定性。JW01每2秒发一帧如果我的程序以100ms的间隔轮询一次readJW01CO2调用可能处理不到完整帧下一次循环继续。这个设计天然就具备“丢帧自动恢复”的能力只要有一帧数据完整到达程序就能正确解析。实际测试中即使出现偶发串口干扰导致某一帧校验失败被丢弃也不会影响后续数据因为下一次循环会继续从缓冲区找下一个帧头0xFF。也就是说这个程序对错位帧是自愈的不需要额外复位逻辑。还有一个容易忽视的点如果要长时间运行这个系统建议定期清空串口缓冲区防止异常情况下垃圾数据堆积。// 可以放在loop的超时判断里 if (JW01Serial.available() 100) { JW01Serial.flush(); // 或者直接写 while(JW01Serial.available()) JW01Serial.read(); }flush()在Arduino-ESP32里是等待数据发送完成不是清空接收缓冲所以要清空接收缓冲区的话更准确的做法是while (JW01Serial.available()) { JW01Serial.read(); }每隔一段时间执行一次这个清理能有效防止缓冲区脏数据累积导致的解析延迟。4. 常见问题与排查技巧实录4.1 串口读不到任何数据如果串口监视器里什么都打印不出来或者一直停在“Waiting for sensor data...”优先检查几个地方先把TX和RX对调一下。很多新手第一次接线会把传感器的TX接到主控的TX上这会导致两边都在发送、谁也不收。正确的接法是传感器的TX接主控的RX传感器的RX接主控的TX。再看两个GND是否连在一起。如果传感器用独立电源供电而GND没有和ESP32-S3共地串口通信是绝对跑不起来的。这是仅次于TX/RX接反的第二高频错误。然后确认波特率。JW01默认9600但是有些卖家发货前会把模块配置成其他波特率比如19200。如果你不确定用一个USB转TTL模块直接接电脑打开串口助手分别试9600、19200、115200看哪个能收到规律性的9字节数据帧。4.2 数据一直跳变或数值为0数值为0通常有两种可能一是读到的帧刚好是传感器自检阶段的数据或者传感器暖机没完成。CO2传感器上电后前几分钟的数据是不准的正常情况下等待1~3分钟再读取。二是解析字节位置不对。不同版本的JW01模块浓度字节在帧里的位置可能有区别。你可以把收到的原始数据打印出来和模块说明书对照一下。跳变的问题就比较复杂。如果某个数值突然从400ppm跳到5000ppm再跳回来多半不是真实现场浓度变化而是供电纹波干扰了串口信号。这个时候回到第2章的供电部分先把滤波电容加上再看数据有没有改善。有一个比较隐蔽的原因是ESP32-S3的GPIO引脚和别的功能冲突了。我用的是IO16和IO17这两个引脚在部分开发板上可能被板上其他外设占用比如某些开发板把IO17用作PSRAM或Flash的引脚。解决方法是换一组不冲突的引脚比如IO18和IO8然后修改代码里的JW01_RX_PIN和JW01_TX_PIN宏定义。4.3 上电后开发板反复重启开发板反复重启几乎是供电问题的高频特征但也有特殊情况如果代码里初始化了串口之后执行到某个地方卡死看门狗会把芯片复位掉。可以先用最简单的Blink程序烧录测试如果Blink正常跑再排除代码层面的问题把怀疑重心放到供电上。我那个板子最后定位到的原因就是USB供电能力不足。解决后我用了一个小技巧确认硬件是否真的稳定在代码里加了一段把WiFi打开的测试程序然后反复开关WiFi连接观察电压波形。如果电压在WiFi启动瞬间掉到4.5V以下说明电源余量还有问题。4.4 几个值得长期保留的实操习惯做了一晚上供电踩坑总结几条可能对别人也有用的经验第一任何传感器上电前先看规格书的电流参数。工作电流、峰值电流、启动电流这三个数字直接决定你该配多大的电源。JW01这类NDIR传感器峰值电流往往比标称工作电流高一倍以上不能只看平均值。第二一块万用表比一百个调试技巧都重要。测电压、测电流、查通断几乎所有的硬件问题最后都能用万用表找到蛛丝马迹。串口出问题先量TX/RX两个引脚的电平极性对不对供电出问题先量VCC/GND两端电压稳不稳。第三用独立电源给传感器供电时共地处不要随便接。共地点要选在传感器的GND引脚附近不要从电源适配器那一端飞一根长线过去那样会导致地回路面积过大反而引入更多噪声。我在第二次改版时把共地点放在了面包板同一块区域内数据明显更干净。第四串口数据多看原始字节。不要只看解析后的ppm数值。把原始帧打印出来你能看到帧头是不是0xFF、帧长度是不是9字节、校验位是否合理。这些东西能帮你区分是硬件问题还是解析代码问题。我个人现在做ESP32相关项目养成了一个习惯不管传感器标称电压是3.3V还是5V先量一次上电瞬间的电流再决定电源方案。电源余量留足后面一切调试都顺畅电源余量不够代码写得再漂亮也白搭。这个项目目前跑在我书桌上实时显示着室内CO2浓度。实测室内正常通风情况下大约是450~550ppm门窗紧闭一个晚上能到1200ppm以上提醒效果很直接。后续我打算再接一个OLED屏幕做到设备端显示再通过WiFi把数据推到Home Assistant这些都是在现有硬件不变的基础上就可以扩展的。如果你也在折腾类似的传感器项目供电部分少走点弯路整套流程能顺利不少。