3、ADC数据采集与缓存:ADC驱动开发、DMA传输机制、双缓冲策略

好,咱们今天聊聊ADC数据采集与缓存这块。说实话,这是雷达信号处理里最基础也最容易翻车的一环。你想想看,ADC采进来的数据要是丢了或者乱了,后面做FFT、做CFAR全是白搭。我这些年调试下来,发现很多问题其实都出在驱动和缓存策略上。

3.1 ADC驱动开发——别小看初始化顺序

ADC驱动开发,说白了就是让芯片的模数转换器按照你的心意干活。我个人的习惯是,先把硬件手册翻烂,搞清楚几个关键寄存器:采样率配置、分辨率选择、通道使能、触发源选择。

这里有个坑,我踩过好几次——初始化顺序。有些ADC芯片要求先配置时钟,再配置通道,最后才使能转换。顺序搞反了,出来的数据全是乱的。我曾经在一个项目里,ADC采出来的波形怎么看怎么不对,折腾了两天才发现是初始化顺序的问题。

核心要点:ADC驱动初始化三步走——时钟先行、通道次之、使能收尾。

代码示例,我一般这么写:

// ADC初始化函数
void ADC_Init(ADC_HandleTypeDef* hadc) {
    // 第一步:配置时钟
    HAL_ADC_SetClock(hadc, ADC_CLOCK_SYNC_PCLK_DIV4);
    
    // 第二步:配置通道
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = ADC_CHANNEL_0;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
    HAL_ADC_ConfigChannel(hadc, &sConfig);
    
    // 第三步:使能ADC
    HAL_ADC_Enable(hadc);
    
    // 校准
    HAL_ADCEx_Calibration_Start(hadc, ADC_CALIB_OFFSET);
}

嗯,这里要注意,校准这一步很多人会忘。ADC内部有偏移误差,不做校准的话,直流偏置可能偏出去好几个LSB。我在做某型相控阵雷达时,就是因为没做校准,导致零中频信号一直有个固定的直流分量,后面做I/Q补偿费了好大劲。

3.2 DMA传输机制——让CPU喘口气

ADC采数据,如果每次都用CPU去读,那CPU啥也别干了。雷达信号处理里,采样率动辄几十兆甚至上百兆,CPU根本扛不住。这时候DMA就派上用场了。

DMA传输机制,说白了就是硬件级别的数据搬运工。你配置好源地址(ADC数据寄存器)、目的地址(内存缓冲区)、传输长度,DMA控制器就自己干活去了,CPU可以腾出手来做别的。

我建议你关注这几个参数:

  • 传输宽度:ADC是12位还是16位?一般用半字(16位)传输,省带宽。
  • 循环模式:雷达信号是连续采集的,必须开循环模式,不然DMA传完一轮就停了。
  • 中断触发:半传输中断和完全传输中断,这两个是双缓冲策略的基础。
小技巧:DMA的优先级设置要高于普通外设中断,否则在高负载下可能丢数据。我习惯把DMA中断优先级设为最高。

代码配置示例:

// DMA配置
void DMA_Init(void) {
    __HAL_RCC_DMA1_CLK_ENABLE();
    
    hdma_adc.Instance = DMA1_Stream0;
    hdma_adc.Init.Channel = DMA_CHANNEL_0;
    hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;   // 外设地址不变
    hdma_adc.Init.MemInc = DMA_MINC_ENABLE;       // 内存地址递增
    hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc.Init.Mode = DMA_CIRCULAR;            // 循环模式,关键!
    hdma_adc.Init.Priority = DMA_PRIORITY_HIGH;
    
    HAL_DMA_Init(&hdma_adc);
    
    // 关联ADC和DMA
    __HAL_LINKDMA(hadc, DMA_Handle, hdma_adc);
}

为什么会强调循环模式?因为雷达信号处理是流式处理,ADC一直在采,DMA一直在搬,CPU一直在算。三者必须流水线作业,任何一个环节卡住,数据就断了。

3.3 双缓冲策略——别让数据追尾

双缓冲策略,这是嵌入式信号处理的经典套路。你想想看,如果只有一个缓冲区,CPU在处理数据的时候,DMA还在往里写,那不就乱套了吗?

双缓冲的思路很简单:两个缓冲区轮流用。DMA往缓冲区A写的时候,CPU处理缓冲区B的数据;等DMA写满了A,自动切到B,CPU则处理A。如此往复,互不干扰。

我一般这么实现:

  1. 申请两个缓冲区:buf0和buf1,大小相同,比如1024个采样点。
  2. DMA配置为循环模式,目标地址在buf0和buf1之间切换。
  3. 利用DMA的半传输中断和完全传输中断来切换缓冲区。
关键点:半传输中断表示buf0写满了,完全传输中断表示buf1写满了。这两个中断交替触发,CPU就在中断服务函数里切换处理缓冲区。

代码实现思路:

// 双缓冲缓冲区定义
#define BUF_SIZE  1024
uint16_t adc_buf0[BUF_SIZE];
uint16_t adc_buf1[BUF_SIZE];
volatile uint8_t active_buf = 0;  // 0: buf0正在被DMA写, 1: buf1正在被DMA写

// DMA半传输中断回调
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) {
    // buf0已满(假设当前DMA正在写buf1)
    if(active_buf == 0) {
        process_buffer(adc_buf0, BUF_SIZE);
        active_buf = 1;
    }
}

// DMA完全传输中断回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    // buf1已满(假设当前DMA正在写buf0)
    if(active_buf == 1) {
        process_buffer(adc_buf1, BUF_SIZE);
        active_buf = 0;
    }
}

这里有个细节,我提醒一下:中断服务函数里不要做耗时操作。process_buffer如果是个复杂的信号处理函数,千万别放在中断里跑。我一般是在中断里设置一个标志位,然后在主循环或者RTOS任务里去处理数据。

避坑指南:我曾经在一个项目里,把FFT计算直接放在了DMA中断里,结果中断执行时间太长,导致DMA下一次传输覆盖了还没处理完的数据。嗯,从那以后我再也不敢在中断里干重活了。

3.4 实际调试中的几个坑

讲几个我实际调试中遇到的典型问题,你遇到了可以少走弯路:

问题现象 可能原因 解决办法
ADC数据全是0 ADC未使能或时钟未配置 检查初始化顺序,确保时钟先于使能
数据有周期性毛刺 DMA传输宽度不匹配 检查ADC输出位宽和DMA数据宽度是否一致
数据出现半个周期错位 双缓冲切换时机不对 检查半传输中断和完全传输中断是否正常触发
CPU占用率过高 中断里做了太多处理 把数据处理移到主循环或任务中

最后说一句,ADC数据采集与缓存这块,调试手段很重要。我习惯在缓冲区里放一个已知的测试信号,比如正弦波,然后通过串口或者以太网把数据发到PC上,用Python或者MATLAB画出来看看。波形对了,说明驱动和DMA没问题;波形不对,那就逐级排查吧。

个人经验:调试ADC时,先用低频采样率(比如1kHz)验证逻辑,没问题了再切到高频。低频下波形容易观察,问题也容易定位。我见过有人一上来就开100MHz采样率,结果波形乱成一团,根本不知道问题出在哪。

好了,ADC数据采集与缓存就讲这么多。下一章咱们聊聊数字下变频(DDC)的实现,那又是另一番天地了。