电赛ADC模块-AD9220的HAL库并行GPIO_dma配置
为了方便采集负电压或大于3.3v的信号,以及为后级的频域分析提供高质量的采样数据,我选用了 AD9220 这款高速 ADC。商家仅提供了for循环读取数据故不适用于可控采样率的信号处理类题目,本以为写个驱动只是手到擒来,没想到在配合 STM32 做 DMA 采样的时候,着实让我折腾了好一阵子。从时序的精准把控,到 DMA 搬运时遇到的各种数据异常,踩了不少坑。今天这篇文章,就把这段时间死磕 AD9220 采样的心得和解决方案记录下来,希望能帮正在调这块芯片的同学少走点弯路。
AD9220模块的简单介绍如下:
12 Bit 分辨率:能够提供 4096个量化台阶,保证了信号还原的细腻程度,非常适合对动态范围有一定要求的场景。
最高10 MSPS 采样率:每秒最高一千万次的采样速度。根据奈奎斯特采样定律,理论上可以处理最高 5MHz 的信号,应对常规的百 kHz 级别信号(如谐波分析、信号分离)绰绰有余。
并行数据输出:采用 12 根数据线直接输出标准二进制转换结果,配合时钟信号(CLK),极其适合使用微控制器的连续 GPIO 端口进行读取。
宽输入范围:模块在信号输入端配有单端转差分运放,使得输入范围拓宽为±5v。
使用方法:需要开一个IO口输出PWM,该信号的频率即为采样率,在延迟(官方说明为13ns)之后会在12个数据输出端返回标准二进制结果.从第一个时钟上升沿对模拟信号进行采样开始,对应的数字结果要经过 3 个时钟周期后才会出现在输出引脚上。因此每次搬运需要舍弃掉前四个采样点
下面为配置流程:
1、尽量将 AD9220 的 12 根数据线接到 STM32 的同一个 GPIO 端口上,并且按顺序排列,全部设置为GPIO_Input,并配置为下拉(Pull-down)。
2. 定时器配置 —— 核心“错相”设计
根据单片机主频设置预分频器(PSC)和自动重装载值(ARR),以产生目标采样率的周期。
设置PWM Generation CH1以产生pwm波,占空比设置在50%(pulse为arr的一半)
设置CH2为Output Compare CH2(输出比较模式),注意不需要勾选对应的 GPIO 输出,因为这只是单片机内部的一个触发事件。
结合咱们前面查阅的数据手册,输出延迟最大为 19ns。如果 CH1 是在计数器为 0 时翻转(上升沿),那么我们把 CH2 的比较值(CCR2)设在总计数周期的后半段(例如ARR * 3 / 4的位置)。这样一来,DMA 读取数据的动作就完美避开了数据线上跳变毛刺的危险期,正好踩在数据最稳的平坦区。
3、开启dma请求
针对TIM2_CH2开启dma请求,normal模式
4、软件使能
最后,用一小段流程说明在代码里如何让这套配置跑起来:
先设置 DMA 传输完成回调函数(接管中断)。
调用
HAL_DMA_Start_IT告诉 DMA 从哪搬到哪,搬目标数据的n+4个。调用
__HAL_TIM_ENABLE_DMA挂载通道 2 的 DMA 触发源。调用
HAL_TIM_PWM_Start和HAL_TIM_OC_Start,发射时钟脉冲,系统开始自动运行。
下面是我自己写的配套驱动代码,仅供参考
#include "bsp_system.h" #include <math.h> // 需要与你自己的名称保持一致 extern DMA_HandleTypeDef hdma_tim2_ch2; extern TIM_HandleTypeDef htim2; /** * @brief DMA 传输完成回调函数 */ static void AD9220_DMA_CpltCallback(DMA_HandleTypeDef *hdma) { if (hdma->Instance == hdma_tim2_ch2.Instance) { AD9220_Stop_DMA(); /* 呼叫应用层的回调函数,交由 main.c 去执行具体逻辑 */ AD9220_ConvCpltCallback(); } } /** * @brief 弱定义应用层回调函数 * 如果 main.c 中没有写此函数,编译器会执行这个空函数;如果写了,则优先执行 main.c 中的版本。 */ __weak void AD9220_ConvCpltCallback(void) { /* 默认不执行任何操作 */ } /** * @brief 启动 AD9220 的 DMA 采集 * @param adc_buffer 目标数组指针 * @param buffer_length 采集长度 */ void AD9220_Start_DMA(uint16_t *adc_buffer, uint32_t buffer_length) { // 1. 停止之前的传输,防止状态错误 HAL_TIM_Base_Stop(&htim2); HAL_DMA_Abort(&hdma_tim2_ch2); // 2. 配置自定义的传输完成回调 hdma_tim2_ch2.XferCpltCallback = AD9220_DMA_CpltCallback; // 3. 启动 DMA (我这里是从 GPIOC搬到目标内存,需要根据你的配置进行修改) HAL_DMA_Start_IT(&hdma_tim2_ch2, (uint32_t)&GPIOC->IDR, (uint32_t)adc_buffer, buffer_length); // 4. 【核心防毛刺逻辑】:开启定时器 通道2(CC2) 的 DMA 请求 // 此时 DMA 的触发时刻取决于 TIM2_CCR2 的值,从而实现错相读取,完美避开数据翻转沿 __HAL_TIM_ENABLE_DMA(&htim2, TIM_DMA_CC2); // 5. 启动时钟与触发源 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // CH1 输出 PWM 作为 AD9220 的转换时钟 HAL_TIM_OC_Start(&htim2, TIM_CHANNEL_2); // CH2 输出比较事件用来触发 DMA } /** * @brief 停止 AD9220 的 DMA 采集 */ void AD9220_Stop_DMA(void) { // 安全起见,先关闭通道输出 HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); HAL_TIM_OC_Stop(&htim2, TIM_CHANNEL_2); __HAL_TIM_DISABLE(&htim2); // 停止定时器基础时钟 // 关闭 通道2 的 DMA 请求并终止 DMA 传输 __HAL_TIM_DISABLE_DMA(&htim2, TIM_DMA_CC2); HAL_DMA_Abort(&hdma_tim2_ch2); } //ADC数据处理,仅供参考 void process_data_ad9220(const uint16_t *data_ori, fftin *data_processed) { // AD9220 是 12 位 ADC,假设输入范围为 10V (例如 ±5V,根据实际运放配置) const float32_t voltage_scale = 10.0f / 4096.0f; float32_t sum = 0.0f; float32_t dc_offset_raw = 0.0f; // 1. 计算直流偏置 (舍弃前 4 个可能不稳定的数据点) for (uint32_t i = 0; i < FFT_N; i++) { sum += (float32_t)(data_ori[i + 4] & 0x0FFF); } dc_offset_raw = sum / (float32_t)FFT_N; // 2. 去除直流偏置,缩放电压,并填充虚部为 0 for (uint32_t i = 0; i < FFT_N; i++) { float32_t raw_centered = (float32_t)(data_ori[i + 4] & 0x0FFF) - dc_offset_raw; data_processed->cmp[2 * i] = raw_centered * voltage_scale; data_processed->cmp[2 * i + 1] = 0.0f; } }