ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32+MQ5气体报警器信号链设计:从ADC采样到浓度换算全解析

STM32+MQ5气体报警器信号链设计:从ADC采样到浓度换算全解析 简介这是一份基于STM32单片机实现可燃气体监测与报警的完整嵌入式工程源码面向物联网、智能安防以及电子设计初学者解决MQ5气体浓度采集、OLED实时显示、蜂鸣器报警提醒和串口数据上报等实际问题。压缩包共235个文件、6.63MB既包含.c/.h源文件、Keil工程文件.uvprojx等核心代码也附带编译生成的.o/.axf/.map等过程文件方便查看构建结果源文件覆盖STM32标准外设驱动、ADC采样、OLED屏幕驱动、串口通信等模块整体结构清晰便于对照学习。该项目已有265人学习浏览适合正在练习STM32外设驱动和气体传感器应用的开发者也适合作为课程设计参考。代码提供从硬件初始化、气体信号读取与模数转换到预设阈值判断、声光报警和上位机数据监控的完整流程配合串口调试助手即可实时观察浓度变化进一步可扩展无线上报或云端监测项目切入点和可靠性设计均有示范价值。1. 气体报警器信号链为什么STM32读到的电压不能直接用拿到这套STM32MQ5的源代码工程最先要弄明白的不是OLED怎么显示也不是串口怎么发数据而是从传感器到ADC引脚之间那段小小的模拟链路。MQ5是半导体式气体传感器检测到甲烷、液化气浓度变化时内部电阻会在几十千欧到几百千欧之间来回变动这个阻值必须先转成电压才能被STM32的12位ADC读取。分压电阻取值不对、采样时间不够或者供电纹波偏大后续的显示、报警、串口上报都会跟着错。这篇把整个信号链拆开讲从硬件接线、ADC参数、滤波和浓度换算到OLED刷新和蜂鸣器报警最后给出用串口调试助手反推传感器状态和校准阈值的调试方法。适合正在做课程设计、想快速跑通气体检测demo或者准备把MQ5接进自己智能家居项目的开发者。2. MQ5硬件接口与信号调理负载电阻、ADC输入范围与供电去耦2.1 MQ5的输出为什么不能直接接ADCMQ5内部是一个MOS型气敏元件加热丝把二氧化锡半导体加热到工作温度空气中氧分子吸附在半导体表面形成氧负离子当被测气体与氧负离子反应时半导体载流子浓度变化表现为器件电阻Rs下降。气体浓度越高Rs越小。这个Rs的变化范围很宽从洁净空气中的数兆欧到高浓度下的几十千欧完全落在ADC的阻抗匹配范围之外所以必须通过负载电阻RL把Rs转换成电压信号公式是Vout Vcc * RL / (RL Rs)市面上常见的MQ5模块已经帮你把RL焊接在板上并引出AO模拟输出和DO数字输出。如果你用的是裸传感器就要自己搭这个分压电路如果用的是模块也不要直接相信AO引脚能和STM32安全对接——很多模块在5V供电时AO满量程会超过3.3V需要先确认模块的设计输出电压范围。2.2 引脚连接与电源约束源代码里包含stm32f10x_adc.c说明数据读取走的是AO口加ADC通道。以最常用的STM32F103C8T6为例推荐的连接方式如下表MQ5模块引脚STM32F103C8T6说明AOPA0 / PA1接ADC1_IN0或ADC1_IN1二选一DO不接模块内置比较器输出本项目用ADC所以可忽略VCC5V模块加热丝需要稳定5V供电GNDGND必须与单片机共地需要注意F103系列中PA0到PA3不是FT引脚不能承受5V。如果实测模块AO在洁净空气中已经超过3.3V就不能直接进ADC要用电阻分压或运放跟随把信号压到3.3V以内。先上电用万用表量一下AO对地电压比什么都可靠。2.3 负载电阻取值与灵敏度权衡如果你手头是裸传感器RL的取值决定了这个系统的测量区间。RL取1kΩ时低浓度下Rs很大输出随浓度变化不明显但高浓度段不容易饱和RL取4.7kΩ时低浓度灵敏度更好但甲烷浓度超过5000ppm后电压变化趋于平缓ADC分辨率会浪费在饱和区。做家庭泄漏报警我一般取1k到2.2k之间原因是燃气报警更关注低浓度段的灵敏度和可重复性而不是测满量程。工程里习惯用Rs/R0来描述传感器状态R0是传感器在洁净空气中的阻值。MQ5数据手册上的灵敏度曲线横轴是ppm纵轴是Rs/R0判断报警阈值时直接把当前Rs和R0做比值再对照曲线逻辑判断是否超标比死记电压值更通用。2.4 供电去耦与参考电压MQ5的加热丝电流不小H型加热功耗接近750mW5V供电时电流有150mA左右。如果把传感器和单片机共用一个LDO浓度突变时加热电流的波动会直接影响ADC参考电压表现为采样值抖动甚至跳变。工程做法是传感器单独供电或者至少在传感器供电引脚放一个100μF电解电容加一个0.1μF陶瓷电容并联去耦ADC参考端再单独加LC滤波。STM32F103的VDDA引脚需要串联一个10Ω电阻再接到3.3V配合对地电容能明显改善采样稳定性。3. ADC多通道采样与浓度换算从原始码值到ppm3.1 标准库ADC初始化参数源码使用的标准外设库ADC初始化先打开时钟再配置GPIO为模拟输入最后配置ADC1。下面这段是单通道软件触发的典型写法ADC引脚用PA0void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟 72MHz / 6 12MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; // 12位右对齐 ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); } uint16_t Read_MQ5_ADC(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); }这里最关键的参数是ADC_SampleTime_239Cycles5也就是239.5个ADC时钟周期。ADC时钟12MHz时单个通道采样时间约20μs。选最长采样时间不是浪费时间是因为MQ5模块的AO输出阻抗偏高采样电容充电时间不够会导致转换结果偏低且不稳定。如果发现采样值周期性偏低优先检查这里有没有改成最短采样周期。如果模块AO接的是PA1把通道号换成ADC_Channel_1即可。3.2 滑动平均与中值去尖峰MQ5本身响应速度不快T90大约在10到30秒量级主循环没必要用高频率去读ADC。我习惯每50ms采一次连续16次做滑动平均既消掉电源纹波带来的随机噪声又不至于让显示值滞后太多。代码可以这样写#define FILTER_N 16 uint16_t MQ5_Filter_Once(void) { static uint16_t sample_buf[FILTER_N]; static uint8_t idx 0; uint32_t sum 0; sample_buf[idx] Read_MQ5_ADC(); idx (idx 1) % FILTER_N; for (uint8_t i 0; i FILTER_N; i) { sum sample_buf[i]; } return (uint16_t)(sum / FILTER_N); }滑动平均适合压制白噪声但对打火机瞬间喷射这类尖峰干扰不够敏感。更稳妥的做法是先取16个样本做中值滤波去掉最大最小几个值再平均代价是内存占用多一点。MCU是F103C8T6RAM有20KB开一个16字节的缓冲区毫无压力。注意滤波窗口越大显示越平滑但报警响应越迟钝窗口调到20以上时建议每100ms才触发一次滤波计算。3.3 浓度换算对数坐标线形插值MQ5的灵敏度曲线在双对数坐标上近似一条折线直接对ADC码值做线性变换会得到错误结果。以甲烷为例数据手册上的典型曲线有几个近似标定点1000ppm附近Rs/R0约1.03000ppm处大约0.7510000ppm处大约0.45。查表加对数线性插值是这样的思路typedef struct { float log_ppm; // log10(ppm) float log_ratio; // log10(Rs/R0) } MQ5_CalPoint; const MQ5_CalPoint mq5_table[] { { 3.0f, 0.0f }, // 1000ppm, Rs/R0≈1.00 { 3.48f, -0.125f }, // 3000ppm, Rs/R0≈0.75 { 4.0f, -0.347f }, // 10000ppm, Rs/R0≈0.45 }; float MQ5_Calc_PPM(float rs_over_r0) { float log_ratio log10f(rs_over_r0); for (int i 1; i 3; i) { if (log_ratio mq5_table[i].log_ratio) { float t (log_ratio - mq5_table[i-1].log_ratio) / (mq5_table[i].log_ratio - mq5_table[i-1].log_ratio); float log_ppm mq5_table[i-1].log_ppm t * (mq5_table[i].log_ppm - mq5_table[i-1].log_ppm); return powf(10.0f, log_ppm); } } return 10000.0f; }R0的取值方式在洁净空气中上电预热两分钟等ADC稳定后用当前电压反推Rs记成R0。之后每次采样先算当前Rs再除以R0得到比值带入查表函数就是当前估算的ppm浓度。必须承认这种查表换算受传感器个体差异影响比较大并不适合做精准计量但作为泄漏报警判断浓度量级完全够用。4. OLED显示、蜂鸣器报警与串口数据帧状态输出的可读性设计4.1 OLED显示模块I2C地址与刷新策略工程里OLED.c和stm32f10x_i2c.c一起出现说明用的是硬件I2C驱动。最常见的0.96寸OLED分辨率128x64控制芯片是SSD13067位地址0x3C8位写地址0x78。如果你用的是0.91寸128x32的小屏驱动流程一样只是显示页数从8页变成4页初始化代码里的页面参数要跟着改。OLED刷新不适合整屏全量重写。浓度值每秒刷新两到三次足够报警状态发生变化时才重画状态区域。显示内容我建议分成三块第一行显示当前ppm浓度数值第二行显示报警状态第三行显示传感器预热倒计时或系统运行时间。OLED的I2C速率设到400kHz即可太快反而容易在长排线下出错每次写入一页显存而不是一个字节一停能省下大量CPU时间。SSD1306初始化完成后写入显存的起始地址是0x00到0x7F每页128字节。绘制字符时要用取模软件提前把字库生成好中文字符16x16点阵显示占用两页如果OLED屏小建议只放数字和英文缩写把“ALARM”“READY”这类状态词直接拼写在代码里。4.2 蜂鸣器报警有源无源驱动差异和PWM生成蜂鸣器分有源和无源两种。有源蜂鸣器内部带振荡电路只要给直流电平就会响控制代码最简单GPIO翻转高低电平即可无源蜂鸣器需要外部提供2.7kHz左右的方波才能发出最大声压。源码里带了stm32f10x_tim.c大概率就是给无源蜂鸣器输出PWM用的。用定时器TIM2输出PWM占空比50%时声音最响报警节奏用另一个定时器或主循环做间歇控制void TIM2_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 72MHz/(1000*26)≈2.77kHz TIM_TimeBaseStructure.TIM_Prescaler 26 - 1; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // 占空比50% TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }占空比调节音量要注意无源蜂鸣器和喇叭不一样音量主要由驱动功率决定占空比低于30%时声音有明显失真感。如果报警声太刺耳不要只调占空比应该降低PWM频率到2.5kHz左右同时把报警节奏改成响100ms停200ms的脉冲模式。做产品时记得在蜂鸣器两端反向并联一个二极管防止感性负载关断瞬间的尖峰击穿三极管。4.3 串口调试助手数据上报与帧格式串口数据上报有两种风格一种是人眼直接看的ASCII字符串用SSCOM、XCOM这类串口调试助手打开就能读另一种是二进制帧便于PC端上位机解析。调试阶段我推荐ASCII方式先把数据链路跑通void MQ5_Report_To_UART(uint16_t ppm, uint8_t alarm) { char buf[64]; sprintf(buf, ppm:%d alarm:%d\r\n, ppm, alarm); for (char *p buf; *p ! \0; p) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)*p); } }输出效果类似ppm:1234 alarm:1每行末尾带回车换行串口调试助手按文本模式显示即可。如果后续要接上位机做曲线绘制改成二进制帧更好帧头0xAA 0x55之后跟浓度高字节、浓度低字节、报警标志、校验和前5个字节异或作为最后一个字节。注意USART发送时每次都要等待USART_FLAG_TXE置位否则连续发送会丢字节。波特率选9600或115200都可以如果调试助手读出来是乱码先检查单片机实际主频和串口助手设置的波特率是否一致。5. 串口调试三板斧预热判定、阈值校准与数据反推5.1 上电预热前两分钟的数据不能当真MQ5传感器通电瞬间加热丝温度还没稳定Rs会从一个很大的值逐渐下降反映到ADC上就是电压持续漂移。我习惯在固件里加一个预热计时器上电后两分钟内OLED显示“HEAT”串口正常输出浓度但不参与报警判断。判断预热完成的依据不是固定时间而是看连续30次滤波后的ADC差值是否小于±1%两个条件取较慢的一个环境温度越低需要的时间越长。5.2 阈值校准用干净空气和打火机气体标定校准步骤很简单先在通风良好的地方让传感器稳定五分钟读出洁净空气下的ADC值反推R0然后用打火机不点火状态下对准传感器轻喷一下观察串口读到的ppm估算值取一个比洁净空气高、又不会在家人炒菜时误报的报警点。工程上常见的起点是让报警阈值对应Rs/R0等于洁净空气值的0.6倍左右对应偏高浓度甲烷。阈值参数建议存到STM32内部Flash中。F103的Flash按扇区管理最后一个扇区大小1KB先擦除再写入掉电后不丢失这样阈值只有第一次需要校准。源包里带stm32f10x_flash.c正好就是干这个用的把uint16_t alarm_threshold按地址写入就行。5.3 数据异常快速定位表现象可能原因检查顺序串口无输出USART时钟未开启或GPIO复用配置错先量TX引脚电平再看串口助手波特率输出一直是0xFFFADC通道号配置错或AO断开万用表量PA0电压确认不是0数值周期性跳变采样时间太短或VDDA纹波大把采样时间改到239.5周期检查去耦电容浓度只升不降传感器污染或长期通电老化在洁净空气中重新跑R0校准蜂鸣器不响三极管基极电阻太大或PWM频率不对示波器量驱动管集电极波形调试时先在主循环里固定输出一个已知字符串确认串口链路通畅后再接浓度数据能省下大量排查时间。看到浓度值随打火机气体明显上升后再调整报警阈值到合适位置最后别忘了用Flash把校准值存下来。本文还有配套的精品资源点击获取
返回列表