基于天空星HC32F4A0的MQ-9可燃气体传感器驱动移植与浓度检测实战
基于天空星HC32F4A0的MQ-9可燃气体传感器驱动移植与浓度检测实战
最近在做一个智能家居的环境监测项目,需要检测厨房的可燃气体泄漏。我选用了MQ-9传感器,搭配国产的天空星HC32F4A0PITB开发板。整个过程从看资料、配置引脚、写驱动到数据读取,踩了几个小坑,但最终调通了。今天我就把整个驱动移植和浓度检测的实战过程分享出来,如果你也在用HC32或者类似的ARM Cortex-M芯片做传感器开发,这篇教程应该能帮你省不少时间。
咱们的目标很明确:让HC32F4A0能通过MQ-9传感器,既读取模拟量浓度值,也能判断数字量报警信号,最终在串口上打印出气体浓度的百分比。
1. 认识MQ-9传感器:它如何“闻”到气体?
在写代码之前,咱们得先搞清楚手头的“兵器”。MQ-9模块其实挺简单的,核心是一小片二氧化锡(SnO2)材料。这材料有个特点:在干净空气里导电性很差,但一旦遇到一氧化碳、甲烷这类可燃气体,它的电导率就会蹭蹭往上涨。模块通过测量这个电导率的变化,就能知道气体浓度了。
模块上有两个输出口,这也是我们编程要处理的两个关键点:
- AO (Analog Output) 模拟输出:这个引脚会输出一个电压信号,电压值随着气体浓度变化。浓度越高,电压越高。我们需要用单片机的ADC(模数转换器)功能来读取这个电压,并换算成浓度值。这是做定量分析的关键。
- DO (Digital Output) 数字输出:这个引脚更“直白”,它内部有个比较器。当气体浓度超过你通过模块上蓝色电位器设定的阈值时,DO引脚就直接输出高电平(比如3.3V);低于阈值就输出低电平(0V)。我们直接用单片机的GPIO读取高低电平就行,适合做简单的报警开关。
模块需要供电,工作电压是3.3V到5V,和咱们的开发板是兼容的。它工作时电流大约150mA,加热丝需要一定的功率来维持敏感材料的工作温度。
注意:MQ-9传感器需要一段预热时间(俗称“烧机”),刚上电时读数不稳定,通常需要预热几十秒到几分钟,具体看数据手册。实际项目中,上电后最好先延时一段时间再开始正式采样。
2. 硬件连接与引脚规划
接线很简单,模块就4根线(VCC, GND, DO, AO)。关键是要把AO和DO接到开发板正确的引脚上。
根据原始资料里的代码,作者是这么规划的:
- AO引脚:连接到了HC32F4A0的PC1引脚。为什么选这个脚?因为PC1复用了ADC1的第11输入通道(ADC_CH11),正好用来接模拟信号。
- DO引脚:连接到了PA1引脚。这就是一个普通的GPIO输入引脚,用来读取高低电平。
在你的实际项目中,完全可以换成其他具有ADC功能的引脚和任意GPIO,只要在代码里把引脚定义改对就行。下面这个表格是代码里用到的引脚定义,一目了然:
| 信号名称 | 开发板引脚 | 功能 | 对应宏定义 |
|---|---|---|---|
| AO (模拟输出) | PC1 | ADC1 通道11输入 | PORT_MQ9_AO,GPIO_MQ9_AO |
| DO (数字输出) | PA1 | GPIO 输入 | PORT_MQ9_DO,GPIO_MQ9_DO |
| VCC | 3.3V/5V | 电源 | - |
| GND | GND | 地 | - |
3. 驱动代码移植与解析(手把手教学)
拿到原始代码,咱们不能直接照搬,得理解每一行在干什么。我把它拆解成几个核心函数,咱们一个一个来看。
3.1 头文件定义 (bsp_mq9.h)
头文件就像一份“配置清单”,把所有用到的参数和函数声明都放在这里。
#ifndef _BSP_MQ9_H_ #define _BSP_MQ9_H_ #include "hc32_ll.h" // 1. ADC相关配置 #define FCG_MQ9_ADC FCG3_PERIPH_ADC1 // 使能ADC1的时钟 #define PORT_ADC CM_ADC1 // ADC1外设实例 #define CHANNEL_ADC ADC_CH11 // 使用第11通道 // 2. 引脚定义 #define PORT_MQ9_AO GPIO_PORT_C // AO引脚所在端口:C口 #define GPIO_MQ9_AO GPIO_PIN_01 // AO引脚:PC1 #define GPIO_MS1100_AO_REMAP ADC12_PIN_PC1 // PC1的ADC功能重映射(这个名称可能是笔误,但功能是映射) #define PORT_MQ9_DO GPIO_PORT_A // DO引脚所在端口:A口 #define GPIO_MQ9_DO GPIO_PIN_01 // DO引脚:PA1 // 3. 采样参数 #define SAMPLES 30 // ADC采样次数,用于求平均值滤波 // 4. 函数声明 void ADC_MQ9_Init(void); // 初始化ADC和GPIO unsigned int Get_Adc_MQ9_Value(void); // 获取ADC原始值(已滤波) unsigned int Get_MQ9_Percentage_value(void); // 获取气体浓度百分比 char Get_MQ9_DO_value(void); // 获取DO引脚数字状态 #endif这里有个细节,GPIO_MS1100_AO_REMAP这个宏的名字看起来像是其他传感器(MS1100)的,但在MQ-9的代码里它被用来做PC1的ADC通道重映射。咱们理解它的作用就行,在实际项目中如果改名会让代码更清晰。
3.2 初始化函数 ADC_MQ9_Init
这个函数是驱动的核心,负责把单片机的ADC和GPIO配置好,让它们能正确读取传感器信号。
void ADC_MQ9_Init(void) { // 第一步:解锁寄存器写保护(HC32系列很多外设寄存器默认是锁定的) LL_PERIPH_WE(LL_PERIPH_ALL); stc_gpio_init_t stcGpioInit; (void)GPIO_StructInit(&stcGpioInit); // 用默认值初始化结构体 // 第二步:配置AO引脚(PC1)为模拟输入模式 stcGpioInit.u16PinAttr = PIN_ATTR_ANALOG; // 关键!必须设为模拟属性,否则ADC读不到 stcGpioInit.u16PinDir = PIN_DIR_IN; // 方向为输入 // 其他参数如上下拉、中断等,在模拟模式下通常无效,但按规范关闭 stcGpioInit.u16PullUp = PIN_PU_OFF; stcGpioInit.u16ExtInt = PIN_EXTINT_OFF; GPIO_Init(PORT_MQ9_AO, GPIO_MQ9_AO, &stcGpioInit); // 第三步:配置DO引脚(PA1)为数字输入模式,并开启内部上拉 stcGpioInit.u16PinAttr = PIN_ATTR_DIGITAL; // 数字属性 stcGpioInit.u16PullUp = PIN_PU_ON; // 开启上拉,保证引脚有确定电平 GPIO_Init(PORT_MQ9_DO, GPIO_MQ9_DO, &stcGpioInit); // 第四步:使能ADC1的时钟(外设要工作,必须先给时钟) FCG_Fcg3PeriphClockCmd(FCG_MQ9_ADC, ENABLE); // 第五步:配置ADC1的基本参数 stc_adc_init_t stcAdcInit; (void)ADC_StructInit(&stcAdcInit); stcAdcInit.u16ScanMode = ADC_MD_SEQA_SINGLESHOT; // 单次扫描模式,适合单通道采样 stcAdcInit.u16Resolution = ADC_RESOLUTION_12BIT; // 12位分辨率,读数值范围0-4095 stcAdcInit.u16DataAlign = ADC_DATAALIGN_RIGHT; // 数据右对齐,方便处理 ADC_Init(PORT_ADC, &stcAdcInit); // 第六步:将ADC通道11映射到PC1引脚(告诉ADC,第11通道的信号来自PC1) ADC_ChRemap(PORT_ADC, CHANNEL_ADC, GPIO_MS1100_AO_REMAP); // 第七步:使能我们需要的ADC通道(序列A,通道11) ADC_ChCmd(PORT_ADC, ADC_SEQ_A, CHANNEL_ADC, ENABLE); }提示:配置AO引脚时,
PIN_ATTR_ANALOG(模拟属性)是必须的。如果错误地配置为数字属性,ADC将无法正确读取引脚上的模拟电压,你可能会一直读到0或者一个固定的错误值。这个坑我踩过,调试了半天才发现是引脚模式设错了。
3.3 核心数据读取函数
初始化完成后,就要开始读数据了。原始代码提供了几个层次的数据读取函数,咱们来捋一捋。
第一层:单次ADC采样 (adc_GET)这个函数负责启动一次ADC转换,并等待转换完成。它加入了超时机制,防止程序卡死。
unsigned int adc_GET(void) { static uint16_t adcValue; __IO uint32_t TimeOut = 0UL; ADC_Start(PORT_ADC); // 发出启动转换命令 // 等待转换完成标志位EOCA(End Of Conversion A)置位 while(SET != ADC_GetStatus(PORT_ADC, ADC_FLAG_EOCA)) { if(TimeOut > 500) // 超时判断,大约500ms { ADC_ClearStatus(PORT_ADC, ADC_FLAG_EOCA); ADC_Stop(PORT_ADC); printf("ERROR = ADC 等待序列 A 转换【超时】!!\r\n"); return 0; // 超时返回0 } TimeOut++; delay_ms(1); // 延时1ms,注意这个delay_ms需要你自己实现或由系统提供 } ADC_ClearStatus(PORT_ADC, ADC_FLAG_EOCA); // 清除标志位,为下次转换准备 adcValue = ADC_GetValue(PORT_ADC, CHANNEL_ADC); // 读取转换结果 return adcValue; }第二层:均值滤波 (Get_Adc_MQ9_Value)传感器信号难免有毛刺,直接读一次值跳动会很大。常见的做法是连续采样多次,然后取平均值。这个函数就干了这件事。
unsigned int Get_Adc_MQ9_Value(void) { uint32_t Data = 0; for(int i = 0; i < SAMPLES; i++) // SAMPLES在头文件里定义为30 { Data += adc_GET(); // 累加30次采样值 delay_ms(5); // 每次采样间隔5ms,避免过于密集 } Data = Data / SAMPLES; // 求平均值 return Data; }注意:
SAMPLES(采样次数)和delay_ms(5)(采样间隔)这两个值是滤波的关键参数。30次平均对于MQ-9这类变化相对缓慢的气体信号通常足够了。如果觉得响应速度慢,可以适当减少次数,比如10次;如果觉得数据跳动大,可以增加次数或调整间隔。这需要在实际环境中测试调整。
第三层:换算为百分比 (Get_MQ9_Percentage_value)ADC读出来的是原始数字量(0-4095),我们需要把它转换成更有意义的浓度百分比。
unsigned int Get_MQ9_Percentage_value(void) { int adc_max = 4095; // 12位ADC的最大值 int adc_new = 0; int Percentage_value = 0; adc_new = Get_Adc_MQ9_Value(); // 获取滤波后的ADC值 // 核心换算公式: (当前ADC值 / 最大ADC值) * 100% Percentage_value = ((float)adc_new / (float)adc_max) * 100.f; return Percentage_value; }这里需要强调一下:这个百分比是电压百分比,不是气体浓度的真实ppm值!它表示当前传感器输出电压占ADC量程(通常是VREF,比如3.3V)的比例。要得到真实的ppm浓度,你需要根据MQ-9的数据手册,找到其灵敏度特性曲线(Rs/R0 vs. ppm),并通过公式进行换算。这个换算比较复杂,涉及对数计算和温度补偿。对于很多报警应用,用这个电压百分比设定一个阈值已经够用了。
第四层:读取数字报警信号 (Get_MQ9_DO_value)这个就简单了,直接读GPIO电平。
char Get_MQ9_DO_value(void) { if( GPIO_ReadInputPins(PORT_MQ9_DO, GPIO_MQ9_DO) == RESET ) // 读取PA1引脚电平 { return 0; // 低电平,表示气体浓度未超过模块设定的阈值 } else { return 1; // 高电平,表示浓度超标,报警! } }模块上那个蓝色的电位器就是用来调节这个报警阈值的。顺时针拧,灵敏度降低(需要更高浓度才报警);逆时针拧,灵敏度升高。
4. 在main函数中调用与验证
驱动写好了,最后就是在主函数里把它们用起来。原始代码给的例子很清晰:
#include "board.h" #include "bsp_uart.h" #include "stdio.h" #include "bsp_mq9.h" // 包含我们刚写的驱动头文件 int32_t main(void) { board_init(); // 开发板基础初始化(系统时钟、滴答定时器等) uart1_init(115200U); // 初始化串口1,用于打印数据 ADC_MQ9_Init(); // 初始化MQ-9传感器(ADC和GPIO) printf("ADC Demo Start...\r\n"); while(1) { // 读取并打印气体浓度百分比 printf("可燃气体含量 = %d%%\r\n\n", Get_MQ9_Percentage_value() ); delay_ms(1000); // 每秒打印一次 } }把代码编译下载到天空星HC32F4A0开发板,打开串口助手(波特率115200),你就能看到类似这样的输出:
ADC Demo Start... 可燃气体含量 = 15% 可燃气体含量 = 16% ...这时候,你可以尝试向传感器附近呼一口气(含二氧化碳)或者用打火机(注意安全,不要点火!)释放少量丁烷气体,观察百分比数值的变化。同时,你也可以调节模块上的电位器,观察Get_MQ9_DO_value()函数的返回值变化,体验数字报警功能。
整个移植过程就是这样。核心就是理解传感器原理、正确配置ADC、做好数据滤波和换算。代码本身不复杂,但细节决定成败,特别是引脚模式和ADC通道的映射,一定要对照数据手册和代码仔细检查。希望这篇实战教程能帮你顺利把MQ-9用起来。
