基于STM32_HAL库的DMA应用
文章目录
- 基于STM32_HAL库的DMA应用
- 一、DMA介绍
- 1.1 DMA的核心作用
- 1.2 DMA数据传输方向
- 1. 外设到内存(Peripheral-to-Memory)
- 2. 内存到外设(Memory-to-Peripheral)
- 3. 内存到内存(Memory-to-Memory)
- 1.3 DMA数据传输方式
- 1. 正常模式(Normal Mode)
- 2. 循环模式(Circular Mode)
- 1.4 DMA控制器通道
- 1.5 DMA控制器的优先级
- 1. 优先级分类
- 2. 优先级仲裁规则
- 二、DMA控制器相关API函数
- 2.1 初始化指定的 DMA 通道函数HAL_DMA_Init()原型及功能
- 2.2 停止|重置DMA通道函数HAL_DMA_DeInit()原型及功能
- 2.3 启动 DMA 传输(非中断方式)函数HAL_DMA_Start()原型及功能
- 2.4 启动 DMA 并启用中断函数HAL_DMA_Start_IT()原型及功能
- 2.5 立即中止正在进行的 DMA 传输函数HAL_DMA_Abort()原型及功能
- 2.6 以中断方式中止 DMA函数HAL_DMA_Abort_IT()原型及功能
- 2.7 阻塞式等待 DMA 传输完成函数HAL_DMA_PollForTransfer()原型及功能
- 2.8 判断 DMA 中断标志是否被置位宏函数__HAL_DMA_GET_FLAG()原型及功能
- 2.9 DMA方式串口发送数据函数HAL_UART_Transmit_DMA()原型及功能
- 2.10 DMA方式串口接收数据函数HAL_UART_Receive_DMA()原型及功能
- 2.11 启用某个 UART 中断功能函数__HAL_UART_ENABLE_IT()原型及功能
- 2.12 检查指定的 UART 状态标志位是否被置位函数__HAL_UART_GET_FLAG()原型及功能
- 2.13 清除 UART 的 IDLE(空闲)标志位函数__HAL_UART_CLEAR_IDLEFLAG()原型及功能
- 2.14 停止通过 DMA 进行的 UART 发送或接收操作函数HAL_UART_DMAStop()原型及功能
- 2.15 获取当前 DMA 传输还剩下多少个数据单位尚未完成函数__HAL_DMA_GET_COUNTER()原型及功能
- 三、内存👉内存搬运
- 3.1 CubeMX配置
- 3.2 代码实现
- 四、内存👉外设搬运
- 4.1 CubeMX配置
- 4.2 代码实现
- 五、外设👉内存搬运
- 5.1 CubeMX配置
- 5.2 代码实现
一、DMA介绍
STM32 的 DMA(Direct Memory Access,直接存储器访问)是一种硬件模块,用于在 外设(如ADC、USART、SPI、I2C等)与 内存(RAM) 之间,或在 内存与内存之间,实现 无需CPU干预 的数据传输。
1.1 DMA的核心作用
不通过CPU,自动搬运数据,减少CPU负担,提高效率。例如:
- 从ADC采集数据直接写入内存
- 接收串口数据后直接存入缓冲区
- 内存中大块数据搬运
1.2 DMA数据传输方向
在STM32F103C8T6的DMA控制器中,三种数据传输方向决定了数据流动的路径,是DMA配置的核心要素之一。以下是清晰的分类和说明:
1. 外设到内存(Peripheral-to-Memory)
特点
- 数据源:外设寄存器(如ADC、USART的DR寄存器)。
- 数据目标:内存(如数组、变量)。
- 典型应用:
- ADC采集数据存入内存数组。
- 串口接收数据到缓冲区。
2. 内存到外设(Memory-to-Peripheral)
特点
- 数据源:内存(如待发送的数据数组)。
- 数据目标:外设寄存器(如SPI/I2C的数据寄存器)。
- 典型应用:
- 通过USART发送字符串。
- SPI驱动显示屏批量写入数据。
3. 内存到内存(Memory-to-Memory)
特点
- 数据源和目标:均为内存地址(如数组间复制)。
- 仅DMA1支持:需手动触发,无硬件外设请求。
- 典型应用:
- 快速初始化大块内存(如清零RAM)。
- 数据备份或格式转换。
1.3 DMA数据传输方式
在STM32F103C8T6的DMA控制器中,数据搬运模式主要分为两种核心模式,它们决定了DMA传输的连续性和工作方式。以下是详细的介绍:
1. 正常模式(Normal Mode)
特点
- 单次触发,单次传输:
每次使能DMA后,仅完成一次设定的数据传输(DMA_BufferSize
),之后自动关闭DMA通道。 - 需手动重启:
传输完成后需重新配置并启动DMA(或由外设再次触发),适合非连续数据场景。 - 中断标志:
传输完成会置位TC
(Transfer Complete)标志,可触发中断。
典型应用
- 单次ADC采样数据搬运。
- 非周期性的串口数据发送(如按键触发发送)。
2. 循环模式(Circular Mode)
特点
- 自动循环,无需干预:
传输完成后自动重置地址和计数器,无限循环传输数据,形成闭环。 - 双缓冲技巧:
结合传输完成(TC
)和半传输完成(HT
)中断,可实现双缓冲(Ping-Pong Buffer),提升实时性。 - 外设持续触发:
适合外设连续产生数据(如ADC连续采样、串口接收数据流)。
典型应用
- 实时音频信号采集(ADC+DMA循环模式)。
- 高速传感器数据流(如陀螺仪SPI通信)。
- 串口接收不定长数据(配合IDLE中断)。
1.4 DMA控制器通道
STM32F103 有 2 个 DMA 控制器,DMA1 有 7 个通道,DMA 2 有 5 个通道。一个通道每次只能搬运一个外设的数据!! 如果同时有多个外设的 DMA 请求,则按照优先级进行响应。STM32F103C8T6 只有 DMA1 !
-
DMA1有7个通道
-
DMA2有5个通道
1.5 DMA控制器的优先级
在STM32F103系列单片机中,DMA控制器的优先级管理是一个关键功能,它决定了当多个DMA请求同时发生时,哪个请求会被优先处理。以下是关于DMA优先级的详细介绍:
1. 优先级分类
STM32F103的DMA控制器支持两种优先级管理机制:
(1) 软件可编程优先级
每个DMA通道可以独立配置以下4个优先级等级(通过DMA_InitStruct.DMA_Priority
设置):
- 最高优先级(DMA_Priority_VeryHigh)
- 高优先级(DMA_Priority_High)
- 中优先级(DMA_Priority_Medium)
- 低优先级(DMA_Priority_Low)
(2) 硬件固定优先级(自然优先级)
当多个通道的软件优先级相同时,硬件根据通道编号自动仲裁:
- 通道编号越小,优先级越高(通道1 > 通道2 > … > 通道7)。
2. 优先级仲裁规则
- 软件优先级优先:
不同优先级的请求中,更高软件优先级的通道总是先被处理。
例如:通道3(高优先级)会抢占通道1(低优先级),即使通道1编号更小。 - 硬件优先级补充:
若多个通道的软件优先级相同,则按通道编号决定顺序。
例如:通道2和通道4均为中优先级时,通道2优先。 - 传输中的抢占:
- 低优先级传输可被高优先级请求打断(当前传输完成后切换)。
- 同优先级请求需等待当前传输完成。
二、DMA控制器相关API函数
2.1 初始化指定的 DMA 通道函数HAL_DMA_Init()原型及功能
📌 函数原型:
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
✅ 参数:
参数 | 类型 | 说明 |
---|---|---|
hdma | DMA_HandleTypeDef * | 指向 DMA 通道句柄的指针,包含初始化结构体配置 |
🔁 返回值:
HAL_OK
: 成功HAL_ERROR
: 错误(通常为参数错误)HAL_BUSY
: 控制器忙
🎯 功能:
初始化指定的 DMA 通道,并配置:
- 数据方向
- 数据宽度
- 地址增量
- 优先级
- 模式(普通、循环)
2.2 停止|重置DMA通道函数HAL_DMA_DeInit()原型及功能
📌 函数原型:
HAL_StatusTypeDef HAL_DMA_DeInit(DMA_HandleTypeDef *hdma);
✅ 参数:
参数 | 类型 | 说明 |
---|---|---|
hdma | DMA_HandleTypeDef * | 指定要释放的 DMA 通道句柄 |
🔁 返回值:
HAL_OK
: 成功HAL_ERROR
: 错误(通常为参数错误)HAL_BUSY
: 控制器忙
🎯 功能:
- 停止 DMA 通道
- 重置为默认状态
- 清除中断标志
2.3 启动 DMA 传输(非中断方式)函数HAL_DMA_Start()原型及功能
📌 函数原型:
HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
✅ 参数:
参数 | 含义 |
---|---|
hdma | DMA 通道句柄 |
SrcAddress | 源地址 |
DstAddress | 目的地址 |
DataLength | 数据项数量(以数据宽度计) |
🔁 返回值:
HAL_OK
: 成功HAL_ERROR
: 错误(通常为参数错误)HAL_BUSY
: 控制器忙
🎯 功能:
- 启动 DMA 传输(非中断方式)
- 手动启动一次传输
2.4 启动 DMA 并启用中断函数HAL_DMA_Start_IT()原型及功能
📌 函数原型:
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
✅ 参数:
参数 | 含义 |
---|---|
hdma | DMA 通道句柄 |
SrcAddress | 源地址 |
DstAddress | 目的地址 |
DataLength | 数据项数量(以数据宽度计) |
🔁 返回值:
HAL_OK
: 成功HAL_ERROR
: 错误(通常为参数错误)HAL_BUSY
: 控制器忙
🎯 功能:
- 启动 DMA 并启用中断
- 完成时调用中断回调(非阻塞)
2.5 立即中止正在进行的 DMA 传输函数HAL_DMA_Abort()原型及功能
📌 函数原型:
HAL_StatusTypeDef HAL_DMA_Abort(DMA_HandleTypeDef *hdma);
✅ 参数:
参数 | 类型 | 说明 |
---|---|---|
hdma | DMA_HandleTypeDef * | 指定要中止的 DMA 通道句柄 |
🔁 返回值:
HAL_OK
: 成功中止HAL_ERROR
: 中止失败
🎯 功能:
- 立即中止正在进行的 DMA 传输
- 清除使能位
2.6 以中断方式中止 DMA函数HAL_DMA_Abort_IT()原型及功能
📌 函数原型:
HAL_StatusTypeDef HAL_DMA_Abort_IT(DMA_HandleTypeDef *hdma);
✅ 参数:
参数 | 类型 | 说明 |
---|---|---|
hdma | DMA_HandleTypeDef * | 指定要中止的 DMA 通道句柄 |
🔁 返回值:
HAL_OK
: 成功中止HAL_ERROR
: 中止失败
🎯 功能:
- 以中断方式中止 DMA
- 中止后触发
HAL_DMA_AbortCpltCallback
2.7 阻塞式等待 DMA 传输完成函数HAL_DMA_PollForTransfer()原型及功能
📌 函数原型:
HAL_StatusTypeDef HAL_DMA_PollForTransfer(DMA_HandleTypeDef *hdma, HAL_DMA_LevelTypeDef CompleteLevel, uint32_t Timeout);
✅ 参数:
参数 | 含义 |
---|---|
hdma | DMA 句柄 |
CompleteLevel | HAL_DMA_FULL_TRANSFER 或 HAL_DMA_HALF_TRANSFER |
Timeout | 等待超时(ms) |
🔁 返回值:
HAL_OK
,HAL_TIMEOUT
🎯 功能:
- 阻塞式等待 DMA 传输完成
- 适合不使用中断场景
2.8 判断 DMA 中断标志是否被置位宏函数__HAL_DMA_GET_FLAG()原型及功能
✅ 1. 宏原型:
#define __HAL_DMA_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->ISR & (__FLAG__)) != 0U)
💡 这是一个 宏定义(不是函数),用于读取 DMA 状态寄存器中的某个标志位是否被置位。
✅ 2. 参数说明:
参数 | 类型 | 说明 |
---|---|---|
__HANDLE__ | DMA_HandleTypeDef * | 指向 DMA 通道的句柄(通常是 &hdma_xxx ) |
__FLAG__ | uint32_t | 要检查的标志位宏定义(如下表) |
常见标志位如下(适用于 DMA1
的各个通道):
宏定义 | 含义 |
---|---|
DMA_FLAG_TCx | 通道 x 的传输完成标志(Transfer Complete) |
DMA_FLAG_HTx | 通道 x 的半传输完成标志(Half Transfer) |
DMA_FLAG_TEx | 通道 x 的传输错误标志(Transfer Error) |
DMA_FLAG_GIx | 通道 x 的全局中断标志(全局中断 = TC + HT + TE) 例如 DMA_FLAG_TC1 、DMA_FLAG_HT3 、DMA_FLAG_TE2 |
💡 其中
x
是通道号(17,F1 系列为 DMA1_Channel17)
🔁 3. 返回值:
返回类型 | 说明 |
---|---|
uint32_t (0 或 1) | 如果指定标志位被置位 → 返回 1 ,否则返回 0 |
🎯 4. 功能说明:
- 用于判断 DMA 中断标志是否被置位。
- 可在中断服务函数或轮询中使用。
- 一般配合
__HAL_DMA_CLEAR_FLAG()
使用来手动清除中断标志位(如果 HAL 没有自动清除)。
🧪 示例用法:
if (__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1))
{// 通道1的传输完成__HAL_DMA_CLEAR_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1);// 执行接下来的操作...
}
2.9 DMA方式串口发送数据函数HAL_UART_Transmit_DMA()原型及功能
📌 1. 函数原型:
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
✅ 2. 参数说明:
参数名 | 类型 | 说明 |
---|---|---|
huart | UART_HandleTypeDef * | 指向 UART 句柄的指针(例如:&huart1 ) |
pData | uint8_t * | 指向要发送的数据缓冲区 |
Size | uint16_t | 要发送的数据长度(单位:字节) |
🔁 3. 返回值:
返回值 | 含义 |
---|---|
HAL_OK | 操作成功 |
HAL_BUSY | UART 当前正在忙于另一项传输 |
HAL_ERROR | 参数错误或初始化失败 |
HAL_TIMEOUT | (不适用于本函数,一般不会返回) |
🎯 4. 功能说明:
- 启动一个 UART 数据发送过程,由 DMA 自动完成传输。
- 使用 DMA 可以大幅减轻 CPU 负担,适用于高速或大批量数据发送。
- 在传输完成后,HAL 会调用回调函数
HAL_UART_TxCpltCallback()
(需要用户自己实现)。 - 非阻塞方式,函数调用后立即返回,不等待传输完成。
🧪 5. 使用示例:
示例 1:简单发送字符串
uint8_t message[] = "Hello DMA UART!\r\n";HAL_UART_Transmit_DMA(&huart1, message, sizeof(message) - 1);
💡 提示:
- 如果串口未配置 DMA,函数会失败。
- 如果正在发送其他数据,函数会返回
HAL_BUSY
。
🔁 6. 配合中断回调使用(推荐):
你可以定义 HAL_UART_TxCpltCallback()
来处理传输完成后的任务:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{if (huart->Instance == USART1){// 发送完成,执行后续动作HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 比如闪灯提示}
}
2.10 DMA方式串口接收数据函数HAL_UART_Receive_DMA()原型及功能
📌 1. 函数原型:
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
✅ 2. 参数说明:
参数名 | 类型 | 说明 |
---|---|---|
huart | UART_HandleTypeDef * | 指向 UART 外设的句柄(例如 &huart1 ) |
pData | uint8_t * | 接收数据的缓冲区指针(DMA 将接收到的数据放入此内存) |
Size | uint16_t | 要接收的数据长度(单位:字节) |
🔁 3. 返回值:
返回值 | 说明 |
---|---|
HAL_OK | 操作成功 |
HAL_BUSY | UART 当前正忙于接收其他数据 |
HAL_ERROR | 初始化或参数错误 |
🎯 4. 功能说明:
- 启动一次 DMA 接收过程,UART 接收到的数据会被 DMA 自动搬运到内存缓冲区。
- 是 非阻塞方式,即函数调用后立即返回,不会等待数据接收完成。
- 接收完成时,会自动调用回调函数
HAL_UART_RxCpltCallback()
(需要用户实现)。 - 可以实现大容量或高速串口数据的接收,CPU 开销小。
🧪 5. 使用示例:
示例 1:接收 10 字节数据
uint8_t rxBuffer[10];HAL_UART_Receive_DMA(&huart1, rxBuffer, 10);
💡 提示:接收完成后会触发中断,调用
HAL_UART_RxCpltCallback()
。
示例 2:实现回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart->Instance == USART1){// 数据接收完成后的处理逻辑,例如处理 rxBuffer 中的数据process_received_data(rxBuffer);}
}
2.11 启用某个 UART 中断功能函数__HAL_UART_ENABLE_IT()原型及功能
📌 1. 宏原型:
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) ((__HANDLE__)->Instance->CR1 |= (__INTERRUPT__))
⚠️ 注意:这是一个宏定义,不是普通函数。
✅ 2. 参数说明:
参数名 | 类型 | 说明 |
---|---|---|
__HANDLE__ | UART_HandleTypeDef * | UART 句柄指针(如 &huart1 ) |
__INTERRUPT__ | uint32_t | 要启用的中断类型宏(见下表) |
常用的 UART 中断类型:
宏定义 | 说明 |
---|---|
UART_IT_TXE | 发送数据寄存器为空中断(Transmit Data Register Empty) |
UART_IT_TC | 发送完成中断(Transmission Complete) |
UART_IT_RXNE | 接收寄存器非空中断(Received Data Ready to Be Read) |
UART_IT_IDLE | 空闲线路中断(Idle Line Detected) |
UART_IT_ERR | 错误中断(帧错误、过载、噪声等) |
🎯 3. 功能说明:
- 宏通过设置 UART 的控制寄存器来启用某个 UART 中断功能。
- 启用中断后,若对应条件发生(如接收到数据),则会触发 UART 的中断服务函数
USARTx_IRQHandler()
。 - 中断处理程序中会进一步调用 HAL 的中断处理函数,例如
HAL_UART_IRQHandler()
。 - 宏本身不会启用 NVIC 中的中断通道(需单独调用
HAL_NVIC_EnableIRQ()
)。
🧪 4. 示例代码:
示例:启用 UART1 的接收中断(RXNE)和空闲中断(IDLE)
// 启用 RXNE 和 IDLE 中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);// 同时,确保在 NVIC 中打开了中断通道(通常在初始化时)
HAL_NVIC_EnableIRQ(USART1_IRQn);
示例:中断服务函数中调用 HAL 的统一中断处理
void USART1_IRQHandler(void)
{HAL_UART_IRQHandler(&huart1); // HAL 会自动处理 RXNE、IDLE、TC 等事件
}
2.12 检查指定的 UART 状态标志位是否被置位函数__HAL_UART_GET_FLAG()原型及功能
📌 1. 宏原型:
#define __HAL_UART_GET_FLAG(__HANDLE__, __FLAG__) (((__HANDLE__)->Instance->SR & (__FLAG__)) == (__FLAG__))
📘 说明:这是一个宏定义,用于读取 UART 状态寄存器(
SR
)中的特定位。
✅ 2. 参数说明:
参数名 | 类型 | 说明 |
---|---|---|
__HANDLE__ | UART_HandleTypeDef * | 指向 UART 外设句柄的指针(如 &huart1 ) |
__FLAG__ | uint32_t | 需要检查的标志位(如 UART_FLAG_RXNE ) |
常用 UART 标志位(可作为 __FLAG__
传入):
宏定义 | 含义 |
---|---|
UART_FLAG_TXE | 发送数据寄存器为空 |
UART_FLAG_TC | 发送完成 |
UART_FLAG_RXNE | 接收寄存器非空,有数据可读 |
UART_FLAG_IDLE | 空闲线路检测标志 |
UART_FLAG_ORE | 溢出错误 |
UART_FLAG_NE | 噪声错误 |
UART_FLAG_FE | 帧格式错误 |
UART_FLAG_PE | 奇偶校验错误 |
🔎 这些标志来源于 UART 的状态寄存器 SR(Status Register)。
🔁 3. 返回值:
返回值 | 类型 | 含义 |
---|---|---|
true (非零) | uint32_t | 标志位置位,表示事件已发生 |
false (0) | uint32_t | 标志位未被置位,事件未发生 |
🎯 4. 功能说明:
- 检查指定的 UART 状态标志位是否被置位。
- 本质是对 UART 的
SR
(Status Register)寄存器执行按位与判断。 - 可用于判断是否发生了某些事件,例如:
- 是否接收到数据?
- 是否发送完成?
- 是否出现错误?
🧪 5. 示例代码:
示例 1:判断是否收到数据
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE))
{uint8_t data = (uint8_t)(huart1.Instance->DR); // 读取接收数据
}
示例 2:判断 UART1 是否发送完成
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC))
{// 数据已完全发送完毕HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
2.13 清除 UART 的 IDLE(空闲)标志位函数__HAL_UART_CLEAR_IDLEFLAG()原型及功能
📌 1. 宏原型:
#define __HAL_UART_CLEAR_IDLEFLAG(__HANDLE__) \do { __IO uint32_t tmpreg; tmpreg = (__HANDLE__)->Instance->SR; \tmpreg = (__HANDLE__)->Instance->DR; UNUSED(tmpreg); } while(0)
📌 说明:这是一个宏,用于清除 UART 的 IDLE(空闲)标志位。
✅ 2. 参数说明:
参数名 | 类型 | 说明 |
---|---|---|
__HANDLE__ | UART_HandleTypeDef * | UART 外设句柄指针(如 &huart1 ) |
🎯 3. 功能说明:
- UART 接收到数据后,如果检测到 一段时间内 RX 引脚没有继续接收数据,会设置 IDLE(空闲)标志位。
- 当启用了
UART_IT_IDLE
中断后,接收到 IDLE 中断后必须手动清除 IDLE 标志,否则该中断会不断触发。 - 清除 IDLE 标志的方式是:
- 先读取 UART 的状态寄存器
SR
- 再读取 数据寄存器
DR
- 先读取 UART 的状态寄存器
- 这个宏就是封装了这一操作。
🧪 4. 示例代码:
示例:使用 DMA + 空闲中断接收串口变长数据帧
void USART1_IRQHandler(void)
{HAL_UART_IRQHandler(&huart1); // HAL 处理其他 UART 中断// 手动处理 IDLE 中断(需要先启用 UART_IT_IDLE 中断)if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)){__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除 IDLE 标志位// 此处可调用用户的帧处理逻辑,比如判断实际接收长度uint16_t len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);handle_uart_frame(rxBuffer, len);}
}
✅
__HAL_DMA_GET_COUNTER()
返回 DMA 剩余传输字节数,配合可得出实际接收长度。
2.14 停止通过 DMA 进行的 UART 发送或接收操作函数HAL_UART_DMAStop()原型及功能
📌 1. 函数原型:
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);
✅ 2. 参数说明:
参数名 | 类型 | 说明 |
---|---|---|
huart | UART_HandleTypeDef * | 指向 UART 外设句柄的指针,例如 &huart1 |
🔁 3. 返回值:
返回值枚举 | 含义 |
---|---|
HAL_OK | 操作成功 |
HAL_ERROR | 发生错误 |
HAL_BUSY | 外设正在忙 |
HAL_TIMEOUT | 操作超时(通常不会在此函数中返回) |
🎯 4. 功能说明:
HAL_UART_DMAStop()
的主要功能是:
- 停止通过 DMA 进行的 UART 发送或接收操作(无论是 TX 还是 RX)。
- 禁用 DMA 请求:
- 清除 UART 控制寄存器中的
DMAT
或DMAR
位(关闭发送/接收 DMA 请求)。
- 清除 UART 控制寄存器中的
- 停止 DMA 通道:
- 停止
huart->hdmarx
或huart->hdmatx
中的 DMA 操作。
- 停止
- 释放资源,为下一次操作做准备。
🧪 5. 示例代码:
示例:接收到一帧变长数据后停止 DMA 接收
#define RX_BUF_LEN 64
uint8_t rx_buf[RX_BUF_LEN];void USART1_IRQHandler(void)
{HAL_UART_IRQHandler(&huart1);if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)){__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲标志HAL_UART_DMAStop(&huart1); // ✅ 停止 DMA 接收// 获取实际接收长度uint16_t recv_len = RX_BUF_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx);process_uart_frame(rx_buf, recv_len); // 处理接收到的数据// 重新启动 DMA 接收HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_LEN);}
}
2.15 获取当前 DMA 传输还剩下多少个数据单位尚未完成函数__HAL_DMA_GET_COUNTER()原型及功能
📌 1. 宏原型:
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR)
📌 它实际上是读取 DMA 控制器的当前剩余传输数量寄存器(CNDTR)。
✅ 2. 参数说明:
参数名 | 类型 | 说明 |
---|---|---|
__HANDLE__ | DMA_HandleTypeDef * | 指向 DMA 通道的句柄,比如 huart1.hdmarx |
🔁 3. 返回值:
类型 | 说明 |
---|---|
uint16_t | 返回 DMA 通道尚未完成传输的字节数(即 CNDTR 的当前值) |
🎯 4. 功能说明:
- DMA 在传输过程中,CNDTR 寄存器会不断减少,表示还有多少数据尚未传输。
__HAL_DMA_GET_COUNTER()
宏返回的是DMA 剩余数据量,因此你可以据此计算出:- 实际已经接收到(或发送出)的数据字节数。
- 这个宏非常适合用于 DMA + 空闲中断 模式下判断已接收的数据长度。
🧪 5. 示例代码:
示例:计算 DMA 实际接收的数据长度
#define RX_BUF_LEN 64
uint8_t rx_buf[RX_BUF_LEN];void USART1_IRQHandler(void)
{HAL_UART_IRQHandler(&huart1);if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)){__HAL_UART_CLEAR_IDLEFLAG(&huart1);HAL_UART_DMAStop(&huart1);// ❗ 计算接收的数据长度uint16_t received_len = RX_BUF_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx);process_uart_data(rx_buf, received_len);// 重新启动 DMA 接收HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_LEN);}
}
三、内存👉内存搬运
实现需求:使用DMA的方式将数组A中的数据拷贝到数组B中,搬运完之后将数组B中的内容打印到屏幕
3.1 CubeMX配置
-
配置Debug
-
打开RCC配置时钟,将高速时钟配置成Crystal/Ceramic Resonator
-
点击配置时钟选项,把时钟配置成高速外部时钟HSE,并且通过PLLCLK倍频到72MHZ,按下回车之后就为芯片内部的功能分配好了对应的时钟频率。
-
打开左侧的DMA选项,首先选择:内存到内存,然后点击Add添加一个,接着配置通道、方向和优先级,然后配置DMA数据传输方式为:正常模式,然后配置指针递增模式为:源地址 和 目标地址 同时递增。
-
接着我们打开USART1,配置为:异步通信模式,配置:波特率、数据位、停止位、奇偶校验位等
-
接着对我们的工程进行配置,配置完成之后点击右上角的 GENERATE CODE 生成工程
3.2 代码实现
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes *//* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
#define BUF_SIZE 16
/* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */
uint32_t srcBuffer[BUF_SIZE] = { // 源数组0x00000000, 0x11111111, 0x22222222, 0x33333333,0x44444444, 0x55555555, 0x66666666, 0x77777777, 0x88888888, 0x99999999, 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD, 0xEEEEEEEE, 0xFFFFFFFF
};uint32_t desBuffer[BUF_SIZE]; // 目标数组/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int fputc(int ch, FILE *f)
{unsigned char temp[1]={ch};HAL_UART_Transmit(&huart1,temp,1,0xffff);return ch;
}
/* USER CODE END 0 */int main(void)
{/* USER CODE BEGIN 1 */int i = 0;/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */// DMA开启数据传输HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)srcBuffer, (uint32_t)desBuffer, sizeof(uint32_t) * BUF_SIZE); // 等待数据传输完成while(__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1)); // 打印数组内容for(i = 0; i< BUF_SIZE; i++){printf("BUF[%d] = %x\r\n", i, desBuffer[i]);}/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
业务代码写完之后,我们把它编译并且烧录进MCU里面,硬件连接USB-TTL模块,软件打开串口调试助手,就会发现我们成功实现了:使用DMA搬运方式内存到内存之间的数据搬运哈哈👏
四、内存👉外设搬运
实现需求:使用DMA的方式将内存数据搬运到串口1发送寄存器,同时闪烁LED1。
4.1 CubeMX配置
-
配置Debug
-
打开RCC配置时钟,将高速时钟配置成Crystal/Ceramic Resonator
-
点击配置时钟选项,把时钟配置成高速外部时钟HSE,并且通过PLLCLK倍频到72MHZ,按下回车之后就为芯片内部的功能分配好了对应的时钟频率。
-
打开USART1,配置为:异步通信模式,配置:波特率、数据位、停止位、奇偶校验位等
-
将PB8配置成GPIO_Output模式,PB8连接的是LED,并设置PB8初始化为高电平
-
打开左侧的DMA选项,首先选择:内存到外设,然后点击Add添加一个,接着配置通道、方向和优先级,然后配置DMA数据传输方式为:正常模式,然后配置指针递增模式为:内存地址(源地址)递增,目标地址不递增。
-
接着对我们的工程进行配置,配置完成之后点击右上角的 GENERATE CODE 生成工程
4.2 代码实现
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
#define BUF_SIZE 1000
/* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */
unsigned char sendBuf[BUF_SIZE] = {0}; // 发送数据缓冲区
/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP *//* USER CODE END PFP */int main(void)
{/* USER CODE BEGIN 1 */int i = 0;/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */for(i = 0; i < BUF_SIZE; i++){sendBuf[i] = 'A';}// 将数据通过串口DMA发送HAL_UART_Transmit_DMA(&huart1, sendBuf, BUF_SIZE);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */// 主函数一直运行LED闪烁HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8); HAL_Delay(100);}/* USER CODE END 3 */
}
业务代码写完之后,我们把它编译并且烧录进MCU里面,硬件连接USB-TTL模块,软件打开串口调试助手,就会发现串口助手接收到1000个数据的同时LED也在闪烁,实现了使用DMA的方式内存到外设之间的数据搬运。
五、外设👉内存搬运
实现需求:使用DMA的方式将串口接收缓存寄存器的值搬运到内存中,同时闪烁LED。
5.1 CubeMX配置
- 配置Debug
-
打开RCC配置时钟,将高速时钟配置成Crystal/Ceramic Resonator
-
点击配置时钟选项,把时钟配置成高速外部时钟HSE,并且通过PLLCLK倍频到72MHZ,按下回车之后就为芯片内部的功能分配好了对应的时钟频率。
-
打开USART1,配置为:异步通信模式,配置:波特率、数据位、停止位、奇偶校验位等,并使能USART1中断。
-
将PB8配置成GPIO_Output模式,PB8连接的是LED,并设置PB8初始化为高电平
-
打开左侧的DMA选项,首先选择:DMA1,然后点击Add添加两个,接着配置通道、方向和优先级,然后配置DMA数据传输方式为:正常模式,然后配置指针递增模式为:内存地址(源地址)递增,目标地址不递增。
-
接着对我们的工程进行配置,配置完成之后点击右上角的 GENERATE CODE 生成工程
5.2 代码实现
/* main.c */
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"/* USER CODE BEGIN PV */
uint8_t rcvBuf[BUF_SIZE] = {0}; // 接收数据缓冲区
uint8_t rcvLen = 0; // 接收一帧数据长度
/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能USART1空闲中断HAL_UART_Receive_DMA(&huart1, rcvBuf, BUF_SIZE); // 使能DMA方式接收串口数据中断/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8);HAL_Delay(300);/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
/* main.h */
#define BUF_SIZE 100
/* stm32f1xx_it.c */
#include "main.h"
#include "stm32f1xx_it.h"/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
extern uint8_t rcvBuf[BUF_SIZE]; // 接收数据缓冲区
extern uint8_t rcvLen; // 接收一帧数据长度
/* USER CODE END PV *//* External variables --------------------------------------------------------*/
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
extern UART_HandleTypeDef huart1;
/* USER CODE BEGIN EV *//* USER CODE END EV *//******************************************************************************/
/* Cortex-M3 Processor Interruption and Exception Handlers */
/******************************************************************************/
/*** @brief This function handles Non maskable interrupt.*/
void NMI_Handler(void)
{/* USER CODE BEGIN NonMaskableInt_IRQn 0 *//* USER CODE END NonMaskableInt_IRQn 0 *//* USER CODE BEGIN NonMaskableInt_IRQn 1 */while (1){}/* USER CODE END NonMaskableInt_IRQn 1 */
}/*** @brief This function handles Hard fault interrupt.*/
void HardFault_Handler(void)
{/* USER CODE BEGIN HardFault_IRQn 0 *//* USER CODE END HardFault_IRQn 0 */while (1){/* USER CODE BEGIN W1_HardFault_IRQn 0 *//* USER CODE END W1_HardFault_IRQn 0 */}
}/*** @brief This function handles Memory management fault.*/
void MemManage_Handler(void)
{/* USER CODE BEGIN MemoryManagement_IRQn 0 *//* USER CODE END MemoryManagement_IRQn 0 */while (1){/* USER CODE BEGIN W1_MemoryManagement_IRQn 0 *//* USER CODE END W1_MemoryManagement_IRQn 0 */}
}/*** @brief This function handles Prefetch fault, memory access fault.*/
void BusFault_Handler(void)
{/* USER CODE BEGIN BusFault_IRQn 0 *//* USER CODE END BusFault_IRQn 0 */while (1){/* USER CODE BEGIN W1_BusFault_IRQn 0 *//* USER CODE END W1_BusFault_IRQn 0 */}
}/*** @brief This function handles Undefined instruction or illegal state.*/
void UsageFault_Handler(void)
{/* USER CODE BEGIN UsageFault_IRQn 0 *//* USER CODE END UsageFault_IRQn 0 */while (1){/* USER CODE BEGIN W1_UsageFault_IRQn 0 *//* USER CODE END W1_UsageFault_IRQn 0 */}
}/*** @brief This function handles System service call via SWI instruction.*/
void SVC_Handler(void)
{/* USER CODE BEGIN SVCall_IRQn 0 *//* USER CODE END SVCall_IRQn 0 *//* USER CODE BEGIN SVCall_IRQn 1 *//* USER CODE END SVCall_IRQn 1 */
}/*** @brief This function handles Debug monitor.*/
void DebugMon_Handler(void)
{/* USER CODE BEGIN DebugMonitor_IRQn 0 *//* USER CODE END DebugMonitor_IRQn 0 *//* USER CODE BEGIN DebugMonitor_IRQn 1 *//* USER CODE END DebugMonitor_IRQn 1 */
}/*** @brief This function handles Pendable request for system service.*/
void PendSV_Handler(void)
{/* USER CODE BEGIN PendSV_IRQn 0 *//* USER CODE END PendSV_IRQn 0 *//* USER CODE BEGIN PendSV_IRQn 1 *//* USER CODE END PendSV_IRQn 1 */
}/*** @brief This function handles System tick timer.*/
void SysTick_Handler(void)
{/* USER CODE BEGIN SysTick_IRQn 0 *//* USER CODE END SysTick_IRQn 0 */HAL_IncTick();/* USER CODE BEGIN SysTick_IRQn 1 *//* USER CODE END SysTick_IRQn 1 */
}/******************************************************************************/
/* STM32F1xx Peripheral Interrupt Handlers */
/* Add here the Interrupt Handlers for the used peripherals. */
/* For the available peripheral interrupt handler names, */
/* please refer to the startup file (startup_stm32f1xx.s). */
/******************************************************************************//*** @brief This function handles DMA1 channel4 global interrupt.*/
void DMA1_Channel4_IRQHandler(void)
{/* USER CODE BEGIN DMA1_Channel4_IRQn 0 *//* USER CODE END DMA1_Channel4_IRQn 0 */HAL_DMA_IRQHandler(&hdma_usart1_tx);/* USER CODE BEGIN DMA1_Channel4_IRQn 1 *//* USER CODE END DMA1_Channel4_IRQn 1 */
}/*** @brief This function handles DMA1 channel5 global interrupt.*/
void DMA1_Channel5_IRQHandler(void)
{/* USER CODE BEGIN DMA1_Channel5_IRQn 0 *//* USER CODE END DMA1_Channel5_IRQn 0 */HAL_DMA_IRQHandler(&hdma_usart1_rx);/* USER CODE BEGIN DMA1_Channel5_IRQn 1 *//* USER CODE END DMA1_Channel5_IRQn 1 */
}/*** @brief This function handles USART1 global interrupt.*/
void USART1_IRQHandler(void)
{/* USER CODE BEGIN USART1_IRQn 0 *//* USER CODE END USART1_IRQn 0 */HAL_UART_IRQHandler(&huart1);/* USER CODE BEGIN USART1_IRQn 1 */if((__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) == SET)) // 判断IDLE标志位是否被置位{__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除标志位HAL_UART_DMAStop(&huart1); // 停止DMA传输,防止干扰uint8_t temp=__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);rcvLen = BUF_SIZE - temp; //计算数据长度HAL_UART_Transmit_DMA(&huart1, rcvBuf, rcvLen); //发送数据HAL_UART_Receive_DMA(&huart1, rcvBuf, BUF_SIZE); //开启DMA}/* USER CODE END USART1_IRQn 1 */
}/* USER CODE BEGIN 1 *//* USER CODE END 1 */
业务代码写完之后,我们把它编译并且烧录进MCU里面,硬件连接USB-TTL模块,软件打开串口调试助手,就会发现我们成功实现了我们的需求: