利用STM32 DMA循环模式实现无CPU干预的ADC多通道采样与环形缓冲区设计

引言

在嵌入式应用中,ADC采样是常见需求,但传统中断方式在高速、多通道场景下会频繁打断CPU,导致效率低下。STM32的DMA(直接内存访问)控制器配合循环模式,可让ADC数据自动传输至内存,无需CPU干预。本文以STM32F4系列为例,讲解如何配置DMA循环模式实现多通道ADC连续采样,并设计环形缓冲区管理数据。

原理概述

DMA循环模式

DMA循环模式(Circular Mode)在传输完成后自动重置传输计数器,并继续从起始地址传输数据,形成循环。对于ADC采样,这意味着DMA会持续将ADC转换结果写入内存缓冲区,当缓冲区满时自动覆盖旧数据,始终保持最新数据。

ADC多通道扫描模式

ADC的扫描模式(Scan Mode)可依次转换多个通道,转换结果按通道顺序存入ADC数据寄存器。结合DMA,每次转换完成后触发DMA传输,将结果搬运至内存。

环形缓冲区设计

环形缓冲区(Ring Buffer)是一种固定大小的先进先出(FIFO)结构,通过读写指针循环使用内存。在DMA循环模式下,DMA写入数据,CPU读取数据,两者通过指针协调,避免数据覆盖或丢失。

硬件与软件准备

  • 硬件:STM32F407开发板,板载3个ADC通道(如PA0、PA1、PA2)
  • 软件:STM32CubeIDE(或Keil MDK),STM32 HAL库

配置步骤

1. 时钟配置

使用STM32CubeMX或手动配置,使能ADC时钟(APB2)和DMA时钟(AHB1)。

// 使能时钟
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_DMA2_CLK_ENABLE();

2. ADC配置

  • 设置ADC为扫描模式、连续转换模式
  • 配置通道序列和采样时间
  • 设置DMA请求为循环模式
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void ADC_Init(void) {
    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1.Init.Resolution = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode = ENABLE;  // 扫描模式
    hadc1.Init.ContinuousConvMode = ENABLE;  // 连续转换
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 3;  // 3个通道
    HAL_ADC_Init(&hadc1);

    // 配置通道序列
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = ADC_CHANNEL_0;  // PA0
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    sConfig.Channel = ADC_CHANNEL_1;  // PA1
    sConfig.Rank = 2;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    sConfig.Channel = ADC_CHANNEL_2;  // PA2
    sConfig.Rank = 3;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}

3. DMA配置

配置DMA流,关联ADC1,设置循环模式、数据宽度等。

void DMA_Init(void) {
    hdma_adc1.Instance = DMA2_Stream0;  // ADC1使用DMA2 Stream0
    hdma_adc1.Init.Channel = DMA_CHANNEL_0;
    hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;  // 内存地址递增
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  // ADC数据16位
    hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode = DMA_CIRCULAR;  // 循环模式
    hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_adc1);

    // 关联DMA和ADC
    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
}

4. 启动ADC和DMA

void ADC_Start_DMA(void) {
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 3);  // 缓冲区大小3(每个通道一个元素)
}

环形缓冲区实现

由于DMA循环模式会持续写入缓冲区,我们需要设计环形缓冲区来管理数据。这里使用一个稍大的缓冲区(如N个采样周期),并维护读写索引。

#define ADC_BUFFER_SIZE 300  // 例如存储100次采样(3通道)
volatile uint16_t adc_buffer[ADC_BUFFER_SIZE];  // DMA写入缓冲区
volatile uint32_t write_index = 0;  // DMA写入位置(由中断更新)
volatile uint32_t read_index = 0;   // CPU读取位置

// DMA传输完成中断回调(每次传输3个数据后触发)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        // 更新写索引(假设DMA传输了3个数据)
        write_index += 3;
        if (write_index >= ADC_BUFFER_SIZE) {
            write_index = 0;
        }
    }
}

// 读取最新一组数据(返回0成功,-1失败)
int Read_ADC_Data(uint16_t* data) {
    if (write_index == read_index) {
        return -1;  // 无新数据
    }
    // 读取三个通道数据
    for (int i = 0; i < 3; i++) {
        data[i] = adc_buffer[read_index + i];
    }
    read_index += 3;
    if (read_index >= ADC_BUFFER_SIZE) {
        read_index = 0;
    }
    return 0;
}

注意:上述环形缓冲区实现简单,但存在竞态条件。更健壮的做法是使用无锁环形缓冲区,通过原子操作或临界区保护。

完整示例代码

以下为综合示例,包含初始化、启动和主循环读取。

#include "stm32f4xx_hal.h"

// 全局变量
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
#define BUFFER_SIZE 300
volatile uint16_t adc_buffer[BUFFER_SIZE];
volatile uint32_t write_index = 0;
volatile uint32_t read_index = 0;

// 初始化函数(略,参考上述配置)
void SystemClock_Config(void);
void GPIO_Init(void);
void ADC_Init(void);
void DMA_Init(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    GPIO_Init();
    ADC_Init();
    DMA_Init();

    // 启动ADC DMA
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, BUFFER_SIZE);

    uint16_t data[3];
    while (1) {
        if (Read_ADC_Data(data) == 0) {
            // 处理数据,例如打印
            printf("CH0:%d CH1:%d CH2:%d\n", data[0], data[1], data[2]);
        }
        // 其他任务
    }
}

// DMA传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        write_index += 3;  // 每次传输3个数据
        if (write_index >= BUFFER_SIZE) {
            write_index = 0;
        }
    }
}

// 读取函数(同上)
int Read_ADC_Data(uint16_t* data) {
    if (write_index == read_index) {
        return -1;
    }
    for (int i = 0; i < 3; i++) {
        data[i] = adc_buffer[read_index + i];
    }
    read_index += 3;
    if (read_index >= BUFFER_SIZE) {
        read_index = 0;
    }
    return 0;
}

注意事项

  1. 缓冲区大小:DMA循环模式要求缓冲区大小必须是传输数据量的整数倍,否则可能错位。
  2. 数据一致性:当CPU读取数据时,DMA可能正在写入同一区域,需确保读写索引同步。建议使用临界区或关中断保护。
  3. 中断优先级:DMA中断优先级应高于其他可能阻塞的中断,避免数据丢失。
  4. 内存对齐:DMA缓冲区建议使用__attribute__((aligned(4)))ALIGN_32BYTES,确保访问效率。
  5. 多通道顺序:ADC转换顺序与DMA传输顺序一致,读取时按相同顺序解析。
  6. 功耗考虑:连续转换和DMA传输会增加功耗,低功耗应用可考虑定时触发或单次转换。

总结

利用STM32的DMA循环模式,可以高效实现ADC多通道连续采样,大幅降低CPU负载。结合环形缓冲区,可安全地管理数据流。本文提供了完整的配置和代码示例,开发者可根据实际需求调整通道数、缓冲区大小和采样率。此方案适用于数据采集、传感器监测等场景,是嵌入式开发中的实用技巧。