基于STM32的MQ137氨气传感器:ADC采集与浓度换算完整实现
简介一套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共地AOPA1ADC1_IN1模拟浓度信号DO可选PA0GPIO 输入阈值比较输出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 项目一次跑通。本文还有配套的精品资源点击获取