ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

GD32F407 ADC+DMA多通道数据采集教程:实现16路模拟量采样

GD32F407 ADC+DMA多通道数据采集教程:实现16路模拟量采样 简介一套面向嵌入式开发者的GD32F407多通道ADC采集与DMA传输完整工程源码适合正在学习GD32系列单片机、需要实现多路模拟量同步采样的工程师或高校学生。项目基于梁山派GD32F407ZGT6开发板使用KEIL MDK-ARM V5.35环境与GD32F4xx标准固件库V3.0.0代码结构清晰可直接参考或移植。资源包共220个文件包含81个H头文件、67个C源文件以及编译生成的HEX、AXF、MAP、工程配置文件等既可用于二次开发也可对照学习GD32外设初始化、DMA传输与中断处理的完整流程。压缩包整体仅3.68MB轻量易下载。已有901人学习使用适合正在调试ADC采样不稳定、多通道切换耗时或希望提高采集效率的开发者作为实战参考。1. 项目背景与整体方案设计1.1 为什么选择GD32F407的ADCDMAGD32F407是兆易创新推出的一款基于ARM Cortex-M4内核的高性能MCU主频可以拉到200MHz片上集成了3个12位ADC每个ADC最多支持16个外部输入通道。做工业数据采集、电池电压监测、多路传感器信号采样时经常会遇到通道数多、数据量大、CPU不能一直忙于搬数据的尴尬场景。这时候用ADC规则组扫描DMA搬运是最省心也最稳定的做法。我之前在做一个16路模拟量采集模块输入信号是0~3.3V的电压要求所有通道同时持续采样采样结果不能丢同时CPU还要去处理通信和显示任务。如果用中断方式每次转换完都去读数据寄存器一个通道进一次中断16个通道就是16倍的中断开销CPU几乎什么都干不了。GD32F407的DMA直接内存访问正好可以解决这个问题ADC连续扫描16个通道每转换完一个通道就把结果自动搬运到内存数组里全程不需要CPU干预。实测下来CPU占用几乎可以忽略不计采样率还能跑得比中断方式高很多。这套方案非常适合对时序要求不太苛刻、但需要多通道连续采集的项目比如环境监测、自动化设备模拟量输入采集、电池组电压监控这类的场景。如果你手头正好有一颗GD32F407也想从零实现一个稳定可靠的16路ADC采样程序这篇内容可以直接照抄。1.2 16路采样的硬件资源规划先梳理一下需要用到的片内外设资源。GD32F407一共有3个ADC分别是ADC0、ADC1、ADC2每个ADC都有一套独立的规则通道regular channel和注入通道injected channel。规则通道就是我们做定时循环扫描时最常用的一组通道最多可以配置16个转换序列。也就是说用ADC0单独一个外设把它的16个规则通道全部利用起来刚好可以实现16路模拟输入采样。这里有一个需要注意的地方GD32F407的ADC0输入通道0~15中部分型号的通道15内部会连接到温度传感器或内部参考电压上具体要看芯片型号和数据手册的管脚定义。我在做主板上16路采集时特意避开了通道15改用ADC0的通道0~14外加ADC1的通道0来实现16路避免和内部功能冲突。如果你用的板子明确把ADC_IN15引出来了数据手册也确认可以当普通模拟输入用那直接用ADC0的0~15也可以。DMA资源方面GD32F407的DMA有DMA0和DMA1两个控制器每个控制器带8个通道。ADC0的DMA请求固定映射到DMA0的通道0这个关系需要记住因为DMA配置里的外设请求号不能配错。我在第一次配置时就是把这个映射关系搞错了结果DMA一直不工作后来翻开参考手册的DMA请求映射表才找到原因。至于ADC的触发方式最简单的做法是配置为软件触发连续转换模式让ADC自己不停地转换转换完的数据由DMA自动搬走。如果项目对采样时刻有严格要求也可以用定时器触发ADC转换比如用TIM2的TRGO事件触发这样能精确控制采样间隔。我这里先讲软件连续模式后面会补充定时器触发的改法。2. ADC采样原理与关键参数配置2.1 ADC采样工作原理逐次逼近型ADCGD32F407内部集成的ADC是12位的逐次逼近型SARADC。这个原理其实很好用生活里的猜数字游戏来理解假如要猜一个0~4095之间的数字SAR ADC不是一位一位傻猜而是先猜中间值2048比较输入电压和中间值对应电压的大小如果输入电压高说明答案在2048~4095之间于是把最高位记为1再猜中间值3072依此类推经过12次比较就能精确逼近输入电压。所以12位ADC每次转换需要至少12个比较周期再加上采样阶段充电的时间总共要几十个ADC时钟周期。理解这个原理对配置采样时间很有帮助。ADC转换时间由两部分组成采样阶段和比较阶段。采样阶段是让内部的采样保持电容充分充电到输入电压这个时间不能太短否则信号源阻抗较高时会采不准比较阶段则是逐次逼近的时间。在GD32固件库里我们只需要配置采样时间即ADC_SAMPLETIME_55POINT5之类的宏比较时间是固定的12个周期。采样时间越长转换越准但整体速度越慢这是一个需要权衡的点。我实测16路通道每路设置55.5个ADC时钟周期采样时间在ADC时钟为30MHz时单通道转换时间大概是55.51267.5个周期也就是2.25us16路一轮下来约36us。这个速度对于电压采集场景完全够用而且抗干扰能力也不错。如果你的信号源输出阻抗很低可以适当缩短采样时间到13.5个周期速度能提升不少。2.2 分辨率、采样时间与转换速度的取舍GD32F407的ADC是12位分辨率也就是满量程对应的数字量是4095。因为它没有可调节分辨率功能所以我们要在采样时间和转换速度之间做平衡。具体配置时adc_channel_length_config设置规则组通道数量adc_regular_channel_config逐个设置每个通道在序列中的位置和采样时间。关键参数我在工程里是这样定的ADC时钟频率APB2时钟设为60MHzADC预分频选6分频得到10MHz这个是ADC的输入时钟。对于GD32F407ADC时钟建议不要超过40MHz10MHz比较保守稳定性最好。采样时间选55.5周期如上所述既保证精度速度又能接受。转换模式独立模式下软件触发连续转换。也就是adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE)开启扫描模式adc_external_trigger_config(ADC0, ADC_EXTERNAL_TRIGGER_MODE_NONE, ENABLE)把外部触发关掉然后用软件触发启动。DMA请求使能ADC0的DMA请求同时DMA设置为循环模式。有一点要特别提醒对于多通道扫描每一个通道的采样时间是可以独立配置的。如果你某几个通道外接的传感器阻抗特别高可以把这几个通道单独设置成更大的采样时间比如239.5周期其他通道用小采样时间这样精度和速度兼顾。看实际需求选择合适的采样时间没有绝对最优的参数。比如只做监测采样率要求不高那全部设成55.5周期甚至239.5周期都行稳定性更好。3. DMA传输机制与配置实操3.1 DMA为什么是ADC采样的最佳搭档DMA模块的作用相当于一个搬运工它可以在没有CPU参与的情况下把外设数据寄存器里的数据搬到指定的内存数组里。每次ADC转换完数据寄存器的内容就会被DMA立刻搬走所以下一个通道的转换结果不会覆盖上一个通道。如果不用DMA规则组多通道扫描时CPU必须赶在下一个通道转换完成前把数据读走一旦没读走原来的数据就被覆盖了。这是很多初学者采集到乱码数据的根本原因。DMA的循环模式还额外解决了一个问题16个通道扫描完一轮后DMA的传输计数器会自动重新装载下一轮转换结果继续从头写入缓冲区。这样一来ADC和DMA就形成了一个自动流水线我们只需要定期去缓冲区取数即可。配合中断或者直接主循环查询可以做到数据零丢失。还有一个常被忽略的好处因为DMA是硬件搬运数据写入内存的时机和ADC转换完成是严格同步的所以每次从缓冲区读到的数据时序上不会错位。这一点在中位值滤波、平均值滤波时特别重要否则把某两个通道的数据搞混了滤波算法再正确也是白搭。3.2 基于GD32F407的ADCDMA配置步骤GD32F407的固件库函数和STM32标准库非常像但函数命名有一点区别。我这里直接给出一份我调试通过的配置代码配合注释一步一步说明。#include gd32f4xx.h #include gd32f4xx_dma.h #include gd32f4xx_adc.h #define ADC_SCAN_CHANNEL_NUM 16 // 扫描通道数 #define ADC_SAMPLE_TIMES 8 // 每通道采样次数缓冲区分组平均 uint16_t adc_buffer[ADC_SCAN_CHANNEL_NUM]; uint16_t adc_dma_buffer[ADC_SCAN_CHANNEL_NUM * ADC_SAMPLE_TIMES]; void gpio_config_adc(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_GPIOC); // 配置ADC输入引脚为模拟模式 gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3); gpio_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); gpio_mode_set(GPIOB, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3); gpio_mode_set(GPIOB, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); gpio_mode_set(GPIOC, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3); gpio_mode_set(GPIOC, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); } void dma_config_adc(void) { dma_single_data_parameter_struct dma_init_struct; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0, DMA_CH0); dma_init_struct.periph_addr (uint32_t)(ADC_RDATA(ADC0)); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory0_addr (uint32_t)adc_dma_buffer; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.direction DMA_PERIPHERAL_TO_MEMORY; dma_init_struct.number ADC_SCAN_CHANNEL_NUM * ADC_SAMPLE_TIMES; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_16BIT; dma_init_struct.memory_width DMA_MEMORY_WIDTH_16BIT; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_single_data_mode_init(DMA0, DMA_CH0, dma_init_struct); dma_circulation_enable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0); } void adc_config(void) { rcu_periph_clock_enable(RCU_ADC0); rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV6); // ADC时钟 APB2/6 adc_clock_config(ADC0, ADC_CLK_DIV6); adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, ADC_SCAN_CHANNEL_NUM); // 依次配置16个通道的采样时间和序列位置 adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 1, ADC_CHANNEL_1, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 2, ADC_CHANNEL_2, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 3, ADC_CHANNEL_3, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 4, ADC_CHANNEL_4, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 5, ADC_CHANNEL_5, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 6, ADC_CHANNEL_6, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 7, ADC_CHANNEL_7, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 8, ADC_CHANNEL_8, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 9, ADC_CHANNEL_9, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 10, ADC_CHANNEL_10, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 11, ADC_CHANNEL_11, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 12, ADC_CHANNEL_12, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 13, ADC_CHANNEL_13, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 14, ADC_CHANNEL_14, ADC_SAMPLETIME_55POINT5); adc_regular_channel_config(ADC0, 15, ADC_CHANNEL_15, ADC_SAMPLETIME_55POINT5); adc_external_trigger_config(ADC0, ADC_EXTERNAL_TRIGGER_MODE_NONE, ENABLE); adc_external_trigger_source_config(ADC0, ADC_EXT_TRIGGER_DEFAULT); adc_dma_mode_enable(ADC0); adc_enable(ADC0); delay_1ms(1); // 等待ADC稳定 adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL); }代码里有几个关键点解释一下。adc_dma_buffer为什么设置成16*8个元素因为我想让每通道连续采样8次然后软件做平均滤波这样比单次采样更稳。如果你不需要平均直接把ADC_SAMPLE_TIMES改成1缓冲区就是16个元素。ADC_RDATA(ADC0)是读取ADC0转换结果数据寄存器的宏注意在GD32固件库里数据寄存器地址不能直接写死成一个普通变量地址必须这样传给DMA的外设地址否则DMA会一直搬运一个错误的地址。还有dma_circulation_enable循环模式必须开启否则传输完一轮之后DMA就停了ADC继续转换时数据就会覆盖。我见过很多人配完DMA发现第一次采到数据后面数据就不刷新了绝大多数都是因为忘了开循环模式。3.3 定时器触发改法可选如果项目要求严格的采样间隔可以把ADC的外部触发切换成定时器触发下面是在连续转换模式基础上要改的部分。// 以TIM2为例配置为500Hz触发一次 void timer_trigger_config(void) { rcu_periph_clock_enable(RCU_TIMER2); timer_parameter_struct timer_initpara;; timer_struct_para_init(timer_initpara); timer_initpara.prescaler 59; // 60MHz/60 1MHz timer_initpara.period 1999; // 1MHz/2000 500Hz timer_initpara.alignedmode TIMER_COUNTER_EDGE; timer_initpara.counterdirection TIMER_COUNTER_UP; timer_initpara.clockdivision TIMER_CKDIV_DIV1; timer_init(TIMER2, timer_initpara); timer_primary_output_config(TIMER2, ENABLE); timer_master_output_trigger_config(TIMER2, TIMER_TRI_OUT_SRC_UPDATE); timer_enable(TIMER2); }然后把adc_external_trigger_config改成使用外部触发触发源选ADC_EXT_TRIGGER_SOURCE_TIMER2_CH0之类的对应宏。具体以固件库版本和芯片手册为准。定时器触发的好处是每一轮16通道的转换启动时刻非常均匀后续做FFT或信号分析时不会有时间抖动。4. 16路数据的可靠采集与处理4.1 多通道循环采集的缓冲区设计缓冲区设计是这套方案能不能稳定运行的核心。我见过很多工程里直接把DMA缓冲区定义成16个uint16_t然后dma_transfer_number_config也设置成16这样做当然能采集但有个隐患如果主循环读取缓冲区的时机正好和DMA写入某个通道数据重叠可能会读到半个通道的数据虽然16位读写一般是原子的但多通道之间的关联性会被破坏。尤其是要做16路同时性数据分析时最好用双缓冲区或者使用DMA传输完成中断来处理数据而不是随时乱读。我比较推荐两种做法一种是中断统一处理关闭DMA传输完成中断在中断里把adc_dma_buffer整块拷贝到另一个数组。因为DMA是循环模式每传输完一轮16*8128个数据就会触发一次传输完成中断在中断里拷贝出来的数据是完整的一帧。要注意中断里不能做太多事毕竟拷贝128个16位数据很快但如果你在中断里做16路平均、滤波、协议打包那中段时间就拉长了会影响ADC采样。另一种是查询标志位在主循环里持续检查dma_flag_get(DMA0, DMA_CH0, DMA_INT_FLAG_FTF)传输完成标志置位后拷贝数据并清标志。这种方式代码简单适合采集速率不高、主循环不忙的场景。我这里的项目用了第一种因为还要配合Modbus通信串口那边用的也是DMA空闲中断主循环不能阻塞太久。4.2 数据处理平均值滤波与坏值剔除拿到了原始数据之后不能直接用尤其是工业现场模拟信号特别容易混入毛刺和干扰。我习惯在DMA中断里做一层简单的软件滤波对每个通道的8个原始样本先去掉最大值和最小值然后对剩余6个求平均。这样既滤掉了尖峰脉冲又不影响响应速度代码量也小。uint16_t filtered_value[16]; void process_adc_data(void) { uint16_t i; for (i 0; i 16; i) { uint16_t min_val 4095, max_val 0; uint32_t sum 0; uint8_t j; for (j 0; j ADC_SAMPLE_TIMES; j) { uint16_t val adc_dma_buffer[i * ADC_SAMPLE_TIMES j]; if (val min_val) min_val val; if (val max_val) max_val val; sum val; } sum - min_val; sum - max_val; filtered_value[i] (uint16_t)(sum / (ADC_SAMPLE_TIMES - 2)); } }这段代码要加在DMA传输完成中断里或者拷贝出完整一帧后在主循环里调用。我这里用的是128个样本不是16个样本原因是每个通道采8次再滤波可信度更高。如果你的系统对实时性要求高可以把ADC_SAMPLE_TIMES设为1此时滤波就没意义了直接读原始缓冲即可。还有人会问16路通道之间的数据会不会串位只要DMA配置正确外设地址固定为ADC的数据寄存器内存地址按顺序递增就不会串。串位通常出现在你没有开扫描模式或者通道序列配置错了。调试时可以用万用表给不同通道加不同电压然后打印出来的值按通道号递增就能很容易发现有没有串位。5. 工程实践中的避坑指南5.1 引脚冲突与参考电压处理ADC输入引脚复用到模拟功能后就不能再配置成GPIO输出或者其他复用功能这点大家都知道。但容易踩的坑是某些开发板上ADC的输入引脚同时连接了板上外设比如按键、LED、传感器这会导致采样值被拉高或拉低。我一开始就在PA1上接了板载的按钮ADC采样结果永远不是预期的电压排查了半天才把跳线帽拔掉。另一个常见问题是参考电压。GD32F407的ADC参考电压信号叫VREF一般直接连接到VDDA。如果你的主供电是3.3V且电源纹波较大那么ADC测量结果会跟着纹波跳。设计PCB时一定要在VDDA引脚附近放一个0.1uF和1uF的电容并且在电源入口加磁珠隔离数字电源。我自己做了一块小板刚开始没加隔离采样值在低三位跳动加了磁珠和滤波电容之后跳动能控制在2个LSB以内。5.2 实测波形与性能优化心得项目做完后我做了几组实测。输入用可调直流稳压源从0V逐步调到3.3V读取DMA缓冲区的数据。线性度表现很好满量程误差大概在±3个LSB左右。16路同时采集时ADC单通道转换时间2.25us16路一轮大约36us对应数据更新率约27.7kHz即每通道每秒能产生约27.7k个有效样本。对于大多数传感器采集场景这个速率已经非常充裕。如果想让采样速度更快可以把ADC时钟从10MHz提升到30MHz采样时间改成13.5周期这样单通道转换时间约25.5周期对应的单通道转换时间只需要0.85us16路一轮13.6us。速度快了但对电源噪声更敏感精度可能略微下降。我平时用默认配置只有在需要高速采集时才调整这个参数。最后分享一个调试小技巧用逻辑分析仪或者串口打印每隔一段时间把filtered_value[0]的十六进制打印出来同时给通道0输入一个缓慢变化的三角波。如果在屏幕上能看到平滑的连续变化说明DMA和ADC工作都正常。如果看到数值跳变很激进优先检查DMA内存地址对齐、缓冲区大小是否匹配以及是否开启了DMA循环模式。这三个问题的排查顺序基本能解决90%的ADCDMA采集异常。本文还有配套的精品资源点击获取
返回列表