07 · SPI Peripherals¶
Not flashed to hardware
Reasoned through against the Arduino core's documented SPI library
API (SPI.begin(), SPI.beginTransaction(), SPISettings) shared by
ESP8266 and ESP32, and Adafruit's documented Adafruit_ST7735/SD
library APIs as representative SPI peripheral drivers. Not compiled
or flashed to physical hardware in this environment.
I2C vs. SPI¶
Where I2C (module 06) shares two wires among many addressed devices,
SPI ("Serial Peripheral Interface") uses more wires but moves data
much faster and doesn't need addressing at all — each device gets its
own dedicated CS/SS (Chip Select) line, while MOSI, MISO, and
SCK are shared. SPI is the standard choice for anything that needs
higher throughput than I2C comfortably provides: SD cards, TFT/color
displays, and fast ADCs.
On NodeMCU-style ESP8266 boards, hardware SPI pins are fixed:
D7=MOSI, D6=MISO, D5=SCK, and you choose any free GPIO as CS. ESP32
defaults to GPIO23=MOSI, GPIO19=MISO, GPIO18=SCK (VSPI), also with a
free-choice CS pin.
Basic SPI transaction pattern¶
// spi-basic-transaction.ino
#include <SPI.h>
const int CS_PIN = D8; // ESP8266; pick any free GPIO on ESP32
void setup() {
Serial.begin(115200);
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // deselected by default (active-low CS)
SPI.begin();
}
byte readRegister(byte reg) {
// SPISettings documents clock speed, bit order, and SPI mode --
// wrapping each transaction lets multiple SPI devices at different
// speeds share the same bus safely.
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW); // select this device
SPI.transfer(reg); // send the register address to read
byte value = SPI.transfer(0x00); // clock out a dummy byte, read the reply
digitalWrite(CS_PIN, HIGH); // deselect
SPI.endTransaction();
return value;
}
void loop() {
byte value = readRegister(0x0F);
Serial.printf("Register 0x0F: 0x%02X\n", value);
delay(1000);
}
The exact register protocol (what byte to send, what comes back) is chip-specific and documented per datasheet — the pattern above (select, transfer, deselect, wrapped in a transaction) is the universal shape almost every SPI driver library follows internally.
Driving an ST7735 color TFT display¶
// spi-tft-display.ino
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#define TFT_CS D8
#define TFT_DC D3 // data/command select line, SPI displays need this too
#define TFT_RST D4
Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
void setup() {
Serial.begin(115200);
// INITR_BLACKTAB is documented as the common default for most
// 1.8" ST7735 breakout modules; other tab colors exist for other
// manufacturing runs of the same chip.
tft.initR(INITR_BLACKTAB);
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(2);
tft.setCursor(0, 0);
tft.println("Hello SPI!");
}
void loop() {}
Reading/writing an SD card over SPI¶
// spi-sd-card.ino
#include <SPI.h>
#include <SD.h>
const int SD_CS_PIN = D8;
void setup() {
Serial.begin(115200);
// SD.begin(csPin) documents returning false on a missing/unformatted
// card or wrong CS pin -- always guard on it before using the card.
if (!SD.begin(SD_CS_PIN)) {
Serial.println("SD card init failed!");
return;
}
File logFile = SD.open("/log.txt", FILE_WRITE); // FILE_WRITE appends
if (logFile) {
logFile.println("Sensor node booted");
logFile.close(); // must close to flush -- data can be lost otherwise
Serial.println("Wrote to log.txt");
} else {
Serial.println("Failed to open log.txt");
}
}
void loop() {}
Sharing an SPI bus between two devices¶
Both the TFT and SD card examples can share MOSI/MISO/SCK since
each uses its own CS pin — the key rule (documented by the SPI library
and followed above) is that only one device's CS should ever be LOW
at a time, which beginTransaction()/endTransaction() pairs make easy
to guarantee.
How It Actually Works¶
Unlike I2C's shared open-drain lines, SPI is a push-pull, point-to-multipoint bus with dedicated MOSI/MISO/SCK lines plus one Chip Select (CS) per device — the ESP's SPI hardware peripheral is a shift register clocked by SCK: on each clock edge (rising or falling, per your chosen SPI mode 0–3, which fixes clock polarity and phase) one bit shifts out on MOSI from the master's TX buffer while simultaneously one bit shifts in on MISO into the master's RX buffer — the full-duplex nature isn't a feature you opt into, it's the physical mechanism, which is why SPI.transfer(byte) always returns a byte even when you only "wanted" to send.
CS is what actually addresses a device on a shared bus: a slave's SPI shift register only responds to clock edges while its own CS line is held LOW by the master, so multiple SPI devices can share MOSI/MISO/SCK as long as each has its own CS pin and the master only asserts one CS at a time — accidentally toggling CS mid-transfer, or sharing a CS line between two devices with different SPI modes, is the real mechanism behind "SPI device works alone but not with others on the same bus." Clock speed is bounded by real capacitive/inductive limits of the traces and the slave device's own maximum SCK spec (found in its datasheet, often 1–20MHz depending on part) — pushing SPISettings faster than the slave supports doesn't error at the software level, it just produces bit errors as the slave's input sampling window no longer lines up with a valid, settled signal.
(These examples were written and reasoned through at the register/protocol level but were not flashed to a physical board for this pass — verify timing-sensitive details against your exact chip datasheet before relying on them in production.)
Exercise¶
- Write the basic register-read sketch and explain what each of
SPISettings's three arguments controls. - Write the TFT sketch and explain why
TFT_DCis needed for SPI displays but not for something like an SD card. - Write the SD card sketch and explain the documented failure mode when
SD.begin()returns false. - Sketch (in comments) how you'd wire a TFT and SD card on the same SPI
bus with two separate CS pins, and confirm both
beginTransaction()calls never overlap.