3. 串口DMA发送原理:TXE/TC标志与DMA请求的关系,发送缓冲区设计思路
串口DMA发送的核心在于理解两个关键状态标志——TXE (发送数据寄存器空) 和 TC (发送完成)——与DMA请求之间的协同关系。只有理清这一机制,才能设计出高效、可靠的发送缓冲区。
3.1 TXE与TC标志的硬件行为
在STM32的USART外设中,TXE和TC标志分别对应发送过程中的不同阶段:
| 标志 | 全称 | 置位条件 | 清除方式 | DMA触发时机 |
|---|---|---|---|---|
| TXE | 发送数据寄存器空 | DR寄存器数据已移入移位寄存器 | 写入DR寄存器 | TXE=1时触发DMA请求 |
| TC | 发送完成 | 移位寄存器发送完毕,且TXE=1 | 软件写1清除或读SR后写DR | 不直接触发DMA |
关键区别: TXE标志表示“可以写入下一个数据”,而TC标志表示“所有数据已经物理发送完毕”。在DMA发送中,DMA请求由TXE触发,TC则用于判断整帧发送是否结束。
3.2 DMA请求与TXE的联动机制
当USART使能DMA发送(USART_CR3中的DMAT=1)时,硬件会自动将TXE标志与DMA请求信号连接。其工作流程如下:
- 初始状态: TXE=1,DMA控制器收到第一个请求,从内存读取数据写入USART_DR。
- 数据转移: 写入DR后TXE自动清零,DMA请求撤销;数据从DR移入移位寄存器后,TXE再次置1,触发下一个DMA请求。
- 循环往复: DMA根据配置的传输长度(NDT)自动完成所有数据的搬运,无需CPU干预。
- 传输完成: DMA传输计数器归零,产生DMA传输完成中断(TCIF),此时USART可能仍在发送最后一位数据。
避坑提示: DMA传输完成中断 ≠ 数据物理发送完成。在DMA中断中立即关闭USART或修改GPIO可能导致最后几个字节丢失。必须等待USART的TC标志置位后,才能认为数据真正发送完毕。
3.3 发送缓冲区设计思路
基于上述原理,发送缓冲区的设计需要解决三个核心问题:数据来源、DMA描述符管理、完成回调。以下是两种典型设计模式:
3.3.1 单次发送缓冲区(线性模式)
适用于不定长、低频发送场景。每次发送前将数据拷贝到DMA缓冲区,启动DMA传输,在完成回调中释放缓冲区。
// 单次发送缓冲区示例
typedef struct {
uint8_t *data; // 数据指针
uint16_t len; // 数据长度
uint8_t in_use; // 使用标志
} UART_TxBuf_t;
UART_TxBuf_t tx_buf;
void UART_DMA_Send(uint8_t *pData, uint16_t len) {
// 等待前一次发送完成
while (tx_buf.in_use);
// 拷贝数据到DMA缓冲区(或直接使用外部缓冲区)
memcpy(tx_buf.data, pData, len);
tx_buf.len = len;
tx_buf.in_use = 1;
// 启动DMA传输
HAL_UART_Transmit_DMA(&huart1, tx_buf.data, tx_buf.len);
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 等待TC标志(硬件自动处理,但可在此确认)
tx_buf.in_use = 0; // 释放缓冲区
}
}
3.3.2 乒乓发送缓冲区(双缓冲模式)
适用于连续、高速发送场景。使用两个缓冲区交替工作,一个用于DMA发送,另一个用于CPU填充数据,消除等待时间。
注意: STM32的DMA不支持硬件乒乓模式(即自动切换内存地址),需要软件在DMA完成中断中手动切换缓冲区指针。HAL库的HAL_UART_Transmit_DMA不支持连续双缓冲,需使用寄存器级操作或LL库。
// 乒乓发送缓冲区结构
#define TX_BUF_SIZE 256
typedef struct {
uint8_t buf[2][TX_BUF_SIZE]; // 双缓冲区
uint8_t active_buf; // 当前DMA使用的缓冲区索引
uint16_t pending_len[2]; // 每个缓冲区待发送长度
uint8_t ready[2]; // 缓冲区是否就绪
} UART_PingPongTx_t;
UART_PingPongTx_t tx_pp;
void UART_PingPong_Start(void) {
// 初始化DMA为循环模式,但通过软件控制传输长度
// 实际使用中通常采用正常模式+完成中断切换
tx_pp.active_buf = 0;
tx_pp.ready[0] = 0;
tx_pp.ready[1] = 0;
}
void UART_PingPong_Write(uint8_t *data, uint16_t len) {
uint8_t next_buf = tx_pp.active_buf ^ 1; // 切换到另一个缓冲区
// 等待目标缓冲区空闲
while (tx_pp.ready[next_buf]);
// 填充数据
memcpy(tx_pp.buf[next_buf], data, len);
tx_pp.pending_len[next_buf] = len;
tx_pp.ready[next_buf] = 1;
// 如果DMA空闲,立即启动传输
if (tx_pp.ready[tx_pp.active_buf] == 0) {
tx_pp.active_buf = next_buf;
HAL_UART_Transmit_DMA(&huart1, tx_pp.buf[next_buf], tx_pp.pending_len[next_buf]);
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
uint8_t done_buf = tx_pp.active_buf;
tx_pp.ready[done_buf] = 0; // 标记缓冲区可用
// 检查另一个缓冲区是否有数据待发送
uint8_t other_buf = done_buf ^ 1;
if (tx_pp.ready[other_buf]) {
tx_pp.active_buf = other_buf;
HAL_UART_Transmit_DMA(&huart1, tx_pp.buf[other_buf], tx_pp.pending_len[other_buf]);
}
}
}
3.4 发送缓冲区设计对比
| 特性 | 单次缓冲区 | 乒乓缓冲区 | 环形缓冲区 |
|---|---|---|---|
| 内存占用 | 低(1个缓冲区) | 中(2个缓冲区) | 高(可配置) |
| CPU等待时间 | 有(等待发送完成) | 无(交替填充) | 无(持续写入) |
| 实现复杂度 | 简单 | 中等 | 较高 |
| 适用场景 | 低频、短帧 | 中高频、固定帧 | 高频、变长帧 |
| DMA模式 | 正常模式 | 正常模式+中断切换 | 循环模式+半满中断 |
3.5 TXE与DMA请求的时序关系
下图展示了TXE标志、DMA请求与数据发送之间的精确时序关系:
3.6 发送缓冲区设计的关键考量
- DMA传输完成与TC的同步: 在DMA完成中断中,建议先检查USART的TC标志(或等待其置位),再释放缓冲区或切换缓冲区。可以使用
while(!(USART1->SR & USART_SR_TC));或利用HAL库的HAL_UART_GetState()判断。 - 缓冲区大小对齐: DMA传输要求内存地址对齐(通常为1字节对齐,但建议4字节对齐以提高性能),且缓冲区大小不应超过DMA的NDT寄存器最大值(65535)。
- 临界区保护: 在乒乓缓冲区或环形缓冲区中,CPU填充数据和DMA读取数据可能同时操作同一缓冲区,需要使用关中断或信号量保护临界区。
- 缓存一致性: 如果使用带Cache的MCU(如STM32H7),DMA访问内存时可能绕过Cache,导致CPU写入的数据未被DMA读取。需使用
SCB_CleanDCache_by_Addr()或配置MPU为非缓存区域。
工程经验: 在高速串口(如921600bps以上)发送时,建议使用乒乓缓冲区+双DMA(一个用于发送,一个用于内存搬运)或使用DMA的循环模式+半满中断,避免CPU频繁进入中断处理缓冲区切换。