6.22. SPI in code

machine.SPI wraps a hardware SPI controller; CS lines are ordinary Pin outputs managed by the script. Construct an SPI instance with the bus id, the desired clock rate, and (if needed) the mode:

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

The id selects which hardware SPI block to use; the available numbers and the SCK/MOSI/MISO pins they map to depend on the board (see the OpenMV Boards). baudrate is the SCK frequency in hertz – the actual rate the hardware achieves may be slightly lower due to clock division, which the printed value of the SPI object will show.

The CS pin is constructed with value=1 so it idles deasserted. Every transaction asserts CS (drives it low), moves the bytes, and deasserts CS (drives it high) again.

6.22.1. Reading, writing, exchanging

Three methods cover the common cases:

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() is the write-only fast path; the controller pushes the bytes and discards whatever the peripheral sent back on MISO. read() is the mirror image – it clocks N SCK pulses while sending a fixed dummy byte (0 by default) on MOSI and stores the MISO bytes. write_readinto() is the full-duplex form: it sends the bytes from one buffer and stores the simultaneous MISO bytes into another. Many peripherals use this pattern – “send a command byte, then read the response in the next transfer” – so the two operations naturally fit into one write_readinto call.

Most peripherals expect the CS line to stay asserted for the entire transaction (command bytes through response bytes), so keep the cs.value(0) / cs.value(1) brackets around the whole sequence, not around each method call.

6.22.2. A typical sensor read

Many SPI sensors organise their state as a set of internal registers and follow the same exchange shape: send the register address (with a read/write flag in the top bit), then read or write the register’s bytes. A read of register 0x0F on such a device:

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]

The first MISO byte is junk (the device was still receiving the command at that point); the second MISO byte holds the register contents. The exact command byte format – which bit is the read/write flag, whether the address auto-increments on multi-byte reads – is in the device’s data sheet.

6.22.3. Bit-banging

The SPI instance above uses a hardware SPI block: a dedicated peripheral inside the MCU with its own shift register and clock generator that produces the SCK / MOSI / MISO waveforms in silicon. Software just hands it a byte; the bits move on the wire without further CPU help, leaving the CPU free to do other work in parallel.

The alternative is bit-banging: software loops over each bit and toggles GPIO pins directly to produce the same waveform. There is no hardware peripheral involved – the CPU drives SCK low, sets MOSI, drives SCK high, samples MISO, and so on for every bit of every byte. That ties the CPU up for the whole transaction and runs slower than the hardware block can, but it works on any pin and does not need a hardware block to be free.

machine.SoftSPI is the bit-bang implementation of the same 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"))

Use it when the device needs to be on pins that are not wired to a hardware SPI block, or when the hardware blocks are all in use. 500 kHz is a comfortable ceiling on most cams; the CPU stays busy for the whole transaction.