双缓冲
(UART、SPI、I2C)概念上,绝对都有双缓冲机制;但在具体表现和名字上,I2C 是个“奇葩”。
一、 为什么要都有?(为了不让你“过劳死”)
首先肯定一点:它们都有双缓冲(Double Buffering)机制。 即:“移位寄存器(干活的)” + “数据寄存器(缓存的)”。
如果没有双缓冲,会发生什么?
- 发送时:发完一个字节,CPU 必须在微秒级的瞬间把下一个字节填进去,慢一丁点,波形就会断开(Gap)。
- 接收时:收完一个字节,CPU 必须瞬间读走,慢一丁点,下一个位进来了,数据就覆盖了。
所以,为了让 CPU 哪怕去喝口茶(处理个中断)也不影响通信,UART、SPI、I2C 全部标配双缓冲。
二、 三兄弟的具体表现
虽然原理一样,但因为协议特性不同,它们的TXE / RXNE 性格完全不一样。
1. UART(串口)—— 最标准的模范生
UART 是异步通信,没有时钟线,全靠波特率约定。
- 双缓冲:有。
- 标志位:
- TXE:完全一样。表示 TDR 空了,可以写下一个。
- RXNE:完全一样。表示 RDR 有数据,赶紧读。
- 老鸟提示:UART 因为速度慢(比如 9600 或 115200),CPU 响应时间很充裕,所以双缓冲带来的“紧迫感”不强。但在高速(如 3Mbps)下,没有双缓冲根本跑不起来。
2. SPI —— 急性子的“快枪手”
SPI 是同步通信,速度极快(可达 50MHz+)。
- 双缓冲:极其重要。
- 标志位:
- TXE:最关键。SPI 追求连续传输(Burst Transfer),为了让波形中间没有空隙,你必须在上一字节还没发完(Shift Register 忙)时,就把下一个字节塞进 TDR(TXE=1 时)。这样硬件可以无缝衔接。
- RXNE:同上。
- 老鸟提示:SPI 的双缓冲是为了速度。写 SPI 驱动如果中间有延时,效率会大打折扣。
3. I2C —— 磨磨唧唧的“控制狂” (重点!)
I2C 也有双缓冲,也有 TXE/RXNE,但它的行为完全不同。
- 区别核心:I2C 有一根 SCL(时钟线),而且支持时钟延展(Clock Stretching)。
- TXE (发送空):
- 在 SPI/UART 里,如果你不填数据,也就是闲着。
- 在 I2C 里(特别是从机模式):如果 TXE=1 了(盘子空了),而 CPU 还没来得及填数据,I2C 硬件会强制拉低 SCL 线!整个总线都会暂停,直到你把数据填进去,它才松手。
- RXNE (接收非空):
- 在 SPI/UART 里,你不读,新数据会把旧数据覆盖(Overrun)。
- 在 I2C 里:如果你不读走数据,硬件也会拉低 SCL 线,让对方(主机)暂停发送。
- 结论:I2C 的双缓冲不仅是缓存,还起到了流控(Flow Control) 的作用。
三、 资深工程师的“进阶眼界”
作为资深开发,你不能只盯着 TXE 和 RXNE 这两个名字,因为换个芯片可能就不认识了。你需要掌握的是FIFO(先进先出队列) 的概念。
1. 从“双缓冲”到“FIFO”
普通的 STM32F1 系列(如 F103)是1个字节深度的双缓冲。
但高级的芯片(如 STM32F4/H7,或者 NXP/TI 的芯片,以及 ESP32),它们的串口和 SPI 往往带有 FIFO。
- 区别:
- 双缓冲:备餐区只能放 1 盘菜。
- FIFO:备餐区可以放 16 甚至 128 盘菜。
2. 标志位的变化
一旦有了 FIFO,TXE 和 RXNE 的定义就变了(或者多了新标志):
- TXE (Transmit Empty) -> 可能变成 TXFNF (Transmit FIFO Not Full)。
- 意思:“只要 FIFO 没满,你就可以往里猛灌数据,不用一次一次等。”
- RXNE (Receive Not Empty) -> 可能变成 RXFNE (Receive FIFO Not Empty)。
- 甚至会有“水位中断”:比如 FIFO 攒够了 8 个字节,再触发中断让你一次性读走,减少 CPU 进中断的次数。
四、 总结一张表
| 特性 | UART | SPI | I2C |
|---|---|---|---|
| 是否有双缓冲? | 有 | 有 | 有 |
| TXE (发送空) 含义 | 可以在发当前字节时准备下一字节 | 为了连续传输,必须利用它实现无间隙发送 | 带有流控属性,不填数据可能会拉低 SCL 暂停总线 |
| RXNE (接收非空) 含义 | 有数据需读取 | 必须配合发送 dummy byte 才会产生 | 带有流控属性,不读数据可能会拉低 SCL |
| 如果不处理后果 | Overrun (丢数据) | Overrun (丢数据) | Clock Stretch(总线挂起/卡死) |
最后的“坑”提醒
在 I2C 编程中(特别是 STM32 的硬件 I2C),很多 bug 都是因为在这个标志位上处理慢了,导致硬件自动拉住了 SCL 线,结果主程序以为 I2C 坏了(超时)。
我的建议:
初学 I2C 时,尽量先用 GPIO 模拟(Bit-banging),因为模拟的时序完全由你控制,没有这些复杂的缓冲和硬件流控逻辑。等熟悉了协议,再挑战 STM32 的硬件 I2C 驱动(那可是出了名的难写,被称为“鬼见愁”)。