ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32+MQ-7一氧化碳检测系统:从ADC采集到OLED显示与串口上报

STM32+MQ-7一氧化碳检测系统:从ADC采集到OLED显示与串口上报 简介一套基于 STM32F10x 系列单片机与 MQ-7 一氧化碳传感器的环境监测项目源代码面向嵌入式初学者与电子设计爱好者旨在实现一氧化碳浓度的实时采集、本地显示、超标报警与数据上报。项目中STM32 通过 ADC 模块读取 MQ-7 输出的模拟信号经换算得到气体浓度值并实时显示在 OLED 屏幕上同时设定报警阈值当浓度超标时驱动蜂鸣器发出警报并将浓度数据通过串口发送至调试助手方便上位机监控与记录。工程使用标准外设库开发压缩包为 rar 格式共 229 个文件大小约 6.34MB其中包含多个 C 语言源文件、头文件、Keil 工程配置文件、启动文件、链接脚本以及编译产生的目标文件等目录结构完整可直接用 Keil 打开并重新编译。该资源已有 964 人浏览学习适合作为单片机课程设计、毕业设计或综合实验的参考。学习本项目可以掌握传感器信号采集、OLED 显示驱动、蜂鸣器控制、串口通信以及工程组织等多个嵌入式开发要点是一份难得的实战案例。1. 一套能跑通的气体监测小系统拆开看就是四个外设的协同不少嵌入式初学者卡在“传感器采集 显示 报警 上传”这种组合项目上不是因为某个外设驱动多难而是不清楚数据从传感器到串口助手这条链路里每个环节该做什么、时序怎么配合。这套基于STM32的方案用MQ-7一氧化碳传感器做气体浓度采集通过ADC把模拟电压转为数字量一路送到OLED屏幕刷新显示一路用阈值判断驱动蜂鸣器报警同时把浓度值按帧格式通过USART发给串口调试助手。适合正在做课程设计、实验室课题或者想练手完整数据链路的开发者。和网上那些只贴片段代码的项目不同下面把硬件连接、HAL库配置、数据换算和联调顺序一次讲透。2. MQ-7一氧化碳传感器工作原理与STM32的ADC采集实现2.1 MQ-7的加热周期与电导率变化逻辑MQ-7属于气敏电阻型传感器内部有两组发热丝和一组测量电极。低电导率对应的就是清洁空气当一氧化碳浓度升高二氧化锡半导体表面吸附气体分子电导率上升测量电极两端的电压随之改变。关键点在于MQ-7需要交替工作在两个电压下加热电压1.5V持续60秒和5V持续90秒这个高低压循环让传感器内部温度周期性变化从而提升对一氧化碳的灵敏度。直接用5V给加热丝供电读出的数据会漂移得很厉害。常见做法是通过单片机GPIO控制三极管或MOS管切换加热电压但简化版电路常用一个固定5V加热配合软件延时来近似。实际项目里如果只是室内一氧化碳浓度监测非工业级固定5V加热也能拿到可用的变化趋势阈值判断依然有效。但要注意传感器上电后需要预热3到5分钟输出电压才会进入稳定区间。刚上电就读数据ADC值会虚高容易误报警。MQ-7模块输出的是模拟电压AO引脚量程大约0到5V对应浓度从几十ppm到1000ppm左右。STM32的ADC输入范围通常是0到3.3V所以模块的AO不能直接接PA0这类引脚最好先确认模块是否板载了电压比较电路。很多市售MQ-7模块会同时输出AO和DODO端是电位器可调的阈值比较输出接LM393比较器AO才是原汁原味的模拟量。如果模块上没有做电平转换需要在AO和STM32的ADC引脚之间加分压电阻典型用10K4.7K分压把5V映射到约3.2V。2.2 Cubemx配置ADC为单通道扫描模式以STM32F103C8T6为例PA1作为ADC1通道1的输入引脚。打开STM32CubeMX在Pinout视图里把PA1配置为ADC1_IN1然后在Analog - ADC1里做如下设置Mode: Independent modeScan Conversion Mode: DisabledContinuous Conversion Mode: EnabledDiscontinuous Conversion Mode: DisabledNumber Of Conversion: 1External Trigger Source: Timer 1 Trigger Out eventRank 1采样时间: 55.5 Cycles这里把连续转换模式打开配合定时器触发可以保证每次读取的电压值都是当前时刻的真实采样而不是上一次转换的残留。采样时间设长一点对高频噪声有一定抑制作用。如果用的是LQFP48封装的芯片PA1在其他复用功能冲突时会显示黄色警告此时先把冲突引脚的功能关掉。配置完成后生成代码HAL库会自动初始化ADC主循环里只需要开启转换并等待结果即可。下面这段代码实现了单次读取电压值并换算为传感器输出电压的完整逻辑。/* stm32f1xx_hal_msp.c中ADC的MSP初始化由CubeMX自动生成用户无需手工写GPIO配置 */ uint16_t adc_value 0; float voltage 0.0f; float sensor_voltage 0.0f; HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); /* STM32F103的ADC是12位分辨率参考电压默认接VDDA(3.3V) */ voltage (float)adc_value * 3.3f / 4096.0f; /* 如果使用了分压电阻需要按比例还原传感器实际输出电压 */ sensor_voltage voltage * (14.7f / 10.0f);这段代码的逻辑是从启动ADC转换到轮询完成拿到12位原始值后先换算成分压后的电压再按分压比还原成模块AO的真实输出电压。HAL_ADC_Stop非常重要如果不停止转换下一次启动时会有些芯片在连续模式下直接返回上次缓存结果导致数据不更新。参数说明上参考电压3.3V必须和实际硬件一致如果板子是用LDO输出的3.3V且精度一般建议在代码里加一个软件校准系数。2.3 浓度与电压的数据拟合公式MQ-7的数据手册会给出一个灵敏度特性曲线图横轴是气体浓度ppm纵轴是传感器电阻比Rs/Ro。但手册并不直接给出线性公式需要自己取点拟合。更常用的做法是拿一个已知浓度的标准气标定但个人开发者没有这个条件所以一般退而求其次用电压变化做线性映射。比如预热稳定后测得清洁空气电压为0.3V某次测量电压为1.2V可以近似认为浓度成比例上升。也就是说项目里展示的“浓度值”本质上是电压的线性折算不代表真实ppm精度。真正的工业级一氧化碳检测仪需要定期用标准气体校准个人DIY项目重视的是相对变化趋势和阈值报警的可靠性。如果你需要写报告或答辩如实说明“本系统实现的是浓度相对指示绝对精度受限于传感器个体差异”比强行宣称高精度更有说服力。3. OLED显示屏驱动HAL库点亮0.96寸SSD1306并实时刷新浓度3.1 I2C接口的OLED与STM32接线这个项目里OLED选I2C接口的型号最省引脚0.96寸的SSD1306驱动芯片只需要SDA和SCL两根线外加VCC和GND。以STM32F103C8T6来说硬件I2C1对应PB6SCL和PB7SDA拉上拉电阻到3.3V。OLED模块的VCC接3.3V还是5V要仔细看模块背面的丝印说明SSD1306的驱动电压范围是3.3V到5V但逻辑电平要能匹配STM32的3.3V输出。建议VCC接3.3V避免电平不匹配导致通信不稳定。CubeMX里把PB6和PB7配置为I2C1的SCL和SDAI2C1的速度模式设为Standard Mode100KHz就行。速度快了线长的话容易出现ACK失败OLED这种小屏显示刷新率要求不高100KHz足够。生成代码后HAL库提供的I2C底层接口是HAL_I2C_Mem_Write需要基于这个函数移植SSD1306的驱动。3.2 SSD1306初始化序列与显存刷新流程SSD1306的初始化本质上就是向它的控制寄存器写入一系列配置参数。标准流程是关闭显示、设置显示时钟分频、设置多路复用比、设置显示偏移、开启内部升压、设置对比度、开启显示。这组序列值在绝大多数SSD1306数据手册里都能找到复制过来注意I2C地址是0x3C还是0x3D取决于模块上地址选择电阻的焊接位置。/* ssd1306.h中定义的I2C地址和屏幕参数 */ #define OLED_ADDR 0x3C /* 7位地址左移一位后是0x78 */ #define OLED_WIDTH 128 #define OLED_HEIGHT 64 /* ssd1306.c中初始化函数的核心部分 */ static void OLED_Init_Sequence(void) { uint8_t init_cmds[] { 0xAE, 0x00, /* 关闭显示 */ 0xD5, 0x00, 0x80, /* 显示时钟分频 */ 0xA8, 0x00, 0x3F, /* 多路复用比64行 */ 0xD3, 0x00, 0x00, /* 显示偏移0 */ 0x40, /* 起始行0 */ 0x8D, 0x00, 0x14, /* 开启内部升压 */ 0x20, 0x00, 0x00, /* 内存寻址模式水平 */ 0xA1, /* 列段重映射 */ 0xC8, /* 行扫描方向重映射 */ 0xDA, 0x00, 0x12, /* COM引脚硬件配置 */ 0x81, 0x00, 0xCF, /* 对比度0xCF */ 0xD9, 0x00, 0xF1, /* 预充电周期 */ 0xDB, 0x00, 0x40, /* VCOMH电压 */ 0xA4, /* 关闭全屏点亮 */ 0xA6, /* 正常显示非反色 */ 0xAF /* 开启显示 */ }; OLED_Write_Command(init_cmds, sizeof(init_cmds)); }这段初始化序列的思路是按SSD1306的寄存器地址逐一写入配置值顺序不能乱尤其是0x8D这个开升压命令必须在显示开启之前完成否则屏幕大概率不亮。OLED_Write_Command函数内部通过HAL_I2C_Mem_Write实现区别在于控制字节要写成0x00命令模式而不是0x40数据模式。参数说明上对比度0xCF是常用的可视性较好值如果你在强光下用可以调大到0xFF但注意功耗会上升。3.3 显示浓度值的坐标规划与刷新策略OLED显示一氧化碳浓度最少需要两行内容一行是标题比如 CO Concentration一行是动态变化的具体数值。128x64的屏幕为这个布局留了足够空间。刷新策略上有一个细节容易被忽略SSD1306没有局部刷新每次写入数据都是往显存缓冲区里改然后用一次大数据包把整个缓冲区送给屏幕。这意味着你不可能只刷新某个数字而不影响其他区域。/* 主循环中更新显示的核心代码 */ char disp_buf[8]; sprintf(disp_buf, %05.1f, concentration_ppm); OLED_ClearBuffer(); OLED_SetCursor(0, 0); OLED_DrawString(CO:, font_7x10, OLED_COLOR_WHITE); OLED_SetCursor(28, 0); OLED_DrawString(disp_buf, font_7x10, OLED_COLOR_WHITE); OLED_SetCursor(68, 0); OLED_DrawString(ppm, font_7x10, OLED_COLOR_WHITE); OLED_SendBuffer();这段代码先把显存缓冲区清空再在指定坐标画标题和数值最后一次性把缓冲内容推到屏幕。清屏再重绘的缺点是会有一瞬间的闪烁但100Hz以下刷新率人眼基本无感。如果你想避免闪烁可以改成先改缓冲区里的具体像素再整包发送。OLED_SendBuffer内部是一次I2C写操作数据量是128乘64除以8等于1024个字节加上控制字节总耗时约10毫秒这部分时间不宜放在中断里做。4. 蜂鸣器报警逻辑与串口数据上行的联合设计4.1 有源蜂鸣器与无源蜂鸣器的驱动差别蜂鸣器报警电路是整个系统的输出执行机构驱动方式取决于蜂鸣器类型。有源蜂鸣器内部自带振荡电路只要通电就会发声控制逻辑非常简单阈值超标就拉高GPIO反之拉低。无源蜂鸣器必须给一定频率的方波才能发声改变频率还能改变音调适合做“嘀嘀嘀”的交替报警音。本项目如果选有源蜂鸣器代码量最少可靠性也最高一个GPIO口就能搞定不占用定时器资源。CubeMX里把PB5配置为GPIO_Output初始化好默认电平为低报警时拉高。硬件上蜂鸣器模块通常自带三极管驱动所以STM32的GPIO直接接模块的I/O引脚就行不需要额外加放大电路。如果是裸蜂鸣器两脚那种必须通过三极管或ULN2003驱动不能直接接GPIO。4.2 两级阈值报警与回差消抖一氧化碳报警不能只设一个点因为室内和工业现场的安全标准不同。常见做法是设两个阈值轻度报警比如50ppm和重度报警比如150ppm。轻度报警蜂鸣器慢速响每500ms切换一次重度报警快速响每100ms切换一次。同时还要加回差机制防止浓度在阈值附近波动时蜂鸣器反复通断。回差的意思是浓度升到50ppm以上才触发报警但降到45ppm以下才解除报警中间5ppm的区间是滞回带。/* 报警状态机的核心逻辑 */ #define ALARM_LOW_THRESHOLD 50.0f #define ALARM_LOW_RELEASE 45.0f #define ALARM_HIGH_THRESHOLD 150.0f #define ALARM_HIGH_RELEASE 140.0f uint8_t alarm_state 0; /* 0正常1轻度2重度 */ uint32_t last_toggle 0; uint8_t buzzer_out 0; if (concentration_ppm ALARM_HIGH_THRESHOLD) { alarm_state 2; } else if (concentration_ppm ALARM_LOW_THRESHOLD) { alarm_state 1; } else if (alarm_state 1 concentration_ppm ALARM_LOW_RELEASE) { alarm_state 0; } else if (alarm_state 2 concentration_ppm ALARM_HIGH_RELEASE) { alarm_state 0; } uint32_t now HAL_GetTick(); if (alarm_state 1 (now - last_toggle 500)) { buzzer_out !buzzer_out; last_toggle now; } else if (alarm_state 2 (now - last_toggle 100)) { buzzer_out !buzzer_out; last_toggle now; } else if (alarm_state 0) { buzzer_out 0; } HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, buzzer_out ? GPIO_PIN_SET : GPIO_PIN_RESET);这段代码用HAL_GetTick做非阻塞延时主循环不会因为蜂鸣器而卡住。回差值在轻度报警和重度报警里分别用RELEASE值实现逻辑上先判断是否升级到重度再判断是否降级。这里有一个容易踩的坑如果不小心把重度报警的解除条件写成降到90ppm就解除那么从重度回到正常可能直接跳跃导致在140到90这个区间内蜂鸣器一直响所以重度释放值要设置在轻度释放值之下且留有余量。4.3 串口数据帧格式设计与打包输出串口调试助手接收的数据必须要有明确格式否则你打开SSCOM看到的全是乱码或者无法解析的数据。常见做法是设计一个简单的自定义文本帧帧头、数据值、帧尾和校验。文本帧的好处是直接用串口助手人类可读不需要上位机解析程序。/* 通过串口发送格式化数据帧 */ void Send_CO_Data(float ppm, uint8_t alarm_level) { uint8_t tx_buf[32]; uint16_t len 0; tx_buf[len] $; tx_buf[len] C; tx_buf[len] O; tx_buf[len] ,; len sprintf((char *)tx_buf[len], %.1f, ppm); tx_buf[len] ,; tx_buf[len] 0 alarm_level; tx_buf[len] #; tx_buf[len] \r; tx_buf[len] \n; HAL_UART_Transmit(huart1, tx_buf, len, 100); }帧结构用$CO开头#结尾中间是浓度和报警等级以逗号分隔。HAL_UART_Transmit的最后一个参数是超时时间100毫秒如果串口未打开或者线路异常这个函数会阻塞100毫秒后返回错误码实际使用中这个错误码可以丢弃不处理但如果你在追求实时性可以用DMA方式代替阻塞发送。波特率设置通常用9600或者115200数据位8、停止位1、无校验这是最通用的配置SSCOM和XCOM都能直接识别。5. 系统联调顺序与数据校准的实用技巧5.1 先点亮屏幕再调传感器最后测串口联调阶段最忌讳一口气把全部外设接好上电一旦出问题很难定位是哪个环节。推荐的调通顺序是先刷一个OLED亮屏测试程序确认I2C通信和初始化序列没问题再单独读ADC用串口把原始电压值打印出来看传感器预热后电压是否稳定再写蜂鸣器测试代码用按键或直接改阈值触发最后才把四部分合在一起跑。这样每步只验证一个模块出了一个新问题修复时间不会超过五分钟。常见问题是I2C的SDA拉不低模块没供电或地址错误和ADC一直读数不变连续模式下没有重新启动转换这些都是单步测试阶段就能立刻暴露的。5.2 预热特性和数据滤波的妥协建议MQ-7的响应时间大约是30到60秒但恢复时间可能长达几分钟。这意味着浓度突然升高时显示值不会瞬间跳到峰值而是缓慢爬升。如果你的项目演示时被人怀疑“数据变化太慢”这不是BUG是传感器本身的特性。为了视觉效果好一点可以在代码里对连续多次ADC采样做滑动平均滤波比如每200毫秒采一次取最近16次平均值。这能让数字跳动更平滑但对真实响应时间几乎没有帮助。参数推荐值说明传感器预热时间3-5分钟上电后前几分钟电压漂移大ADC采样间隔200ms太频繁噪声大太慢响应差滑动滤波窗口16次约3.2秒的平滑窗口串口发送间隔1秒与OLED刷新频率对齐轻度报警阈值50ppm可调需验证模块输出电压对应关系重度报警阈值150ppm可调需回差配合在代码里串口发送间隔和OLED刷新频率最好共用同一个定时器分频逻辑避免各刷各的导致数据看起来不同步。你可以在主循环里用一个500毫秒的软件计数器每两次计数触发一次串口和OLED同时更新这样调试助手里显示的值和屏幕上完全一致答辩现场不会出现两边数值对不上的尴尬。5.3 用万用表验证传感器输出电压与代码读数的对应关系一个很实用但很多人没做的就是硬件层面对比把万用表打在直流电压挡红表笔接MQ-7模块的AO引脚黑表笔接GND看万用表的电压读数再对比串口打印出来的电压值。两者误差应该在0.05V以内。如果误差明显排查方向是ADC参考电压不准板载LDO输出可能只有3.26V左右、分压电阻精度不够、或者是ADC的采样时间过短导致电容充电不充分。这一步做好了后面所有浓度换算都有底。分压电阻的精度会影响整个量程的一致性有条件就用1%精度金属膜电阻5%的碳膜电阻在温度变化下偏差较大。如果想要更稳直接把模块改为3.3V供电省掉分压电阻的环节代价是量程上限降低但家庭环境的一氧化碳检测够用。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进