Photo by Alexandre Debiève on Unsplash

Serial Peripheral Interface (SPI)

Common in Raspberry Pi projects, SPI is a synchronous (uses a dedicated clock signal), high-speed, full duplex (information passes both ways) serial communication protocol arranged as a controller and peripherals. A simple, high speed (especially over asynchronous) protocol with complete control over bits transmitted. Its limitations are just one controller, each peripheral requires a chip select line (CS) (although you could daisy chain in some applications) and has no error checking.

It uses four lines: Controller In Peripheral Out (COSI); Controller Out Peripheral In (CISO); Serial Clock (SCLK); and the fourth, Chip Select (CS), if there are multiple peripheral. Clock signal is generated by the controller, on each clock cycle the controller writes a bit to the peripheral and shifts its register, the controller reads a bit from the controller and it then shifts its register. After 8 clock cycles (for example) the contents of each device register has transposed.

SPI diagram
SPI transfer

Multiple peripherals can be connected to the same controller, which takes the chip select line low to select the relevant peripheral (so there will be a CS for each peripheral on the controller).

Two other bits are used to define how the clock signal is understood – Clock Polarity (CKP) and Clock Edge (CKE). CKP sets idle as low (0) or high (1) and CKE sets transfer on rising1Low to high. (1) or falling2High to low. (0) edge, giving modes. Mode zero is the most common (CKP=0 and CKE=0), data is sampled when clock signal goes high.


CircuitPython supports multiple interfaces including SPI with busio.SPI Class. It doesn’t control CS, which is a simple digital line, so use digitalio..DigitalInOut Class. The board Class gives board specific pin names. The examples here are using a Raspberry Pi Pico however which doesn’t label the SPI pins:

import board
import busio
import digitalio

Lets deal with CS first, most devices expect to be taken low to select but remember to confirm on their data sheet (note this this is the same technique used to turn on the LED if you used board.LED):

cs.digitalio.DigitalInOut(board.22)        # Which pin CS is on
cs.direction = digitalio.Direction.OUTPUT  # Set it as an output
cs.value = True                            # Set the line high (True)

spi = busio.SPI(clock=board.GP2, 

The process is lock, configure, send/receive and unlock. A common construct is a while loop with a pass statement3Python’s “do-nothing” or null operation. until the lock is available (release() and acquire() methods are implemented in busio so you can’t use with, more on that later):

while not spi.try_lock():


in_data = bytearray(8)
out_data = bytearray(b'\x01\x02\x03\x04\x05\x06\x07\x08')

cs.value = False           # Pull CS low
spi.readinto(in_data)      # Read
spi.write(out_data)        # Write
cs.value = True            # Release CS
spi.unlock()               # Release SPI lock

Note the paradigm communication is always duplex, CircuitPython readinto and writeout are sending null data in the opposing direction.

Managing CS and locks directly invites errors. Adafruit has a library, adafruit_bus_device, which has the SPIdev class that does implement release() and acquire() (so you can use a with statement) and manages CS.

Adafruit has a really good article on SPI and I2C, as does Sparkfun. Of course Wikipedia has more information then anyone really needs too.

  • 1
    Low to high.
  • 2
    High to low.
  • 3
    Python’s “do-nothing” or null operation.