# 利用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)。 ```c // 使能时钟 __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); ``` ### 2. ADC配置 - 设置ADC为扫描模式、连续转换模式 - 配置通道序列和采样时间 - 设置DMA请求为循环模式 ```c 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,设置循环模式、数据宽度等。 ```c 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 ```c void ADC_Start_DMA(void) { HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 3); // 缓冲区大小3(每个通道一个元素) } ``` ## 环形缓冲区实现 由于DMA循环模式会持续写入缓冲区,我们需要设计环形缓冲区来管理数据。这里使用一个稍大的缓冲区(如N个采样周期),并维护读写索引。 ```c #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; } ``` 注意:上述环形缓冲区实现简单,但存在竞态条件。更健壮的做法是使用无锁环形缓冲区,通过原子操作或临界区保护。 ## 完整示例代码 以下为综合示例,包含初始化、启动和主循环读取。 ```c #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负载。结合环形缓冲区,可安全地管理数据流。本文提供了完整的配置和代码示例,开发者可根据实际需求调整通道数、缓冲区大小和采样率。此方案适用于数据采集、传感器监测等场景,是嵌入式开发中的实用技巧。