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主动去查标志位,而不是等中断来通知。流程很简单:
- 等待TXE置位——确认THR为空,可以写入新数据
- 写入THR——CPU把要发的字节塞进去
- 重复1-2——直到所有数据发完
- 等待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引脚之间的协作关系。
从时序图里你能看到: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)),既保证了发送不中断,又给了其他任务运行的机会。