
1. 为什么这个项目值得做CO2监测的底层逻辑1.1 一个被低估的环境指标如果你和我一样平时捣鼓Arduino十有八九是在玩LED、舵机、超声波测距突然有一天要做一个环境监测系统大概率第一个想到的是温湿度传感器。这没错但等你真正把DHT11接上去、屏幕上能显示温度和湿度之后你会发现一个问题这套系统根本回答不了这屋子现在闷不闷、适不适合长时间待着。二氧化碳浓度才是那个真正决定室内舒适度、甚至影响注意力和睡眠质量的指标。密闭卧室睡一晚上CO2浓度轻松飙到2000ppm以上人会头昏脑涨办公区下午两三点人一多、窗户一关900到1200ppm是常事。很多写字楼里所谓的空气质量差与其说是PM2.5的问题不如说根本就是CO2超标。之前我做过一个JYW01有些商家标JW01模块的室内监测项目从接线到跑通demo只花了不到五分钟但深入之后发现里面的门道远比想象的多NDIR测量原理、UART帧解析、传感器校准、数据漂移每一个都值得单独拎出来讲清楚。这篇文章把这套东西完整记录下来适合刚接触Arduino的新手也适合那些准备把传感器真正用起来的进阶玩家。1.2 JW01传感器的工作原理解密市面上卖的JW01模块绝大多数是NDIR原理的CO2传感器。NDIR全称Non-Dispersive InfraRed翻译过来叫非色散红外核心组件就三样一个红外光源、一个空气可以自由进出的气室、一个只让特定波长红外光通过的滤光片加探测器。CO2分子会强烈吸收4.26微米波长的红外光。光源发出的红外线穿过气室时有一部分会被中间的CO2吃掉探测器看到的光强就变弱了。浓度越高被吃掉的光越多传感器再把光强衰减量换算成ppm数值。你可以把它理解成隔着雾看路灯雾越浓灯光越模糊CO2就是那团雾。NDIR的好处是抗干扰能力强、寿命比较长不会像电化学传感器那样用两年就需要换敏感电极。1.3 先搞清楚你手里的JW01是哪个版本这是最容易踩坑的一步。JW01模块市面上存在好几个版本外观长得差不多但输出接口完全不同。我见过的至少有三种UART串口版模块上带TX、RX引脚按固定协议输出数字信号精度最高也是本文主要讲解的方案。PWM版通过高电平脉冲宽度来表示浓度值周期通常是1004ms需要测脉宽换算。模拟电压版带AO引脚输出电压范围一般是0.4V到2V对应0到5000ppm接Arduino的模拟口读取。版本判断方法很简单看丝印。模块背面或者说明书上会明确标注UARTPWM或Analog Output。如果你手上只有裸板没有说明书还有个土办法上电后用万用表量AO引脚如果有的电压电压会随着你朝传感器哈气而明显升高那就是模拟版如果板上只有四根针且标注了TX、RX那就是UART版。这一步一定不要省很多新手照着别人的接线图接完发现读数全是0最后才发现是版本不对。下面所有内容默认你用的是UART版如果你手里是PWM或模拟版可以直接跳到3.4节看备用方案。2. 硬件准备与接线动手前的最后一步检查2.1 材料清单与选型建议做这套环境监测系统核心硬件非常简单我列个清单都是手边常备的东西器件型号/规格数量说明主控板Arduino Uno或其他兼容板1Nano也可以接线逻辑一样CO2传感器JW01 UART版1注意版本确认是串口输出面包板400孔或630孔均可1方便临时接线杜邦线公对公、公对母各若干若干建议备不同颜色方便区分USB线A转B方口线Uno用1顺便给板子供电可选OLED屏、LED、蜂鸣器、继电器若干进阶联动用后面会讲选型建议如果预算允许优先买带UART输出的版本因为串口数据可以读到精确的整数ppm调试时也能直接看到原始字节流学习价值高。模拟版的换算受电源电压波动影响比较大精度差一些但胜在接线简单应急时可用。2.2 针脚定义与接线表JW01 UART版模块引脚一般就四个有些会额外引出VCC、GND双电源引脚功能一样VCC、GND、TX、RX。接线表如下JW01引脚接Arduino引脚VCC5VGNDGNDTX数字引脚10软串口RXRX数字引脚11软串口TX可选AOA0仅模拟版需要这里有个容易翻车的点为什么不把传感器的TX直接接Arduino的0号引脚硬串口RX理论上可以而且接线更省心。但Arduino Uno的0号和1号引脚同时连接着板载USB转串口芯片你烧录程序时USB串口和传感器会抢这两根线经常导致上传失败或者烧录过程中传感器数据把编程器搞懵。我建议宁可多用两个数字口也不要去碰硬件串口图个清静。另一个细节是电平匹配。JW01模块大多设计为5V TTL电平和Uno的5V系统直接对接没问题。如果你用的是3.3V版本的模块有些低功耗版支持3.3V供电接5V供电的Uno时需要做电平转换或者干脆换一块ESP32、STM32那种原生3.3V主控否则长期运行有烧坏RX引脚的风险。2.3 给传感器的3秒预热与上电观察接线完成后先别急着插USB线养成一个习惯上电前再核对一遍VCC和GND。反接电源是传感器第一杀手炸了只能再买一块。通电之后观察模块上有没有指示灯亮起用手靠近传感器通风格栅等一两分钟。第一次上电时传感器内部的红外光源需要预热稳定这时候串口输出的数值往往会偏高甚至达到800到1000ppm。别慌这不是坏了是没稳定。等它跑个三五分钟数值会慢慢回落最终稳定在400到600ppm附近——这个范围正好是大气中CO2的正常浓度。怎么确认它开始正常输出数据了最简单的验证方法先打开Arduino IDE自带的串口监视器把波特率设置成9600下文代码里会说明为什么是9600如果能看到规律出现的一堆十六进制字符比如\xFF\x01\x86...这样开头的内容恭喜你传感器在喊话了。如果什么都看不到大概率是TX、RX接反了把它俩对调再试一次。3. 完整代码从串口字节流到可读的ppm3.1 两种数据输出模式主动上报 vs 查询应答接好线之后你要先确认手里的传感器是哪种工作模式因为代码逻辑完全不同。JW01这类NDIR模块常见的串口模式有两种主动上报模式传感器上电后自发地每隔大概2秒向外发送一帧数据不需要主控做任何操作你只要蹲在串口上收就行。查询应答模式传感器平时不吭声你给它发一条指定的查询指令它才回你一帧数据。这种方式的好处是主控可以按自己的节奏采集不会错过数据也不会有堆积。怎么判断通电后打开串口监视器如果不需要发任何字节就能看到数据帧那就是主动上报如果一片空白发一条查询指令才有反应那就是查询应答。下面代码我会把两种模式都写出来你根据实际测试结果二选一即可。3.2 UART模式完整代码附逐行注释先上查询应答模式的完整代码这是最稳妥的方案。我用的是SoftwareSerial软串口把传感器挂在10、11号引脚上#include SoftwareSerial.h // 定义软串口引脚 #define CO2_RX 10 // 接传感器的TX #define CO2_TX 11 // 接传感器的RX SoftwareSerial co2Serial(CO2_RX, CO2_TX); // 查询指令。很多NDIR二氧化碳模块都用这一条标准指令 byte queryCmd[9] {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; byte buf[9]; unsigned long lastQueryTime 0; const unsigned long queryInterval 2000; // 每2秒查询一次 void setup() { Serial.begin(9600); // 硬件串口接电脑用于调试输出 co2Serial.begin(9600); // 软串口接JW01传感器 Serial.println(JW01 CO2 Sensor Demo Start); } void loop() { // 到时间了就发一次查询指令 if (millis() - lastQueryTime queryInterval) { lastQueryTime millis(); co2Serial.write(queryCmd, 9); } // 检查软串口缓冲区凑齐9个字节再处理 if (co2Serial.available() 9) { for (int i 0; i 9; i) { buf[i] co2Serial.read(); } // 帧头校验FF 01 86这是标准CO2应答帧的标志 if (buf[0] 0xFF buf[1] 0x01 buf[2] 0x86) { int co2 (buf[3] 8) | buf[4]; // 高字节左移8位再或上低字节 // 校验和验证从第2个字节累加到第8个字节取反加一 byte sum 0; for (int i 1; i 8; i) { sum buf[i]; } sum (~sum) 1; if (sum buf[8]) { Serial.print(CO2: ); Serial.print(co2); Serial.println( ppm); } else { Serial.println(Checksum error); } } } }如果你测试发现传感器是主动上报模式那更简单把setup里的串口初始化保留然后把loop整个换成下面这段void loop() { if (co2Serial.available() 9) { for (int i 0; i 9; i) { buf[i] co2Serial.read(); } if (buf[0] 0xFF buf[1] 0x01 buf[2] 0x86) { int co2 (buf[3] 8) | buf[4]; Serial.print(CO2: ); Serial.print(co2); Serial.println( ppm); } } }注意这里我没有校验校验和因为主动上报模式下数据量不大主控闲着也是闲着建议还是把校验逻辑加上防止误码导致显示异常数值。3.3 十六进制解析原理为什么CO2是两个字节很多新手看到(buf[3] 8) | buf[4]这行代码会懵为什么ppm值要拆成两个字节因为CO2量程通常是0到5000ppm甚至到10000ppm一个字节最多只能表示0到255一个byte根本装不下所以协议设计者用两个字节来装浓度高字节放在第4个字节位置低字节放在第5个字节位置。换算方法就是代码里那行高字节左移8位相当于乘以256再按位或上低字节。比如高字节是0x02十进制2低字节是0x58十进制88结果就是2×25688600也就是600ppm。校验和的作用是防止传输过程中数据被干扰破坏。JW01这类模块常见的校验算法是从第二个字节0x01开始到第八个字节结束把这7个字节累加然后取反再加一。如果传感器端生成的校验字节和主控这边算出来的结果一致说明这一帧数据在传输过程中没被破坏不一致就果断丢掉不要显示否则你会看到偶尔蹦出来的几百几千的离谱读数。3.4 模拟输出模式代码备用方案如果你拿到手的模块是模拟电压版没有串口代码更短#define AO_PIN A0 void setup() { Serial.begin(9600); } void loop() { int raw analogRead(AO_PIN); // 读取0~1023的原始值 float voltage raw * (5.0 / 1023.0); // 换算成实际电压 // 假设0.4V对应0ppm2V对应5000ppm按线性关系换算 float co2 (voltage - 0.4) * 5000.0 / (2.0 - 0.4); if (co2 0) co2 0; Serial.print(CO2: ); Serial.print(co2); Serial.println( ppm); delay(2000); }模拟版最大的问题就是精度受限。Arduino的analogRead分辨率只有10位也就是0到1023对应0到5V每一步的电压分辨率大约5毫伏换算成CO2大约是15ppm。看着还行但电源电压稍微波动一下读数就开始跳了。所以我只把它当备用方案有条件还是建议用UART版。4. 从读数到系统可视化与联动控制4.1 加一块OLED屏幕显示实时CO2浓度串口监视器里的数字只有插着电脑才能看到日常使用总得有个屏幕。我常用的方案是一块0.96寸I2C接口的SSD1306 OLED屏128×64分辨率四根线接上就能用。接线OLED的VCC接5V部分版本是3.3V注意看模块背面丝印GND接GNDSCL接Arduino的A5SDA接A4。Uno的I2C引脚固定在这两个位置上别接错了。库文件用Adafruit SSD1306和Adafruit GFXArduino库管理器里搜索即可安装。显示代码的核心部分#include Wire.h #include Adafruit_SSD1306.h #include Adafruit_GFX.h Adafruit_SSD1306 display(128, 64, Wire, -1); void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // 大多数OLED地址是0x3C display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); } void displayCO2(int co2) { display.clearDisplay(); display.setCursor(10, 20); display.print(CO2:); display.print(co2); display.println(ppm); display.display(); }在之前的loop代码里拿到有效的co2值后调用displayCO2(co2)即可。如果显示不出来先跑一遍I2C地址扫描程序确认设备地址到底是0x3C还是0x3D不要盲目按网上教程硬试。4.2 设置两级阈值提醒小心与强制通风有屏幕还不够真正实用的环境监测系统必须有告警机制。我建议分两级阈值小于800ppm算良好800到1200ppm算临界超过1200ppm算超标。为什么选1200这个数室内空气质量标准GB/T 18883里推荐的CO2日平均浓度上限是1000ppm但实际测量中办公室下午经常到1200甚至更高考虑到一般居住场景1200作为告警线比较合理没那么容易误报。代码逻辑很简单在解析出co2后加判断#define GREEN_LED 5 #define YELLOW_LED 6 #define RED_LED 7 #define BUZZER 8 void setAlert(int co2) { if (co2 800) { digitalWrite(GREEN_LED, HIGH); digitalWrite(YELLOW_LED, LOW); digitalWrite(RED_LED, LOW); digitalWrite(BUZZER, LOW); } else if (co2 1200) { digitalWrite(GREEN_LED, LOW); digitalWrite(YELLOW_LED, HIGH); digitalWrite(RED_LED, LOW); digitalWrite(BUZZER, LOW); } else { digitalWrite(GREEN_LED, LOW); digitalWrite(YELLOW_LED, LOW); digitalWrite(RED_LED, HIGH); digitalWrite(BUZZER, HIGH); } }蜂鸣器如果一直响会非常烦人实用做法是加一个响两秒停十秒的间歇逻辑用一个变量记录上次响的时间在loop里判断。这个留给你自己扩展思路和串口查询定时是一样的。4.3 与继电器联动控制新风系统再进一步把CO2读数和继电器结合就能实现CO2超标自动开新风的效果。继电器模块的接线是IN引脚接Arduino数字口VCC和GND接5V和GND输出侧接你家的新风风扇或者排气扇。代码只需要在setAlert的第三档里把digitalWrite(RELAY, HIGH)和LOW加上即可。这里必须强调一句继电器输出侧如果是220V强电操作前务必断电接线要用电工胶带做好绝缘并且不要用面包板来过渡强电线整个项目建议只控制低压直流风扇。我做实验用的是12V的电脑散热风扇接了一个30A的固态继电器既安全又能模拟真实风机的开关逻辑。如果你完全没接触过强电就不要碰220V了用低压方案验证逻辑就好。5. 实测结果与数据解读你的传感器在说谎吗5.1 拿到数据后我做的第一件事户外校准很多人的流程是接线、烧代码、看数字、发朋友圈完事。但真正做过几次测量之后你会发现同一块传感器今天在户外读数是430ppm过两天却变成520ppm数值明显漂了。这是因为NDIR传感器的零点会受温度、器件老化和气室污染影响出厂标定只能保证全新状态下是准的。所以传感器到手如果条件允许先把它拿到室外通风处没风的日子阳台也行上电稳定至少10分钟记录当前读数。如果读数是400到500ppm之间说明传感器状态正常不需要额外校准。如果读数明显偏高或偏低比如持续显示600ppm以上可能需要做一次零点校准。不同的模块校准方式不一样常见的有两种一是模块板上有一个小按键长按几秒进入校准模式二是通过串口发送特定的零点校准指令。具体操作一定要以你买的模块说明书为准不要照搬我这里的通用说法因为厂家不同指令差异很大。有一个更重要的知识点必须提醒很多NDIR模块带ABC自动校准功能Automatic Baseline Correction它的逻辑是让传感器每24小时自动找一次最低值作为零点。这个功能在正常通风环境下很省心但如果你的传感器长期放在密闭房间里房间里的最低CO2浓度可能也有1000ppmABC就会把1000当成本底零点修正掉这样你的真实读数是1500屏幕上却只显示500。怎么避开如果你要长期监测密闭环境关掉ABC或者在每周固定时间把传感器拿到户外通一次风让它重新校准。很多商家不会在详情页里提这个坑但实际影响很大。5.2 一组真实的室内实测数据为了让你对CO2数值有个直观概念我放一组我自己实测的数据用的是校准后的JW01测量间隔1分钟不同场景下的典型值测试场景CO2浓度备注户外公园微风420-480 ppm可作为校准基准办公室工位早晨开窗550-650 ppm刚通风完比较理想办公室下午8人长期关窗1100-1500 ppm下午两三点浓度最高卧室关窗睡一晚1800-2600 ppm两个人睡一间更夸张厨房做饭门窗紧闭2000-3000 ppm燃气燃烧会产生大量CO2室内开新风/风扇10分钟后600-800 ppm降幅非常明显看到这些数据你就会明白室内CO2超标不是理论问题而是每天都在发生的现实。我做完这套系统后第一件事就是把卧室的通风策略改了睡前窗户留条缝早晨起来头昏的情况基本消失。5.3 数值异常的排查思路如果你跑起来之后发现读数不正常别急着怀疑传感器坏了按这个清单一项项排查数值一直是0或者固定不变大概率是TX、RX接反或软串口引脚定义写反了。检查SoftwareSerial co2Serial(CO2_RX, CO2_TX)这行第一个参数是Arduino的RX引脚必须接传感器的TX。数值跳变剧烈比如从600突然跳到3000又跳回700检查供电电压是否稳定试着用独立5V电源给传感器供电同时确认模块底部有没有短路虚焊。数值比正常高200到400ppm且长期不下降参考5.1节做一次户外校准或ABC自动校准。串口监视器能看到乱码但解析不出来波特率或帧头不对确认你的模块协议是不是FF 01 86开头有些模块帧头不同需要看手册改代码。刚上电的半小时数值一直在缓慢爬升这是传感器在预热和自校准属于正常现象等数值稳定后再记录。6. 避坑实录我在JW01项目里踩过的四个坑6.1 坑一SoftwareSerial和USB串口打架第一次做的时候我图省事把传感器的TX直接接到了Uno的0号引脚上结果上传代码时一直报错报错信息里还夹杂着一些乱码。查了半天才反应过来0号引脚同时连着板载的USB转串口芯片软件串口数据直接灌进了USB串口里把上传流程冲垮了。解决办法有两条一是全程用10、11号这种普通数字口做软串口二是如果你必须用硬件串口比如传感器数据量大需要稳定接收那就换Mega这类有多个硬件串口的板子把传感器接在Serial1上把USB调试留在Serial。我现在做项目基本都是这个思路Uno跑跑简单demo可以稍微复杂一点就换板。6.2 坑二电源不稳导致读数漂移有段时间传感器读数总在550到700之间周期性摆动一开始以为是传感器质量问题后来用万用表量了一下Arduino的5V输出发现电压在4.85V到4.95V之间浮动。传感器内部红外光源的工作电流大瞬时波动直接影响探测精度读数自然跟着跳。后来我把传感器从Arduino的5V引脚挪到一个独立5V电源模块上GND还是和Arduino共地读数立刻稳多了。如果你不想多加一个电源至少检查一下USB供电的质量尽量插在电脑主板USB口或者质量好一点的充电器上别用那种接出来一堆线、带好几个外设的USB HUB。这个坑在新手项目里极其常见很多人最后怀疑传感器坏了其实是电源先不行。6.3 坑三传感器贴在墙角测了个寂寞第一次做固定安装时我把传感器粘在墙角一个书柜后面理由是走线方便、不挡害。结果读数显示室内CO2永远是400到500ppm身处房间里的我明显觉得闷却看不到数据上涨。原因很简单墙角空气流通差人呼出的CO2主要积聚在房间中部和上半部藏在书柜后面的传感器根本接触不到新鲜空气样本。正确做法是把传感器放在离地面0.8到1.5米高度、靠近人员活动区域但不会被直吹到的地方。你可以用一个USB延长线底座把传感器放在桌面上尽量避免贴墙、贴地、放在空调出风口正下方、被窗帘遮挡这四种位置。同样的硬件摆位不同读数天差地别。6.4 坑四校验和到底怎么算有次我手写代码时把校验和计算写成了对全部9个字节累加结果无论怎么算都和传感器返回的校验字节对不上一度怀疑传感器坏了。后来翻协议才发现校验和是从第二个字节开始算到第八个字节第一个帧头0xFF不参与累加最后一个校验字节自己也不算进去。正确算法我再说一遍取buf[1]到buf[7]共7个字节累加到一个byte变量里让它自然溢出然后取反加一得到的值和buf[8]比对。这里要注意很多语言中byte溢出是自动截断的但如果你用的是int类型累加就要手动 0xFF否则校验值永远不对。这类帧头校验错误不会导致程序崩溃但会让你百思不得其解。6.5 几个零碎但关键的小贴士拔插传感器的串口线之前最好先把Arduino断电。带电拔插虽然大概率没事但静电和瞬态电压容易把模块引脚打坏我已经损失过一块板子了。如果模块支持ABC自动校准又需要长期密闭监测记得去查手册关掉它或者定期户外通风校准否则数据会被人为压平。买模块时尽量选有说明书或数据手册的店铺哪怕多花十块钱。省钱的代价是你可能需要花一晚上逆向协议而协议里的帧头、校验方式、量程这些参数没有文档只能靠猜。软件串口在9600波特率下传9个字节耗时大约9.4毫秒完全够用。但如果你同时跑OLED刷新、继电器开关等操作要注意别在读取数据帧的过程中做耗时操作否则会丢字节导致每次都要等下一个2秒周期才能重新凑齐一帧。按这个流程走一遍从拆快递到串口监视器里看到稳定的ppm数值确实用不了5分钟。但真正让这套系统从能显示数字变成有用的环境监测工具靠的是后续的校准、摆位和联动设计。我个人建议新手别急着上220V继电器那一套先把UART解析、校验和、OLED显示跑通理解每一个字节的含义再考虑联动也不迟。这套代码和协议思路换到MH-Z19、SenseAir S8这些其他主流NDIR模块上也基本通用最大的区别也就是查询指令和量程参数不同。后面你还可以把数据通过ESP32传到本地网络做历史曲线和远程推送那又是另一个值得折腾的项目了。