引言

在嵌入式系统中,ADC连续采样是数据采集的基础。然而,当采样率较高或数据处理复杂时,CPU无法及时搬移数据,导致数据覆盖或丢失。STM32F4系列内置的DMA控制器支持双缓冲模式,结合乒乓缓冲设计,可让ADC数据在后台无缝传输,CPU仅在缓冲区切换时介入,从而实现零丢点采样。本文以STM32F407为例,详细讲解实现方案。

原理剖析

DMA双缓冲机制

传统DMA使用单缓冲区,传输完成后触发中断,CPU需在中断中处理数据,期间新数据可能覆盖旧数据。双缓冲模式为DMA提供两个内存缓冲区(Buffer0和Buffer1),当DMA正在填充Buffer0时,CPU可安全处理Buffer1;反之亦然。STM32F4的DMA控制器通过DMA_SxCR寄存器的DBM位启用双缓冲,并利用DMA_SxM0ARDMA_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连续采样,双缓冲交替处理。

// 缓冲区定义(需按采样率调整大小)
#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从数据搬运中解放,专注于数据处理,极大提升了系统实时性。实际应用中,需根据具体采样率和处理复杂度调整缓冲区大小,并注意中断优先级和缓存一致性等细节。希望本文能帮助你在嵌入式数据采集项目中游刃有余。