基于GD32E230的MQ-6丙烷传感器ADC+DMA数据采集实战

发布时间:2026/7/18 15:55:29

基于GD32E230的MQ-6丙烷传感器ADC+DMA数据采集实战 基于GD32E230的MQ-6丙烷传感器ADCDMA数据采集实战最近在做一个智能燃气报警的小项目用到了MQ-6气体传感器来检测丙烷浓度。我发现很多刚开始接触GD32的朋友在配置ADC和DMA采集传感器数据时容易卡壳不是数据不对就是采集效率太低。今天我就以立创的GD32E230C8T6开发板为例手把手带你走一遍MQ-6传感器的驱动流程重点就是如何用ADCDMA实现高效、准确的数据采集并把原始ADC值转换成咱们能看懂的浓度百分比。这篇文章适合正在学习GD32或者需要做气体检测、环境监测项目的朋友。跟着做下来你不仅能驱动MQ-6更能掌握ADC和DMA配合使用的核心方法这套方法用在其他模拟传感器上也是通用的。1. 认识我们的“搭档”MQ-6气体传感器在写代码之前咱们得先了解要驱动的对象。MQ-6传感器是一个用来检测可燃气体特别是丙烷、丁烷、天然气的模块成本低用起来也方便。它核心的原理是这样的传感器内部有一种叫二氧化锡SnO₂的材料在干净的空气里它的导电性比较差。一旦周围有可燃气体这些气体会吸附在材料表面导致它的导电性变强气体浓度越高导电性就越好。模块内部已经集成了电路能把这个导电性的变化转换成电压信号输出给我们单片机。模块有4个引脚用2.54mm的排针引出非常方便插在面包板或扩展板上VCC和GND接3.3V到5V的电源。AO模拟输出这是咱们重点要用的引脚。它会输出一个模拟电压电压值随着气体浓度变化。浓度越高电压越高。我们需要用单片机的ADC模数转换器来读取这个电压。DO数字输出模块上通常还有个比较器电路比如LM393和一个可调电阻。当AO引脚电压超过某个你设定的阈值通过拧可调电阻来调DO就会输出高电平否则是低电平。这相当于一个简单的开关量报警可以直接接到单片机的普通IO口上读取。简单来说AO引脚用于精确测量浓度DO引脚用于简单的超标报警。我们这个教程主要玩转AO引脚的精确采集。2. 硬件连接与引脚规划咱们用的是立创的GD32E230C8T6开发板。首先得确定把传感器的线接到哪里。根据GD32E230的数据手册不是所有引脚都能接ADC。我们计划使用ADC0的第1通道这个通道对应芯片的PA1引脚。所以把MQ-6模块的AO引脚接到开发板的PA1上。DO引脚我们接一个普通的GPIO就行比如PB1。同时别忘记给模块接上3.3V电源和地线。总结一下硬件连接传感器引脚开发板引脚单片机功能说明AOPA1ADC0_IN1模拟信号输入用于ADC采集DOPB1GPIO输入数字开关量输出读取高低电平VCC3.3V电源模块供电GNDGND地共地3. 工程搭建与代码移植原始资料里提供了完整的bsp_mq6.c和bsp_mq6.h文件。咱们不用从头造轮子重点在于理解它。你可以从提供的资料链接里下载这两个文件然后放到自己工程的BSP板级支持包目录下。在bsp_mq6.h头文件里定义了所有用到的硬件资源和参数咱们先来过一遍// bsp_mq6.h 中的关键定义 #include gd32e23x.h // 1. 时钟定义 #define RCU_MQ6_GPIO_AO RCU_GPIOA // PA1的时钟 #define RCU_MQ6_GPIO_DO RCU_GPIOB // PB1的时钟 #define RCU_MQ6_ADC RCU_ADC // ADC0时钟 #define RCU_MQ6_DMA RCU_DMA // DMA时钟 // 2. 外设与通道定义 #define PORT_ADC ADC0 // 使用ADC0 #define CHANNEL_ADC ADC_CHANNEL_1 // 使用通道1 (PA1) #define PORT_DMA DMA0 // 使用DMA0 #define CHANNEL_DMA DMA_CH0 // 使用通道0 // 3. GPIO引脚定义 #define PORT_MQ6_AO GPIOA #define GPIO_MQ6_AO GPIO_PIN_1 // AO接PA1 #define PORT_MQ6_DO GPIOB #define GPIO_MQ6_DO GPIO_PIN_1 // DO接PB1 // 4. 采集参数 #define SAMPLES 30 // 每个通道采样30次用于求平均滤波 #define CHANNEL_NUM 1 // 总共使用1个ADC通道 // 声明DMA缓冲区用于存放ADC转换结果 extern uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM];这里有个关键点gt_adc_val数组是一个二维数组第一维是采样次数30第二维是通道数1。DMA会源源不断地把ADC转换的结果自动存到这个数组里完全不需要CPU干预。4. 核心驱动ADC与DMA的配置详解重头戏来了咱们打开bsp_mq6.c重点分析ADC_DMA_Init()这个初始化函数。我会把每一步拆开讲清楚为什么要这么做。4.1 开启硬件时钟任何外设要工作第一步永远是打开它的时钟开关。void ADC_DMA_Init(void) { /* 使能引脚时钟 */ rcu_periph_clock_enable(RCU_MQ6_GPIO_AO); // 打开GPIOA时钟 rcu_periph_clock_enable(RCU_MQ6_GPIO_DO); // 打开GPIOB时钟 /* 使能ADC时钟 */ rcu_periph_clock_enable(RCU_MQ6_ADC); // 打开ADC时钟 /* 使能DMA时钟 */ rcu_periph_clock_enable(RCU_MQ6_DMA); // 打开DMA时钟 /* 配置ADC时钟 */ rcu_adc_clock_config(RCU_ADCCK_APB2_DIV4); // ADC时钟APB2时钟/4注意rcu_adc_clock_config(RCU_ADCCK_APB2_DIV4)这行配置了ADC模块自身的时钟频率。ADC转换需要时间这个时钟不能太快否则转换会出错。通常保证ADC时钟不超过14MHz这里系统主频72MHzAPB2也是72MHz除以4后是18MHz在安全范围内。4.2 配置GPIO引脚模式时钟打开后配置引脚功能。AO引脚是模拟输入必须配置为模拟模式这样信号才能直接进入ADC模块。DO是数字输入配置为浮空输入即可。/* 配置DO为输入模式 */ gpio_mode_set(PORT_MQ6_DO, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_MQ6_DO); /* 配置AO为浮空模拟输入模式 */ gpio_mode_set(PORT_MQ6_AO, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_MQ6_AO);4.3 配置DMA设置“自动搬运工”DMA直接内存访问就像一个“自动搬运工”。它的作用是当ADC转换完成一个数据后自动把这个数据从ADC的数据寄存器“搬”到我们指定的内存数组gt_adc_val里整个过程不占用CPU。dma_parameter_struct dma_single_data_parameter; // 定义DMA配置结构体 dma_deinit(CHANNEL_DMA); // 先清除通道0之前的配置 // 开始配置DMA参数 dma_single_data_parameter.periph_addr (uint32_t)(ADC_RDATA); // 外设地址ADC数据寄存器 dma_single_data_parameter.periph_inc DMA_PERIPH_INCREASE_DISABLE; // 外设地址不增加始终读同一个寄存器 dma_single_data_parameter.memory_addr (uint32_t)(gt_adc_val); // 内存地址我们的数组 dma_single_data_parameter.memory_inc DMA_MEMORY_INCREASE_ENABLE; // 内存地址自增存到数组下一个位置 dma_single_data_parameter.periph_width DMA_PERIPHERAL_WIDTH_16BIT; // 外设数据宽度16位 dma_single_data_parameter.memory_width DMA_MEMORY_WIDTH_16BIT; // 内存数据宽度16位 dma_single_data_parameter.direction DMA_PERIPHERAL_TO_MEMORY; // 传输方向外设到内存 dma_single_data_parameter.number SAMPLES * CHANNEL_NUM; // 传输数据量30个数据 dma_single_data_parameter.priority DMA_PRIORITY_HIGH; // 优先级设为高 dma_init(CHANNEL_DMA, dma_single_data_parameter); // 初始化DMA通道 dma_circulation_enable(CHANNEL_DMA); // 使能循环模式 dma_channel_enable(CHANNEL_DMA); // 启动DMA通道这里有几个关键点循环模式dma_circulation_enable非常有用。它让DMA在搬完30个数据后不是停止而是自动回到数组开头继续搬运覆盖旧数据。这样就实现了一个“环形缓冲区”ADC可以持续不断地转换DMA持续不断地搬运我们随时都能读到最新的30个采样值。地址不自增外设地址ADC_RDATA不自增因为我们始终是从ADC的同一个数据寄存器里读数。地址自增内存地址自增这样每次搬运的数据会依次存到数组gt_adc_val[0],gt_adc_val[1]...里。4.4 配置ADC设置“采样员”DMA配置好“搬运工”接下来配置ADC这个“采样员”。/* 配置ADC为独立模式 */ // adc_sync_mode_config(ADC_SYNC_MODE_INDEPENDENT); // 单ADC时此配置可省略 /* 使能连续转换模式 */ adc_special_function_config(ADC_CONTINUOUS_MODE, ENABLE); /* 使能扫描模式 */ adc_special_function_config(ADC_SCAN_MODE, ENABLE); /* 数据右对齐 */ adc_data_alignment_config(ADC_DATAALIGN_RIGHT); /* ADC0规则组通道数量设为1 */ adc_channel_length_config(ADC_REGULAR_CHANNEL, CHANNEL_NUM); /* 配置规则通道通道1采样顺序0采样时间55.5个周期 */ adc_regular_channel_config(0, CHANNEL_ADC, ADC_SAMPLETIME_55POINT5); /* 设置ADC分辨率为12位 */ adc_resolution_config(ADC_RESOLUTION_12B); /* 禁用外部触发使用软件触发 */ adc_external_trigger_source_config(ADC_REGULAR_CHANNEL, ADC_EXTTRIG_REGULAR_NONE); adc_external_trigger_config(ADC_REGULAR_CHANNEL, ENABLE); /* 使能DMA请求 */ adc_dma_mode_enable();这里配置了ADC的工作模式连续转换扫描模式使能后ADC会按照我们设定的通道顺序这里只有一个通道不停地循环转换。采样时间ADC_SAMPLETIME_55POINT5表示ADC对输入电压进行采样的时间长度是55.5个ADC时钟周期。时间越长采样越准确但转换速度越慢。对于MQ-6这种变化不快的传感器这个时间足够了。12位分辨率ADC把0-3.3V的电压分成2^124096个等级。读到的ADC值范围是0-4095对应0V-3.3V。软件触发我们没有用外部信号来触发ADC开始转换而是用程序控制后面会看到adc_software_trigger_enable。使能DMAadc_dma_mode_enable()是关键它告诉ADC每转换完一个数据就向DMA发一个请求让DMA来把数据搬走。4.5 校准并启动ADCADC模块比较精密上电后需要一点时间稳定并且进行一次自校准来保证转换精度。/* 使能ADC */ adc_enable(); /* 等待ADC稳定 */ delay_1ms(1); // 延时1ms /* 开启ADC自校准 */ adc_calibration_enable(); /* 开启软件触发ADC转换 */ adc_software_trigger_enable(ADC_REGULAR_CHANNEL); }调用adc_software_trigger_enable后ADC就正式开始连续转换了。由于我们开启了DMA和循环模式从此以后ADC就会不知疲倦地转换DMA就会不知疲倦地把数据搬到gt_adc_val数组里完全不用CPU操心。5. 读取数据与浓度计算初始化完成后数据已经在后台自动采集了。我们只需要从gt_adc_val数组里读取并处理即可。5.1 读取并滤波ADC原始值直接读一次ADC值可能受干扰跳动所以通常采集多次求平均。我们的DMA已经帮我们连续存储了30个最新的样本。unsigned int Get_Adc_Dma_Value(char CHx) { unsigned char i 0; unsigned int AdcValue 0; /* 因为采集 SAMPLES 次故循环 SAMPLES 次 */ for(i0; i SAMPLES; i) { /* 累加 */ AdcValue gt_adc_val[i][CHx]; // CHx是通道索引我们只有0通道 } /* 求平均值 */ AdcValue AdcValue / SAMPLES; return AdcValue; }这个函数就是把DMA缓冲区里某个通道的30个最新数据加起来求个平均值这样得到的数值就稳定多了。5.2 将ADC值转换为浓度百分比MQ-6的AO引脚输出电压在0-VCC之间变化。我们假设气体浓度越高电压越高ADC值越大。一个最简单的线性化方法就是把ADC值相对于满量程4095的比例算出来当作一个“浓度百分比”参考。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_Dma_Value(0); // 获取通道0滤波后的ADC值 Percentage_value ((float)adc_new / adc_max) * 100; // 计算百分比 return Percentage_value; }提示这只是一个非常粗略的估算实际的气体浓度和ADC值不是严格的线性关系而且受环境温湿度影响很大。在产品中你需要根据传感器数据手册的曲线或者自己进行标定来建立ADC值和实际浓度ppm的换算公式。这里用百分比输出主要是为了演示和快速看到数值变化。5.3 读取数字输出DODO引脚就简单了就是个普通的高低电平。char Get_MQ6_DO_value(void) { if( gpio_input_bit_get(PORT_MQ6_DO, GPIO_MQ6_DO) RESET ) { return 0; // 低电平未检测到高浓度气体 } else { return 1; // 高电平检测到气体浓度超过阈值 } }你可以通过调节模块上的那个蓝色可调电阻电位器来改变DO触发的灵敏度。6. 在主函数中调用测试最后我们在主函数里把上面的功能跑起来。#include gd32e23x.h #include systick.h #include bsp_usart.h #include bsp_mq6.h #include stdio.h int main(void) { systick_config(); // 初始化系统滴答定时器用于延时 ADC_DMA_Init(); // 初始化ADC和DMA usart_gpio_config(115200U); // 初始化串口用于打印数据 printf(ADCDMA demo start\r\n); while(1) { // 每秒打印一次计算出的浓度百分比 printf(Gas Concentration: %d%%\r\n, Get_MQ6_Percentage_value() ); delay_1ms(1000); // 延时1秒 } }把代码编译下载到开发板打开串口助手波特率115200你就能看到每秒输出一个百分比数值。向MQ-6传感器附近吹一口气含二氧化碳或者用打火机注意安全别点火释放少量气体应该能看到这个百分比数值有明显的上升。整个流程走下来你会发现ADCDMA的模式一旦配好后期读取数据就特别省心CPU资源被大大释放可以去做其他任务。这个框架是GD32采集模拟传感器的通用方法希望你能举一反三用到自己的项目中去。如果在配置过程中遇到问题不妨回头检查一下时钟、引脚模式和DMA的地址配置这几个是最容易出错的地方。

相关新闻