☰
基于STM32的MQ137氨气传感器:ADC采集与浓度换算完整实现
2026/10/19 12:32:30 网站建设 项目流程

简介:一套MQ137氨气检测模块开发资料包,面向STM32等嵌入式开发者,适用环境监测、工业安全等需实时采集氨气浓度的场合,也适合刚接触气体传感器的工程师作为入门参考。资源内含完整说明书、传感器通用原理图,以及模拟量输出与TTL输出两套参考程序,清晰展示了如何将MQ137的电阻变化转换为浓度数据;配合ADC0809中文资料,能更好理解模拟量采集与标定换算。压缩包共37个文件,仅2.05MB,以PDF文档、Keil工程源码、编译输出和备份配置为主,工程包含C源文件、启动代码、hex烧录文件等,可在Keil中直接打开查看或烧录到目标板验证。已有3791人浏览学习,适合用来快速搭建MQ137检测原型、调试数据处理流程,并在此基础上扩展显示与报警功能,缩短项目开发周期。全部资料目录结构清晰,文档与示例程序一一对应,按模块阅读即可完整掌握从硬件接线到软件实现的整个环节。

1. MQ137 氨气检测:STM32 从 ADC 采集到浓度换算的完整链路

不少第一次接触 MQ137 的人在同一个地方翻车:模块通电后模拟电压一直跳,拿浓氨水靠近它读数却纹丝不动,于是怀疑传感器是坏的。其实 MQ137 的工作链路是一条明确的链:电压 → 等效电阻 → Rs/R0 比值 → 对数曲线换算浓度,中间任何一环断了,读数就是个黑匣子。这份资源把整条链路打包完整了:STM32 的 ADC 采样、滑动滤波、Rs/R0 标定、ppm 浓度换算、串口输出和阈值报警,全部集成在同一个工程里。适合正在做氨气监测、智慧农业、实验室环境监测的嵌入式开发者,也适合第一次接触气体传感器、想搞懂数据手册曲线怎么变成代码的学生。

2. 项目结构与硬件准备:MQ137 模块接线和工程文件分布

2.1 MQ137 的气敏特性与 AO 输出

MQ137 本质上是“电阻变色龙”。它内部一个加热电阻持续加热二氧化锡气敏层,气敏层吸附氨气后电导率上升,引脚间的等效电阻 Rs 会下降。模块上有一枚比较器,把 Rs 的变化转成数字电平给 DO 口,但这个口只能告诉你“超没超过阈值”,拿不到具体浓度。要算 ppm,必须读 AO 口的模拟电压,电压是电源 VCC 在负载电阻 RL 与 Rs 之间分压的结果。

这个特性带来两个直接后果。第一,采样电路对 VCC 的稳定性敏感,VCC 掉 0.2V 会直接反应在电压上,所以别用那种输出波动大的劣质 USB 供电,我一般先拿实验室电源确认模块电压稳定再接单片机。第二,上电瞬间气敏层的温度还没起来,Rs 很大,AO 电压很低,看起来像“没有氨气”;等加热了几分钟,Rs 才慢慢降到稳定值,输出进入正常范围,很多人在刚上电时看到的几百 ppm 假峰值就是这么来的。

2.2 模块与 STM32 的接线方案

不同 MQ137 模块的引脚定义略有出入,但核心引脚一致:VCC、GND、DO、AO。接线时最需要盯住的是工作电压问题。我手上的模块 VCC 给 3.3V 就能工作,AO 输出范围在 0~3.3V 以内,ADC 直接读没问题。如果你买的模块明确要求 5V 供电,或者你想把加热器单独接到 5V 上,那 AO 的输出会超过 3.3V,直接进 STM32 的 ADC 引脚就可能烧坏——这是最普遍的接线事故。

5V AO 的安全转接方式,我一般会先加一个电压跟随器,再经两支 10kΩ 电阻分压到 0~2.5V。直接从引脚处挂两个电阻把 AO 分压也行,应急能用,但要注意分压网络会把板载 RL 并联掉,改变原分压比。所以用了分压就一定要带着分压网络整体标定,不要先用纯 5V 标定再按比例换算回 3.3V。接线示意如下:

MQ137 引脚STM32 引脚说明
VCC3.3V / 模块单独 5V传感器供电
GNDGND共地
AOPA1(ADC1_IN1)模拟浓度信号
DO(可选)PA0(GPIO 输入)阈值比较输出
H(如有)5V / GND 对加热器独立回路

PA1 对应 ADC1 的第 1 通道,工程代码默认使用这个通道。如果板子的管脚布局不同,换到 PA3、PA4 等其他 ADC 通道都行,只需要同步修改 sConfig.Channel 里配置的通道编号。

2.3 源码包目录结构

这套工程基于 STM32 HAL 库,源码目录整理如下:

MQ137_STM32/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ └── mq137.h │ └── Src/ │ ├── main.c │ ├── mq137.c // 传感器读取、滤波、浓度换算 │ ├── adc.c // ADC1 初始化 │ ├── uart.c // 串口 1 初始化与发送 │ └── stm32f1xx_it.c // 中断处理 ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ ├── MDK-ARM/ │ └── MQ137_STM32.uvprojx // Keil 工程文件 ├── 文档/ │ ├── MQ137 说明书.pdf │ └── 标定记录模板.xlsx └── README.md

工程分层的思路很简单:main.c 只做初始化和循环调度,mq137.c 负责全部传感器逻辑,adc.c、uart.c 管底层外设。这样做的好处是二次开发时不用在一千行的 main.c 里大海捞针,把 mq137.c 移植到另一个工程也就是几分钟的事,因为对外 API 都是独立成函数的,这为后面要讲的多传感器扩展留好了位置。

3. 核心代码拆解:ADC 采样、电压滤波与 ppm 公式推导

MQ137 的浓度不能直接读出来,它需要至少四个步骤:初始化 ADC → 读取原始值 → 转成电压 → 换算成电阻和浓度。每一步都有可以优化的细节。

3.1 ADC 初始化与采样时间设置

ADC 初始化的要点是分辨率、对齐方式和采样时间。设置为 12 位、右对齐、软件触发,这是最通用的一组配置:

// adc.c —— ADC1 初始化,通道 1 对应 PA1 void MQ137_ADC_Init(void) { ADC_InitTypeDef adcInit; GPIO_InitTypeDef gpioInit; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_ADC1_CLK_ENABLE(); gpioInit.Pin = GPIO_PIN_1; gpioInit.Mode = GPIO_MODE_ANALOG; // PA1 配置成模拟输入 HAL_GPIO_Init(GPIOA, &gpioInit); adcInit.Resolution = ADC_RESOLUTION_12B; // 12 位精度 0~4095 adcInit.DataAlign = ADC_DATAALIGN_RIGHT; // 右对齐,方便直接取值 adcInit.ScanConvMode = DISABLE; adcInit.ExternalTrigConv = ADC_SOFTWARE_START; HAL_ADC_Init(&hadc1); ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_1; sConfig.SamplingTime = ADC_SAMPLINGTIME_239CYCLES_5; // 采样时间拉长 HAL_ADC_ConfigChannel(&hadc1, &sConfig); }

采样时间是个关键参数。239.5 周期是一个偏保守的选择,相比 1.5 周期,它给内部采样电容更充足的充电时间,采集到的电压更稳定。对 MQ137 这种输出阻抗较高、信号变化又慢的传感器来说,快速采样只会引入噪声,没有任何好处。如果你跑代码时发现单次采样值毛刺很多,先别急着加滤波器,把采样时间从 55.5 周期调到 239.5 周期再试,很多“玄学跳动”其实是采样时间太短导致的。

3.2 电压读取窗口与滑动滤波

裸的 ADC 值直接换算浓度会出现很大的跳动,因为气敏层本身有微观的吸附和脱附噪声。我一般会做一个小滑动窗口:连续取 10 次读数,每次间隔 10ms,窗口宽约 100ms,取算术平均作为一次有效采样。这个时间尺度能滤掉大部分高频噪声,响应延迟又不至于太大。

// mq137.c —— 连续采样 10 次取平均,返回电压值 float MQ137_ReadVoltage(void) { uint32_t sum = 0; uint8_t i; for (i = 0; i < 10; i++) { HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { sum += HAL_ADC_GetValue(&hadc1); } HAL_Delay(10); // 每次间隔 10ms,共 100ms 采样窗口 } float adc_average = (float)sum / 10.0f; return adc_average * 3.3f / 4095.0f; // 12 位 ADC 与 3.3V 参考电压 }

第 11 行的 3.3f 是 ADC 参考电压。如果你的板子参考电压不是精确的 3.3V,比如实测是 3.29V,那换算出来的电压整体就会偏低。常见漏点是把参考电压与传感器供电混为一谈,后面的避坑章节会专门讲。这里建议用万用表实测 VREF,或者读取芯片内部的参考电压通道来自矫正。

3.3 电压转电阻:分压表达式

得到电压后,继续推导传感器等效电阻 Rs。电压是从 VCC 经 Rs 与负载电阻 RL 分压后、取在 RL 两端的信号,所以有:

// mq137.c —— 电压转传感器电阻 float MQ137_GetRs(float voltage) { const float RL = 10.0f; // 模块板载负载电阻 10kΩ const float VCC = 3.3f; // 模块供电电压,与电路一致 if (voltage < 0.001f) voltage = 0.001f; // 防止对 0 求倒数 if (voltage >= VCC - 0.001f) voltage = VCC - 0.001f; // 防止负电阻 return (VCC - voltage) / voltage * RL; }

RL 的实际值应该从模块上确认,有的是 10kΩ,有的是 47kΩ,代码写错会整体偏移。电压取接近 0 或接近 VCC 时,这个公式数值上都会爆炸,所以上下限保护是必要的。常见的误用在换取 5V 供电的模块后还沿用 3.3V 的 VCC 常数,Rs 会直接算错,后面查曲线自然得到一个荒谬的浓度。

3.4 关键一步:换算 ppm 浓度

换算的本质是从数据手册的灵敏度特性曲线上取点。MQ137 数据手册会画一条 NH3 浓度与 Rs/R0 的对数曲线,横轴是浓度,纵轴是比值,近似直线。代码里做的事是:取直线上的已知点,拟合出 log(ppm) 与 log(Rs/R0) 的一次函数,再对任意测得的 Rs/R0 反推浓度。

// mq137.c —— 浓度换算,使用对数直线拟合 float MQ137_GetPPM(float Rs) { float ratio = Rs / R0_CLEAN_AIR; // R0 是洁净空气下标定的值 float log_ratio, log_ppm; if (ratio <= 0.0f) ratio = 0.001f; // 取数据手册 NH3 曲线两点: // 10ppm -> Rs/R0 = 0.35 // 100ppm -> Rs/R0 = 0.15 log_ratio = log10f(ratio); log_ppm = -2.721f * log_ratio - 0.242f; // 两点式拟合系数 return powf(10.0f, log_ppm); }

R0_CLEAN_AIR 是在 mq137.c 顶部定义的一个标定常量,默认值需要按第 4 章的标定流程替换,否则所有浓度计算都会偏移。为什么取 10ppm 和 100ppm 这两个点?因为数据手册上的这条线在这段区间线性度最好,两点定一条直线就能覆盖 5~500ppm 的主要量程,斜率是负的,说明 Rs/R0 随浓度增大而减小,这是半导体气体传感器的通用规律。更严谨的做法是你自己采集 3~5 个浓度点的数据,在 Excel 里做对数回归,把斜率和截距替换进代码。

3.5 温度补偿预留接口

MQ137 对温度变化有响应,环境温度从 20°C 升到 30°C 时,同一浓度的读数可能漂移 10% 以上。最简单的补偿是硬件上把传感器放在通风但不受阳光直射的位置,软件上做一个温度系数的线性校正:

// mq137.c —— 温度补偿修正,T 为环境温度 ℃ float MQ137_GetPPM_Temperature(float ppm_raw, float temperature) { float k_temp = 0.85f; // 温度影响系数,需实测标定 float t_ref = 25.0f; // 参考温度 return ppm_raw * (1.0f + k_temp * (temperature - t_ref) / 100.0f); }

这个补偿系数 0.85 是示意值,不是通用常数。实际项目中我会把传感器和温度探头放在一起,记录 24 小时内的温湿度与读数变化,用回归算出本系统真正的 k_temp。所以这段代码故意写成可配置的样子,给二次开发留好接口。

4. 系统联调:串口数据帧、报警阈值与标定流程

代码写完后,还需要一套可观察的手段确认结果,串口输出是最直接的方式。同时给报警加一个消抖机制,标定流程要能反复执行而不是只靠改代码。

4.1 串口输出与数据帧协议

我习惯用紧凑的文本帧输出,方便在串口助手或者 Python 脚本里直接抓看。每一帧包含电压、电阻、浓度和报警状态:

// mq137_uart.c —— 组装发送数据帧 char tx_buf[64]; uint8_t alarm_flag; snprintf(tx_buf, sizeof(tx_buf), "$MQ137,%.2f,%.2f,%.2f,%d\r\n", voltage, rs, ppm, alarm_flag); HAL_UART_Transmit(&huart1, (uint8_t*)tx_buf, strlen(tx_buf), 1000);

帧格式定义好后,上位机解析就方便了:按逗号分隔,第一个字段是设备标识,之后依次是电压、Rs、ppm、报警位,$ 和 \r\n 用于帧同步。如果你准备把多个节点挂到同一个网关,建议增加设备 ID 字段,比如把 $MQ137 改成 $NODE01 的形式,不然多节点数据会在上位机里打架。

4.2 阈值报警与消抖设计

直接用单次浓度判断容易误报,因为 MQ137 的瞬时读数本来就带波动。加一个连续计数消抖:浓度超阈值时计数器加一,低于阈值时计数器清零,计数达到 3 才置位报警,防止尖峰误触发。

// mq137_threshold.c —— 连续三次超阈值才报警 uint8_t MQ137_CheckAlarm(float ppm, float threshold) { static uint8_t counter = 0; if (ppm >= threshold) { if (counter < 3) counter++; return counter >= 3 ? 1 : 0; } else { counter = 0; return 0; } }

阈值参数我建议放在 main.c 头部的一个宏定义里,或者写进一个配置结构体,不要散落在函数体中。修改阈值时只动一个位置。浓度滞回也可以加,比如浓度降到阈值的 90% 以下才解除报警,避免在临界点附近反复触发。

4.3 标定流程:R0 的五步操作法

换新传感器后最常见的坑是直接沿用旧代码里的 R0,读数自然不准。标定流程如下:

步骤操作效果
1传感器预热 24h 以上气敏层彻底活化
2放到通风洁净的空气中确认环境无氨气干扰
3连续运行代码,记录电压值获得稳定的洁净空气电压
4用公式算出当前 Rs,记为 R0得到本颗传感器的基准
5把 R0 值更新到 R0_CLEAN_AIR替换代码内默认值

第 4 步的具体做法是,用串口助手采集至少 30 分钟的数据,取电压平均值,然后把 VCC 和 RL 代入公式算 Rs。若现场有标准浓度氨气气瓶,可以用多点浓度反推更精确的 R0,没有气瓶时这个流程也足够把默认值修到可用水平。标定完成后,把 R0 写回代码或存入片内 Flash,避免每次上电都重新标定。

5. MQ137 避坑指南:预热、分压、漂移和误标定的五个现场问题

5.1 上电读数剧烈跳动,甚至出现 100 多 ppm 的假峰值

现象:刚接电的前几分钟,串口打印的浓度从 0 一路冲到 100 以上,然后又慢慢掉回个位数。

原因:气敏层温度建立需要时间,曲线数据是在传感器完全工作温度下标定的,冷机状态读到的 Rs 没有参考价值。另外加热回路供电不稳定也会放大这个现象。

解决:上电后让代码连续跑 30 分钟再开始读数据,或者直接忽略前 30 分钟的输出。我在正式部署的设备里加了一个预热计时变量,前 1800 秒内只打印不上送数据,报警逻辑也不会在这段时间生效。

5.2 通氨气后浓度读数整体偏低

现象:对着传感器通入已知浓度的氨气源,读到的浓度只有实际值的一半左右。

原因:VCC 实际值低于代码里的常量。比如 3.3V 的 USB 供电,插在不同口上实测只有 3.25V,代码里还按 3.3V 算,电阻偏小,浓度自然偏低。

解决:用万用表实测模块 VCC,把代码里的 3.3f 改成实测值。如果板子有 ADC 内部参考电压通道,优先用内部参考校准后再反推电压,比手动改常数更接近真值。

5.3 串口输出乱码,且换电脑后表现还不一样

现象:数据帧打印出来是一堆乱码,比如$MQ137,?这样的格式。

原因:串口波特率不匹配,或者工程配置的晶振频率与实际板载晶振不同,导致串口波特率生成误差过大。

解决:先确认板载晶振是 8MHz 还是 16MHz,在工程配置里选对;再把串口波特率改成 115200,两边保持一致后重新上电。如果还是乱码,排查 USB 转串口模块的驱动,先用单片机点灯确认基础时钟是否正常。

5.4 换上同型号的第二颗传感器,读数相差一倍

现象:同样环境下,新传感器和旧传感器的浓度结果差距很大。

原因:气敏元件制造工艺导致个体差异较大,每颗传感器的 R0 都不同,这是 MQ 系列的通用特性。

解决:每换一颗传感器必须重新走一遍标定流程。默认 R0 只能保证代码“能跑”,要作为测量设备使用,必须按第 4 章的五步流程更新 R0_CLEAN_AIR。这是批量部署时最容易翻车的地方,曾有同事图省事统一用同一个 R0,结果一半节点读数偏差大到不可用。

5.5 不接传感器时浓度输出也在乱跳

现象:PA1 引脚悬空,没有接任何传感器,浓度输出仍然在几十 ppm 范围波动。

原因:GPIO 引脚悬空时,内部噪声和外部电磁干扰会被 ADC 拾取,得到的电压值没有实际意义。

解决:用示波器或万用表在 AO 端确认信号是否存在。调试阶段,如果确定不用传感器,要么将该 ADC 通道禁用,要么在硬件上把 PA1 下拉到地,让输入电平落在一个已知位置。

6. 二次开发:把单点检测扩展成带显示和温湿度补偿的监测节点

6.1 加一块 OLED 屏,结果立刻看得见

把 mq137.c 的接口封装成简洁的float MQ137_GetPPM(void)之后,加 OLED 显示就很简单了,在 main.c 的循环里调用一次:

uint8_t line1[16], line2[16]; float ppm = MQ137_GetPPM(); sprintf((char*)line1, "NH3: %.1f ppm", ppm); OLED_ShowString(0, 0, line1);

早期版本我没放显示,是因为滤波和补偿参数都在动态调整,显示太早会干扰在线调参。等算法调明白了,再把显示接口加上去,这是增量开发的习惯,不建议一开始就把所有外设堆上去。

6.2 把 R0 标定做成按键操作

一个更实用的扩展是把 R0 改成可按键标定。将 R0_CLEAN_AIR 从常量改为写入片内 Flash 的变量,长按按键把当前算出的 Rs 作为新 R0 存起来。这样现场运维就不用重新刷固件,也避免每次都拿电脑改代码。

实现上只需要把静态常量变为一个全局变量,加几行按键扫描和 Flash 读写逻辑。做完这步之后,换传感器的现场操作就变成了:上电预热 30 分钟,放到洁净空气中,长按按键 3 秒,标定完成。

6.3 验证这轮改造是否完整

扩展完成后,我一般会同时开三个窗口:串口助手看实时数据,Python 脚本画浓度时间曲线,再记录温湿度变化。若 30 分钟内的浓度波动在 ±5% 以内、报警触发时间与实际情况吻合,这轮改造就算闭环了。

从那次 MQ137 批量部署踩坑之后,我给自己定了条规矩:凡是气敏类传感器的项目,必须带标定状态机制作,换传感器必须重标定,预热期间的数据绝不能参与判定。希望这个拆解能帮到你,祝你的 MQ137 项目一次跑通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询