基于天空星STM32F407的MQ-6丙烷传感器驱动移植与浓度检测实战
基于天空星STM32F407的MQ-6丙烷传感器驱动移植与浓度检测实战
最近在做一个智能厨房安全监控的小项目,需要检测液化气(主要成分是丙烷)的泄漏。市面上常见的MQ-6传感器模块价格便宜、灵敏度高,正好符合需求。但网上的代码要么不完整,要么和我的天空星STM32F407开发板对不上,调试起来挺费劲。
今天我就把自己在天空星开发板上成功驱动MQ-6传感器的完整过程分享出来,从模块原理、硬件接线到代码逐行解析,手把手带你实现气体浓度的检测。无论你是刚接触STM32的新手,还是正在做相关项目的朋友,这篇教程都能让你少走弯路。
1. 认识你的“嗅觉器官”:MQ-6传感器模块
在写代码之前,咱们得先搞清楚要用的传感器是什么、怎么工作的。这就像打仗前得先了解自己的武器。
MQ-6是一种半导体式气敏传感器,它的核心是二氧化锡(SnO2)材料。在干净的空气中,二氧化锡的电导率比较低。一旦周围存在丙烷、丁烷、天然气这类可燃气体,气体分子会吸附在材料表面,导致它的电导率增大。空气中气体浓度越高,电导率就越大。
模块通常长这样,有4个引脚(2.54mm间距排针):
- VCC:接电源,3.3V或5V都行。
- GND:接地。
- AO:模拟量输出。直接输出一个电压信号,电压值会随着气体浓度变化。我们需要用单片机的ADC(模数转换器)来读取这个电压。
- DO:数字量输出。模块上有个比较器芯片(比如LM393),它会将AO的电压和一个可调的阈值电压进行比较。当气体浓度超过你设定的阈值(通过模块上的电位器调节),DO引脚就会输出高电平(或低电平,取决于设计),相当于一个“超标报警”开关信号。
简单理解:AO给你的是“浓度值”(具体有多浓),DO给你的是“报警信号”(浓不浓超标了)。我们的教程会教你怎么读取这两种信号。
模块关键参数:
- 工作电压:3.3V - 5V(和天空星开发板的3.3V系统完美匹配)
- 工作电流:约150mA(功耗不算小,供电要保证)
- 检测气体:对丙烷、丁烷、液化气、天然气灵敏度很高
提示:新买的MQ-6传感器需要预热!首次使用或长时间闲置后,最好先通电预热24-48小时,其输出才会稳定。平时使用也建议预热几分钟。
2. 硬件连接:把传感器“插”到开发板上
硬件连接是第一步,接错了后面全白搭。我们使用天空星STM32F407开发板。
连线方案:
| MQ-6模块引脚 | 连接到天空星开发板 | 说明 |
|---|---|---|
| VCC | 3.3V引脚 | 提供电源 |
| GND | GND引脚 | 共地,必须接! |
| AO | PC1引脚 | 我们将用ADC1的第11通道来读取模拟电压 |
| DO | PA1引脚 | 配置为普通GPIO输入,读取高低电平 |
为什么选PC1和PA1?
- AO需要ADC功能:STM32的ADC引脚是固定的,不是每个引脚都能接ADC。查天空星开发板的原理图或STM32F407的数据手册可知,PC1引脚对应着ADC1的第11输入通道,正好可用。
- DO只需要普通输入:PA1是一个普通的GPIO,配置成输入模式就能读取DO引脚的电平状态。你完全可以根据你的板子布局换成其他方便的GPIO口,代码里改一下宏定义就行。
用杜邦线按照上表连接好。接线时务必断电操作,避免短路。
3. 软件驱动移植:一步步编写代码
硬件准备好了,现在来啃最核心的软件部分。我们会在工程里创建两个文件:bsp_mq6.c(驱动实现)和bsp_mq6.h(驱动头文件)。
3.1 创建头文件 (bsp_mq6.h)
头文件主要做两件事:引脚宏定义和函数声明。把硬件连接关系用代码定义清楚,后面改起来方便。
#ifndef _BSP_MQ6_H_ #define _BSP_MQ6_H_ #include "stm32f4xx.h" /* 时钟宏定义 */ #define RCC_MQ6_GPIO_AO RCC_AHB1Periph_GPIOC // PC1的GPIO时钟 #define RCC_MQ6_GPIO_DO RCC_AHB1Periph_GPIOA // PA1的GPIO时钟 #define RCC_MQ6_ADC RCC_APB2Periph_ADC1 // ADC1的时钟 /* ADC相关宏定义 */ #define PORT_ADC ADC1 // 使用ADC1 #define CHANNEL_ADC ADC_Channel_11 // 使用第11通道,对应PC1 /* AO引脚(PC1)宏定义 */ #define PORT_MQ6_AO GPIOC #define GPIO_MQ6_AO GPIO_Pin_1 /* DO引脚(PA1)宏定义 */ #define PORT_MQ6_DO GPIOA #define GPIO_MQ6_DO GPIO_Pin_1 // 采样次数,用于软件滤波,取平均值让读数更稳定 #define SAMPLES 30 /* 函数声明 */ void ADC_MQ6_Init(void); // 初始化ADC和GPIO unsigned int Get_Adc_MQ6_Value(void); // 获取ADC原始值 unsigned int Get_MQ6_Percentage_value(void); // 获取浓度百分比 char Get_MQ6_DO_value(void); // 获取DO报警状态 #endif3.2 编写驱动源文件 (bsp_mq6.c)
这是重头戏,我们分函数讲解。
3.2.1 初始化函数ADC_MQ6_Init
这个函数负责把用到的GPIO和ADC外设配置好,让它们进入工作状态。
#include "bsp_mq6.h" #include "board.h" // 天空星开发板的板级支持包,包含了delay等函数 void ADC_MQ6_Init(void) { /* 1. 开启时钟 —— 给外设“上电” */ RCC_AHB1PeriphClockCmd(RCC_MQ6_GPIO_AO, ENABLE); // 使能GPIOC时钟(PC1) RCC_AHB1PeriphClockCmd(RCC_MQ6_GPIO_DO, ENABLE); // 使能GPIOA时钟(PA1) RCC_APB2PeriphClockCmd(RCC_MQ6_ADC, ENABLE); // 使能ADC1时钟 /* 2. 配置AO引脚(PC1)为模拟输入模式 */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_MQ6_AO; // 引脚是PC1 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; // 模拟输入模式,这是ADC专用模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度配置,模拟输入时此项影响不大 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // 不上拉也不下拉 GPIO_Init(PORT_MQ6_AO, &GPIO_InitStructure); /* 3. 配置DO引脚(PA1)为浮空输入模式 */ GPIO_InitStructure.GPIO_Pin = GPIO_MQ6_DO; // 引脚是PA1 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; // 普通输入模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // 浮空输入,因为模块DO已有输出驱动能力 GPIO_Init(PORT_MQ6_DO, &GPIO_InitStructure); /* 4. ADC公共配置 (ADC1和ADC2/3共用的一些设置) */ ADC_CommonInitTypeDef ADC_CommonInitStruct; ADC_DeInit(); // 先将ADC1复位到默认状态,这是个好习惯 ADC_CommonInitStruct.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; // 不用DMA ADC_CommonInitStruct.ADC_Mode = ADC_Mode_Independent; // 独立模式,ADC1单独工作 ADC_CommonInitStruct.ADC_Prescaler = ADC_Prescaler_Div4; // ADC时钟分频,这里ADC时钟=84MHz/4=21MHz ADC_CommonInitStruct.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; // 采样间隔,默认即可 ADC_CommonInit(&ADC_CommonInitStruct); /* 5. ADC1自身配置 */ ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 单次转换模式,每次读取都要触发一次 ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐,方便读取 ADC_InitStruct.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; // 软件触发,不用外部事件 ADC_InitStruct.ADC_NbrOfConversion = 1; // 只进行1个通道的转换 ADC_InitStruct.ADC_Resolution = ADC_Resolution_12b; // 12位分辨率,结果范围0-4095 ADC_InitStruct.ADC_ScanConvMode = DISABLE; // 非扫描模式,因为我们只用了1个通道 ADC_Init(ADC1, &ADC_InitStruct); // 注意这里直接用了ADC1,和PORT_ADC宏对应 /* 6. 配置ADC1的通道11,并设置采样时间 */ // 参数:ADC1, 通道11, 采样序列第1个, 采样周期为480个时钟周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 1, ADC_SampleTime_480Cycles); // 采样时间越长,精度一般越高,但转换速度越慢。480周期对于气体传感器这种慢变化信号足够了。 /* 7. 使能ADC1 */ ADC_Cmd(ADC1, ENABLE); }注意:
ADC_Init函数里的第一个参数,我直接写了ADC1,这和你头文件里PORT_ADC的宏定义(也是ADC1)是一致的。确保这里不要写错成其他ADC外设。
3.2.2 读取ADC原始值Get_Adc_MQ6_Value
初始化完成后,这个函数负责启动一次ADC转换,并把结果读回来。为了读数稳定,我们采用了软件平均滤波——连续采样30次取平均值。
unsigned int Get_Adc_MQ6_Value(void) { uint32_t Data = 0; // 用于累加采样值 // 循环采样SAMPLES次(30次) for(int i = 0; i < SAMPLES; i++) { // 启动一次软件触发转换 ADC_SoftwareStartConv(ADC1); // 等待转换结束标志位EOC(End Of Conversion)置位 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 读取转换结果,并累加 Data += ADC_GetConversionValue(ADC1); // 短暂延时,避免采样过于密集 delay_ms(5); } // 计算平均值并返回 Data = Data / SAMPLES; return Data; }3.2.3 计算浓度百分比Get_MQ6_Percentage_value
ADC读回来的是0-4095的数字(12位分辨率),这个函数把它转换成更直观的百分比。
unsigned int Get_MQ6_Percentage_value(void) { int adc_max = 4095; // 12位ADC最大值 int adc_new = 0; int Percentage_value = 0; adc_new = Get_Adc_MQ6_Value(); // 获取当前ADC值 // 核心换算公式: (当前值 / 最大值) * 100% // 注意:这里用float强制转换确保计算精度,结果再转回整数 Percentage_value = ((float)adc_new / (float)adc_max) * 100.f; return Percentage_value; }重要提醒:这个百分比不是真实的丙烷浓度百分比(PPM)!它只是电压值与满量程电压的比值。MQ-6的输出电压与气体浓度是非线性关系,需要根据传感器数据手册的曲线进行校准和换算,才能得到准确的PPM值。对于报警应用,你可以通过实验,确定一个“安全百分比阈值”。想获得精确浓度,需要更复杂的标定。
3.2.4 读取数字报警信号Get_MQ6_DO_value
这个函数最简单,就是读取DO引脚的电平状态。
char Get_MQ6_DO_value(void) { // 读取PA1引脚的电平 if( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == RESET ) // RESET通常代表低电平 { return 0; // 返回0,表示气体浓度未超过模块设定的阈值(未报警) } else { return 1; // 返回1,表示气体浓度超过阈值(报警!) } }提示:模块上那个蓝色的电位器就是用来调节报警阈值的。顺时针旋转,灵敏度降低(需要更高浓度才报警);逆时针旋转,灵敏度升高。你可以用打火机释放少量气体(注意安全!)来测试和调节。
4. 在主函数中调用与测试
驱动写好了,最后就是在主函数里把它们用起来。
#include "board.h" #include "bsp_uart.h" #include <stdio.h> #include "bsp_mq6.h" int main(void) { // 1. 初始化系统时钟、延时函数等 board_init(); // 2. 初始化串口1,用于打印数据到电脑,波特率115200 uart1_init(115200U); // 3. 初始化MQ-6传感器(配置ADC和GPIO) ADC_MQ6_Init(); printf("MQ-6丙烷传感器测试程序启动\r\n"); while(1) { // 4. 读取并打印浓度百分比 printf("当前丙烷浓度(相对值): %d%%\r\n", Get_MQ6_Percentage_value()); // 5. 读取并打印数字报警状态 char alarm = Get_MQ6_DO_value(); if(alarm == 1) { printf("状态: 报警!检测到高浓度气体!\r\n"); } else { printf("状态: 正常\r\n"); } printf("-----------------\r\n"); // 延时1秒后再进行下一次读取 delay_ms(1000); } }编译、下载、观察现象:
- 将代码编译后下载到天空星开发板。
- 用串口调试助手(如XCOM、Putty)打开对应的串口(通常是USART1),波特率设为115200。
- 观察输出。在清洁空气中,百分比应该是一个较低的稳定值。
- 用打火机在传感器附近快速释放少量气体(切勿长时间对准或点燃!),你会看到百分比数值迅速上升,当超过模块设定的阈值时,状态会变为“报警!”。
- 尝试旋转模块上的电位器,改变报警阈值,再次测试。
5. 可能遇到的问题与调试心得
读数跳动大:这是模拟传感器的通病。除了代码里自带的软件滤波,你可以:
- 增加
SAMPLES的采样次数,比如加到50或100。 - 在硬件上,在AO引脚和GND之间加一个0.1uF~1uF的滤波电容。
- 尝试更复杂的数字滤波算法,比如中值滤波、一阶滞后滤波。
- 增加
ADC读数始终为0或4095:
- 检查接线:确保AO线接到了PC1,VCC和GND没有接反或接触不良。
- 检查代码:确认
ADC_RegularChannelConfig函数中的通道号是ADC_Channel_11。 - 检查时钟:确保
board_init()正确配置了系统时钟,ADC的时钟分频配置正确(本例中为21MHz,在STM32F407的ADC允许范围内)。
DO状态读取不对:
- 确认DO线接到了PA1。
- 用万用表测量一下DO引脚对GND的电压。在清洁空气中,调节电位器,看电压是否会在高低电平之间变化。这能帮你确定是模块问题还是代码问题。
- 检查代码中
GPIO_ReadInputDataBit读取的电平与模块实际输出电平是否逻辑对应。有些模块设计是浓度高输出低电平,这时判断条件就要反过来。
如何得到真实的PPM浓度值? 这需要查阅MQ-6的数据手册,找到其灵敏度特性曲线(Rs/R0 vs. 气体浓度)。通常步骤是:
- 在清洁空气中长时间预热,读取此时的ADC值作为
R0(对应传感器在空气中的电阻)。 - 在已知浓度的标准气体中,读取ADC值得到
Rs。 - 根据
Rs/R0的比值,去曲线图上查找或代入公式计算,得出浓度PPM。 - 这个过程需要标准气体和密闭腔体,比较复杂。对于一般的定性报警应用,用我们教程里的相对百分比和DO报警功能已经足够。
- 在清洁空气中长时间预热,读取此时的ADC值作为
好了,整个驱动和测试流程就完成了。希望这篇教程能帮你顺利在天空星开发板上把MQ-6传感器用起来。在实际项目中,你可以把报警状态连接到蜂鸣器或LED,实现一个简单的气体泄漏报警器。如果遇到问题,不妨多看看数据手册,或者用调试器一步步跟踪一下程序,嵌入式开发就是这样在动手和调试中积累经验的。
