Skip to main content

SPI

SPI is how chips talk when they need to be fast. Unlike UART, it has a clock wire, so the two sides never have to agree on a speed in advance, and several parts can share the same wires.

PiSCLKMOSIMISOCE0CE1Part 1Part 2CESCLKMOSI01001110the letter N, highest bit first

SPI uses four wires:

  • SCLK is the clock. The Pi sets the pace, and each tick moves one bit.
  • MOSI carries data out of the Pi.
  • MISO carries data into the Pi.
  • CE is chip enable, also called chip select. The Pi pulls it low to say "I'm talking to you".

Several parts can share the bus. The clock and data wires go to every part, but each part gets its own chip enable wire. A part only listens while its own wire is low, and ignores everything else. The Pi has two of those wires, so it can talk to two parts this way.

Every tick moves a bit both ways. One bit goes out on MOSI as another comes in on MISO, so sending and receiving are the same action. To read from a part, you send it something, even if that's only filler.

At 1 MHz, that's a million bits a second, or 125,000 bytes. If you're short on pins and don't need that speed, I²C does the same sharing with only two wires.

What uses it​

These parts all speak SPI:

  • Flash memory, such as the W25Q128
  • Small displays, such as the ST7789
  • Analog-to-digital converters, such as the MCP3008, which read voltages the Pi's pins can't
  • SD cards, which can be run in SPI mode

SPI only moves the bytes. What they mean is up to the part, and its data sheet lists them.

Try it with one wire​

Join MOSI straight to MISO with a jumper wire, and every bit the Pi sends arrives back at it. Like the UART loopback, this needs no other parts.

19211Pin 1 is at the SD card end
  • Pin 19GPIO 10MOSI: the Pi sends here
  • Pin 21GPIO 9MISO: the Pi listens here
Add the Raspberry Pi library first

Pi and its devices come from a library, not from the language, so this code won't build without it. In IntelliJ, choose Tools | Nylon | Add Nylon Library… and pick raspberry-pi. That adds one line to your project's build.markup.code:

(+dependencies : RepositoryDependency coordinate={"com.grandtheorem:raspberry-pi:0.1.0"} /)

Every file starts with its namespace. Pi is the board.

namespace loopback

import grandtheorem.rpi.Pi

Ask for SPI on chip enable 0, at 1 MHz.

entrypoint.default main() {
let spi = Pi.spi(0, 1000000)

One byte to send, the letter N again, and room for one to come back.

let send: u8[] = [u8(78)]
let received = mut u8[1]

transfer does both at once. There's no timeout, because the Pi's clock decides when each bit moves, so it's finished when it returns.

spi.transfer(send, received)

println("Byte ${received[0]} came back")
}

With the wire in place, the run window shows the N's number:

Byte 78 came back

Pull the wire out and run it again, and the number will probably change, often to 0, because nothing is driving MISO. If you see that with the wire in place, check the wire first. If you have more than one board, check the run window too: its first line, Running ... on, names the board the program went to, and it should be the one you wired.

Connecting a real part​

A W25Q128 flash chip is a good first part, because it can tell you what it is. Send it the byte 0x9F and it answers with three bytes of ID. This wiring uses all four SPI wires, plus power:

1719202123241Pin 1 is at the SD card end
  • Pin 173.3 VTo the flash's VCC
  • Pin 19GPIO 10MOSI, to the flash's DI
  • Pin 20GNDTo the flash's GND
  • Pin 21GPIO 9MISO, to the flash's DO
  • Pin 23GPIO 11SCLK, to the flash's CLK
  • Pin 24GPIO 8CE0, to the flash's CS
warning

Power the flash from 3.3 V, not 5 V. The Pi's pins work at 3.3 volts, and a part running at 5 volts can damage them.

The start is the same as before, with the flash on chip enable 0.

namespace flash

import grandtheorem.rpi.Pi

entrypoint.default main() {
let flash = Pi.spi(0, 1000000)

The command is 0x9F, followed by three filler bytes, because the chip's answer only comes back while the Pi keeps clocking.

let send: u8[] = [u8(0x9F), u8(0), u8(0), u8(0)]
let received = mut u8[4]

The first byte back arrived while the command was going out, so it means nothing. The answer is the three after it.

flash.transfer(send, received)

println("Maker ${received[1]}, type ${received[2]}, size ${received[3]}")
}

A W25Q128 answers:

Maker 239, type 64, size 24

Other flash chips give different numbers, and some W25Q128 variants report a type of 112. All zeros, or all 255s, usually means a wire is wrong. If you have more than one board, check the run window too: its first line, Running ... on, names the board the program went to, and it should be the one you wired.

Good to know​

Keep both arrays the same length. transfer stops at the end of the shorter one, so any bytes past that aren't sent or read.

You choose the speed, and the Pi rounds it down. Pi.spi takes 3,815 Hz to 125 MHz, and runs at the nearest speed it can make at or below the one you asked for. Long wires need a slower clock, and your part's data sheet gives its limit.

A second part uses chip enable 1. Wire its chip enable to GPIO 7 (pin 26), and ask for Pi.spi(1, 1000000). It shares the other three wires.

note

These programs ran on a Raspberry Pi 3 B+ and a Raspberry Pi 4, as this page shows them.