基于天空星GD32F407的MQ-135空气质量传感器ADC+DMA驱动移植实战

发布时间:2026/5/19 17:30:37

基于天空星GD32F407的MQ-135空气质量传感器ADC+DMA驱动移植实战 基于天空星GD32F407的MQ-135空气质量传感器ADCDMA驱动移植实战最近在做一个室内环境监测的小项目需要检测空气质量就选了MQ-135这款传感器。它价格便宜能检测多种有害气体挺适合咱们DIY或者学习用。但是要把它的数据稳定、高效地读进单片机光靠简单的ADC轮询可不够尤其是在需要连续监测的时候。所以这次我决定用上GD32F407的ADC配合DMA功能实现“后台自动采集”让CPU解放出来干别的活。下面我就把在天空星GD32F407开发板上移植MQ-135驱动的整个过程手把手分享给你。1. 认识你的传感器MQ-135MQ-135传感器核心是一层二氧化锡(SnO2)材料这材料在干净空气里导电性差电阻大。一旦周围空气中有氨气、硫化物、苯或者烟雾等污染气体它的电导率就会上升电阻下降。咱们就是通过测量这个电阻变化来感知空气质量的。模块有四个引脚VCC3.3V-5V、GND、DO数字输出、AO模拟输出。AO引脚这是传感器的“原始”电压信号直接反映了敏感材料电阻的变化。电压越高通常意味着气体浓度可能越高具体关系需查传感器曲线。我们需要用单片机的ADC模数转换器来读取这个模拟电压。DO引脚模块上自带了一个LM393电压比较器。它把AO的电压和一个可调的阈值电压通过板载电位器调节进行比较输出一个干净的数字高低电平。当空气质量“差”到超过你设定的阈值时DO输出低电平否则为高电平。这个引脚可以直接接到单片机的普通GPIO上配置为输入模式来读取。简单说AO用于精确测量DO用于简单报警。咱们这次两个都用上。2. 硬件连接与引脚规划首先把传感器和天空星开发板连起来。连线很简单MQ-135模块引脚连接到天空星GD32F407说明VCC3.3V 或 5V 电源引脚模块工作电压3.3V-5V开发板上有对应排针GNDGND地引脚共地非常重要AOPC1我们计划用ADC0的第11通道DOPA1任意GPIO输入即可这里选PA1为什么选PC1因为根据GD32F407的数据手册PC1引脚复用了ADC0/1/2的第11输入通道。我们计划使用ADC0。PA1就是一个普通的GPIO用于数字输入。注意MQ-135传感器需要预热刚上电时读数不稳定最好预热1-2分钟再读取数据这样才准确。3. 工程搭建与代码移植咱们的代码结构清晰主要就是两个文件bsp_mq135.c和bsp_mq135.h。你可以直接从提供的资料包里找到或者跟着我下面的一步步创建。3.1 头文件定义 (bsp_mq135.h)头文件里主要是宏定义和函数声明把硬件相关的引脚、外设通道都定义好这样程序看起来清晰以后换引脚也方便。#ifndef _BSP_MQ135_H_ #define _BSP_MQ135_H_ #include gd32f4xx.h // 时钟定义 #define RCU_MQ135_GPIO_AO RCU_GPIOC // AO引脚PC1的时钟 #define RCU_MQ135_GPIO_DO RCU_GPIOA // DO引脚PA1的时钟 #define RCU_MQ135_ADC RCU_ADC0 // ADC0时钟 #define RCU_MQ135_DMA RCU_DMA1 // DMA1时钟 // DMA与ADC外设定义 #define PORT_DMA DMA1 // 使用DMA1 #define CHANNEL_DMA DMA_CH0 // 使用DMA1的通道0 #define PORT_ADC ADC0 // 使用ADC0 #define CHANNEL_ADC ADC_CHANNEL_11 // 使用ADC0的通道11 (对应PC1) // GPIO引脚定义 #define PORT_MQ135_AO GPIOC #define GPIO_MQ135_AO GPIO_PIN_1 // PC1 #define PORT_MQ135_DO GPIOA #define GPIO_MQ135_DO GPIO_PIN_1 // PA1 // 采样参数 #define SAMPLES 30 // 每个通道采样30次用于求平均滤波 #define CHANNEL_NUM 1 // 总共使用的ADC通道数目前就MQ135的AO一个 // 声明DMA缓冲区在.c文件中定义 extern uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM]; // 函数声明 void ADC_DMA_Init(void); // 初始化ADC和DMA unsigned int Get_Adc_Dma_Value(char CHx); // 获取指定通道的平均ADC值 unsigned int Get_MQ135_Percentage_value(void); // 获取MQ135的百分比值 char Get_MQ135_DO_value(void); // 获取DO引脚的数字状态 #endif3.2 核心驱动实现 (bsp_mq135.c)这里是重头戏包含了初始化和数据读取的所有逻辑。咱们重点看ADC_DMA_Init这个函数它完成了大部分硬件配置。#include bsp_mq135.h #include board.h // 包含了delay_ms等板级支持函数 // DMA缓冲区一个二维数组行是采样次数列是通道。 // 我们的数据会由DMA自动填到这里。 uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM]; void ADC_DMA_Init(void) { dma_single_data_parameter_struct dma_single_data_parameter; // 1. 打开所有需要用到的外设时钟 rcu_periph_clock_enable(RCU_MQ135_GPIO_AO); // 打开GPIOC时钟 rcu_periph_clock_enable(RCU_MQ135_GPIO_DO); // 打开GPIOA时钟 rcu_periph_clock_enable(RCU_MQ135_ADC); // 打开ADC0时钟 rcu_periph_clock_enable(RCU_MQ135_DMA); // 打开DMA1时钟 // 2. 配置ADC时钟ADC工作时钟不能太快这里设为PCLK2的4分频 adc_clock_config(ADC_ADCCK_PCLK2_DIV4); // 3. 配置GPIO引脚模式 // DO引脚PA1配置为浮空输入用于读取数字电平 gpio_mode_set(PORT_MQ135_DO, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_MQ135_DO); // AO引脚PC1配置为模拟输入模式这是ADC采集所必需的 gpio_mode_set(PORT_MQ135_AO, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_MQ135_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); // 数据右对齐 // 5. 配置ADC通道 adc_channel_length_config(PORT_ADC, ADC_ROUTINE_CHANNEL, CHANNEL_NUM); // 规则组通道数设为1 // 将ADC0的通道11CHANNEL_ADC配置为规则组的第0个扫描序列采样时间15个周期 adc_routine_channel_config(PORT_ADC, 0, CHANNEL_ADC, ADC_SAMPLETIME_15); // 6. 其他ADC参数 adc_resolution_config(PORT_ADC, ADC_RESOLUTION_12B); // 12位分辨率 adc_external_trigger_config(PORT_ADC, ADC_ROUTINE_CHANNEL, EXTERNAL_TRIGGER_DISABLE); // 禁用外部触发用软件触发 adc_dma_request_after_last_enable(PORT_ADC); // 使能规则组转换完成后产生DMA请求 adc_dma_mode_enable(PORT_ADC); // 使能ADC的DMA传输 // 7. 使能ADC并校准 adc_enable(PORT_ADC); delay_ms(1); // 等待ADC稳定 adc_calibration_enable(PORT_ADC); // ADC校准提高精度 // 8. 配置DMA关键步骤 dma_deinit(PORT_DMA, CHANNEL_DMA); // 先复位DMA通道0的配置 // 填充DMA初始化结构体 dma_single_data_parameter.periph_addr (uint32_t)(ADC_RDATA(PORT_ADC)); // 外设地址ADC0的数据寄存器 dma_single_data_parameter.periph_inc DMA_PERIPH_INCREASE_DISABLE; // 外设地址不递增始终读同一个寄存器 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个样本 * 1个通道 dma_single_data_parameter.priority DMA_PRIORITY_HIGH; // DMA通道优先级高 // 初始化DMA dma_single_data_mode_init(PORT_DMA, CHANNEL_DMA, dma_single_data_parameter); // 9. 选择DMA通道的外设映射ADC0对应DMA_SUBPERI0 dma_channel_subperipheral_select(PORT_DMA, CHANNEL_DMA, DMA_SUBPERI0); // 10. 使能DMA循环模式并启动 dma_circulation_enable(PORT_DMA, CHANNEL_DMA); // 循环模式传输完指定数量后自动从头开始实现连续采集 dma_channel_enable(PORT_DMA, CHANNEL_DMA); // 使能DMA通道 // 11. 软件触发ADC开始转换一旦开始由于是连续模式DMA循环就会一直自动采集下去 adc_software_trigger_enable(PORT_ADC, ADC_ROUTINE_CHANNEL); }配置看起来很多但其实逻辑是一条线初始化硬件 - 配置ADC怎么采 - 配置DMA怎么搬数据 - 启动它们。一旦ADC_DMA_Init()执行完ADC就会以你设定的速度取决于ADC时钟和采样时间不停地转换每转换完一个数据DMA就自动把它搬到gt_adc_val数组里完全不用CPU干预。3.3 数据读取与处理函数初始化好了数据在后台自动存着我们怎么用呢看下面几个函数。// 函数获取指定通道的平均ADC值 // 参数 CHx: 通道索引我们只有一个通道所以传0 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]; // 从DMA缓冲区读取 } // 计算平均值实现简单的软件滤波使读数更稳定 AdcValue AdcValue / SAMPLES; return AdcValue; } // 函数将ADC值转换为百分比0-100% // 假设ADC值越大气体浓度越高电压越高。12位ADC最大值是4095。 unsigned int Get_MQ135_Percentage_value(void) { int adc_max 4095; int adc_new 0; int Percentage_value 0; adc_new Get_Adc_Dma_Value(0); // 获取通道0的平均值 Percentage_value ((float)adc_new / adc_max) * 100; return Percentage_value; } // 函数读取DO引脚的数字状态 // 返回 1: 空气质量“正常”DO输出高电平 // 返回 0: 空气质量“差”DO输出低电平触发报警 char Get_MQ135_DO_value(void) { if( gpio_input_bit_get(PORT_MQ135_DO, GPIO_MQ135_DO) RESET ) { return 0; // 低电平检测到污染 } else { return 1; // 高电平未检测到或浓度低 } }Get_Adc_Dma_Value函数的关键在于它直接读取的是DMA缓冲区gt_adc_val里的数据。这个缓冲区里的数据是DMA在后台实时更新的所以你每次读到的都是最新的采样值。求平均是为了滤除一些偶然的干扰。4. 在主函数中调用与验证最后咱们在main函数里把整个流程跑起来看看效果。#include board.h #include bsp_mq135.h #include stdio.h // 为了使用printf int main(void) { // 系统初始化时钟、延时函数等 board_init(); // 串口初始化用于打印数据到电脑 bsp_uart_init(); // 初始化MQ135的ADC和DMA ADC_DMA_Init(); printf(MQ-135 ADCDMA Demo Start\r\n); while(1) { // 读取并打印AO的百分比值 printf(Air Quality ADC Value: %d%%\r\n, Get_MQ135_Percentage_value() ); // 读取并打印DO的状态 if(Get_MQ135_DO_value() 1) { printf(DO Status: Normal\r\n); } else { printf(DO Status: Poor Air Quality Detected!\r\n); } delay_ms(1000); // 每秒打印一次 } }把代码编译下载到天空星开发板打开串口助手波特率根据你的bsp_uart_init设置通常是115200你就能看到每秒输出一次ADC百分比和DO状态。对着传感器吹口气含二氧化碳或者用打火机勿点燃靠近一下看看数值和状态会不会变化。调试小贴士如果ADC读数一直为0或4095检查硬件连接特别是AO线是否接到了PC1以及地线是否接好。如果DO状态不变化尝试调节模块上的蓝色电位器改变比较器的阈值。顺时针旋转通常提高灵敏度更容易输出低电平报警。数值跳动这是正常的传感器本身有一定波动。我们代码里做了30次平均滤波已经平滑了很多。如果要求更高可以增加SAMPLES次数或使用更复杂的滤波算法。DMA不工作检查RCU_MQ135_DMA和PORT_DMA、CHANNEL_DMA的定义是否正确对应到DMA1和通道0。这是最容易出错的地方。好了整个移植过程就是这样。这套ADCDMA的驱动框架是通用的你以后接其他模拟传感器比如MQ-2、MQ-7或者光敏电阻到不同的ADC通道只需要在头文件里改一下引脚和通道定义再稍微调整一下adc_routine_channel_config函数的参数就可以快速复用非常方便。希望这篇教程能帮你搞定MQ-135并理解ADC与DMA协同工作的妙处。

相关新闻