基于梁山派GD32F470的MQ-4甲烷传感器ADC/DMA驱动移植与浓度监测实战最近在做一个智能家居安全监测的小项目需要检测厨房的天然气泄漏于是就用上了MQ-4甲烷传感器。这个传感器价格便宜灵敏度也不错但怎么把它和咱们的梁山派GD32F470开发板连起来稳定地读取数据呢今天我就把整个移植和调试过程包括ADC和DMA的配置手把手分享给大家让你也能快速实现甲烷浓度的实时监测。1. 认识MQ-4甲烷传感器MQ-4传感器核心是一个二氧化锡SnO2气敏材料。简单来说它在干净空气里导电性很差电阻很大。一旦周围有甲烷、天然气这类可燃气体它的导电性就会变好电阻下降。传感器模块就是把这个电阻变化转换成一个我们能测量的电压信号。这个模块有两个输出口咱们得搞清楚AOAnalog Output模拟量输出。直接输出一个电压值气体浓度越高电压越高。这个电压是连续变化的需要我们用单片机的ADC模数转换器来读取。DODigital Output数字量输出。模块内部用一个叫LM393的电压比较器把AO的电压和一个可调的阈值电压做比较。超过阈值就输出高电平比如3.3V否则输出低电平0V。这个阈值可以通过模块上的一个蓝色电位器来调节相当于调节报警的灵敏度。DO口我们直接用单片机的普通GPIO输入模式读取就行。模块基本参数工作电压3.3V - 5V 梁山派开发板有5V和3.3V输出可以直接用工作电流约150mA接口4个2.54mm间距的排针VCC, GND, DO, AO2. 硬件连接与引脚规划首先把传感器模块和梁山派开发板连起来。连线很简单就四根线传感器引脚梁山派开发板引脚说明VCC5V电源正极接5V输出GNDGND电源地接任意GNDDOPF10数字输出接普通GPIOAOPC1模拟输出必须接带有ADC功能的引脚这里有个关键点AO引脚必须连接到具有ADC模数转换功能的单片机引脚上。GD32F470的很多引脚都有复用功能不是随便一个引脚都能接ADC。怎么知道PC1有没有ADC功能呢你需要查看芯片的数据手册Datasheet。在GD32F4xx系列的数据手册里有详细的引脚功能定义表。PC1对应的是ADC0的第11输入通道ADC0_IN11或者ADC1/2的通道11。我们代码里选择用ADC0。DO引脚我们选了PF10这是一个普通的GPIO配置成输入模式来读取高低电平。3. 工程搭建与代码移植咱们的目标是在你的工程里新建两个文件bsp_mq4.c和bsp_mq4.h“bsp”是板级支持包的意思用来放外设驱动。把下面提供的代码复制进去就行。3.1 头文件定义 (bsp_mq4.h)这个文件主要进行宏定义把引脚、外设等用容易理解的名字定义好后面改起来也方便。#ifndef _BSP_MQ4_H_ #define _BSP_MQ4_H_ #include gd32f4xx.h // 时钟定义 #define RCU_MQ4_GPIO_AO RCU_GPIOC // AO引脚(PC1)的时钟 #define RCU_MQ4_GPIO_DO RCU_GPIOF // DO引脚(PF10)的时钟 #define RCU_MQ4_ADC RCU_ADC0 // 使用的ADC0时钟 #define RCU_MQ4_DMA RCU_DMA1 // 使用的DMA1时钟 // DMA定义 #define PORT_DMA DMA1 // 使用DMA1 #define CHANNEL_DMA DMA_CH0 // 使用DMA1的通道0 // ADC定义 #define PORT_ADC ADC0 // 使用ADC0 #define CHANNEL_ADC ADC_CHANNEL_11 // 使用ADC0的通道11 (对应PC1) // GPIO引脚定义 #define PORT_MQ4_AO GPIOC // AO引脚端口 #define GPIO_MQ4_AO GPIO_PIN_1 // AO引脚号 (PC1) #define PORT_MQ4_DO GPIOF // DO引脚端口 #define GPIO_MQ4_DO GPIO_PIN_11 // DO引脚号 (PF11原文为PF10请注意核对) // 采样参数 #define SAMPLES 30 // 每个通道采样30次用于求平均滤波 #define CHANNEL_NUM 1 // 使用的ADC通道数目前就MQ4一个 // 声明DMA缓冲区在.c文件中定义 extern uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM]; // 函数声明 void ADC_DMA_Init(void); unsigned int Get_Adc_Dma_Value(char CHx); unsigned int Get_MQ4_Percentage_value(void); char Get_MQ4_DO_value(void); #endif注意原文中DO引脚定义为PF10但代码里是GPIO_PIN_11这可能是笔误。请根据你的实际连接确认是PF10还是PF11并修改GPIO_MQ4_DO的定义。如果是PF10应改为GPIO_PIN_10。3.2 核心驱动实现 (bsp_mq4.c)这里是重头戏包含了ADC和DMA的初始化、数据读取和浓度计算。#include bsp_mq4.h #include systick.h // 用于延时函数 // DMA缓冲区一个二维数组用于存放DMA搬运过来的ADC原始值 // SAMPLES行30次采样CHANNEL_NUM列1个通道 uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM]; /****************************************************************** * 函数名称ADC_DMA_Init * 函数说明初始化ADC和DMA实现自动采集模拟电压 * 函数形参无 * 函数返回无 ******************************************************************/ void ADC_DMA_Init(void) { dma_single_data_parameter_struct dma_single_data_parameter; // 1. 打开所有需要用到的外设时钟 rcu_periph_clock_enable(RCU_MQ4_GPIO_AO); // 打开GPIOC时钟 rcu_periph_clock_enable(RCU_MQ4_GPIO_DO); // 打开GPIOF时钟 rcu_periph_clock_enable(RCU_MQ4_ADC); // 打开ADC0时钟 rcu_periph_clock_enable(RCU_MQ4_DMA); // 打开DMA1时钟 // 2. 配置ADC时钟ADC工作时钟不能太快这里设为系统时钟的4分频 adc_clock_config(ADC_ADCCK_PCLK2_DIV4); // 3. 配置GPIO引脚模式 // DO引脚配置为上拉输入、下拉输入或浮空输入均可这里用无上下拉的输入模式 gpio_mode_set(PORT_MQ4_DO, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_MQ4_DO); // AO引脚必须配置为模拟输入模式这是ADC采集的前提 gpio_mode_set(PORT_MQ4_AO, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_MQ4_AO); // 4. 配置ADC工作模式 adc_sync_mode_config(ADC_SYNC_MODE_INDEPENDENT); // 独立模式只用了一个ADC adc_special_function_config(PORT_ADC, ADC_CONTINUOUS_MODE, ENABLE); // 使能连续转换模式 adc_special_function_config(PORT_ADC, ADC_SCAN_MODE, ENABLE); // 使能扫描模式多通道时才需要单通道也建议开启 adc_data_alignment_config(PORT_ADC, ADC_DATAALIGN_RIGHT); // 数据右对齐方便阅读 adc_resolution_config(PORT_ADC, ADC_RESOLUTION_12B); // 12位分辨率0-4095 // 5. 配置ADC规则组通道告诉ADC要转换哪个通道按什么顺序 adc_channel_length_config(PORT_ADC, ADC_REGULAR_CHANNEL, CHANNEL_NUM); // 规则组通道数为1 // 配置第0个扫描顺序的通道为ADC_CHANNEL_11采样时间15个周期时间越长越准确但也越慢 adc_regular_channel_config(PORT_ADC, 0, CHANNEL_ADC, ADC_SAMPLETIME_15); // 6. 配置ADC触发方式 adc_external_trigger_config(PORT_ADC, ADC_REGULAR_CHANNEL, EXTERNAL_TRIGGER_DISABLE); // 禁用外部触发只用软件触发 // 7. 配置ADC与DMA联动 adc_dma_request_after_last_enable(PORT_ADC); // 使能规则组通道转换完成后产生DMA请求 adc_dma_mode_enable(PORT_ADC); // 使能ADC的DMA传输 // 8. 使能ADC并校准 adc_enable(PORT_ADC); delay_1ms(1); // 等待ADC稳定 adc_calibration_enable(PORT_ADC); // ADC校准消除内部误差非常重要 // 9. 配置DMA直接存储器访问 // DMA的作用是ADC一转换完数据就自动把这个数据搬到我们指定的数组里完全不需要CPU干预。 dma_deinit(PORT_DMA, CHANNEL_DMA); // 先复位DMA通道 // 填充DMA初始化结构体 dma_single_data_parameter.periph_addr (uint32_t)(ADC_RDATA(PORT_ADC)); // 外设地址ADC0的数据寄存器 dma_single_data_parameter.periph_inc DMA_PERIPH_INCREASE_DISABLE; // 外设地址不递增始终读同一个ADC寄存器 dma_single_data_parameter.memory0_addr (uint32_t)(gt_adc_val); // 内存地址我们的数组 dma_single_data_parameter.memory_inc DMA_MEMORY_INCREASE_ENABLE; // 内存地址递增依次填充数组 dma_single_data_parameter.periph_memory_width DMA_PERIPH_WIDTH_16BIT; // 数据宽度16位ADC是12位用16位存 dma_single_data_parameter.direction DMA_PERIPH_TO_MEMORY; // 传输方向从外设ADC到内存 dma_single_data_parameter.number SAMPLES * CHANNEL_NUM; // 传输数据量30个数据 dma_single_data_parameter.priority DMA_PRIORITY_HIGH; // DMA优先级高 // 初始化DMA dma_single_data_mode_init(PORT_DMA, CHANNEL_DMA, dma_single_data_parameter); // 10. 选择DMA通道对应的外设ADC0对应DMA_SUBPERI0 dma_channel_subperipheral_select(PORT_DMA, CHANNEL_DMA, DMA_SUBPERI0); // 11. 使能DMA循环模式并启动 dma_circulation_enable(PORT_DMA, CHANNEL_DMA); // 循环模式传输完30个数据后自动从头开始实现连续采集 dma_channel_enable(PORT_DMA, CHANNEL_DMA); // 使能DMA通道 // 12. 软件触发ADC开始转换 // 一旦触发ADC就会开始连续转换DMA会自动搬运数据到gt_adc_val数组。 adc_software_trigger_enable(PORT_ADC, ADC_REGULAR_CHANNEL); } /****************************************************************** * 函数名称Get_Adc_Dma_Value * 函数说明从DMA缓冲区中取出指定通道的ADC值并计算平均值 * 函数形参CHx - 通道序号我们只有0通道 * 函数返回指定通道ADC采样的平均值 ******************************************************************/ unsigned int Get_Adc_Dma_Value(char CHx) { unsigned char i 0; unsigned int AdcValue 0; // 累加SAMPLES次采样的数据 for(i 0; i SAMPLES; i) { AdcValue gt_adc_val[i][CHx]; } // 求平均值实现简单的软件滤波使读数更稳定 AdcValue AdcValue / SAMPLES; return AdcValue; } /****************************************************************** * 函数名称Get_MQ4_Percentage_value * 函数说明读取MQ4的AO值并换算成百分比浓度0-100% * 函数形参无 * 函数返回甲烷浓度百分比整数 ******************************************************************/ unsigned int Get_MQ4_Percentage_value(void) { int adc_max 4095; // 12位ADC的最大值 int adc_new 0; int Percentage_value 0; adc_new Get_Adc_Dma_Value(0); // 获取通道0即PC1的平均ADC值 // 将ADC值0-4095线性映射到百分比0-100 Percentage_value ((float)adc_new / adc_max) * 100; return Percentage_value; } /****************************************************************** * 函数名称Get_MQ4_DO_value * 函数说明读取MQ4的DO引脚电平状态 * 函数形参无 * 函数返回0未检测到高浓度甲烷1检测到高浓度甲烷 ******************************************************************/ char Get_MQ4_DO_value(void) { // 读取PF10引脚的电平 if(gpio_input_bit_get(PORT_MQ4_DO, GPIO_MQ4_DO) RESET) // RESET表示低电平 { return 0; // 浓度低于阈值 } else { return 1; // 浓度高于阈值报警 } }4. 在主函数中调用与测试最后我们在main.c函数里初始化这些外设然后循环打印浓度值。#include gd32f4xx.h #include systick.h #include bsp_usart.h // 串口驱动用于printf打印 #include bsp_mq4.h #include stdio.h int main(void) { // 1. 系统初始化 nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 设置中断优先级分组 systick_config(); // 初始化系统滴答定时器用于delay_ms usart_gpio_config(115200U); // 初始化串口波特率115200用于打印 printf(MQ-4 Methane Sensor Demo Start\r\n); // 2. 初始化MQ-4传感器包含ADC和DMA初始化 ADC_DMA_Init(); // 3. 主循环 while(1) { // 读取并打印甲烷浓度百分比 printf(Methane Concentration: %d%%\r\n, Get_MQ4_Percentage_value()); // 读取并打印数字输出状态 if(Get_MQ4_DO_value()) { printf(Warning: High concentration detected! (DO is HIGH)\r\n); } delay_1ms(1000); // 每隔1秒读取一次 } }将代码编译下载到梁山派开发板打开串口助手波特率115200你就能看到每秒输出一次的甲烷浓度百分比了。对着传感器吹口气含二氧化碳或者靠近打火机释放的少量气体注意安全切勿明火可以看到百分比数值上升DO状态也可能跳变为1。5. 调试心得与常见问题ADC读数跳动大这是正常的传感器本身和电源都会有噪声。我们代码里已经做了软件平均滤波采样30次求平均。如果还觉得跳可以尝试增加SAMPLES的次数比如加到100。在传感器VCC和GND之间并联一个10uF和0.1uF的电容进行硬件滤波。检查开发板的3.3V或5V电源是否稳定。DO口不报警模块上那个蓝色的电位器就是用来调节报警阈值的。用螺丝刀慢慢旋转它可以改变比较器的参考电压从而调节灵敏度。顺时针旋转一般是提高灵敏度更容易报警。百分比值代表真实浓度吗不直接代表。这个百分比只是将ADC读数线性映射到了0-100%。MQ-4的输出电压与气体浓度之间并非严格的线性关系且受温湿度影响。要进行定量测量需要先校准在洁净空气中读取一个ADC值作为“零点”。在一个已知浓度的标准甲烷气体中读取另一个ADC值。根据两点拟合出转换公式通常是指数或对数关系。我们的Get_MQ4_Percentage_value函数只是一个简单的线性演示实际项目需要替换为校准后的公式。DMA缓冲区gt_adc_val的数据会自动更新吗会的。在ADC_DMA_Init函数中我们使能了DMA的循环模式。ADC会不停地转换DMA也会自动地、循环地将新数据覆盖到gt_adc_val数组中。Get_Adc_Dma_Value函数每次读取的都是最近30次采样的平均值实现了实时更新。好了整个MQ-4传感器从硬件连接到软件驱动、从数据采集到浓度显示的流程就完成了。这套ADCDMA的采集方式不占用CPU时间效率很高你可以把核心CPU资源用在其他更重要的任务上比如网络通信、用户界面显示等。希望这篇教程能帮你快速上手。