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 参数配置)先到期,则返回 Nonereadline() 读取直到并包括下一个换行符为止,对基于行的协议很有用。

一个简单的循环,把收到的内容原样回送:

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)

读取路径上不发生内存分配,这在堆碎片化时,或者当分配延迟会突破字节间时序预算时,就显得很重要。