多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于Arduino与JW01的室内CO2监测仪:串口协议解析与DIY教程

基于Arduino与JW01的室内CO2监测仪:串口协议解析与DIY教程 刚拿到JW01二氧化碳传感器那块小模块时我心想这不就是个红外面板加一颗小单片机的事吗结果一查资料发现它居然直接走串口输出数据和Arduino搭起来简直是无脑接。这个桌面级环境监测系统我用了不到半小时就搭完了真正动手接线加烧录代码确实5分钟内能搞定。套装非常简单JW01二氧化碳传感器负责测CO2浓度Arduino UNO负责读数据OLED屏负责把数值摆在你眼前。新手想学串口协议解析或者单纯想给家里卧室加个CO2仪表这篇文章可以直接照着做代码我完整贴在下面了。很多人在室内待久了觉得头晕、犯困总以为是自己没睡够其实大概率是二氧化碳浓度涨上来了。室外一般400~450ppm门窗紧闭的卧室开一会儿空调分分钟飙到1500ppm以上这时候人是真的会变迟钝。所以把CO2浓度可视化成一个小仪表放在书桌、婴儿房、会议室或者自习室都是很实在的用途。下面从头拆解这个项目。1. 项目思路拆解为什么是JW01搭配Arduino1.1 室内CO2监测到底解决了什么问题先把这个需求说清楚。人体呼出的气体里CO2浓度非常高长时间待在不通风的空间室内的二氧化碳浓度会快速上升。按照常见室内空气标准长期居住和办公建议控制在1000ppm以内超过这个值以后人会觉得闷、注意力下降、甚至头晕。夏天开空调关窗、冬天开暖气关窗的时候最容易出现这种状况。这套系统解决的就是“看不见摸不着”的问题。你可以把它理解成一个CO2的“温度计”只不过量的是空气质量。放在书桌上抬头就能看到当前浓度超过阈值LED灯和屏幕提示就会提醒你“该开窗了”。对过敏人群、家里有小孩老人的用户来说这个设备比空气净化器上的数值更直接因为净化器只显示PM2.5不显示CO2。这个项目也适合刚接触Arduino的玩家。它不涉及复杂的PWM、中断或者无线通信核心就一件事从串口读取传感器数据解析成能看懂的数字。学会了这套串口协议解析流程以后换任何UART接口的传感器PM2.5、甲醛、温湿度等思路都一样。这也是我把这个项目写出来的另一个原因——它是入门串口通信的最短路径之一。1.2 传感器选型对比JW01凭什么几十块钱还能用现在市面上的CO2传感器方案不少我一开始也纠结过要不要上更高端的模块。把常见的几款放在一起对比一下你就能理解为什么最终选了JW01。型号原理接口量程参考价格特点JW01系列NDIR红外UART0-2000 / 0-5000ppm较低成本低、数字输出稳定、适合桌面仪表MH-Z14ANDIR红外UART / PWM0-5000ppm中等功能更全支持自校准SGP30MOS金属氧化物I2C400-60000ppm中等eCO2是估算值首次开机需校准SCD30NDIR红外I2C / UART0-40000ppm较高高精度还集成温湿度JW01最大的优势是“NDIR红外原理却卖到了很便宜的价格”。NDIR就是非分散红外检测简单说就是利用CO2分子对特定波长红外光的吸收特性来算浓度这才是真正的CO2传感器不像SGP30那样靠金属氧化物电阻变化估算SGP30对酒精、有机物气体会有干扰。如果你的项目预算充足、精度要求高那SCD30毫无疑问更强。但做桌面环境监测JW01完全够用——它的精度标称一般在±50ppm ±5%读数附近家用场景下这个误差完全可以接受。而且JW01走UART串口直接输出数字量不需要ADC转换也不用自己校准电压和浓度曲线。相比之下老式模拟量输出的传感器还要自己去标定麻烦不少。1.3 为什么是Arduino而不是树莓派或STM32Arduino UNO在这个项目里属于“杀鸡用牛刀”但也正因为如此才更适合大多数人来复现。树莓派虽然性能强但你要多准备电源、SD卡、操作系统还得处理串口权限问题一个简单的桌面仪表没必要整这么复杂。STM32则是另一条学习曲线光环境搭建就能劝退不少新手。Arduino生态最大的福利就是库和资料。SoftwareSerial库解决了UNO只有一个硬件串口的问题OLED显示用Adafruit SSD1306库直接调用整个代码量控制在150行以内。而且Arduino UNO输出的是5V电平正好和常见的JW01模块匹配不需要额外加电平转换。如果你手头只有Arduino Nano代码也完全通用引脚定义一样就行。这背后的选型逻辑很简单能用最少的时间跑通、让读者把注意力放在传感器协议这个核心知识点上方案就是好方案。如果一上来就上STM32RTOS那就跑偏了。2. 硬件准备、接线与JW01串口协议2.1 物料清单和接线表这个项目需要的硬件很少多数是Arduino玩家手里现成的东西。模块型号/规格数量说明主控板Arduino UNO / Nano1其他兼容板也行CO2传感器JW01-CO2系列1建议量程0-2000或0-5000ppmOLED屏0.96寸 SSD1306 I2C1128x64分辨率LED普通5mm发光二极管1做阈值报警指示灯电阻330Ω1给LED限流杜邦线母对母、公对母若干若干推荐不同颜色方便区分接线看上去不难但有一点特别容易搞反传感器的TX要接到Arduino的RX引脚传感器的RX要接到Arduino的TX引脚也就是交叉连接。我的SoftwareSerial定义是SoftwareSerial co2Serial(11, 10)其中11号引脚负责接收10号引脚负责发送所以JW01的TX必须接Arduino的11脚JW01的RX必须接Arduino的10脚。OLED用的是I2C接口UNO上SDA是A4SCL是A5这两个不用记库默认会用Wire库找对应引脚。还需要强调一个点D0和D1是Arduino的硬件串口也就是USB转串口复用的那一组。如果你把传感器接到D0/D1上上传程序的时候会被干扰而且连电脑串口监视器也会冲突。所以尽量用D10/D11这类普通数字引脚通过SoftwareSerial来跑传感器。这样电脑的串口监视器还能正常打印调试数据两不误。2.2 JW01串口协议看懂一帧数据再写代码JW01这类模块上电后会周期性主动上报数据常见的是9600波特率、8位数据位、无校验、1位停止位。数据帧一般是9个字节格式大致如下。字节位置值含义Byte00xFF帧头固定为FFByte10x04数据上报标识Byte20x00保留位Byte3XXCO2浓度高字节Byte4XXCO2浓度低字节Byte5XX温度高字节部分固件支持否则为0x00Byte6XX温度低字节Byte70x00保留位Byte8XX校验和浓度值是按大端方式拼出来的CO2 (Byte3 8) | Byte4。比如Byte3是0x01、Byte4是0x2C那CO2浓度就是0x012C换算成十进制是300ppm。校验和常见有两种算法一种是Byte1到Byte7之和取低8位另一种是Byte1到Byte7之和取反再加1也就是补码。不同批次或不同固件版本可能有差异这在低价CO2模块里很常见。我写代码的时候干脆把两种校验都兼容了这样无论模块用的哪一种都能通过校验。但这不代表每个型号的帧格式都一样有的版本第5、6字节可能不是温度而是别的数据有的版本帧长也不一定是9字节。最稳妥的做法是拿到新模块先抓一次原始数据确认帧头和浓度字节位置再解析。这也是我下面专门写一节串口抓包的原因。2.3 上板之前先抓包用USB-TTL验证传感器很多新手犯的错误是焊好线就写代码结果屏幕上一直显示0或者乱码然后开始怀疑人生。我的习惯是任何串口传感器到手先不接Arduino直接用USB-TTL转串口模块接电脑打开串口助手看一下原始输出。接线方式和接Arduino一样USB-TTL的TX接传感器的RXUSB-TTL的RX接传感器的TX然后共地、供电。打开串口助手波特率设9600正常情况下每1到2秒能看到一帧数据类似FF 04 00 01 2C 00 00 00 5B这样的十六进制串。如果没有主动上报可以尝试发送一组查询指令FF 01 86 00 00 00 00 00 79部分模块固件兼容这类查询帧收到指令后会回一帧数据。这一步花不了两分钟但能帮你确认三件事模块供电是否正常、波特率是否正确、数据帧格式到底是怎样的。如果串口助手都读不到数据那说明模块本身就有问题就不用费劲去查Arduino代码了。做了这步再往下走你的Arduino代码会写得非常有底气。2.4 接线避坑供电、共地、电平这三件事第一供电。JW01常见版本需要5V供电如果接到Arduino的3.3V上模块可能能亮但输出数据不稳定甚至完全不工作。所以老老实实从Arduino的5V引脚取电。第二共地。如果传感器不是由Arduino直接供电而是用外部电源那传感器的GND和Arduino的GND必须连在一起否则串口信号没有参考电平数据就是乱码。共地算是串口通信里最基础也最容易被忽略的问题。第三电平。Arduino UNO是5V逻辑电平常见JW01版本也是TTL电平所以直连没问题。但如果你后面换ESP32这类3.3V主控就要确认传感器输出电平是否兼容很多模块直接用3.3V也能读到数据但稳妥起见还是加个电平转换模块或者用分压电阻。第四OLED的I2C地址常见是0x3C也有少部分模块是0x3D如果OLED初始化失败先改地址再检查接线。3. Arduino完整代码从串口解析到屏幕显示3.1 开发环境与依赖库准备代码要在Arduino IDE里编译上传。IDE版本用1.8.x或2.x都行2.x也有挺好用的自动补全。需要安装两个库Adafruit SSD1306和Adafruit GFX。打开Arduino IDE菜单栏找到“工具 - 管理库”在搜索框里输入Adafruit SSD1306找到后安装再输入Adafruit GFX同样安装。Adafruit GFX是图形库SSD1306负责驱动OLED两者是搭配使用的。安装完库最好重启一下IDE避免编译时找不到头文件。如果你用的不是Adafruit库而是U8g2库代码会有差异但功能类似。我这篇文章统一用Adafruit库因为相对更常见一些。3.2 代码设计思路和模块划分这个项目的代码可以分成几个模块串口读取函数、校验函数、OLED刷新函数、LED报警逻辑。整个主循环不用delay()卡死而是用millis()控制显示刷新频率这样串口读取始终是及时的不会因为刷新屏幕把传感器数据漏掉。读取串口的思路是先检查缓冲区里是否至少有9个字节然后用peek()看第一个字节是不是0xFF帧头如果是就整帧读出来做校验不是就丢弃当前字节继续找。这样即使串口里混入了干扰字节也能慢慢“爬”回正确位置。校验通过后提取Byte3和Byte4拼成浓度值再判断数值是否在合理范围内0~5000ppm避免把错误数据刷上屏幕。OLED刷新我控制在200毫秒一次肉眼看起来非常流畅又不会让传感器读取被拖累。LED报警逻辑放在最后阈值超过1000ppm就点亮低于1000ppm就熄灭这个数值你可以自己改。3.3 可直接复制的完整Arduino代码以下是完整代码复制粘贴即可用。我在关键位置都加了注释方便对照理解。#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h #include SoftwareSerial.h // JW01 接线定义 #define CO2_RX_PIN 11 // 接 JW01 TX注意这里是软件的RX #define CO2_TX_PIN 10 // 接 JW01 RX #define ALARM_LED_PIN 8 // 报警LED // OLED 尺寸 #define OLED_I2C_ADDR 0x3C #define OLED_WIDTH 128 #define OLED_HEIGHT 64 // 阈值和刷新间隔 #define CO2_THRESHOLD 1000 // 超过这个值点亮LED单位ppm #define DISPLAY_INTERVAL 200 // 屏幕刷新间隔单位ms SoftwareSerial co2Serial(CO2_RX_PIN, CO2_TX_PIN); Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, Wire, -1); uint8_t frame[9]; uint16_t co2PPM 0; bool co2Valid false; unsigned long lastValidMs 0; unsigned long lastDisplayMs 0; void setup() { Serial.begin(9600); // 电脑串口监视器 co2Serial.begin(9600); // JW01传感器串口 pinMode(ALARM_LED_PIN, OUTPUT); digitalWrite(ALARM_LED_PIN, LOW); if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) { Serial.println(F([ERR] OLED init failed, check address 0x3C/0x3D and wiring.)); while (1) delay(10); } showText(CO2 Monitor, warming up...); } void loop() { readCO2Frame(); // 读取并解析串口数据 if (millis() - lastDisplayMs DISPLAY_INTERVAL) { lastDisplayMs millis(); updateDisplay(); } bool isFresh co2Valid (millis() - lastValidMs 6000); bool alarm isFresh (co2PPM CO2_THRESHOLD); digitalWrite(ALARM_LED_PIN, alarm ? HIGH : LOW); } // 从软件串口读取并解析一帧数据 void readCO2Frame() { while (co2Serial.available() 9) { if (co2Serial.peek() ! 0xFF) { co2Serial.read(); // 不是帧头丢弃 continue; } co2Serial.readBytes(frame, 9); if (checkFrame(frame)) { uint16_t v (frame[3] 8) | frame[4]; if (v 0 v 5000) { // 合理性检查 co2PPM v; co2Valid true; lastValidMs millis(); } } } } // 校验帧数据兼容两种常见校验和算法 bool checkFrame(uint8_t f[9]) { if (f[0] ! 0xFF) return false; uint8_t sum 0; for (int i 1; i 8; i) { sum f[i]; } // 方法1和直接取低8位方法2取反再加1 bool crcOk (f[8] sum) || (f[8] (uint8_t)(~sum 1)); return crcOk; } // 刷新OLED显示 void updateDisplay() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(2); display.setCursor(0, 6); display.print(CO2 ppm); display.setTextSize(4); display.setCursor(8, 30); bool isFresh co2Valid (millis() - lastValidMs 6000); if (isFresh) { display.print(co2PPM); } else { display.print(---); } if (isFresh (co2PPM CO2_THRESHOLD)) { display.setTextSize(1); display.setCursor(0, 56); display.print(1000 WARN); } display.display(); } // 两行提示文字 void showText(const char* line1, const char* line2) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(2); display.setCursor(0, 12); display.println(line1); display.setCursor(0, 36); display.setTextSize(1); display.println(line2); display.display(); }3.4 代码关键点说明这几个地方别改错SoftwareSerial co2Serial(11, 10)这一行第一个参数是接收引脚必须接JW01的TX第二个参数是发送引脚接JW01的RX。有人习惯记成“软件串口RX接传感器TX”这没错但最容易搞混的地方是引脚编号和实际接线对不上烧录后一直没数据。如果遇到这种情况第一件事就是检查TX和RX有没有交叉接对。checkFrame()里兼容了两种校验算法这是针对JW01不同固件版本做的妥协。如果你收到的数据一直校验不过可以把校验逻辑临时去掉直接打印出frame[3]和frame[4]看一眼浓度是否合理。不过说实话我碰到的大部分模块用sum直接取低8位就能通过极少遇到取反加一的版本。OLED刷新用了DISPLAY_INTERVAL200不要改太小否则屏幕会有闪烁感也不要太大不然CO2数值更新看起来卡顿。200毫秒是比较舒服的体验。LED报警阈值CO2_THRESHOLD1000是参考室内空气质量标准定的如果你放在会议室或者教室可以考虑调到800甚至更低提前提醒开窗。4. 实测、校准与常见问题排查4.1 首次上电先预热再判断数据我第一次上电时OLED显示了一个很低的数值大概100多ppm我心想这室内空气也太好了吧后来才反应过来传感器还没稳定。CO2传感器尤其是NDIR原理的通电后需要一段时间让红外光源和探测器进入稳定状态常见建议是预热5到15分钟再参考读数。所以代码里开机显示的“warming up...”并不是随便写的是真的需要等。预热之后再怎么看数据是不是正常一个很有效的验证方法对着传感器轻轻吹一口气但别太近保持20厘米左右的距离呼出的CO2浓度高达数万ppm屏幕上数值会瞬间冲到1500到3000ppm。吹完以后数值又会慢慢回落到正常范围。这个过程基本可以确定传感器、接线和代码全链路都通了。需要注意一点别真的把口水喷到传感器上NDIR模块的进气口如果被水汽或者灰尘堵住响应会变慢数值也会漂移。验证完之后把它放在通风正常、没人对着吹气的位置让它好好跑一会儿。前5分钟数值上下波动都是正常的别急着下结论说数据不准。4.2 数据合理性判断哪些数值是正常的传感器稳定后可以按这几个参照来判断数据是否正常。开窗通风状态下室内CO2浓度应该和室外接近一般在400到600ppm之间。关上门窗正常呼吸半小时浓度会缓慢上升到800到1200ppm。如果房间里待了两个人或者卧室开了一整夜空调早上测到1500ppm甚至2000ppm也不奇怪这时候人通常已经觉得闷得不行了。如果你家里有新风系统或者空气净化器带CO2显示可以把两者的数值放在一起对比。品牌设备的传感器经过出厂校准数值可以作为参考两者的差异如果在50到100ppm以内说明JW01工作得相当正常。如果差异很大比如JW01显示800ppm、参考设备显示1400ppm那就要考虑是不是模块摆放位置不同造成的空气不流通或者传感器本身需要校准了。放置位置也很影响读数。不要贴墙安装不要放在暖气片、空调出风口旁边也不要放在窗口气流直吹的地方。传感器需要和室内空气充分接触背后能留一点空隙是最好的。另外人呼出的气流如果直接吹到传感器进气口数值会异常偏高所以放在桌面侧后方比放在面前更接近真实室内浓度。4.3 校准与维护经验什么时候需要动它家用场景下JW01这类模块一般不需要频繁校准。但如果用了大半年以后发现数值系统性偏高或者明显不合理可以试着做一次简单的“背景校准”。方法是把传感器拿到室外通风处让它在新鲜空气中运行10分钟以上然后记录稳定后的数值理论上应该无限接近室外环境浓度约400到450ppm。如果偏差很大就需要查手册看是否支持外部标定指令。很多低价CO2模块出厂时固件里带有自动基线校准但这套算法通常假设传感器每周至少有一次机会接触到新鲜空气比如开窗通风时如果你长期在密闭空调房运行自动校准可能反而不准。这也是为什么我的代码里保留了“最后有效帧时间”的判断一旦传感器长时间掉线屏幕会显示三个横杠不会拿旧值骗你。维护方面倒不用太操心。每隔一两个月用气吹清理一下外壳和进气口就行别用水洗红外传感器模组内部有精密的光学结构沾水基本就报废了。如果你发现响应速度明显变慢先检查是不是进气口被灰尘堵了再考虑传感器老化。4.4 常见问题排查速查表照着表格就能定位问题现象可能原因解决办法串口监视器无任何输出TX/RX接反、供电不足、波特率不对确认交叉接线JW01接5V波特率9600屏幕显示“---”传感器未上报数据、线松、没预热检查接线是否牢固用USB-TTL抓包确认模块有输出OLED不亮或花屏I2C地址不对、SDA/SCL反了换0x3D试试检查A4/A5接线数值一直为0或不变校验未通过、模块固件帧格式不同临时去掉校验打印frame[3]和frame[4]看数值数值明显偏高模块离出风口太近、被呼吸气流直吹挪到空气流通但无直吹的位置数值开机乱跳传感器未充分预热稳定运行15分钟后再判断有数据但刷新很慢每次读数间隔较长确认模块是否为主动上报模式或尝试发查询指令排查的时候优先从物理层开始供电、共地、TX/RX这三项占故障原因的一大半。串口通信最怕的就是“感觉都接对了”结果问题出在电平不匹配或者共地没做好。用Arduino的5V和GND给传感器供电是最省心的方案外部供电务必记得共地。5. 玩法延伸从桌面仪表变成智能联动装置5.1 加继电器联动排风扇自动通风怎么实现OLED显示CO2只是第一步更实用的玩法是让它自动控制排风扇或者新风机。思路很简单CO2浓度超过某个阈值就打开继电器浓度降下来再关闭。但有一点必须提醒继电器控制220V强电有风险新手建议先用5V小风扇做演示熟悉了原理再考虑强电设备。代码上就是在主循环里加了一个变量控制一个继电器引脚。为了避免阈值附近频繁开关我加了两档滞回逻辑浓度升到1200ppm以上才开启降到1000ppm以下再关闭中间这段“死区”能有效防止继电器不停跳动。#define RELAY_PIN 7 bool relayOn false; void loopRelayControl() { if (!relayOn co2PPM 1200) { digitalWrite(RELAY_PIN, HIGH); relayOn true; } else if (relayOn co2PPM 1000) { digitalWrite(RELAY_PIN, LOW); relayOn false; } }把这个函数加在主循环里每次读完传感器就调用一次。如果你用的是低电平触发继电器模块记得把HIGH和LOW反过来具体看继电器模块的说明书。控制220V设备时继电器必须选有安全标识的正规产品电路做好绝缘不建议新手在没有指导的情况下直接上手强电。5.2 用ESP32或ESP8266上报到手机Arduino UNO串口监视器只能在电脑上看想把数据发到手机上思路有两个。一个是在UNO上用Serial.println(co2PPM)把解析好的数值发到默认串口然后让ESP8266的串口去读ESP8266再把数据通过Wi-Fi上报到手机。另一个更直接的方案是绕开UNO直接用ESP32的硬件串口去读JW01然后把数据推到MQTT或HTTP服务器上。用ESP32的时候要注意一个工程问题JW01常见版本是5V供电而ESP32的GPIO只有3.3V耐压。如果不做电平转换长期使用可能损坏ESP32的引脚。最简单的做法是给传感器RX线路接一个分压电阻比如串联1kΩ再接一个2kΩ到地把5V电平降到3.3V左右。TX方向的输出相对安全但最好也确认一下模块是否兼容3.3V逻辑。如果你的ESP32开发板带有5V输出引脚供电没问题。代码逻辑和Arduino版本几乎一样只是把SoftwareSerial换成Serial1或Serial2这样读取更稳定而且不占用GPIO模拟串口。数据上报到手机以后你还可以搭配定时记录、历史曲线、远程开窗这些更高级的功能那就是一个完整的中控系统了。5.3 更多可玩方向串口绘图、数据记录、语音提醒想先不联网就看数据趋势Arduino IDE自带的串口绘图器Serial Plotter就够用。把代码里改成只向Serial打印co2PPM然后打开工具里的串口绘图器就能看到实时曲线方便观察开窗前后浓度变化。这个方法很适合做对比实验比如测试关窗多久CO2会超标。想要长期记录数据可以给Arduino加一个Micro SD卡模块定时把CO2浓度、时间戳写进CSV文件。有了历史数据就能分析晚上卧室CO2是不是超标、白天办公室哪个时段最闷。再进阶一点还可以把OLED换成彩色TFT屏在一个界面上同时显示CO2、温度和湿度搭配DHT11或DHT20传感器就能扩展成完整的环境监测站。我个人这两周用下来最深的体会是串口协议真没想象中难一旦理解了“帧头 数据域 校验位”这个套路遇到任何UART传感器都能迅速上手。最后再分享一个小技巧新模块到手一定先接USB-TTL抓包看清楚每一帧长什么样再写代码这个习惯能帮你省掉90%的排查时间。这个项目跑通之后下一步可以尝试把CO2数据接入家里的小爱同学或者Home Assistant玩法会一下子打开很多。
返回列表