1. 项目缘起为什么用STM32F103做消防气体检测前阵子有个朋友在做一个老旧厂房的消防改造项目找到我聊起一个具体需求他们想在几个关键区域增加一套独立的、低成本的甲烷气体泄漏监测节点作为主消防系统的补充。要求很简单能实时监测浓度超过阈值就本地声光报警最好还能把数据简单记录一下。预算有限但可靠性不能打折扣。我当时脑子里第一个蹦出来的方案就是STM32F103加MQ-4。这几乎是嵌入式开发里一个经典的“黄金搭档”组合了。STM32F103江湖人称“国民MCU”价格亲民、资料海量、生态成熟对付这种数据采集加逻辑控制的任务绰绰有余。而MQ-4传感器更是气体传感器里的“老熟人”对甲烷天然气主要成分灵敏度高价格便宜模块化做得好接上就能用。这个组合成本能压到很低开发周期也短特别适合这种功能明确、对成本敏感的应用场景。不过真要把这个“经典组合”用稳了里面门道可不少。MQ-4是个模拟输出的半导体气敏传感器它的输出是一个随气体浓度变化的电压信号。STM32F103要读懂这个信号核心就是其内置的ADC模数转换器。这听起来就是“ADC读个电压”的事但实际做下来从电路设计、ADC配置、数据处理到最后的浓度标定每一步都有坑。网上很多教程只给个接线图和读ADC值的代码但读出来的数字怎么变成有意义的“ppm”百万分之一浓度环境温湿度变化了怎么办传感器预热时间要多久这些实战中必须面对的问题往往一笔带过。所以我想结合这次实际项目的经验从头到尾捋一遍基于STM32F103和MQ-4构建一个可靠气体监测节点的全过程。重点不止在“怎么让灯亮起来”更在于“怎么让数据准、系统稳”。我们会深入ADC采样的细节探讨如何提高信噪比并分享一套经过验证的软件滤波与标定方法。2. 核心硬件选型与电路设计要点硬件是系统稳定的基石尤其是传感器电路设计不好后面软件再怎么优化也事倍功半。2.1 STM32F103最小系统与资源分配我们选用的是STM32F103C8T664K Flash20K RAM足够应付这个任务。它内置了3个12位的ADC我们只需要用到其中一个通道。引脚分配规划如下PA0 (ADC1_IN0): 用于连接MQ-4的模拟输出引脚AO。选择PA0是因为它通常是开发板上最容易引出的ADC通道之一。PA8: 配置为PWM输出驱动一个蜂鸣器进行声报警。这里特别提一下有热词提到“stm32f103的pa8做pwm,没有pwm波形输出”这个问题很常见。PA8属于定时器1TIM1的通道1而TIM1是高级定时器。要想正常输出PWM除了基本的GPIO和定时器配置必须开启TIM1的BDTR寄存器的MOE位主输出使能否则引脚就是没输出。很多库函数如标准库的TIM_CtrlPWMOutputs(TIM1, ENABLE)或HAL库的HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1)内部会处理这个但如果你是自己配置寄存器或者配置不完整就很容易踩坑。PC13: 连接一个LED用于光报警。PC13是芯片的引脚但要注意其驱动能力较弱最好加个三极管或MOS管驱动大功率LED。串口 (PA9/PA10): 用于调试输出和可能的后续数据上传。2.2 MQ-4模块接口与供电考量市面上常见的MQ-4模块有4个引脚VCC5V、GND、DO数字输出、AO模拟输出。我们只使用AO引脚。关键设计点加热器供电MQ-4传感器核心是一个需要加热到一定温度的半导体气敏元件。模块上的VCC通常是5V主要是给这个加热器供电的。必须保证加热电压稳定电压波动会直接影响传感器内部温度导致基线漂移和灵敏度变化。建议使用LDO低压差线性稳压器单独为传感器模块供电而不是直接从MCU的5V引脚拉过来避免MCU工作时的电流波动干扰传感器。模拟信号调理MQ-4的AO引脚输出阻抗较高。直接连接到STM32的ADC输入引脚可能会因为ADC采样瞬间的电荷注入导致测量值不准。一个经典且有效的做法是在AO引脚和MCU的ADC输入引脚之间串联一个100-500欧姆的电阻并在MCU的ADC引脚到地之间接一个0.1uF的瓷片电容形成一个简单的RC低通滤波既能滤除部分高频噪声又能为ADC采样保持电路提供电荷提高测量稳定性。分压与量程匹配STM32F103的ADC参考电压Vref通常是接VDD3.3V。MQ-4在洁净空气中的输出电压通常称为V0大约在1V左右具体看模块和传感器个体差异遇到高浓度气体时电压会升高。我们需要确保在预期测量的最大浓度下输出电压不超过3.3V否则会饱和。通常MQ-4模块的输出范围是0~VCC5V所以直接接3.3V的ADC是存在饱和风险的。稳妥的做法是使用两个电阻组成分压电路将0-5V的信号按比例衰减到0-3.3V以内。例如用一个1kΩ和2kΩ的电阻分压比例约为0.67。但要注意分压电路会改变输出阻抗需要重新评估前一级的驱动能力。2.3 ADC采样电路与基准源STM32F103的ADC性能很大程度上取决于参考电压的稳定性。如果VDD3.3V是由开关电源如MP2359产生的其上的纹波噪声会直接耦合进ADC降低信噪比。注意对于精度要求稍高的测量强烈建议使用独立的、低噪声的基准电压源芯片如REF3033输出3.3V连接到MCU的Vref引脚如果芯片有独立引出的话。对于没有独立Vref引脚的型号如C8T6则必须尽全力净化VDD电源例如增加π型LC滤波电路。关于“ADC信噪比”和“ADC输入输出曲线”ADC的理想转换公式是Digital Value (V_in / V_ref) * (2^N - 1)其中N是分辨率12位就是4095。但实际存在量化误差、积分非线性INL、微分非线性DNL等。提高信噪比SNR的方法除了用好的基准源就是在软件上做 oversampling过采样和 averaging平均。例如通过配置ADC以高于信号带宽两倍以上的频率采样然后对多个样本取平均可以在一定程度上提高有效分辨率。3. 软件架构与ADC驱动层实现软件部分我们采用裸机无RTOS的前后台架构主循环处理逻辑定时器中断触发周期性ADC采样。3.1 ADC配置单次、连续、DMA与触发模式的选择STM32F103的ADC功能丰富配置灵活但也容易让人迷惑。单次转换 vs 连续转换我们只需要周期性地读取传感器值比如每秒1次因此选择单次转换模式。ADC在一次触发后完成一次转换就停止功耗更低也避免了连续模式下数据覆盖需要严格同步的问题。软件触发 vs 硬件触发我们使用一个基本定时器如TIM6产生1Hz的中断在中断服务函数里用软件触发ADC开始转换。这种方式简单可靠。硬件触发如用另一个定时器的更新事件触发ADC更精确能实现固定采样间隔但对时钟同步要求高初期调试用软件触发更直观。轮询 vs 中断 vs DMA为了不阻塞主程序我们肯定不用轮询。在中断和DMA之间如果只采集一个通道两者差别不大。但为了代码的优雅和未来的可扩展性比如想同时采集温度传感器我推荐使用DMA直接存储器访问。即使只有一个通道配置为DMA单次传输ADC转换完成自动通过DMA把数据搬到内存的指定变量里完全无需CPU干预效率最高。配置步骤简述以HAL库为例初始化ADC时钟、GPIOPA0模拟输入。配置ADC参数分辨率12位、数据对齐右对齐、扫描模式禁用单通道、连续转换模式禁用、外部触发软件触发、DMA请求使能。配置ADC通道设置采样时间。对于MQ-4这种输出阻抗相对较高的信号源适当增加采样时间可以让采样保持电容充分充电得到更稳定的值。STM32F103的ADC采样时间可以配置建议设置为ADC_SAMPLETIME_239CYCLES_5239.5个周期这是最长的采样时间对高阻抗源最友好。配置DMA内存到外设模式实际是外设到内存单次传输数据宽度半字16位。在定时器中断中调用HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_raw_value, 1)启动一次带DMA的转换。避坑点有热词提到“adc dma nxp”和“adc dma 连续采集与数据环形缓冲”。对于STM32使用DMA单次模式采集单个变量时每次启动前需要确保DMA和ADC处于就绪状态。一个常见的错误是在DMA传输未完成时再次调用HAL_ADC_Start_DMA会导致HAL库报错。稳妥的做法是在定时器中断里先检查ADC是否就绪HAL_ADC_GetState(hadc1) HAL_ADC_STATE_READY再启动转换。3.2 处理“3507读不到ADC”及类似问题“3507”可能是一个特定的ADC原始值。读不到ADC值或值固定不变通常由以下原因导致GPIO模式错误必须设置为模拟输入Analog Mode而不是浮空输入或复用功能。ADC或DMA时钟未使能在RCC中除了ADC时钟使用DMA时也必须使能DMA1的时钟。采样时间太短如前所述对于高阻抗源采样时间不足会导致充电不充分读出的值随机或为0。这是最容易被忽略的一点。DMA配置错误内存地址错误、传输长度错误、DMA优先级问题等。软件触发未执行单次模式需要外部触发软件或硬件。如果配置为软件触发但忘记调用HAL_ADC_Start或HAL_ADC_Start_DMAADC永远不会开始转换。Vref连接问题如果芯片有独立Vref引脚却悬空ADC可能无法工作。调试方法先用万用表测量MQ-4模块AO引脚对地的电压确保有变化。然后用简单的轮询方式读取ADCHAL_ADC_PollForConversion排除DMA和中断的干扰。逐步增加复杂度。4. 从ADC原始值到气体浓度滤波、标定与算法这是整个项目的核心也是区分“玩具”和“工具”的关键。4.1 原始数据预处理软件滤波ADC直接读回来的值adc_raw噪声很大必须滤波。这里不推荐简单的移动平均因为它对突发噪声尖峰抑制不好。我常用的是一阶滞后滤波低通滤波也叫指数加权平均计算量小效果不错。// 一阶滞后滤波系数0alpha1越小滤波越强响应越慢 #define ALPHA 0.1f static uint32_t filtered_adc 0; void update_adc_filter(uint32_t new_raw_value) { filtered_adc (uint32_t)(ALPHA * new_raw_value (1 - ALPHA) * filtered_adc); }对于要求更高的场合可以结合中值滤波连续采样5-7次排序后取中间值再送入一阶滞后滤波。这能有效消除偶发的脉冲干扰。4.2 计算传感器电阻与浓度比值MQ-4的敏感体电阻Rs会随着气体浓度变化。我们需要从ADC值反推Rs。首先将滤波后的ADC值转换为电压V_adcV_adc (filtered_adc / 4095.0f) * V_ref其中V_ref是你的ADC参考电压3.3V或基准源电压。MQ-4模块内部传感器电阻Rs和负载电阻RL通常是10kΩ串联在VCC5V和地之间AO点就是它们的分压点。因此V_adc Vcc * (RL / (Rs RL))可以推导出Rs RL * (Vcc / V_adc - 1)这里Vcc是模块的加热电压通常是5VRL是模块上的负载电阻值查看模块原理图常见为10kΩ。得到Rs后我们需要计算灵敏度比值。MQ-4的数据手册会给出在不同气体浓度下传感器在目标气体中的电阻Rs与在洁净空气中的电阻R0的比值Rs/R0曲线。因此关键是要先测出当前环境洁净空气中的R0。R0的标定将传感器置于已知的洁净空气中室外通风处或使用零气。预热至少24小时是的需要这么久MQ系列传感器需要长时间预热才能稳定。预热时必须持续通电。预热后连续采集一段时间如10分钟的稳定Rs值取平均作为当前环境的R0值。这个值需要存储在非易失性存储器如STM32的Flash中因为每次上电后传感器的特性会有微小变化但R0的标定值可以重复使用一段时间建议每月重新标定一次。4.3 浓度计算与查表/公式拟合得到当前的Rs/R0比值后如何得到浓度ppm呢方法一查表法推荐更准确 根据MQ-4数据手册中的“Rs/R0 - 浓度”曲线图在关键点取一些数据对例如浓度 (ppm)10020050010005000Rs/R02.51.81.00.650.25将这些点存储为数组。在实际测量时计算出的Rs/R0值在这个表格里进行线性插值得到对应的浓度值。例如测得Rs/R0 1.5它介于500ppm1.0和200ppm1.8之间可以用线性公式估算浓度。方法二公式拟合法对于甲烷在一定的浓度范围内其对数坐标下的曲线近似线性。数据手册常给出在特定浓度如1000ppm甲烷下的灵敏度特性。我们可以使用幂函数或对数函数进行拟合。一个常见的经验公式形式是ppm A * (Rs/R0)^B其中A和B是常数需要通过实验数据拟合得出。这种方法计算量稍大且精度依赖于拟合的质量。重要心得MQ系列传感器的重复性和一致性并不算顶级受温湿度影响大。因此不要过分追求ppm级别的绝对精度。对于消防报警应用我们的核心目标是设定一个可靠的报警阈值。更务实的做法是在目标安装环境中使用标准气体或近似环境标定出报警临界点对应的Rs/R0比值或ADC原始值直接以此作为报警判断依据比计算一个可能不准的ppm值更可靠。4.4 温湿度补偿的考虑温湿度对半导体气敏传感器的影响非常大。如果项目应用环境温湿度变化范围广必须考虑补偿。可以增加一个温湿度传感器如DHT11、SHT30或更便宜的NTC热敏电阻湿敏电阻。补偿没有通用公式需要在不同温湿度条件下测量传感器在洁净空气中和目标气体中的响应建立查找表或经验公式。这是一个更复杂的标定过程。对于一般室内消防应用如果环境相对稳定可以暂时忽略但需要在文档中明确其使用环境限制。5. 系统集成、报警逻辑与稳定性优化5.1 主程序逻辑与状态机主循环可以设计为一个简单的状态机初始化状态初始化外设从Flash读取标定的R0值点亮指示灯表示系统启动。预热状态系统上电后进入24小时或设定的预热时间倒计时。此期间正常采样但不进行报警判断可以在显示屏或通过串口提示“预热中”。这是保证数据可靠性的关键步骤绝不能省略。监测状态预热完成后进入正常监测。定时如每秒获取滤波后的ADC值计算当前Rs/R0。与预设的报警阈值THRESHOLD比较。如果超过阈值进入报警状态。同时可以定期如每分钟将浓度比值或估算的ppm值通过串口输出用于记录或上位机显示。报警状态启动蜂鸣器PWM驱动产生急促滴滴声和闪烁LED。报警状态应持续直到浓度值回落至阈值以下并保持一段时间如10秒防抖动才能返回监测状态。5.2 报警阈值设定与防误报策略报警阈值THRESHOLD对应Rs/R0的某个值需要实地标定。通常甲烷的爆炸下限LEL是5%体积浓度即50000ppm。但MQ-4在这么高的浓度下可能已经饱和。消防预警一般设在远低于LEL的水平比如10% LEL5000ppm或更低。防误报策略延时触发单次超阈值不立即报警而是启动一个计时器如3秒持续超阈值才确认报警。这可以过滤掉短暂的干扰。多级报警可以设置两个阈值一个低阈值预警仅亮灯一个高阈值紧急报警声光齐鸣。传感器诊断监测传感器加热器电流如果硬件支持或R0值的长期漂移。如果R0值漂移超过一定范围比如±20%说明传感器可能老化或污染应触发故障指示灯提示需要维护或重新标定。5.3 稳定性与长期运行考量看门狗务必启用独立看门狗IWDG防止程序跑飞。数据保存除了R0标定值还可以定期将重要的报警事件记录带时间戳保存到片内Flash的特定页或外挂的EEPROM中便于事后分析。电源管理如果使用电池供电需要考虑低功耗设计。STM32F103可以进入睡眠模式定时由RTC或外部中断唤醒进行采样。但要注意MQ-4传感器加热器功耗较大约150mA无法频繁断电。一种折中方案是让MCU休眠但传感器持续加热。通信与扩展预留的串口可以连接Wi-Fi模块如ESP-01S或LoRa模块将报警信息和浓度数据上传到云端或中控室实现联网监控。6. 实测调试与常见问题排查理论说完最后还是要靠调试。问题1PWM无输出PA8为例检查时钟确认TIM1的APB2总线时钟已使能。检查GPIO确认PA8已配置为复用推挽输出AF_PP。检查定时器配置ARR自动重装载值、PSC预分频器、CCR捕获比较值即占空比是否已合理设置。最关键一步确认高级定时器的“主输出使能”MOE已开启。在HAL库中调用HAL_TIM_PWM_Start()会自动开启。如果使用标准库需要在初始化后调用TIM_CtrlPWMOutputs(TIM1, ENABLE)。问题2ADC值跳动大硬件检查电源纹波用示波器看VDD和Vref加强电源滤波。检查传感器输出端的RC滤波电路。软件增加ADC采样时间ADC_SAMPLETIME_239CYCLES_5。采用中值滤波一阶滞后滤波的组合。环境确保传感器已充分预热24小时。远离风扇、电机等干扰源。问题3浓度读数不准或漂移重新标定R0在洁净空气中重新执行24小时预热和R0测量流程。检查公式和参数确认Vcc、RL、V_ref等常数值准确无误。确认查表法中的插值计算逻辑正确。考虑环境因素是否温湿度变化剧烈尝试在恒温恒湿环境下测试对比。问题4GD32F103能否直接替换STM32F103热词中提到了“gd32f103和stm32f103可以互换吗”。从引脚和基本外设功能上看GD32F103是高度兼容STM32F103的但存在一些细微差异主频GD32通常能跑到更高频率108MHz vs 72MHz。Flash等待周期GD32的Flash零等待区可能更小高速运行时需要调整Flash等待周期设置否则程序可能跑飞。外设细节某些外设的寄存器默认值或行为可能有微小差别例如ADC的采样时间计算可能不同。功耗与抗干扰存在差异。结论对于本项目如果只是简单替换代码大概率能运行。但为了稳定性特别是ADC这种模拟部件建议在替换后重新测试和校准整个测量链。最好查阅GD32的对应数据手册和参考手册针对性地调整驱动代码。这个基于STM32F103和MQ-4的消防气体监测节点从硬件选型、电路设计到软件滤波、传感器标定每一个环节都关系到最终系统的可靠与否。它不是一个简单的“读电压”项目而是一个涉及模拟电路、数字滤波、传感器特性和嵌入式系统的综合性应用。最深的体会是对待模拟信号和传感器必须抱有敬畏之心充分的预热、精心的滤波和严谨的现场标定远比写出复杂的代码更重要。当你看到那个ADC值终于能稳定地反映环境气体的真实变化时那种成就感就是嵌入式开发的乐趣所在。