4. 轮询发送机制:从THR到TSR的完整旅程

好,咱们今天聊聊UART的发送机制。说实话,发送比接收要简单不少,但里面有几个关键寄存器你得搞明白。我个人习惯把发送过程想象成一条流水线——数据从CPU写入,经过两个缓冲站,最后从引脚飞出去。

4.1 两个核心寄存器:THR与TSR

先看这两个家伙:

寄存器 全称 作用 谁访问它
THR Transmit Holding Register CPU写入数据的缓冲槽 CPU(写)
TSR Transmit Shift Register 真正移位发送的寄存器 UART硬件(只读)

THR是你能直接操作的。你往THR写一个字节,硬件会在合适的时机把它搬到TSR。TSR呢?它负责把并行数据转成串行,一位一位地从TX引脚推出去。CPU碰不到TSR,那是硬件的地盘。

关键理解:THR是CPU和硬件之间的握手点。你只管往THR塞数据,硬件负责从THR搬到TSR,再从TSR发出去。这两个寄存器构成了一个深度为1的缓冲。

4.2 两个标志位:TXE与TC

这两个标志位经常被搞混。我刚开始做UART驱动时也栽过跟头,咱们一次说清楚。

标志位 含义 置位条件 典型用途
TXE Transmit Holding Register Empty THR中的数据已被搬到TSR 通知CPU可以写下一个字节
TC Transmission Complete TSR移位完成,TX引脚空闲 通知CPU整帧发送完毕

说白了,TXE表示“你的数据已经离开THR了,可以再塞一个进来”。TC表示“连TSR里的最后一位都发出去了,整条线空了”。

我在项目里遇到过一个问题:发送完最后一条指令后立刻切GPIO模式,结果TX引脚上还有半个字节没发完,导致总线冲突。后来我养成了习惯——关发送之前一定等TC置位。

注意:只检查TXE就认为发送完成是不安全的。TXE置位只代表THR空了,但TSR可能还在移位。如果你需要确保数据确实从引脚发出去了(比如关闭外设、切换模式、进入低功耗),必须等待TC。

4.3 轮询发送的完整流程

轮询发送,说白了就是CPU主动去查标志位,而不是等中断来通知。流程很简单:

  1. 等待TXE置位——确认THR为空,可以写入新数据
  2. 写入THR——CPU把要发的字节塞进去
  3. 重复1-2——直到所有数据发完
  4. 等待TC置位——确保最后一帧完整发出(可选但推荐)

你想想看,这个流程里CPU大部分时间都在“等”。这就是轮询的代价——CPU被占住了,干不了别的。但好处是代码简单、时序确定,适合短数据块或实时性要求不高的场景。

4.4 轮询发送代码实现

直接上代码。我用的是STM32的寄存器版本,这样你能看到最底层的东西。

// 轮询发送一个字节
void uart_send_byte(uint8_t data)
{
    // 等待THR为空(TXE=1)
    while (!(USART1->SR & USART_SR_TXE));
    
    // 写入数据到THR
    USART1->DR = data;
}

// 轮询发送一个缓冲区
void uart_send_buffer(uint8_t *buf, uint32_t len)
{
    for (uint32_t i = 0; i < len; i++)
    {
        // 等待THR空
        while (!(USART1->SR & USART_SR_TXE));
        
        // 写入当前字节
        USART1->DR = buf[i];
    }
    
    // 等待最后一帧发送完成(TC)
    while (!(USART1->SR & USART_SR_TC));
}

这段代码有几个地方我想强调一下:

  • while循环的条件——检查TXE位是否为1。有些芯片的TXE是低电平有效,记得看数据手册。
  • 写入DR寄存器——在STM32里,读DR和写DR是两个不同的地址映射,写DR就是写THR。
  • 最后的TC等待——很多人会漏掉这一步。如果你只是连续发数据,不关串口,那确实可以省。但如果你要确保所有数据都出去了,这一步不能省。

小技巧:如果你用的是HAL库,HAL_UART_Transmit()内部就是轮询发送。但它的实现里会超时退出,不会死等。我建议你自己写轮询发送时也加个超时机制,防止硬件异常导致CPU卡死。

4.5 轮询发送的时序图

下面这张图展示了轮询发送一个字节时,CPU、THR、TSR和TX引脚之间的协作关系。

轮询发送时序图(单字节) t CPU 查TXE 写THR 查TXE 查TC THR 空 有数据 空 空 TSR 空闲 移位中 移位中 空闲 TX 空闲 发送中 发送中 空闲 标志 TXE=0 TXE=0 TXE=1 TC=0 TC=1 CPU在“查TXE”阶段循环等待,直到硬件将THR数据搬至TSR后释放TXE

从时序图里你能看到:CPU先查TXE,确认THR空后写入数据。硬件自动把THR搬到TSR,开始移位发送。此时TXE重新置位,CPU可以写下一个字节。最后,等TSR发完所有位,TC置位,CPU退出等待。

4.6 轮询发送的优缺点

咱们客观地看一下轮询发送的利弊:

维度 评价
代码复杂度 极低,几行循环就能搞定
CPU占用率 高,发送期间CPU被完全占用
实时性 好,发送延迟可预测
适用场景 短数据块、系统初始化、调试输出
不适用场景 大数据量、多任务系统、低功耗应用

我个人的经验是:轮询发送最适合用在系统刚启动时打印日志,或者发送一些控制指令。一旦数据量大了,或者系统里跑着RTOS,我就会切换到中断发送或DMA发送。但不管用哪种方式,理解THR、TSR、TXE和TC这四个概念是基本功。

避坑指南:我曾经在一个项目里用轮询发送1KB的数据,结果CPU被卡了将近100ms,导致另一个任务错过了中断。后来我加了个小技巧——每发完16个字节就主动让出CPU(比如调用taskYIELD()或delay(1)),既保证了发送不中断,又给了其他任务运行的机会。