3.19. 代码中的 UART¶
machine.UART 封装了一个硬件 UART 通道。用总线 id 和波特率来构造它;其余参数都有合理的默认值:
from machine import UART
uart = UART(3, baudrate=115200)
id 用于选择使用哪个硬件 UART,其取值取决于具体板子(请参阅 OpenMV 开发板,了解给定摄像头上可用的总线编号和引脚分配)。默认帧格式为 8 个数据位、无奇偶校验、一个停止位——也就是大家都熟悉的 “8N1”。
3.19.1. 写入与读取¶
UART 输出通过 write() 进行:
uart.write("hello\n")
uart.write(b"\x01\x02\x03")
write 接受 str(按 UTF-8 编码)或 bytes / bytearray。一旦字节被排入 TX 缓冲区,它就立即返回;硬件会在后台完成把它们时钟移出的工作。
读取通过三个方法进行,具体取决于需求:
n = uart.any() # bytes available to read right now
data = uart.read(8) # up to 8 bytes, or None on timeout
line = uart.readline() # bytes ending in '\n', or None on timeout
any() 在不阻塞的情况下检查 RX 缓冲区。read() 读取固定数量的字节,如果超时(可通过构造函数中的 timeout 参数配置)先到期,则返回 None。readline() 读取直到并包括下一个换行符为止,对基于行的协议很有用。
一个简单的循环,把收到的内容原样回送:
uart = UART(3, baudrate=115200, timeout=100)
while True:
if uart.any():
data = uart.read()
uart.write(data)
不带长度参数的 read() 会持续读取字节,直到接收线路保持安静达到配置的 timeout 时间,然后返回累积到的所有内容。当 timeout=100 时,这里每次调用都会返回一 批 字节——即发送方时钟移出的、字节之间没有 100 ms 间隔的所有数据。如果没有超时,这个调用就没有信号表明发送方已经发完,可能会无限期挂起。
3.19.2. 使用 struct 处理二进制数据¶
通过线路发送整数和浮点数正是 struct 模块的用途。它使用一个格式字符串将定宽值打包成一个 bytes 对象,该格式字符串指明字节序和每个字段的类型:
import struct
uart.write(struct.pack("<lhb", count, temperature_x100, status))
开头的 "<" 选择小端字节序;"l" 是 32 位有符号整数,"h" 是 16 位有符号整数,"b" 是 8 位有符号整数。另一端使用 相同的 格式字符串进行解包:
payload = uart.read(7) # 4 + 2 + 1 = 7 bytes
count, temperature_x100, status = struct.unpack("<lhb", payload)
格式字符串是两端之间的约定。任何不匹配——字节序错误、类型大小错误、字段顺序错误——都会产生无意义的值。
3.19.3. 缓冲与高速率读取¶
一个调用 any() 和 read() 的轮询循环本身就能跟上大多数流量,只要主循环迭代得足够快,能在接收缓冲区填满之前将其排空。当速率上升或循环繁忙时,有两个构造函数选项很重要。
rxbuf 设置软件 RX 缓冲区的大小。默认值为几百字节;对于每批发出数百字节的传感器,或者主循环在两次轮询之间要做很长工作的情况,增大缓冲区可以避免在循环忙于其他事务时丢失到达的字节:
uart = UART(3, baudrate=115200, timeout=100, rxbuf=4096)
在缓冲区已满时到达的任何数据都会丢失;要将 rxbuf 设置为足以覆盖两次排空之间最长的间隔。
对于持续的高速率,readinto() 会读取到一个预先分配的缓冲区中,而不是每次调用都返回一个新的 bytes 对象:
buf = bytearray(256)
while True:
n = uart.readinto(buf)
if n:
process(buf, n)
读取路径上不发生内存分配,这在堆碎片化时,或者当分配延迟会突破字节间时序预算时,就显得很重要。