# 引言 在嵌入式系统中,ADC连续采样是数据采集的基础。然而,当采样率较高或数据处理复杂时,CPU无法及时搬移数据,导致数据覆盖或丢失。STM32F4系列内置的DMA控制器支持双缓冲模式,结合乒乓缓冲设计,可让ADC数据在后台无缝传输,CPU仅在缓冲区切换时介入,从而实现零丢点采样。本文以STM32F407为例,详细讲解实现方案。 # 原理剖析 ## DMA双缓冲机制 传统DMA使用单缓冲区,传输完成后触发中断,CPU需在中断中处理数据,期间新数据可能覆盖旧数据。双缓冲模式为DMA提供两个内存缓冲区(Buffer0和Buffer1),当DMA正在填充Buffer0时,CPU可安全处理Buffer1;反之亦然。STM32F4的DMA控制器通过`DMA_SxCR`寄存器的`DBM`位启用双缓冲,并利用`DMA_SxM0AR`和`DMA_SxM1AR`指定两个缓冲区地址。当DMA传输完成一个缓冲区时,自动切换至另一个,同时置位传输完成中断标志(TCIF),通知CPU处理已满的缓冲区。 ## 乒乓缓冲设计 乒乓缓冲是双缓冲的软件抽象,将两个缓冲区视为“Ping”和“Pong”。DMA交替填充,CPU交替处理。关键在于:CPU处理缓冲区的时间必须小于DMA填充另一个缓冲区的时间,否则仍会丢点。因此,缓冲区大小需根据采样率和处理耗时设计。例如,采样率100kHz,每个样本2字节,若CPU处理耗时100μs,则缓冲区至少需容纳100μs×100kHz=10000样本,即20KB。实际中,通常使用中断或标志位通知CPU,确保及时处理。 # 硬件与CubeMX配置 ## 硬件准备 - STM32F407开发板(或类似F4系列) - 信号源(如函数发生器) - 串口或调试器用于查看数据 ## CubeMX配置步骤 1. **ADC配置**: - 开启ADC1,选择通道(如IN0),采样时间设为最大(如480周期)以提高精度。 - 开启连续转换模式(Continuous Conversion Mode)。 - 设置DMA请求为循环模式(Circular),并启用DMA连续请求(DMA Continuous Requests)。 2. **DMA配置**: - 添加ADC1的DMA流(如DMA2_Stream0),方向为外设到内存。 - 模式设为循环(Circular),数据宽度为半字(Half Word,对应16位ADC)。 - 启用双缓冲(Double Buffer)模式,并设置两个内存地址(在代码中动态设置)。 3. **NVIC配置**: - 启用DMA流的中断(如DMA2_Stream0_IRQn),优先级设为较高。 # 代码实现 以下代码基于HAL库,实现ADC连续采样,双缓冲交替处理。 ```c // 缓冲区定义(需按采样率调整大小) #define BUFFER_SIZE 1024 uint16_t ping_buffer[BUFFER_SIZE]; uint16_t pong_buffer[BUFFER_SIZE]; volatile uint8_t buffer_ready = 0; // 标志位:0=无数据,1=ping满,2=pong满 // DMA中断回调 void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma) { if (hdma->Instance == DMA2_Stream0) { // 判断当前DMA使用的缓冲区(通过DMA_SxCR的CT位) if (__HAL_DMA_GET_CT(hdma) == 0) { buffer_ready = 1; // ping缓冲区已满 } else { buffer_ready = 2; // pong缓冲区已满 } } } // 启动ADC DMA双缓冲 void ADC_DMA_Start(void) { // 设置双缓冲地址(必须先于启动) LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_0, (uint32_t)ping_buffer); LL_DMA_SetMemory1Address(DMA2, LL_DMA_STREAM_0, (uint32_t)pong_buffer); // 启动ADC DMA(循环模式) HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ping_buffer, BUFFER_SIZE); // 注意:HAL_ADC_Start_DMA只设置第一个地址,但双缓冲下DMA会自动切换 } // 主循环处理数据 int main(void) { // 初始化... ADC_DMA_Start(); while (1) { if (buffer_ready == 1) { // 处理ping_buffer数据 process_data(ping_buffer, BUFFER_SIZE); buffer_ready = 0; } else if (buffer_ready == 2) { // 处理pong_buffer数据 process_data(pong_buffer, BUFFER_SIZE); buffer_ready = 0; } } } // 数据处理函数(示例) void process_data(uint16_t *buf, uint32_t len) { // 例如:计算平均值、FFT等 uint32_t sum = 0; for (uint32_t i = 0; i < len; i++) { sum += buf[i]; } printf("Average: %lu\n", sum / len); } ``` **关键点说明**: - 在`HAL_DMA_TC_Callback`中,通过`__HAL_DMA_GET_CT`判断当前DMA正在使用的缓冲区(CT位:0表示M0AR,1表示M1AR)。当CT为0时,说明DMA刚填满M0AR(即ping),因此处理ping;反之处理pong。 - 双缓冲模式下,DMA传输完成中断在每个缓冲区填满时触发,但需注意HAL库的`HAL_DMA_TC_Callback`可能被多次调用,需根据CT位区分。 - 缓冲区大小和数量需根据采样率和处理耗时调整,确保处理时间小于DMA填充时间。 # 注意事项 - **缓冲区对齐**:DMA要求内存地址按数据宽度对齐(半字需2字节对齐),建议使用`__attribute__((aligned(4)))`或`ALIGN_32BYTES`宏。 - **中断优先级**:DMA中断优先级应高于可能阻塞CPU的任务,但低于实时性要求更高的中断(如系统时钟)。 - **缓存一致性**:若使用D-Cache(如STM32F7),需在CPU访问缓冲区前执行`SCB_InvalidateDCache_by_Addr`,否则可能读到脏数据。F4无D-Cache,可忽略。 - **DMA循环模式**:必须使用循环模式,否则DMA在传输完一个缓冲区后停止,无法自动切换。 - **HAL库限制**:HAL的`HAL_ADC_Start_DMA`只接受一个内存地址,但双缓冲下DMA会自动切换,因此只需传入第一个地址。但需确保DMA配置中已启用双缓冲。 - **调试技巧**:使用逻辑分析仪或示波器观察DMA的TC中断频率,确保与理论采样率匹配。若中断频率过高,可能缓冲区过小。 # 总结 通过STM32F4的DMA双缓冲和乒乓缓冲设计,我们实现了ADC连续采样的零丢点传输。该方案将CPU从数据搬运中解放,专注于数据处理,极大提升了系统实时性。实际应用中,需根据具体采样率和处理复杂度调整缓冲区大小,并注意中断优先级和缓存一致性等细节。希望本文能帮助你在嵌入式数据采集项目中游刃有余。