3.22. SPI 代码实践

machine.SPI 封装了一个硬件 SPI 控制器;CS 线则是由脚本管理的普通 Pin 输出。用总线 id、所需的时钟速率以及(如有需要)模式来构造一个 SPI 实例:

from machine import SPI, Pin

spi = SPI(0, baudrate=1_000_000, polarity=0, phase=0)
cs = Pin("P3", Pin.OUT, value=1)         # CS idle high

id 用于选择使用哪个硬件 SPI 模块;可用的编号以及它们所映射的 SCK/MOSI/MISO 引脚取决于具体的板子(参见 OpenMV 开发板)。baudrate 是以赫兹为单位的 SCK 频率——由于时钟分频,硬件实际达到的速率可能略低一些,打印 SPI 对象时会显示出这个实际值。

构造 CS 引脚时使用 value=1,因此它空闲时处于未选中状态。每次事务都会选中 CS(将其拉低)、传输字节,然后再次取消选中 CS(将其拉高)。

3.22.1. 读取、写入、交换

三个方法涵盖了常见场景:

cs.value(0)
spi.write(b"\x10\x20\x30")              # send 3 bytes, ignore what comes back
cs.value(1)

cs.value(0)
data = spi.read(4)                      # read 4 bytes; sends 0x00 while reading
cs.value(1)

rx = bytearray(2)
cs.value(0)
spi.write_readinto(b"\x9F\x00", rx)     # send 0x9F, 0x00; receive 2 bytes
cs.value(1)

write() 是只写的快速路径;控制器推出字节,并丢弃外设在 MISO 上回送的任何内容。read() 则是镜像操作——它输出 N 个 SCK 脉冲,同时在 MOSI 上发送一个固定的哑字节(默认为 0),并存储 MISO 上的字节。write_readinto() 是全双工形式:它从一个缓冲区发送字节,并将同时收到的 MISO 字节存入另一个缓冲区。许多外设都采用这种模式——“发送一个命令字节,然后在下一次传输中读取响应”——因此这两个操作很自然地就合并到一次 write_readinto 调用中。

大多数外设要求 CS 线在整个事务期间(从命令字节到响应字节)保持选中状态,所以请把 cs.value(0) / cs.value(1) 这对括号放在 整个 序列的外侧,而不是套在每一次方法调用上。

3.22.2. 一次典型的传感器读取

许多 SPI 传感器把自身状态组织为一组内部寄存器,并遵循相同的交换形式:先发送寄存器地址(最高位带有读/写标志),然后读取或写入该寄存器的字节。在这类设备上读取寄存器 0x0F

rx = bytearray(2)
cs.value(0)
spi.write_readinto(b"\x8F\x00", rx)     # 0x80 = "read" flag, then reg 0x0F
cs.value(1)
register_value = rx[1]

第一个 MISO 字节是无用数据(此时设备仍在接收命令);第二个 MISO 字节才保存着寄存器的内容。命令字节的确切格式——哪一位是读/写标志、地址在多字节读取时是否自动递增——都记载在设备的数据手册里。

3.22.3. 位拆解(Bit-banging)

上面的 SPI 实例使用的是硬件 SPI 模块:MCU 内部的一个专用外设,带有自己的移位寄存器和时钟发生器,能以硬件方式产生 SCK / MOSI / MISO 波形。软件只需把一个字节交给它即可;这些数据位会在线路上移动,无需 CPU 进一步介入,从而让 CPU 得以并行处理其他工作。

另一种做法是位拆解(bit-banging):软件逐位循环,直接翻转 GPIO 引脚来产生相同的波形。其中不涉及任何硬件外设——CPU 把 SCK 拉低、设置 MOSI、把 SCK 拉高、采样 MISO,如此对每个字节的每一位反复进行。这会在整个事务期间占用 CPU,且比硬件模块跑得慢,但它适用于任何引脚,也不需要有空闲的硬件模块。

machine.SoftSPI 是同一套 SPI API 的位拆解实现:

from machine import SoftSPI, Pin

spi = SoftSPI(baudrate=500_000, polarity=0, phase=0,
              sck=Pin("P2"), mosi=Pin("P0"), miso=Pin("P1"))

当设备所需的引脚没有连接到硬件 SPI 模块、或硬件模块都已被占用时,就可以使用它。500 kHz 在大多数摄像头上是一个较为宽裕的上限;整个事务期间 CPU 都会保持忙碌。