
基于天空星GD32F407的灰度传感器ADCDMA驱动移植与实战最近在做一个智能小车巡线的项目需要用到灰度传感器来识别地面的黑白线。我发现很多朋友在用天空星GD32F407开发板时对ADC和DMA的配合使用不太熟悉特别是如何高效地采集模拟传感器数据。今天我就来分享一下我的实战经验手把手教你如何为灰度传感器移植ADCDMA驱动。这个教程适合正在使用天空星GD32F407开发板的嵌入式开发者特别是那些在做智能小车、颜色分拣等需要精确灰度识别项目的朋友。学完这篇你不仅能掌握灰度传感器的使用更能理解ADCDMA这种高效数据采集方式的精髓。1. 认识我们的“眼睛”灰度传感器在开始写代码之前咱们先来了解一下要用的传感器。灰度传感器就像是给单片机装上了一只“眼睛”它能“看见”物体表面的灰度黑白程度。1.1 传感器工作原理我用的这款传感器模块很简单就三个引脚VCC3.3V-5V、GND、AO模拟输出。它的工作原理是这样的传感器内部有一个白色高亮发光二极管和一个光敏电阻。发光二极管发出白光照射到被测表面比如地面上的黑线或白线不同灰度的表面反射回来的光强度不同。光敏电阻接收到这些反射光它的电阻值会随着光照强度变化——光照越强电阻越小。这个变化的电阻值会转换成电压信号从AO引脚输出。颜色越深越黑反射光越弱输出电压越低颜色越浅越白反射光越强输出电压越高。注意传感器还有一个DO数字输出引脚但咱们用模拟输出更精确所以只用AO引脚。1.2 硬件连接准备连接非常简单就三根线传感器引脚连接到开发板说明VCC3.3V或5V电源工作电压3.3V-5V都可以GNDGND共地很重要AOPC1这是ADC输入引脚为什么选PC1因为根据GD32F407的数据手册PC1引脚有ADC功能具体是ADC0的第11输入通道。这个选择不是随便的后面配置ADC时会用到这个通道号。2. ADCDMA为什么是黄金搭档在讲具体移植前我想先说说为什么用ADCDMA这种方式。这是我踩过坑后的经验之谈。2.1 传统ADC采样的痛点刚开始我用的是最简单的ADC查询方式启动转换→等待转换完成→读取数据。这样做在简单应用里没问题但在需要连续、快速采集的场景下比如小车快速巡线问题就来了CPU被占用等待ADC转换时CPU啥也干不了就在那儿傻等实时性差如果同时要处理其他任务比如电机控制、通信ADC采样就会受影响容易丢数据高速采样时如果来不及读取新数据就把旧数据覆盖了2.2 DMA的妙处DMA直接存储器访问就像请了个“专职搬运工”。它的工作流程是这样的传感器 → ADC转换 → DMA自动搬运 → 内存数组整个过程不需要CPU参与ADC转换完一个数据DMA就自动把它搬到指定的内存位置然后ADC继续转换下一个DMA继续搬...如此循环。这样做的好处很明显解放CPUCPU可以专心处理其他任务保证数据连续性不会因为CPU忙而丢失数据高效批量处理可以一次性采集很多个数据然后统一处理3. 手把手移植驱动代码好了理论讲得差不多了咱们开始实战。我会把每个步骤都讲清楚包括为什么要这样配置。3.1 创建工程文件结构首先在工程里创建两个文件bsp_grayscale.c- 驱动源文件bsp_grayscale.h- 驱动头文件这个结构清晰以后要复用也很方便。3.2 头文件配置bsp_grayscale.h头文件里主要是宏定义把硬件相关的配置都放在这里以后换引脚或换通道改起来方便。#ifndef _BSP_GRAYSCALE_H_ #define _BSP_GRAYSCALE_H_ #include gd32f4xx.h // 引脚时钟定义 #define RCU_GRAYSCALE_OUT RCU_GPIOC // PC1引脚时钟 // 外设时钟定义 #define RCU_GRAYSCALE_ADC RCU_ADC0 // ADC0时钟 #define RCU_GRAYSCALE_DMA RCU_DMA1 // DMA1时钟 // DMA相关定义 #define PORT_DMA DMA1 // 使用DMA1 #define CHANNEL_DMA DMA_CH0 // 使用通道0 // ADC相关定义 #define PORT_ADC ADC0 // 使用ADC0 #define CHANNEL_ADC ADC_CHANNEL_11 // 使用第11通道对应PC1 // GPIO相关定义 #define PORT_GRAYSCALE_OUT GPIOC // PC1所在端口 #define GPIO_GRAYSCALE_OUT GPIO_PIN_1 // PC1引脚 // 采样参数 #define SAMPLES 30 // 每个通道采样30次 #define CHANNEL_NUM 1 // 总共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_Grayscale_Percentage_Value(void); #endif这里有几个关键点需要注意SAMPLES设为30意味着每个通道会连续采样30次然后我们取平均值这样可以减少偶然误差CHANNEL_NUM是1因为我们只接了一个灰度传感器。如果你要接多个这里就改成相应的数量gt_adc_val是二维数组DMA会把采样数据自动存到这里3.3 核心驱动实现bsp_grayscale.c这是重头戏我会分块讲解确保你能看懂每一行代码的作用。3.3.1 全局变量定义#include bsp_grayscale.h #include board.h // DMA缓冲区 - 存储ADC采样数据 uint16_t gt_adc_val[SAMPLES][CHANNEL_NUM];这个二维数组就是DMA的目的地。ADC转换完的数据DMA会自动搬运到这里。[SAMPLES]表示采样次数[CHANNEL_NUM]表示通道数。3.3.2 ADCDMA初始化函数这个函数比较长我把它分成几个逻辑部分讲解。第一部分时钟和引脚配置void ADC_DMA_Init(void) { /* DMA初始化功能结构体定义 */ dma_single_data_parameter_struct dma_single_data_parameter; /* 使能引脚时钟 */ rcu_periph_clock_enable(RCU_GRAYSCALE_OUT); /* 使能ADC时钟 */ rcu_periph_clock_enable(RCU_GRAYSCALE_ADC); /* 使能DMA时钟 */ rcu_periph_clock_enable(RCU_GRAYSCALE_DMA);GD32的每个外设在使用前都要先打开它的时钟这是必须的第一步。就像你要用电器得先插电一样。第二部分ADC基础配置/* 配置ADC时钟 */ adc_clock_config(ADC_ADCCK_PCLK2_DIV4); /* 配置PC1为浮空模拟输入模式 */ gpio_mode_set(PORT_GRAYSCALE_OUT, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_GRAYSCALE_OUT); /* 配置ADC为独立模式 */ adc_sync_mode_config(ADC_SYNC_MODE_INDEPENDENT); /* 使能连续转换模式 */ adc_special_function_config(PORT_ADC, ADC_CONTINUOUS_MODE, ENABLE); /* 使能扫描模式 */ adc_special_function_config(PORT_ADC, ADC_SCAN_MODE, ENABLE);这里有几个重要配置ADC_ADCCK_PCLK2_DIV4ADC时钟是PCLK2的4分频这样ADC工作频率不会太高保证转换精度浮空模拟输入模式这是ADC引脚必须的模式不能配置成上拉或下拉连续转换模式ADC会不停地转换转换完一个立即开始下一个扫描模式虽然我们只有一个通道但用扫描模式是为以后扩展预留第三部分ADC通道和触发配置/* 数据右对齐 */ adc_data_alignment_config(PORT_ADC, ADC_DATAALIGN_RIGHT); /* ADC0设置为规则组使用1个通道 */ adc_channel_length_config(PORT_ADC, ADC_ROUTINE_CHANNEL, CHANNEL_NUM); /* 配置ADC0的通道11扫描顺序为0采样时间15个周期 */ adc_routine_channel_config(PORT_ADC, 0, CHANNEL_ADC, ADC_SAMPLETIME_15); /* ADC0设置为12位分辨率 */ adc_resolution_config(PORT_ADC, ADC_RESOLUTION_12B); /* 禁用外部触发使用软件触发 */ adc_external_trigger_config(PORT_ADC, ADC_ROUTINE_CHANNEL, EXTERNAL_TRIGGER_DISABLE);关键参数说明12位分辨率ADC转换结果是0-40952^12-1数值越大表示电压越高采样时间15个周期给ADC足够的时间对输入电压采样时间太短会影响精度软件触发我们通过程序控制何时开始转换而不是靠外部信号第四部分DMA相关配置/* 使能规则组通道每转换完成一个就发送一次DMA请求 */ adc_dma_request_after_last_enable(PORT_ADC); /* 使能DMA请求 */ adc_dma_mode_enable(PORT_ADC); /* 使能ADC */ adc_enable(PORT_ADC); /* 等待ADC稳定 */ delay_ms(1); /* 开启ADC自校准 */ adc_calibration_enable(PORT_ADC);这里开启了ADC的DMA功能告诉ADC你转换完数据后别找我CPU直接让DMA来搬。第五部分DMA详细配置这是最核心的部分配置DMA如何搬运数据/* 清除DMA通道0之前配置 */ dma_deinit(PORT_DMA, CHANNEL_DMA); /* DMA初始化配置 */ dma_single_data_parameter.periph_addr (uint32_t)(ADC_RDATA(PORT_ADC)); 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; dma_single_data_parameter.direction DMA_PERIPH_TO_MEMORY; dma_single_data_parameter.number SAMPLES * CHANNEL_NUM; dma_single_data_parameter.priority DMA_PRIORITY_HIGH; dma_single_data_mode_init(PORT_DMA, CHANNEL_DMA, dma_single_data_parameter);逐行解释periph_addr外设地址这里是ADC数据寄存器的地址DMA从这里读取数据periph_inc外设地址不自增因为ADC数据总是从同一个寄存器读取memory0_addr内存地址就是我们的gt_adc_val数组memory_inc内存地址自增这样每次搬运的数据会存到数组的下一个位置number传输数量30次采样 × 1个通道 30个数据第六部分DMA通道选择和启动/* DMA通道外设选择 */ dma_channel_subperipheral_select(PORT_DMA, CHANNEL_DMA, DMA_SUBPERI0); /* 使能DMA1通道0循环模式 */ dma_circulation_enable(PORT_DMA, CHANNEL_DMA); /* 启动DMA1的通道0功能 */ dma_channel_enable(PORT_DMA, CHANNEL_DMA); /* 开启软件触发ADC转换 */ adc_software_trigger_enable(PORT_ADC, ADC_ROUTINE_CHANNEL); }提示DMA_SUBPERI0这个参数需要查数据手册。对于ADC0对应的就是DMA_SUBPERI0。如果你用的是其他外设需要根据数据手册195页的表格选择。循环模式很关键当DMA搬完30个数据后会自动回到数组开头继续搬这样就能实现不间断的连续采样。3.3.3 数据读取和处理函数初始化完成后ADC和DMA就开始自动工作了。我们需要的是读取和处理这些数据。获取ADC平均值函数unsigned int Get_Adc_Dma_Value(char CHx) { unsigned char i 0; unsigned int AdcValue 0; /* 因为采集了SAMPLES次故循环SAMPLES次 */ for(i 0; i SAMPLES; i) { /* 累加 */ AdcValue gt_adc_val[i][CHx]; } /* 求平均值 */ AdcValue AdcValue / SAMPLES; return AdcValue; }这个函数做了个简单的数字滤波把30次采样的值加起来然后除以30。这样可以消除偶然的干扰得到更稳定的值。转换为百分比函数unsigned int Get_Grayscale_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的平均值 // 转换为百分比(当前值 / 最大值) × 100 Percentage_value ((float)adc_new / adc_max) * 100; return Percentage_value; }这里有个细节要注意(float)adc_new强制转换为浮点数这样除法结果才是小数。如果直接用整数除4095/4095结果是0整数除法舍去小数部分那就错了。4. 在主函数中使用驱动写好了怎么用呢看主函数的例子#include board.h #include bsp_grayscale.h int main(void) { // 板级初始化 board_init(); bsp_uart_init(); // 初始化串口用于打印 // 初始化ADCDMA ADC_DMA_Init(); printf(ADCDMA demo start\r\n); while(1) { // 获取灰度百分比并打印 printf(%d%%\r\n, Get_Grayscale_Percentage_Value()); // 延时1秒 delay_ms(1000); } }使用起来很简单初始化板子和串口调用ADC_DMA_Init()初始化ADC和DMA在循环中调用Get_Grayscale_Percentage_Value()获取灰度百分比5. 实际测试与调试技巧代码移植完成后实际测试时我发现识别黑色表面时返回值在24%左右识别白色表面时返回值在70%左右这个差异足够让单片机区分黑白线了。如果你的数值范围不一样可能是以下原因5.1 常见问题排查问题1读到的值一直是0或4095检查硬件连接VCC、GND、AO线是否接好检查引脚配置PC1是否配置为模拟输入模式用万用表量一下AO引脚电压看是否有变化问题2数值跳动很大增加采样次数把SAMPLES从30改到50或100检查电源传感器供电是否稳定可以加个滤波电容检查采样时间ADC_SAMPLETIME_15可以改为ADC_SAMPLETIME_28或更长问题3DMA不工作检查时钟ADC、DMA、GPIO时钟是否都使能了检查DMA通道GD32F407的ADC0对应DMA1通道0别选错了检查触发确保调用了adc_software_trigger_enable5.2 性能优化建议在实际项目中我还会做这些优化动态调整采样频率巡线时快速采样比如每10ms一次平时可以慢些每100ms一次滑动平均滤波不仅求平均还保存历史数据让变化更平滑多传感器支持如果要接多个灰度传感器只需增加CHANNEL_NUM然后为每个传感器配置不同的ADC通道6. 扩展应用智能小车巡线实战这个驱动不仅仅能读灰度值在智能小车巡线中我是这样用的// 简单的巡线判断 void line_following(void) { int gray_value Get_Grayscale_Percentage_Value(); if(gray_value 30) // 黑色线 { // 小车向左转 motor_turn_left(); } else if(gray_value 60) // 白色地面 { // 小车向右转 motor_turn_right(); } else // 灰色边缘 { // 直行 motor_go_straight(); } }更高级的做法是用多个传感器比如左中右三个根据三个传感器的值综合判断小车位置实现更稳定的巡线。移植完成后ADC和DMA就在后台自动工作了你只需要调用Get_Grayscale_Percentage_Value()就能得到当前的灰度百分比。这种方式特别适合需要实时性高的应用比如小车巡线、颜色分拣等。