STM32 DMA 循环模式DMA_Mode_Circular详解_dma循环模式-CSDN博客
DMA_Mode_Circular 可以理解成:DMA搬完这一轮后不会停,而是自动从头再来。
在 STM32 标准外设库(StdPeriph)里,DMA 模式有两个典型选项:DMA_Mode_Normal 和 DMA_Mode_Circular,其中 DMA_Mode_Circular 对应循环模式。ST 的文档还明确说明了:在循环模式下,传输计数到 0 后会自动恢复为初始值继续传输;而普通模式到 0 就结束。(STMicroelectronics)
你可以先把它和普通模式对比着理解:
1. Normal 模式
DMA 按你设定的长度搬一次,搬完就停。计数器减到 0 后,不会自动继续。(STMicroelectronics)
2. Circular 模式
DMA 按你设定的长度搬完一轮后,计数器自动装回初值,然后继续下一轮;在支持循环缓冲描述的 ST 资料里,还说明了源地址、目的地址和传输数量会在一轮完成后自动重新装载,所以它特别适合“持续不断”的数据流。(STMicroelectronics)
一、它到底在“循环”什么?
本质上循环的是这几个东西:
- 本轮要搬多少个数据:
CNDTR/NDTR - 当前搬运过程
- 到达末尾后重新开始下一轮
以“ADC 连续采样到数组”为例:
假设你配置:
- 外设地址:
ADC1->DR - 内存地址:
adc_buf[0] - 长度:
8 - 模式:
DMA_Mode_Circular
那么流程就是:
- ADC 每转换完成一次,发出一次 DMA 请求。
- DMA 把
ADC1->DR的值搬到adc_buf[0]、adc_buf[1] ... adc_buf[7]。 - 当 8 个数据搬完后:
- 普通模式:停住。
- 循环模式:计数器恢复成 8,再从头开始写缓冲区。(STMicroelectronics)
所以它非常像一个“硬件自动回卷的缓冲区”。
二、半传输和全传输中断怎么理解?
STM32 DMA 常见会有这些事件标志:
HT:Half Transfer,半传输完成TC:Transfer Complete,整轮传输完成TE:Transfer Error,传输错误GL:全局标志,表示同通道发生了上述某类事件(STMicroelectronics)
这在循环模式里特别有用。
比如缓冲区长度为 8:
- 搬到
adc_buf[0..3]后,触发一次 HT - 搬到
adc_buf[4..7]后,触发一次 TC - 然后 DMA 自动回到开头,又开始下一轮(STMicroelectronics)
这就是很多人常说的“前半缓冲 / 后半缓冲处理”思路。
也就是说:
- DMA 正在写后半段时,CPU 可以处理前半段
- DMA 正在写前半段时,CPU 可以处理后半段
这样就能连续采集、连续处理,CPU 和 DMA 并行工作。
三、为什么很多连续采样都喜欢用 Circular?
因为它很适合数据源不断产生数据的场景。典型有:
ADC 连续采样
ADC 持续出数据,DMA 持续搬到数组;如果 ADC 设成“DMA unlimited”,官方说明这本来就是为了配合 DMA 循环模式使用。若 ADC 设成 unlimited,而 DMA 却是 non-circular,那么 DMA 长度用完后会停止,ADC 可能产生 overrun。(STMicroelectronics)
UART 接收
串口接收往往是持续的、长度不固定的,循环 DMA 能让接收缓冲一直更新,不需要每收满一次就重新启动 DMA。
但要注意:循环 DMA 只负责搬数据,不负责帮你判断一帧从哪开始、到哪结束。变长帧通常还要配合 IDLE 中断、定时器超时或协议头尾判断。
DAC 输出波形
例如输出正弦波查表。DMA 把波形表循环送到 DAC,表尾到了自动回表头,波形就连续输出。
SPI / I2S 连续流数据
比如音频接收、传感器持续采样,循环模式也很常见。
不过 ST 文档也列出了一些 SPI 的限制:某些 SPI 主模式下不能用 DMA circular,并且启用 circular 时 CRC 功能不受管理。(STMicroelectronics)
四、最关键的一个理解:它不是“无限加长缓冲区”
很多初学者会误会成:
开了 Circular,DMA 就会一直往后写,像无限数组一样。
不是。
它是写到你给定缓冲区末尾后,再回到开头覆盖。所以:
- 缓冲区是固定长度的
- 数据会被新数据覆盖
- 你必须及时处理旧数据
这也是为什么循环模式一般都要搭配:
- 半传输中断 HT
- 全传输中断 TC
- 或者定期读取 DMA 当前剩余计数器
ST 也说明了:DMA 的当前数据计数器可以读取;普通模式下传完变成 0,而循环模式下会恢复到初始值继续运行。(STMicroelectronics)
五、StdPeriph 里怎么配?
如果你用的是标准外设库,最核心就是这一句:
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
例如 ADC + DMA 的典型风格大概是这样:
DMA_InitTypeDef DMA_InitStructure;
DMA_DeInit(DMA1_Channel1);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = 8;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 关键
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel1, ENABLE);
其中要点是:
- 外设地址通常固定不变,所以
PeripheralInc = Disable - 内存一般要顺序写数组,所以
MemoryInc = Enable - 数据宽度要和外设寄存器匹配
- 循环模式只决定“这一轮结束后是否自动重来”,不替你决定数据宽度和自增方式
StdPeriph 文档里也明确给出了 DMA_Mode_Normal 和 DMA_Mode_Circular 这两个宏定义。(STMicroelectronics)
六、HAL 和 LL 里对应什么?
如果你后面切 HAL/LL,也是一一对应的:
- StdPeriph:
DMA_Mode_Circular - HAL:
DMA_CIRCULAR - LL:
LL_DMA_MODE_CIRCULAR
ST 的 HAL/LL 文档里同样写明了 DMA 模式可配置为 normal 或 circular。(STMicroelectronics)
七、最常见的几个坑
1)Memory-to-Memory 不能用 Circular
ST 官方资料明确写了:内存到内存模式不能使用 circular mode。
所以如果你是 mem -> mem 拷贝,别开循环。(STMicroelectronics)
2)只开了 Circular,不代表程序就“自动处理数据”
DMA 只是负责搬。
你仍然要决定:
- 什么时候读缓冲
- 读前半还是后半
- 如何避免处理慢导致覆盖
3)UART 变长接收只开 Circular 还不够
循环 DMA 很适合接收,但它不会告诉你“这帧刚刚结束了”。
通常还得配:
- IDLE 中断
- 帧头帧尾
- 长度字段
- 超时判断
4)ADC 连续转换时,DMA 模式要配套
如果 ADC 还在不停产出数据,而 DMA 不是 circular,缓冲写满后 DMA 停掉,就容易出 overrun。ST 对这一点有明确说明。(STMicroelectronics)
5)SPI 某些模式下不能用 Circular
HAL 文档里明确列了限制,特别是某些主接收模式。(STMicroelectronics)
八、一句话记忆法
你可以直接记成:
Normal 模式:搬一圈就停。
Circular 模式:搬一圈后自动回到起点继续搬。
九、什么时候该选它?
选 DMA_Mode_Circular 的判断标准很简单:
适合:
- 持续 ADC 采样
- 串口持续接收
- DAC 循环输出波形
- 音频 / 传感器流式数据
不适合:
- 一次性搬完就结束的任务
- 内存到内存拷贝
- 需要严格“只传一次”的发送任务
十、你现在做 STM32 项目时,最实用的理解
对你写程序最有帮助的,不是死记定义,而是这句:
DMA_Mode_Circular = 让 DMA 围着固定缓冲区反复写。
所以实际工程里你最该关心的是:
- 缓冲区多大合适
- 是否开 HT/TC 中断
- CPU 来不来得及处理
- 会不会覆盖旧数据
- 外设是不是连续数据源
好,我直接给你一个能看懂、能上手、能写代码的版本。
1. DMA 循环模式的直观时序
以 ADC + DMA + 长度8的缓冲区 为例,DMA 设为循环模式后,流程是这样的:
ADC采样: S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 ...
DMA写入: 0 1 2 3 4 5 6 7 0 1 ...
缓冲区: buf[0] ... ... buf[7] 再回到buf[0]
事件: HT中断 TC中断
(半满) (满一轮)
也就是:
- 前 4 个数据写入
buf[0] ~ buf[3],到一半时触发 HT(Half Transfer) - 后 4 个数据写入
buf[4] ~ buf[7],写完整轮时触发 TC(Transfer Complete) - 然后 DMA 不会停,而是重新从
buf[0]开始覆盖写下一轮。ST 的 HAL 文档里明确给出了 DMA 的DMA_NORMAL和DMA_CIRCULAR两种模式,也给出了 DMA 半传输、全传输中断,以及__HAL_DMA_GET_COUNTER()用于读取当前剩余传输数。(STMicroelectronics)
你可以把它记成一句话:
普通模式:搬一轮就停;循环模式:搬一轮后自动回到开头继续搬。
HAL 文档还明确说明:Memory-to-Memory 不允许 Circular mode。(STMicroelectronics)
2. 最典型用途:ADC 连续采样
STM32F1 的 HAL 文档里,HAL_ADC_Start_DMA() 的说明就是:启动 ADC 常规组转换,并通过 DMA 传输结果;同时用户可以在 HAL_ADC_ConvHalfCpltCallback() 和 HAL_ADC_ConvCpltCallback() 里处理半缓冲和整缓冲数据。(STMicroelectronics)
你最应该这样理解
- DMA 在前半区写时,CPU 可以处理后半区
- DMA 在后半区写时,CPU 可以处理前半区
- 这样就形成了连续采样 + 分段处理
3. STM32F103 + HAL 的 ADC 循环 DMA 示例
下面给你一个适合 STM32F103 的基础模板。
假设:
- ADC1 采样通道:
PA0 -> ADC_CHANNEL_0 - DMA:
DMA1_Channel1 - 缓冲区长度:16
- 模式:循环模式
全局变量
#include "main.h"
#define ADC_BUF_LEN 16
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
volatile uint16_t adc_buf[ADC_BUF_LEN];
volatile uint8_t adc_half_ready = 0;
volatile uint8_t adc_full_ready = 0;
ADC 初始化关键部分
void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
DMA 初始化关键部分
关键是这一句:
hdma_adc1.Init.Mode = DMA_CIRCULAR;
完整示例:
void MX_DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_adc1.Instance = DMA1_Channel1;
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;
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);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
HAL 文档中明确列出了 DMA_CIRCULAR,并且 ADC 的 DMA 工作流就是通过 HAL_ADC_Start_DMA() 启动,然后在 HAL_ADC_ConvHalfCpltCallback() / HAL_ADC_ConvCpltCallback() 中处理数据。(STMicroelectronics)
启动采样
void ADC_DMA_Start(void)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);
}
HAL_ADC_Start_DMA() 的官方描述就是:使能 ADC,启动常规组转换,并通过 DMA 传输结果。(STMicroelectronics)
中断服务函数
void DMA1_Channel1_IRQHandler(void)
{
HAL_DMA_IRQHandler(&hdma_adc1);
}
回调函数
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
adc_half_ready = 1;
}
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
adc_full_ready = 1;
}
}
主循环处理
void Process_ADC_FirstHalf(void)
{
// 处理 adc_buf[0] ~ adc_buf[7]
}
void Process_ADC_SecondHalf(void)
{
// 处理 adc_buf[8] ~ adc_buf[15]
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_DMA_Init();
MX_ADC1_Init();
ADC_DMA_Start();
while (1)
{
if (adc_half_ready)
{
adc_half_ready = 0;
Process_ADC_FirstHalf();
}
if (adc_full_ready)
{
adc_full_ready = 0;
Process_ADC_SecondHalf();
}
}
}
4. 这个 ADC 方案为什么稳定
因为它不是“采一个、停一下、再配一次 DMA”,而是:
- ADC 连续出数据
- DMA 自动搬运
- 半缓冲/满缓冲时通知 CPU 处理
HAL 文档对 ADC DMA 模式的描述也正是这个流程:启动 DMA 传输后,结果会被自动搬到目标地址,用户通过 Half/Complete 回调拿到处理时机。(STMicroelectronics)
5. UART 循环 DMA 应该怎么用
STM32F1 HAL 里有 HAL_UART_Receive_DMA(),文档说明它用于 DMA 模式接收一定数量的数据;同时也提供了 HAL_UART_RxHalfCpltCallback 和 HAL_UART_RxCpltCallback 这些回调。(STMicroelectronics)
但串口接收和 ADC 不一样:
- ADC 常常是“固定频率、连续流”
- UART 常常是“变长帧、不定时到来”
所以 UART 用 Circular DMA 时,通常要再配合 IDLE 空闲中断 来判断“一帧收完了”。
HAL 文档明确给出了:
UART_FLAG_IDLE:空闲线检测标志UART_IT_IDLE:空闲中断- 清除 IDLE 标志的方法:读
USART_SR再读USART_DR;HAL 还提供了__HAL_UART_CLEAR_IDLEFLAG。(STMicroelectronics)
6. STM32F103 + HAL 的 UART 循环接收思路
假设:
- USART1 接收
- DMA 接收到
uart_rx_buf[64] - 开循环模式
- 开 UART IDLE 中断
- 每次空闲中断时,读取“当前已经收到多少字节”
全局变量
#define UART_RX_BUF_LEN 64
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
uint8_t uart_rx_buf[UART_RX_BUF_LEN];
volatile uint16_t uart_last_pos = 0;
DMA 初始化核心
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
启动 UART DMA 接收
HAL_UART_Receive_DMA(&huart1, uart_rx_buf, UART_RX_BUF_LEN);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL 文档中列出了 HAL_UART_Receive_DMA(),也列出了 UART_IT_IDLE 和 UART_FLAG_IDLE。(STMicroelectronics)
USART1 中断处理
void USART1_IRQHandler(void)
{
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
uint16_t pos = UART_RX_BUF_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
if (pos != uart_last_pos)
{
if (pos > uart_last_pos)
{
// 数据在 uart_rx_buf[uart_last_pos ... pos-1]
}
else
{
// 发生回卷:
// 先处理 uart_rx_buf[uart_last_pos ... UART_RX_BUF_LEN-1]
// 再处理 uart_rx_buf[0 ... pos-1]
}
uart_last_pos = pos;
}
}
HAL_UART_IRQHandler(&huart1);
}
这里的关键点是:
__HAL_DMA_GET_COUNTER()返回当前 DMA 通道剩余的数据单元数- 所以
总长度 - 剩余长度 = 已写入位置。这个宏的官方说明就在 HAL DMA 文档里。(STMicroelectronics)
7. UART 循环 DMA 的核心难点
不是“能不能收到”,而是“怎么判帧”。
你需要特别注意:
- 循环 DMA 只负责搬数据
- 它不会告诉你协议帧从哪开始、到哪结束。
- 变长协议一般还是要靠 IDLE、中断、帧头帧尾或长度字段。
(STMicroelectronics)
- 数据会覆盖
- 如果你的上层处理太慢,旧数据可能还没处理完就被新数据覆盖。
- 所以循环缓冲区长度要留余量。
- 不要在回调里做太重的事
- 中断里只做“记录位置、置标志”,真正解析放到主循环或任务里。
8. 什么时候该选 Circular
你做 STM32 工程时,下面这个判断最实用:
用 Circular 的场景
- ADC 连续采样
- UART 持续接收
- DAC 周期波形输出
- 音频/I2S/SPI 连续流数据
不建议用 Circular 的场景
- 一次性收 20 字节就结束
- 一次性发一包固定长度数据
- 内存到内存搬运
因为 HAL 明确说明 Memory-to-Memory transfer mode 下 Circular mode 不允许。(STMicroelectronics)
9. 你可以直接这样记
最后给你一个最容易记住的工程化结论:
ADC + Circular DMA
- 重点看 HT/TC 回调
- 适合持续采样
- 最常见、最稳
UART + Circular DMA
- 重点看 IDLE 中断 + DMA剩余计数
- 适合持续接收
- 变长协议特别常用
10. 给你一个最短模板结论
你以后看到这行配置:
hdma.Init.Mode = DMA_CIRCULAR;
就立刻想到:
DMA 会在固定缓冲区里反复写,写到末尾后自动从开头继续。

发表回复