STM32 DMA循环模式实现ADC多通道无CPU干预采样与环形缓冲区设计
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,ADC多通道采样常需兼顾实时性与CPU效率。本文深入解析STM32的DMA循环模式,结合环形缓冲区设计,实现ADC多通道连续采样而无需CPU干预。通过原理讲解、配置步骤、完整代码示例及注意事项,帮助开发者掌握这一高效数据采集方案,提升系统性能。
# 利用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负载。结合环形缓冲区,可安全地管理数据流。本文提供了完整的配置和代码示例,开发者可根据实际需求调整通道数、缓冲区大小和采样率。此方案适用于数据采集、传感器监测等场景,是嵌入式开发中的实用技巧。