3、ADC数据采集与缓存:ADC驱动开发、DMA传输机制、双缓冲策略
好,咱们今天聊聊ADC数据采集与缓存这块。说实话,这是雷达信号处理里最基础也最容易翻车的一环。你想想看,ADC采进来的数据要是丢了或者乱了,后面做FFT、做CFAR全是白搭。我这些年调试下来,发现很多问题其实都出在驱动和缓存策略上。
3.1 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配置
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。如此往复,互不干扰。
我一般这么实现:
- 申请两个缓冲区:buf0和buf1,大小相同,比如1024个采样点。
- DMA配置为循环模式,目标地址在buf0和buf1之间切换。
- 利用DMA的半传输中断和完全传输中断来切换缓冲区。
代码实现思路:
// 双缓冲缓冲区定义
#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任务里去处理数据。
3.4 实际调试中的几个坑
讲几个我实际调试中遇到的典型问题,你遇到了可以少走弯路:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| ADC数据全是0 | ADC未使能或时钟未配置 | 检查初始化顺序,确保时钟先于使能 |
| 数据有周期性毛刺 | DMA传输宽度不匹配 | 检查ADC输出位宽和DMA数据宽度是否一致 |
| 数据出现半个周期错位 | 双缓冲切换时机不对 | 检查半传输中断和完全传输中断是否正常触发 |
| CPU占用率过高 | 中断里做了太多处理 | 把数据处理移到主循环或任务中 |
最后说一句,ADC数据采集与缓存这块,调试手段很重要。我习惯在缓冲区里放一个已知的测试信号,比如正弦波,然后通过串口或者以太网把数据发到PC上,用Python或者MATLAB画出来看看。波形对了,说明驱动和DMA没问题;波形不对,那就逐级排查吧。
好了,ADC数据采集与缓存就讲这么多。下一章咱们聊聊数字下变频(DDC)的实现,那又是另一番天地了。