基于GD32E230C8T6的MQ-3酒精传感器ADC与DMA驱动移植实战最近在做一个便携式酒精检测仪的项目核心就是用国产的GD32E230C8T6单片机来读取MQ-3传感器的数据。很多刚开始接触GD32或者传感器采集的朋友可能会对ADC和DMA的配合使用感到头疼。今天我就把自己在立创开发板上移植MQ-3驱动的整个过程和踩过的坑手把手地分享给大家目标是让你看完就能在自己的板子上跑起来。咱们这个教程会从MQ-3模块的原理讲起然后一步步配置GD32的ADC和DMA最后实现一个能稳定读取酒精浓度百分比并串口输出的程序。整个过程我会尽量讲得细一些特别是那些容易出错的地方。1. 认识MQ-3酒精传感器模块在写代码之前咱们得先搞清楚要驱动的对象是什么。MQ-3模块其实挺简单的它本质上是一个“气敏电阻”。1.1 模块工作原理MQ-3传感器的核心材料是二氧化锡SnO₂。在干净的空气里它的导电性比较差电阻大。一旦周围空气中出现酒精蒸汽材料的导电性就会变好电阻变小而且酒精浓度越高导电性就越好。模块板子上的电路就是把这个电阻的变化转换成了我们可以测量的电压信号。简单理解就是酒精浓度越高模块AO引脚输出的电压就越高。1.2 模块引脚与参数拿到模块你会看到一排4个引脚2.54mm间距分别是VCC供电脚接3.3V或5V都行。GND接地。DO数字量输出。模块上有个比较器电路LM393当AO引脚电压超过一个设定的阈值通过板载电位器可调DO就会输出高电平否则是低电平。你可以把它当成一个简单的“超标报警”开关。AO模拟量输出。这就是传感器最原始的电压信号输出端我们需要用单片机的ADC来读取这个引脚上的电压。模块的主要工作参数如下工作电压3.3V - 5V工作电流约150mA输出信号AO为模拟电压DO为数字开关量注意MQ-3对酒精很敏感但它也会受到汽油、烟雾等气体的干扰。所以它适合做定性或半定量的检测比如判断“有无酒精”或“浓度大致范围”如果要非常精确的定量可能需要更专业的传感器和复杂的校准。2. 硬件连接与引脚规划我使用的是立创的GD32E230C8T6开发板。首先得确定把传感器接在哪个引脚上。2.1 引脚选择思路DO引脚就是个普通的数字输入接任何一个GPIO都行。关键是AO引脚它必须连接到单片机具有ADC模数转换功能的引脚上。我们需要去查GD32E230C8T6的数据手册GD32E230xx_Datasheet_Rev2.3.pdf的第19页附近有引脚定义图。我选择的是PA1引脚因为它复用了ADC0的第1通道ADC_CHANNEL_1。DO引脚我随便选了个PB1。最终的连接方案如下表传感器引脚开发板引脚单片机功能说明VCC3.3V-供电GNDGND-接地AOPA1ADC0_IN1模拟信号输入DOPB1GPIO输入数字报警输出2.2 代码中的引脚宏定义为了方便修改和维护我们把用到的引脚和资源在头文件里用宏定义好。打开或创建bsp_mq3.h文件写入以下内容#ifndef _BSP_MQ3_H_ #define _BSP_MQ3_H_ #include gd32e23x.h // 时钟宏定义 #define RCU_MQ3_GPIO_AO RCU_GPIOA // PA1的GPIO时钟 #define RCU_MQ3_GPIO_DO RCU_GPIOB // PB1的GPIO时钟 #define RCU_MQ3_ADC RCU_ADC // ADC时钟 #define RCU_MQ3_DMA RCU_DMA // DMA时钟 // DMA资源宏定义 #define PORT_DMA DMA1 // 使用DMA1 #define CHANNEL_DMA DMA_CH0 // 使用通道0 // ADC资源宏定义 #define PORT_ADC ADC0 // 使用ADC0 #define CHANNEL_ADC ADC_CHANNEL_1 // 使用通道1 (对应PA1) // AO引脚(模拟输入)宏定义 #define PORT_MQ3_AO GPIOA #define GPIO_MQ3_AO GPIO_PIN_1 // DO引脚(数字输入)宏定义 #define PORT_MQ3_DO GPIOB #define GPIO_MQ3_DO GPIO_PIN_1 // 采样参数 #define SAMPLES 10 // 每个通道采样10次用于求平均滤波 #define CHANNEL_NUM 1 // 使用的ADC通道数这里只有通道1 // 声明外部变量和函数 extern uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM]; // DMA缓冲区 void ADC_DMA_Init(void); unsigned int Get_Adc_Dma_Value(char CHx); unsigned int Get_MQ3_Percentage_value(void); char Get_MQ3_DO_value(void); #endif这里有个关键点gt_adc_val数组是一个二维数组第一维是采样次数第二维是通道序号。因为我们只用了一个通道所以它相当于存储了这个通道连续10次的ADC转换结果。3. ADC与DMA驱动代码详解这是整个项目的核心。我们的目标是让ADC自动、连续地转换并且转换完的数据通过DMA直接搬到内存里完全不需要CPU干预。这样效率最高CPU可以腾出手来做其他事情。3.1 初始化函数 ADC_DMA_Init这个函数比较长我们拆成几部分来看。首先在bsp_mq3.c文件中包含必要的头文件并定义DMA缓冲区。#include bsp_mq3.h #include systick.h #include stdio.h // DMA缓冲区用于存储ADC转换结果 uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM]; void ADC_DMA_Init(void) { /* DMA初始化功能结构体定义 */ dma_parameter_struct dma_single_data_parameter; /* 第一部分开启所有需要的时钟 */ rcu_periph_clock_enable(RCU_MQ3_GPIO_AO); // 开启GPIOA时钟 rcu_periph_clock_enable(RCU_MQ3_GPIO_DO); // 开启GPIOB时钟 rcu_periph_clock_enable(RCU_MQ3_ADC); // 开启ADC时钟 rcu_periph_clock_enable(RCU_MQ3_DMA); // 开启DMA时钟 /* 配置ADC时钟源这里设为APB2时钟的4分频 */ rcu_adc_clock_config(RCU_ADCCK_APB2_DIV4);时钟是第一步也是最容易忘的一步。如果不开时钟后续所有配置都无效。ADC有自己独立的时钟配置函数rcu_adc_clock_config需要注意。/* 第二部分配置GPIO引脚模式 */ // DO引脚PB1配置为浮空输入模式用于读取高低电平 gpio_mode_set(PORT_MQ3_DO, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_MQ3_DO); // AO引脚PA1配置为模拟输入模式这是ADC采集所必需的 gpio_mode_set(PORT_MQ3_AO, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_MQ3_AO);GPIO模式配置是关键。用于ADC的引脚必须设置为GPIO_MODE_ANALOG模拟模式否则无法正确读取外部电压。/* 第三部分配置DMA */ // 先清除DMA通道0之前的配置避免冲突 dma_deinit(CHANNEL_DMA); // 配置DMA参数结构体 dma_single_data_parameter.periph_addr (uint32_t)(ADC_RDATA(ADC0)); // 外设地址ADC0的数据寄存器 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; // 传输数据量10个样本 * 1个通道 dma_single_data_parameter.priority DMA_PRIORITY_HIGH; // DMA优先级设为高 // 初始化DMA通道 dma_init(CHANNEL_DMA, dma_single_data_parameter); // 使能DMA循环模式传输完指定数量后自动从头开始实现连续采集 dma_circulation_enable(CHANNEL_DMA); // 启动DMA通道 dma_channel_enable(CHANNEL_DMA);DMA配置是重点。这里我解释几个关键点periph_addrADC转换完的数据都放在一个叫RDATA的寄存器里DMA就是从这里把数据“搬走”。memory_inc必须使能这样每次搬运后目标内存地址会自动增加数据才能依次存到数组里而不是覆盖同一个位置。number设置DMA一次“搬运循环”要传输多少数据。这里我们设成10意味着DMA搬完10个数据后会触发一次“传输完成”事件如果使能了中断的话然后因为开启了循环模式它会立刻从头开始下一轮搬运。循环模式这个模式对于连续采集非常有用。设置好后DMA就会不知疲倦地自动搬运ADC数据我们只需要从数组里读取即可。/* 第四部分配置ADC */ // 使能连续转换模式ADC转换完一次后立刻开始下一次转换不间断。 adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE); // 使能扫描模式虽然我们只有一个通道但养成好习惯多通道时必需。 adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); // 数据右对齐12位转换结果存放在16位寄存器的低12位这是最常用的方式。 adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); // 设置规则组通道长度我们用了1个通道 adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, CHANNEL_NUM); // 配置规则组通道第0个扫描顺序是通道1采样时间设为13.5个周期 adc_regular_channel_config(ADC0, 0, CHANNEL_ADC, ADC_SAMPLETIME_13POINT5); // 设置ADC分辨率为12位 adc_resolution_config(ADC0, ADC_RESOLUTION_12B); // 禁用外部触发使用软件触发启动转换 adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC_EXTTRIG_REGULAR_NONE); adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); // 使能ADC模块 adc_enable(ADC0); // 等待ADC稳定延时一下 delay_1ms(1); // 开启ADC自校准这个很重要能减少转换误差 adc_calibration_enable(ADC0); /* 第五部分联动ADC与DMA并启动转换 */ // 使能ADC的DMA请求功能每次转换完成就请求DMA来搬运数据 adc_dma_mode_enable(ADC0); // 软件触发ADC开始转换。由于开启了连续模式触发一次就会一直转换下去。 adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); }ADC配置部分有几个参数需要注意采样时间ADC_SAMPLETIME_13POINT5表示采样保持13.5个ADC时钟周期。时间越长采样越准确但转换速度越慢。对于MQ-3这种变化不快的信号这个时间足够了。软件触发我们没用外部信号触发所以用软件触发一次然后靠“连续转换模式”让它自己循环起来。校准adc_calibration_enable一定要调用它能校正ADC内部的误差让读数更准。提示整个初始化的顺序有个小讲究先配DMA再配ADC最后使能ADC的DMA模式和触发转换。这样可以确保ADC一开始转换DMA就已经准备好搬运了不会丢失数据。3.2 读取与计算函数初始化完成后ADC和DMA就在后台自动运行了gt_adc_val数组里会自动更新最新的10个采样值。我们只需要读这个数组并处理就行。// 获取指定通道ADC采样值的平均值 unsigned int Get_Adc_Dma_Value(char CHx) { unsigned char i 0; unsigned int AdcValue 0; // 累加当前通道的 SAMPLES 次采样值 for(i0; i SAMPLES; i) { AdcValue gt_adc_val[i][CHx]; } // 求平均值实现简单的滤波使读数更稳定 AdcValue AdcValue / SAMPLES; // 可以打印出来调试需要初始化串口 // printf(ADC Value:%d\r\n, AdcValue); return AdcValue; } // 将ADC值转换为百分比浓度简易换算 unsigned int Get_MQ3_Percentage_value(void) { int adc_max 4095; // 12位ADC最大值是2^12 - 1 4095 int adc_new 0; int Percentage_value 0; // 获取通道0我们唯一的通道的平均ADC值 adc_new Get_Adc_Dma_Value(0); // 计算百分比(当前值 / 最大值) * 100% // 注意这里用float强制转换是为了避免整数除法丢失精度 Percentage_value ((float)adc_new / adc_max) * 100; return Percentage_value; } // 读取DO引脚状态用于阈值报警 char Get_MQ3_DO_value(void) { // 读取PB1引脚的电平 if( gpio_input_bit_get(PORT_MQ3_DO, GPIO_MQ3_DO) RESET ) { return 0; // 低电平未检测到超标酒精 } else { return 1; // 高电平检测到酒精浓度超过模块设定的阈值 } }这里有两个重要的点平均值滤波Get_Adc_Dma_Value函数对10次采样求平均。这是一个非常基础但有效的软件滤波方法可以消除一些随机干扰让显示的值不会跳得太厉害。百分比换算Get_MQ3_Percentage_value函数把ADC值换算成百分比。这是一个非常简易的线性换算它假设ADC值从0到4095直接对应酒精浓度从0%到100%。在实际应用中传感器响应和ADC电压通常不是完美的线性关系你可能需要根据传感器数据手册做一个更精确的换算表或公式。但对于初步的演示和定性判断这个方法简单直接。4. 主函数与实验验证最后我们写一个简单的主函数来测试整个驱动。确保你的工程已经包含了串口初始化代码bsp_usart.h和bsp_usart.c用于打印结果。#include gd32e23x.h #include systick.h #include bsp_usart.h #include bsp_mq3.h #include stdio.h int main(void) { // 系统初始化 systick_config(); // 初始化系统滴答定时器用于delay_ms函数 ADC_DMA_Init(); // 初始化ADC和DMA usart_gpio_config(115200U); // 初始化串口波特率115200 printf(MQ-3 ADC DMA Demo Start\r\n); while(1) { // 读取并打印酒精浓度百分比 printf(Alcohol Concentration: %d%%\r\n, Get_MQ3_Percentage_value() ); // 读取并打印数字输出状态 if(Get_MQ3_DO_value()) { printf(DO Status: ALARM (Alcohol Detected!)\r\n); } else { printf(DO Status: Normal\r\n); } printf(--------------------\r\n); // 延时1秒 delay_1ms(1000); } }将代码编译下载到开发板连接好串口助手。你应该能看到每秒输出一次信息包括一个百分比数值和DO引脚的状态。实验现象当传感器周围没有酒精时输出的百分比值较低接近0%DO状态为“Normal”。向传感器附近喷洒少量酒精例如医用酒精或含酒精的洗手液百分比数值会迅速上升当超过模块上蓝色电位器设定的阈值时DO状态会变为“ALARM”。5. 调试心得与常见问题没有数据或数据不变首先检查硬件连接尤其是AO和DO线是否接反。然后用万用表量一下AO引脚对地电压对着传感器吹口气或者喷点酒精看电压是否有变化。如果电压有变化但ADC读数不变重点检查GPIO是否配置为模拟输入模式GPIO_MODE_ANALOG以及ADC和DMA的时钟是否使能。ADC值跳动很大这是正常的因为传感器信号和电路都有噪声。我们代码里已经做了10次平均滤波。如果跳动还是太大可以尝试增加SAMPLES的次数比如50次或者调整ADC的采样时间ADC_SAMPLETIME加长采样时间可以提高抗干扰能力但会降低采样率。DMA传输不成功检查dma_init中的number传输数量是否和数组大小匹配。确保dma_circulation_enable被调用。最关键的检查adc_dma_mode_enable是否在adc_enable和adc_calibration_enable之后调用。百分比换算不直观前面说了我们的换算是线性的可能和实际浓度不成比例。你可以先用已知浓度的酒精注意安全记录下几个浓度点对应的ADC原始值然后在代码里做一个查表法或者分段线性拟合这样显示会更准确。这个项目把GD32的ADC和DMA都用上了算是接触到了一个相对高效的采集模式。一旦调通你会发现DMA真是个好东西CPU几乎不用管数据搬运的事。希望这个详细的步骤能帮你顺利驱动MQ-3传感器并理解ADC与DMA协同工作的流程。