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请求信号连接。其工作流程如下:

  1. 初始状态: TXE=1,DMA控制器收到第一个请求,从内存读取数据写入USART_DR。
  2. 数据转移: 写入DR后TXE自动清零,DMA请求撤销;数据从DR移入移位寄存器后,TXE再次置1,触发下一个DMA请求。
  3. 循环往复: DMA根据配置的传输长度(NDT)自动完成所有数据的搬运,无需CPU干预。
  4. 传输完成: 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请求与数据发送之间的精确时序关系:

TXE标志与DMA请求时序关系 USART时钟 TXE 1 1 1 1 1 1 1 DMA请求 请求 请求 请求 请求 请求 请求 请求 DR写入 DMA写 DMA写 DMA写 DMA写 DMA写 DMA写 移位寄存器 发送位0-7 发送位0-7 发送位0-7 发送位0-7 发送位0-7 发送位0-7 TC 1 TXE置1 → DMA请求 → DMA写入DR → TXE清零 → 数据移入移位寄存器 → TXE再次置1(循环)

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频繁进入中断处理缓冲区切换。