手头的Arduino Uno套件里,十有八九躺着一块蓝色的小模块,丝印写着MQ-135。你问它有什么用,网上资料一大堆,但绝大多数教程只是让你读一个串口数值,看灯亮灯灭,看完等于没看。我从最早按教程接线的第一次,到后来把它塞进一个长期跑数据的小项目,前前后后踩了不少坑,也总算把这颗十几块钱的传感器脾气摸得比较透。这篇实验笔记(正好是我这套Uno实验里的第15个)就把Arduino Uno和MQ-135这组经典组合从头到尾捋一遍:它到底能测什么、内部怎么工作、怎么接线、怎么写稳定的读值代码、怎么做校准、怎么把电阻比换算成PPM浓度,最后一节是实测里真正遇到的故障与对策。你完全可以照着步骤复现,也可以直接跳到对应的坑位对照排查。
1. MQ-135到底能测什么:先别急着接线
1.1 一份气体清单和“广谱”的真正含义
MQ-135的说明书写得很热闹:能检测氨气、氮氧化物、酒精、苯系物、烟雾、二氧化碳。但这里有一个必须先讲清楚的概念——它属于广谱型气敏元件,不是单一气体传感器。也就是说,它对上面这些气体都有响应,但没有选择性。如果房间里同时存在酒精和氨气,你拿到的读数是一个混合后的综合结果,没办法拆开说是哪种气体贡献了多少。
这一点决定了它的真实定位是“空气污染程度指示器”,而不是“某某气体浓度计”。你可以拿它判断“当前空气比刚才差了”或者“某个污染源靠近了”,但别指望它像实验室仪器一样告诉你精确到个位的ppm浓度。
1.2 最常见的误用场景
很多初学者把它当成CO2传感器。严格来说,CO2只是它“沾边”的目标之一,而且它对CO2的灵敏度远不如对氨气、酒精那么高。用它做室内通风提醒,看一个粗粒度趋势,判断“该不该开窗”,问题不大;但要写入论文或者对接精密环境监测系统,就得慎重了。
它的经典应用场景其实是氨气检测。农业养殖场里,鸡鸭棚舍粪便发酵会产生大量氨气,用MQ-135做通风联动是社区里非常成熟的做法。其次是烟雾报警的辅助判断——注意是“辅助”,真正做火警系统还是要靠光电式烟雾传感器。下表是我结合datasheet整理的常用目标气体参考范围:
| 目标气体 | 典型检测范围 | 响应灵敏度 | 典型用途 |
|---|---|---|---|
| 氨气 NH3 | 10 ~ 300 ppm | 高 | 养殖场、冷库 |
| 氮氧化物 NOx | 10 ~ 1000 ppm | 高 | 尾气环境监测 |
| 酒精 C2H5OH | 10 ~ 300 ppm | 中高 | 酒精泄漏/气体实验 |
| 苯系物 | 10 ~ 1000 ppm | 中 | 装修污染粗测 |
| 二氧化碳 CO2 | 350 ~ 10000 ppm | 低 | 通风趋势参考 |
| 烟雾 | - | 中 | 烟雾预警辅助 |
1.3 为什么“能力边界”必须先讲
因为后边所有的校准和公式换算,都依附于同一个前提:你想测的到底是哪一种气体。选错了假设,公式算出来的浓度能差出一个数量级,这不是精度问题,是方法论问题。我在一开始就花了整整一段说边界,就是希望你先确定项目目标,再决定用哪种校准方法和换算公式。
2. 二氧化锡敏感层与电压分压:读懂模块的“内功”
2.1 核心原理:表面吸附反应改变电阻
MQ-135的心脏是一块二氧化锡(SnO2)厚膜。它被加热到两三百度以后,表面会吸附空气中的氧气分子,氧分子从半导体材料夺取自由电子,形成氧负离子吸附态,这时候敏感层表面出现一个势垒,整体电阻处于一个较高的水平。
当还原性气体(氨气、酒精、一氧化碳这类)凑过来,它们会和吸附的氧发生反应,把电子“还”回去,势垒降低,电阻随之下降。气体浓度越高,参与反应的分子越多,电阻降得越狠。一句话概括:气体分子扮演了电子搬运工,间接改变了半导体电阻。这个电阻变化量,就是整个传感器输出的原始信号。
2.2 加热器、传感器电阻和负载电阻
SnO2敏感层必须在高温下才灵敏,所以传感器内部封装了一根镍铬合金加热丝,通电后发热。这就是为什么模块一上电就会微微发烫——那真不是故障,是工作状态。手册给的数据是:加热电阻约31.7Ω±3.3Ω,5V供电时加热功率小于0.8W,电流大概150mA左右。这组数字在你以后做低功耗项目时非常重要,它意味着MQ-135不适合电池长期供电,老老实实插电使用就好。
传感器本体在电路上表现为一个可变电阻,记作Rs。模块上还有一个固定电阻,叫负载电阻RL(常见1kΩ或10kΩ,不同厂家板子不一样,也有设计成20k~47kΩ的)。Rs和RL串联,电源从上往下分压,中间抽头接到模拟输出口AO。气体来了,Rs变小,RL两端分到的电压就变大,AO输出的电压也跟着变大。整个过程就是一个再简单不过的电阻分压电路。
2.3 模块上LM393比较器和DO口的真相
再仔细看模块,除了传感器,还有一个LM393双比较器。它的作用是把AO的模拟电压和一个阈值电压做比较,输出一个数字电平。阈值由板上的蓝色电位器调节。所以你手里的模块其实同时给了两路信号:AO是模拟量,用来精确读值;DO是数字量,用来做简单报警。
这里顺便纠正一个流传很广的误解:有人说“MQ-135模块只能读数字没法读模拟”,这是不对的。模块上标着AO的焊盘一直都在,只是很多教程从头到尾只用DO,没提AO而已。
2.4 电压方向别记反
分压公式是:Vout = VCC × RL / (Rs + RL)。气体浓度升高 → Rs下降 → Vout上升 → Arduino模拟口读到的ADC数值变大。反过来,干净空气里Rs大,Vout就小,ADC值也小。电阻越大 → 电压越小 → ADC值越小,这个方向关系建议拿记号笔写在模块背面,省得调试时绕晕。
3. 接线与模块选型:模拟口、数字口、电位器一次说清
3.1 四根线的标准接线
MQ-135成品模块通常是四针:VCC、GND、AO、DO(部分板子还把A、B焊盘引出来)。和Arduino Uno的接线如下:
| MQ-135引脚 | Arduino Uno引脚 | 说明 |
|---|---|---|
| VCC | 5V | 给加热丝和比较器供电,不要接3.3V |
| GND | GND | 和Uno共地 |
| AO | A0 | 模拟输出,接模拟输入 |
| DO | D8 | 数字输出,可选,接数字输入 |
接线顺序建议:先接GND,再接VCC,最后接信号线。拆的时候反过来,先拔信号线。养成这个习惯,能少烧很多传感器。
3.2 供电问题:别让USB口硬扛
前面算过,MQ-135加热电流约150mA,加上Uno板子本身和可能的显示屏、WiFi扩展板,USB口的500mA就会变得非常紧张。这种情况下读数会莫名其妙地跳动,甚至系统重启。建议直接给Uno插9V/12V适配器,或者用一个5V/2A的稳压电源从5V引脚供电。做任何测量项目,电源余量永远要往宽里算。
3.3 裸传感器和成品模块怎么选
市面上有两种形态:一种是六针的裸传感器,只有加热丝引脚H和敏感层引脚A/B,需要自己焊负载电阻;另一种是带LM393的成品模块,插上就能用。新手建议买成品模块,省掉焊接和分压计算的麻烦。如果以后做产品,用裸传感器+自己设计分压电路反而更可控,因为板载RL固定了,想改灵敏度没得选。
裸传感器接线有个细节要特别注意:加热丝引脚和敏感层引脚不能接反。H脚接5V和GND,A端接VCC,B端经过RL到GND,从B端取样。接反轻则数值不对,重则直接烧掉敏感层。
3.4 电位器到底拧哪里,怎么验证
DO口接的是LM393比较器,电位器设定阈值。拧的方向会影响触发灵敏度:把一个酒精棉球靠近传感器,用万用表测DO对地电压,正常会看到电平翻转。如果是高电平报警还是低电平报警,取决于模块上LED接法,不同板子可能相反,以实测为准。
这里也分享一个我在课堂上常用的验证方法:不点火,只用打火机出气口在传感器旁边按一下,或者拿酒精棉球晃两下,观察DO和LED有没有反应。这比点明火安全得多,也足够验证传感器是否存活。测试完别忘了开窗通风。
4. 串口读值实验:从最小程序到稳定滑动平均
4.1 最小读值程序
接线完成后,先把下面的代码烧进去。这是整个实验的起点:看原始数据。
const int gasPin = A0; // 传感器AO接A0 void setup() { Serial.begin(9600); } void loop() { int raw = analogRead(gasPin); // 返回0~1023 float voltage = raw * (5.0F / 1023.0F); // 换算为电压 Serial.print("raw = "); Serial.print(raw); Serial.print(" , voltage = "); Serial.print(voltage, 3); Serial.println(" V"); delay(500); }打开串口监视器,波特率选9600,你会看到一行一行刷新。analogRead返回的是0到1023的整数,对应0V到5V。如果你用万用表实测过5V引脚,发现实际只有4.8V,可以把代码里的5.0F改成实际值,精度会更好一点。
4.2 为什么刚开机读数会“飘”
刚上电的前几分钟,数值经常忽高忽低,甚至一路爬升或者一路下跌。这非常正常:加热丝要把敏感层从室温加热到工作温度,表面吸附反应要慢慢建立平衡,这个过程少说也要几分钟。刚通电5分钟内的读数不要采信。如果传感器在仓库里放了一两个月,第一次上电的稳定时间可能长达数小时,需要有点耐心。
我有一次做长时间记录,开机头十分钟的曲线简直像过山车,差点以为是接线错误。后来在项目里加了一个“预热倒计时”逻辑:上电前5分钟只显示预热状态,不参与任何报警判断,这个问题就再没困扰过我。
4.3 用滑动平均压住抖动
就算预热完成,模拟口读数还是会有小幅抖动,因为传感器本身就是个化学反应过程,不是理想的线性电阻。最简单的处理办法是滑动平均:维护一个固定长度的窗口,每次采样把最老的值踢出去,把新值加进来再求平均。
const int gasPin = A0; const int WINDOW = 10; // 窗口长度,数值越大越平滑,响应越慢 int buffer[WINDOW]; int index = 0; long sum = 0; void setup() { Serial.begin(9600); memset(buffer, 0, sizeof(buffer)); } void loop() { sum -= buffer[index]; buffer[index] = analogRead(gasPin); sum += buffer[index]; index = (index + 1) % WINDOW; int avg = sum / WINDOW; Serial.println(avg); delay(100); }注意窗口长度是个折中:窗口越长越平滑,但响应越迟钝。做报警项目的时候,如果窗口设成50,传感器已经冲到报警值了,你的系统可能还要好几秒才反应过来。我的经验是10到20这个区间比较舒服。
4.4 串口绘图器比监视器好用得多
Arduino IDE从1.6.7版本开始内置了一个串口绘图器(Serial Plotter),位置在“工具”菜单里。它会把一行一个的数字画成实时曲线,比盯着串口监视器里的数字跳来跳去直观一百倍。
测试传感器有没有反应最直接的步骤:先把窗口打开,曲线稳定成一条水平线;然后拿一个酒精棉球从20厘米外缓缓靠近传感器,你会看到曲线迅速往下掉(电阻变小,ADC数值变小——注意这个方向和你想的可能相反);拿开棉球,曲线慢慢爬回原来的水平线。能画出这个“下跌再回升”的过程,就说明传感器和接线都没问题。
5. R0校准:决定所有换算准确度的关键一步
5.1 为什么不能抄别人代码里的R0
网上很多MQ-135教程最后都有PPM换算公式,里面必备一个被称为“校准基准”的R0。有些教程直接把这个数写死在代码里,告诉你“照着抄就行”。千万别照抄。
原因很简单:每一颗MQ-135传感器的制造一致性都很差,同批次不同器件在相同干净空气中的Rs可以差出好几倍。抄一个别人的R0,换算出来的浓度能偏到姥姥家。校准这一步,是每一个使用MQ-135的项目绕不开的宿命。
5.2 校准环境的准备
所谓校准,简单说就是测量传感器在干净空气中的基准电阻R0。关键在于“干净”两个字,我的做法是:
- 新房装修期间不要校准,甲醛、苯系物会污染基准。
- 挑一个有风的天气,把窗户全打开通风半小时以上。
- 或者把传感器放进一个干净的密封袋,扎口前用力灌几口户外新鲜空气,别在汽车尾气多的地方取样。
校准前还有个前置条件:新传感器连续通电48小时,行话叫“烧机”或老化。这一步让敏感层表面状态稳定下来,之后测到的R0才有意义。第一次用就急着校准的话,隔两天你会发现R0自己变了。
5.3 手算R0:一个能直接用的校准程序
先复习一下分压公式变形:Rs = RL × (VCC - Vout) / Vout。在干净空气中测到的Rs,就是我们要找的R0。下面的程序会连续采样50次求平均,然后把R0直接打印出来。
const int gasPin = A0; const float VCC = 5.0F; const float RL = 10.0F; // 改成你模块上实际的负载电阻,单位kΩ void setup() { Serial.begin(9600); delay(3000); // 等串口稳定 } void loop() { float sum = 0; for (int i = 0; i < 50; i++) { sum += analogRead(gasPin); delay(100); } float avg = sum / 50.0F; float vout = avg * VCC / 1023.0F; float rs = RL * (VCC - vout) / vout; // 单位kΩ Serial.print("Vout = "); Serial.print(vout, 3); Serial.print(" V, Rs = "); Serial.print(rs, 3); Serial.println(" kΩ"); Serial.print("建议写入代码的R0 = "); Serial.print(rs, 1); Serial.println(" kΩ"); delay(5000); }RL值一定要按你模块的实际电阻改。很多模块板载电压标签旁边有丝印,标着1K或10K;没有丝印就拿万用表量AO到GND之间的电阻。用错的RL,R0会连带着成倍出错。校准完成后我会把R0和校准日期用马克笔写在模块背面,方便过几个月重新校准。
5.4 把R0存进EEPROM,免得每次改代码
手算的R0是一次性的。做长期项目时,更合理的做法是把它存进EEPROM,并且在代码里留一个“校准模式”:按住按钮上电,系统自动进校准流程并更新R0。下面是这个思路的完整实现。
#include <EEPROM.h> const int gasPin = A0; const int calButton = 2; // 按钮一端接这个引脚,另一端接GND const float VCC = 5.0F; const float RL = 10.0F; // 单位kΩ,按模块实际改 float R0 = 30.0F; // 默认值,第一次使用建议先跑上一节的校准程序 void setup() { Serial.begin(9600); pinMode(calButton, INPUT_PULLUP); EEPROM.get(0, R0); if (R0 <= 0.0F || R0 > 200.0F) R0 = 30.0F; // 防呆 Serial.print("当前R0 = "); Serial.println(R0); } void loop() { if (digitalRead(calButton) == LOW) { Serial.println("开始校准,请确保传感器处于新鲜空气中..."); float sum = 0; for (int i = 0; i < 100; i++) { sum += analogRead(gasPin); delay(100); } float avg = sum / 100.0F; float vout = avg * VCC / 1023.0F; float rs = RL * (VCC - vout) / vout; R0 = rs; EEPROM.put(0, R0); Serial.print("校准完成,新R0 = "); Serial.println(R0, 2); delay(3000); } }这里用到了EEPROM.put和EEPROM.get,可以直接存float,比手动拆字节方便太多。AVR平台上这个库是内置的,不需要额外安装。
5.5 R0会漂移,定期校准是常态
校准不是一劳永逸的。敏感层长期暴露在空气中,会慢慢吸附油污、灰尘,甚至被某些气体“毒化”,导致R0漂移。我的习惯是每1到3个月重新校准一次。那些说“我去年校准过,今年还在用”的代码,换算出来的浓度可信度真的不高。对精度有要求的场景,还可以在设备外壳上贴一个“下次校准日期”标签。
6. 从Rs/R0到PPM:数据手册曲线的拟合与实测对比
6.1 数据手册里那张对数坐标图
MQ-135的datasheet里有一张最关键的图,叫“灵敏度特性曲线”:横轴是气体浓度(ppm),纵轴是Rs/R0,两个坐标轴都是对数刻度。每种气体画成一条近似直线。对数坐标下的直线,意味着背后的数学关系是幂函数:Rs/R0 = a × PPM^b,或者反过来写成 PPM = A × (Rs/R0)^B。
这就是所有PPM换算公式的来源。你不需要搞懂高深数学,只要知道:只要从图上读出两个点,就能算出A和B。
6.2 手算两个点,拟合出你自己的公式
以氨气那条线为例,我从自己手头datasheet的图上读出两个近似点:(10 ppm, 0.5)和(100 ppm, 0.13)。不同批次印刷可能有差异,这里只看方法。
取对数后用直线方程 log10(Rs/R0) = m × log10(PPM) + b:
m = (lg0.13 - lg0.5) / (lg100 - lg10) = (-0.886 - (-0.301)) / (2 - 1) = -0.585 b = lg0.5 - m × lg10 = -0.301 + 0.585 = 0.284
于是转换公式为:
PPM = 10^((log10(Rs/R0) - 0.284) / (-0.585))
这段在代码里实现就是先把Rs/R0取对数,代入算完,再pow(10, 结果)。整个过程Arduino的float运算毫无压力。
6.3 网上流传的“现成公式”能不能用
社区里流传很广的MQ-135 CO2公式是:ppm = 116.6 × pow(Rs/R0, -2.769),氨气公式是:ppm = 102.2 × pow(Rs/R0, -2.473)。这些都是爱好者拟合出来的,拿来做个趋势参考没问题,但你得知道它的几个坑:
第一,在干净空气下,CO2公式用校准后的R0算出来大约只有一百多ppm,而真实大气CO2浓度约420ppm左右,偏低明显。想在绝对数值上更接近,得做一个基线修正。第二,广谱交叉灵敏度会让换算结果被酒精、氨气等干扰气体带偏。第三,这些拟合参数来自某一批手册图纸,换成别的厂家的模块误差会更大。
6.4 一个完整的计算实例
假设你模块的负载电阻RL=10kΩ,VCC=5.0V,校准得到的R0=30kΩ。某一时刻Arduino读到模拟值ADC=639。我们一步步算:
- 电压:Vout = 639 × 5.0 / 1023 ≈ 3.125V
- 传感器电阻:Rs = 10 × (5.0 - 3.125) / 3.125 ≈ 6kΩ
- 比例:Rs/R0 = 6 / 30 = 0.2
- 代入上面拟合的氨气公式: log10(Rs/R0) = lg0.2 = -0.699 log10(PPM) = (-0.699 - 0.284) / (-0.585) ≈ 1.68 PPM ≈ 47.9
也就是说,这个读值对应的氨气浓度大约是48ppm。这个量级在养殖场通风场景里已经属于“需要开风机”的区间。如果不小心用了别人的R0,比如用了30kΩ的两倍60kΩ,算出来会变成两倍多,判断阈值就全乱了。
6.5 不追求绝对浓度时的“比值法”
说实话,对大多数DIY项目来说,绝对ppm浓度的价值没那么大。更鲁棒的做法是直接观测Rs/R0比值的变化。校准完成后,干净空气下Rs/R0约等于1,污染来了比值往下掉。你可以在代码里写:比值小于0.6就报警。这个方法完全绕开了公式拟合误差,只用“相对变化”做判断,实测非常稳定。我后来做的空气盒子就是用这个思路,一年多下来误报率很低。
7. 故障排查与温湿度干扰:实测中真正会遇到的坑
7.1 一张故障排查表
把这几年在课程群和项目里看到的高频问题整理成一张表,按“现象 → 原因 → 处理”对照查:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| ADC一直为0 | AO没接对、没共地 | 检查接线,确认GND连在一起 |
| ADC一直接近1023 | VCC接错、传感器烧了 | 检查供电,替换传感器对比 |
| 数值剧烈跳动 | 预热未完成、供电纹波大、旁边有风 | 预热10分钟以上,换适配器,加滑动平均 |
| DO的LED一直亮 | 电位器阈值设得太低 | 调整电位器,用酒精棉球验证翻转 |
| DO的LED一直不亮 | 阈值太高或传感器坏了 | 反向调电位器,测AO电压判断传感器输出 |
| 数值几乎不变 | 敏感层被油污覆盖或老化钝化 | 通电长时间加热尝试恢复,不行就换 |
| 数值随时间缓慢上涨 | 环境逐渐污染或传感器漂移 | 重新校准R0,排查污染源 |
遇到DO口异常时,先测AO口的电压。AO有正常电压变化而DO不正常,问题在比较器电路或电位器;AO都没反应,问题在传感器本体或供电。
7.2 温湿度干扰:湿度天读数会“假报警”
SnO2敏感层对温度和湿度都有响应,这是材料本身的物理特性。梅雨季节空气湿度高,传感器的基线会整体偏移,有时候明明空气质量不错,报警却莫名其妙触发。温度的影响相对小,但也存在。
应对方法有三种,按性价比排序:
- 校准环境和使用环境尽量一致:冬天校准的R0,到夏天可能偏;尽量在接近实际使用的温湿度条件下校准。
- 配合DHT22做粗补偿:记录温湿度,用经验公式对读数做线性修正。精度一般,但趋势会被拉正。
- 接受它是“相对指标”:做趋势监控和阈值报警,不追求跨季节的绝对浓度一致性。
我的实际建议是第三种,省钱省心。如果项目真的需要精确的CO2、氨气浓度,直接换NDIR原理的传感器(比如MH-Z19),价格高一个数量级,精度也高一个数量级。用MQ-135硬扛绝对精度,是性价比很低的一件事。
7.3 长期存放后的“唤醒”仪式
传感器在抽屉里吃灰半年后重新上电,数值往往特别离谱。这是因为存储期间敏感层表面吸附了水汽、灰尘和有机物。别急着扔,先通电让它“烧”一个晚上到一天,很多情况下数值会慢慢恢复到正常范围。我有一块模块放了半年,刚上电时读值比新件高了快一倍,通电一晚上之后就回到正常水平了。
7.4 电源纹波和接地是隐形杀手
如果你发现数值在供电不稳的场景下乱跳,先怀疑电源而不是传感器。降压模块输出纹波大的时候,ADC参考电压和分压基准一起抖,读数自然跟着抖。我做空气质量盒子时曾经被这个问题折磨了一整天,最后换了一个纹波更小的5V稳压模块,立竿见影。
另外一个容易被忽略的点是Uno的AREF和内部参考电压。如果项目里同时接了其他传感器占了A1、A2,确保没有给AREF引脚接不合适的电压。A0和其他模拟口的参考是同一个,任何干扰都会交叉影响。
7.5 一点收尾的心里话
这个实验做到最后,我最大的体会不是“MQ-135怎么算ppm”,而是知道什么时候该停止打磨一颗便宜的传感器。它适合做趋势判断、适合做报警触发、适合做入门学习,但如果你在它身上反复纠结“为什么测出来的CO2不是420ppm”,那大概率是选错工具了。接受它的性格,把它放到它能发光的位置——这本身也是硬件工程里很重要的一课。