news 2026/9/24 9:49:18

ESP32-S3驱动JW01 CO2传感器:UART通讯与供电避坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32-S3驱动JW01 CO2传感器:UART通讯与供电避坑实践

最近在给家里搭一个室内空气质量监测的小项目,核心需求很明确:实时看到 CO₂ 浓度,方便判断要不要开窗通风。主控我选了 ESP32-S3,传感器选了 JW01 这个便宜的 CO₂ 模块,整体成本压得很低。本以为串口接上就能出数,结果没想到在供电上反复折腾,踩了好几个坑才稳定运行。这篇博客就把整个过程中拆解出的选型思路、传感器协议、供电问题的前因后果、以及完整的 Arduino 代码都分享出来,给准备用 ESP32-S3 或类似开发板驱动串口传感器的朋友做个参考。

这个项目适合谁?如果你手里有 ESP32、ESP8266、Arduino 这类板子,想接一个 UART 输出的气体传感器或者类似模块,被“读数异常”“一开 WiFi 就重启”“串口乱码”这种问题困扰过,那这篇文章应该能帮你少走不少弯路。我会把供电、共地、电平、协议解析这几块讲透,代码也是可以直接抄作业的完整版。

1. 项目背景与选型分析

1.1 为什么选 ESP32-S3 作为主控

最初我考虑过 Arduino Uno、ESP8266 和 ESP32-S3 三个方案。Arduino Uno 上手简单,但板载资源太弱,后期想加 WiFi 上报、OLED 显示或者本地存储,都得外挂一堆模块,板上资源很快就会见底。ESP8266 价格便宜,也能联网,但只有一个 UART 外设,引脚也相对紧张,用起来不够从容。

ESP32-S3 在这个场景里优势很明显:双核 240MHz 的 Xtensa 处理器,处理串口数据绰绰有余;板载 WiFi 和 BLE,方便后续把数据推送到 MQTT 或者 Home Assistant;UART 外设充足,可以专门分一个串口给传感器,不影响 USB 调试口。另外 ESP32-S3 的 3.3V 逻辑电平和常见传感器模块的通信电平更匹配,硬件层面省掉不少麻烦。

还有一点很实用,ESP32-S3 的 Arduino 生态已经非常成熟,开发板支持包在 Arduino IDE 里一键安装,写代码的方式和传统 Arduino 完全一致。对一个做原型验证的项目来说,这种低门槛的工具链意味着更快的迭代速度。

1.2 为什么传感器选择 JW01

CO₂ 传感器的选型,市面上常见的大致分三类:进口高精度模块、国产 NDIR 模块、国产低成本电化学/固体电解质模块。JW01 属于最后一类,单价通常只有几十块钱,和 MH-Z19 这种动辄上百的红外模块相比,价格优势巨大。

JW01 的输出方式是 UART 串口,主动上报数据,不需要像 I2C 那样去查询和时序控制,代码逻辑非常直观。模块本身支持 400~5000ppm 的量程,日常室内监测完全够用。虽然它的精度和响应速度不如高端 NDIR 方案,但对于“看个趋势、决定开不开窗”的需求来说,这个性价比非常划算。

我理解很多玩家会纠结“便宜的传感器数据准不准”这个问题。实际使用中我发现,只要供电稳定、预热充分、并且校准好初始环境,JW01 的数据稳定性还是可以的。当然,如果你要做严格的科研采集或者医疗级监测,那就别省这个钱,直接上 SCD30 或者 MH-Z19 这类更高端的模块。但做 DIY 环境监测,JW01 完全是够格的。

1.3 项目的整体功能规划

最初我只想做一个简单的 CO₂ 读取设备,把数据打印到串口监视器里。但随着项目推进,我对功能做了个规划:

  • 基础功能:通过 UART 读取 JW01 数据,解析出 CO₂ 浓度并显示。
  • 扩展功能:用 ESP32-S3 的 WiFi 将数据发送到 MQTT 服务器,配合 Home Assistant 做室内环境自动化,比如 CO₂ 浓度超过 1000ppm 时自动提醒开窗或启动新风。
  • 人机交互:预留 OLED 屏幕插座,后期可以实时显示浓度曲线。

这套规划的好处是,每一步都在基础硬件上做增量,不用换板子。ESP32-S3 的资源完全撑得起这些扩展,这也是我坚持选它的原因之一。

2. JW01 通信基础与接线方案

2.1 模组引脚定义

JW01 模块通常引出 4 个引脚,分别是 VCC、GND、TX、RX。不同厂家生产的模块引脚顺序可能略有区别,但功能基本一致。接线前我特意检查了模块上丝印标注和配套文档,避免把电源和信号线接错。

  • VCC:电源正极,常见标称 5V。
  • GND:电源地,也是通信参考地。
  • TX:传感器数据发送端,接主控的 RX。
  • RX:传感器数据接收端,接主控的 TX,这个引脚一般用不到,因为 JW01 默认主动上报。

我用万用表量了一下模块正常工作时 VCC 和 GND 之间的电压波纹,发现如果直接用 3.3V 供电,波纹明显偏大不说,传感器输出的字节流也会变得不稳定。所以后来我坚持用 5V 供电,这个问题后面整个“供电踩坑实录”章节会详细展开。

2.2 串口数据帧解析

JW01 的串口参数固定为 9600bps、8 位数据、无校验位、1 位停止位,也就是常见的 9600 8N1。模块上电后以一定间隔主动向上位机发送数据帧,不需要发送任何指令。

我手里这块模块的帧长度为 9 字节,结构如下:

字节序号含义
00xFF固定帧头
10x04命令码,表示数据帧
20x03气体类型标识,这里代表 CO₂
3CO₂ 浓度高字节浓度值高位
4CO₂ 浓度低字节浓度值低位
5~7保留字节可以忽略
8校验字节前 8 个字节累加和的低 8 位

其中 CO₂ 浓度的计算方式为:

浓度值 = 第3字节 × 256 + 第4字节

校验方法很典型:把第 0 到第 7 字节累加,取结果的低 8 位,和第 8 字节比较,相等则说明这一帧数据有效。

需要注意,不同批次或不同厂家生产的 JW01 帧结构可能会有差异,有的模块帧头是 0xFF 0x04,有的可能包含更多信息。我在代码里加了一个帧头检测和校验逻辑,就是为了识别这类差异。如果你手里的模块输出格式不同,建议用串口助手先抓一下原始数据,再对应调整解析代码。

2.3 接线表与常见注意事项

以下是 ESP32-S3 与 JW01 的接线表,我用的是 ESP32-S3-DevKitC 开发板,板载 USB 转串口芯片和 RGB 灯。

JW01 引脚ESP32-S3 引脚说明
VCC5V 引脚必须用 5V,不要接 3.3V
GNDGND共地关键,千万别漏
TXGPIO18开发板串口 RX 引脚
RXGPIO17一般悬空,也可接开发板 TX

接线时有几个点要特别注意。第一,VCC 优先接开发板的 5V 输出引脚,而不是从 USB 的 VBUS 上飞线,这样供电回路更短,压降更小。第二,GND 一定要接上,如果没有共地,串口信号的电平参考点不一致,收到的数据会乱成一团。第三,确认模块 TX 的引脚别接错,接反了会一直收不到数据。

3. 供电踩坑实录

3.1 坑1:贪方便,把 JW01 接到了 3.3V

第一次接线时,我顺手就把 JW01 的 VCC 接到了开发板的 3.3V 引脚上。我当时想的是,模块标称电压范围如果包含 3.3V,应该没问题。结果上电后串口什么数据都没有,等了好几分钟还是一潭死水。

用万用表测了一下模块 VCC 引脚的电压,3.3V 本身是正常的,但我又测了传感器 TX 引脚的静态电平,发现波形非常怪异,不是干净的高/低电平跳变。后来查了模块资料才明白,JW01 内部虽然有一定稳压处理,但传感器核心的加热和采样电路需要 5V 才能稳定工作。3.3V 供电会让模块处于一种“低压欠驱动”的状态,传感器本身无法正常工作。

这个坑的本质是:看到了模块标称电压范围里的“3.3~5V”就掉以轻心,没仔细看工作电流和工作条件。模块标称的是“可工作电压范围”,不等于“推荐电压”。稳妥起见,直接上 5V 供电。

3.2 坑2:USB 供电,一开 WiFi 就重启

修正供电到 5V 之后,串口数据终于正常了。但是过了一小会儿,我刚把 WiFi 功能加进代码,开发板就开始反复重启,启动日志里能看到“Brownout detector was triggered”的提示。

这个问题本质是电流余量不足。ESP32-S3 在 WiFi 开启瞬间的峰值电流可以到 300mA 左右,再加上 JW01 的 40~60mA 工作电流,稳压器输出电流已经非常接近上限。电脑 USB 口一般只能提供 500mA 电流,如果开发板上的 LDO 稳压器余量不够,稍微有点波动,电压就会被拉低,触发掉电保护。

我的解决方法有两个方向。第一,把 JW01 的 VCC 改接到开发板的 5V 引脚,这样传感器直接吃 USB 的 5V,不经过板上 3.3V LDO,减轻 LDO 负担。第二,调试期间尽量用带独立供电的 USB Hub,或者直接换一个输出不低于 1A 的适配器供电。实测下来,改用独立适配器之后,WiFi 开启时系统重启的问题彻底消失了。

3.3 坑3:外部电源单独为传感器供电,忘记共地

第三个坑是项目后期发生的。我想让 JW01 和 ESP32-S3 分开供电,避免相互干扰,于是用了一个独立的 5V 电源给 JW01 供电。结果一上电,串口数据乱码得非常彻底,偶尔能碰到几个正确的帧头,但数据一律不对。

查了半天,最后才意识到问题出在共地上。两个电源各自独立输出,虽然电压都是 5V,但它们的 GND 参考点之间没有导线相连。JW01 的 TX 引脚输出的信号电平相对它自己的 GND,而 ESP32-S3 的 RX 引脚接收信号时参考的是开发板自己的 GND,两边参考点不一致,信号自然对不上。

解决方式很简单,把两个电源的 GND 引脚用导线连起来,让整个系统有一个统一的参考地。这也是嵌入式系统中“共地是通信前提”这条铁律的最典型体现。

3.4 推荐的供电方案与功耗估算

经历了上面几个坑之后,我把供电方案总结成了几个可以照抄的选项:

供电方案优点缺点适用场景
开发板 USB 直供 5V + JW01 接 5V 引脚最简单,免额外硬件受 USB 口电流限制,WiFi 峰值有风险快速原型、短时间调试
5V/2A 适配器供电开发板电流余量充足,稳定需要额外电源长时间运行、正式部署
18650 锂电池 + 升压模块便携、可移动需要额外的升压/充电板移动监测、离线使用
直流-DC 降压模块从前级取电效率高、纹波小电路复杂、成本略高多设备协同供电

关于功耗估算,我可以给出一个简单的计算过程。ESP32-S3 开启 WiFi 时平均电流大约 200mA,峰值 300mA;JW01 稳定工作电流约 40mA,启动瞬间不算高。所以整个系统的最低供电电流建议不低于 500mA,预留余量后推荐 1A 以上。如果使用 18650 电池,按 2600mAh 容量算,系统平均电流 250mA,理想状态下可以撑 10 小时左右,考虑升压效率和电池衰减,实际约 8 小时,用于短时间移动监测够用。

4. 完整 Arduino 代码实现

4.1 开发环境准备与开发板配置

代码使用 Arduino IDE 编写,项目开始前需要先在开发板管理器里安装 ESP32 支持包。在 Arduino IDE 中打开“文件 — 首选项 — 附加开发板管理器网址”,填入官方支持的 JSON 地址,然后在“工具 — 开发板 — 开发板管理器”中搜索 “esp32” 并安装最新版本。

安装完成后,在“工具 — 开发板”里选择 ESP32S3 Dev Module,确认 USB CDC On Boot 选项设置为 Enabled,否则串口监视器可能看不到打印信息。烧录时选择对应的 COM 口,ESP32-S3 一般需要按住 BOOT 键再点上传,也可以根据开发板自动进入下载模式。

很多人反馈 Arduino IDE 2.x 没有代码补全,其实在偏好设置里把语言服务打开,或者直接换用 VS Code + PlatformIO,体验会好很多。这个项目代码量不大,IDE 自带编辑器足够用。

4.2 程序实现思路

程序整体分为三块:初始化、串口数据解析、周期日志输出。

  • 初始化部分:配置调试串口、传感器串口、板载 LED 引脚。
  • 数据解析部分:从 Serial2 读取字节流,寻找帧头,收满 9 字节后做校验,校验通过则提取 CO₂ 浓度并更新数据状态。
  • 周期性输出部分:每 1 秒通过调试串口打印当前浓度、有效帧计数、无效帧计数和数据时间间隔。

帧丢失处理也很关键。如果超过 5 秒没有收到任何有效帧,就说明通讯或供电异常,通过日志输出提醒排查。

4.3 完整代码清单

下面是完整的 Arduino 代码,针对 ESP32-S3 编写,其他 ESP32 型号也能直接用,只要改一下串口引脚定义即可。

#include <Arduino.h> // 通信引脚定义 #define SENSOR_RX_PIN 18 // 接 JW01 TX #define SENSOR_TX_PIN 17 // 接 JW01 RX(可悬空) #define LED_PIN 48 // 开发板板载 RGB 灯引脚 // JW01 数据帧长度 #define FRAME_LEN 9 // 传感器串口缓冲 uint8_t frame[FRAME_LEN]; uint8_t frameIndex = 0; // 数据状态 float co2ppm = 0.0f; uint32_t goodFrames = 0; uint32_t badFrames = 0; unsigned long lastValidFrameMs = 0; unsigned long lastLogMs = 0; /** * 校验一帧数据 * 规则:前 8 字节累加和的低 8 位 == 第 9 字节 */ bool parseFrame(uint8_t *buf) { if (buf[0] != 0xFF) { return false; } uint8_t sum = 0; for (int i = 0; i < 8; i++) { sum += buf[i]; } return (sum == buf[8]); } void setup() { // 调试串口 Serial.begin(115200); delay(300); Serial.println(); Serial.println("ESP32-S3 + JW01 CO2 Monitor"); // 传感器串口:9600 8N1 Serial2.begin(9600, SERIAL_8N1, SENSOR_RX_PIN, SENSOR_TX_PIN); // 板载 LED 初始化 pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); Serial.println("System ready. Waiting for sensor data..."); } void loop() { // 从串口读取一字节,按帧结构拼装 while (Serial2.available()) { uint8_t b = Serial2.read(); // 等待帧头 if (frameIndex == 0 && b != 0xFF) { continue; } frame[frameIndex++] = b; // 收满一帧 if (frameIndex == FRAME_LEN) { if (parseFrame(frame)) { // 解析 CO2 浓度 uint16_t raw = (uint16_t)frame[3] * 256 + frame[4]; co2ppm = (float)raw; goodFrames++; lastValidFrameMs = millis(); // LED 闪烁一下表示数据有效 digitalWrite(LED_PIN, LOW); delay(20); digitalWrite(LED_PIN, HIGH); } else { badFrames++; } frameIndex = 0; } } // 每秒打印一次状态 if (millis() - lastLogMs >= 1000) { lastLogMs = millis(); unsigned long age = millis() - lastValidFrameMs; Serial.printf("[%lu ms] CO2: %.0f ppm | good=%u bad=%u | data age: %lu ms\n", millis(), co2ppm, goodFrames, badFrames, age); // 超过 5 秒没收到有效帧,说明可能供电或接线有问题 if (age > 5000) { Serial.println("WARN: No valid frame received for >5s, check power/connection!"); } } }

4.4 关键代码说明

这里解释几个容易被忽略的细节。

帧头检测这段,我用了“只在第一个字节等待 0xFF”的方式,这样做的好处是,即使数据流中出现半个字节的错位,也能快速重新同步,不会被垃圾数据卡住整个解析流程。

校验失败的时候,我没有直接放弃整帧,而是把 badFrames 计数器加一。这样做的好处是可以通过 good/bad 比例判断链路质量。如果 bad 比例持续偏高,大概率是供电不稳、电平不匹配或者接线接触不良。

打印数据时,我特意加上了“data age”这个参数,表示当前显示的浓度值已经多少毫秒没有更新了。这比单纯打印浓度值更实用,因为你看到 CO₂=500ppm 时,无法判断是真实数据还是传感器掉线后保留的旧数据。有了 data age,问题一目了然。

5. 常见问题排查与避坑总结

5.1 典型现象与排查方法

串口监视器乱码:先检查波特率是否设置正确,JW01 固定 9600,而调试串口是 115200,两个串口别搞混。确认信号线是否接反,TX 接 RX 这个基本规则也经常有人突出错。排除以上问题后,重点检查供电和共地,这是最容易出故障的点。

数据一直显示 0 或者满量程:如果解析出来一直为 0,大概率是传感器启动异常,可以检查供电电压是否达到 5V 并保持稳定。如果一直显示接近量程上限,先判断是否处在预热阶段,JW01 上电后需要一定时间预热,数据会从高位慢慢回落。如果没有预热问题,再看周围环境是否存在干扰源,比如靠近空调出风口或者有人密集的空间。

数据跳变剧烈:一帧 800ppm,下一帧突然变成 4000ppm,然后又跳回 900ppm。这种情况先排除接线接触不良,虚接最典型的表现就是数据无规律跳变。然后是供电纹波,给开发板供电的 USB 口如果本身不稳定,很容易导致传感器采样异常。最后再考虑传感器本身老化,可以换个模块交叉验证。

5.2 快速排查速查表

现象首先检查其次检查最终手段
完全无数据接线方向、帧头VCC 电压是否 5V用串口助手抓原始波形
偶发有效、偶发乱码共地线是否可靠信号线是否过长/缠绕缩短信号线或加分压/整形
数据恒为 0预热时间是否足够模块是否损坏换模块测试
数据跳变剧烈接线端子是否牢固供电波纹是否过大换高余量电源测试
WiFi 开启后重启电源电流余量传感器是否接 3.3V接 5V/换大功率适配器
长时间运行后漂移预热不够或老化环境温湿度变化在新鲜空气中重校基准

5.3 提前躲开的坑:电平适配与长期稳定性

关于电平适配,多说一句。JW01 的 TX 引脚输出电平在不同供电条件下可能不同。我测过手里这块模块,在 5V 供电下,TX 输出高电平约 3.3V,可以直接接 ESP32-S3 的 GPIO。但有些批次的模块输出高电平可能接近 5V,而 ESP32-S3 的 GPIO 虽然有一定耐压能力,安全起见最好串一个 1kΩ 电阻做保护,或者用两个电阻分压到 3.3V。如果你的模块文档没有明确标注输出电平,这个保护措施值得加上。

长期运行时,JW01 因为内部有加热/采样元件,外壳会有一定温度,这是正常现象。但要注意给传感器留出通风空间,别把它密封在完全不透气的壳子里,否则测出来的 CO₂ 数值会一直偏高,失去参考意义。

另外,传感器长时间运行后可能存在基线漂移。我的做法是定期在室外新鲜空气环境下通电半小时左右,让传感器自动校准到 400ppm 附近的基线值。虽然不是严格意义上的手动校准,但对家用场景来说已经能维持不错的参考价值。

这个项目做到现在,已经可以稳定读取 CO₂ 浓度并上传到本地服务了。回过头看,最花时间的其实就是供电那一关,接线和代码反而不是难点。如果你也准备做类似项目,建议第一步就把供电方案定下来,别在 3.3V 和 5V 之间犹豫,也别忘了给系统预留足够的电流余量。后续我打算给这套系统加一个 OLED 显示和联动新风的自动化逻辑,等调试完善了再来更新一篇实践记录。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 9:45:42

恶意代码可视化检测实战:从字节流到CNN图像分类

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:38:29

FineReport迁移替代方案与数据校验全链路实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:38:24

工控现货实战指南:从选型验货到库存管理,避开停产缺货坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:37:47

三相工业设备供电转换选型:电压、频率与UPS链路全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 9:35:02

MTF/SFR详解:摄像头清晰度量化测试原理与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华