I²C, SPI, UART and 1-Wire
A bus is a small set of shared wires with rules on top. Instead of one GPIO per signal, the Pi and its parts exchange bytes — a whole conversation over two to four pins. Nearly every sensor, display and module in the shop uses one of four buses.
I²C — two wires, many devices
I²C uses SDA (data) and SCL (clock). Every device on the bus has a 7-bit address, and they all share the two wires: the controller names an address and only that device answers. Standard speed is 100 kHz, fast mode 400 kHz — slow, but enough for sensors.
On the Pi, bus 1 is on GPIO2 (SDA, physical pin 3) and GPIO3 (SCL, physical pin 5). These pins carry fixed 1.8 kΩ pull-ups, so you don't add any. Typical addresses: the BME280 at 0x76 or 0x77, the ADS1115 at 0x48–0x4B, an I²C LCD at 0x27.
Reading a register with smbus2:
from smbus2 import SMBus
with SMBus(1) as bus:
chip_id = bus.read_byte_data(0x76, 0xD0) # BME280 chip id register
print(f"chip id: 0x{chip_id:02x}") # 0x60
If no device acknowledges the address, the call raises OSError: [Errno 121] Remote I/O error — the same error a real Pi gives for a missing or miswired chip.
SPI — four wires, point to point
SPI is faster and simpler electrically: SCLK (clock), MOSI and MISO (data out and in), and one CS (chip select) per device. The controller holds one device's CS low and clocks bytes out; every byte out brings a byte in at the same time (full duplex). Devices must agree on a mode (0–3), which sets the clock edges used to sample the data.
On the Pi, SPI0 uses SCLK GPIO11 (pin 23), MOSI GPIO10 (pin 19), MISO GPIO9 (pin 21), and has two chip selects: CE0 GPIO8 (pin 24) and CE1 GPIO7 (pin 26).
Reading channel 0 of an MCP3008 with spidev:
from spidev import SpiDev
spi = SpiDev()
spi.open(0, 0) # bus 0, CE0
spi.mode = 0
reply = spi.xfer2([1, 0x80, 0]) # start bit, single-ended, channel 0
value = ((reply[1] & 3) << 8) | reply[2]
print(f"{value} counts = {value * 3.3 / 1024:.2f} V")
UART — a serial conversation
UART is the oldest of the four: two wires (TXD transmit, RXD receive), no clock, and bytes framed by start and stop bits. Because there is no clock wire, both ends must agree on the baud rate in advance — 9600 for a GPS module, 115200 for many others.
On the Pi they are GPIO14 (TXD, pin 8) and GPIO15 (RXD, pin 10), exposed as /dev/serial0 (the simulator also answers to /dev/ttyS0 and /dev/ttyAMA0). With pyserial:
import serial
port = serial.Serial("/dev/serial0", baudrate=9600, timeout=1)
while True:
line = port.readline()
if line:
print(line.decode(errors="replace").strip())
Send or receive at the wrong baud rate and the bytes arrive scrambled — framing errors, exactly as on real hardware.
1-Wire — one data wire, many sensors
1-Wire trades speed for wiring: a single data wire (plus ground) carries both signalling and data, and every device on it has a unique 64-bit ROM code. The Pi's default 1-Wire pin is GPIO4 (physical pin 7), and it needs a 4.7 kΩ pull-up to 3V3. The kernel exposes each device as a folder under /sys/bus/w1/devices.
A DS18B20 thermometer appears as 28-*, and Python reads it as a file:
import glob
device = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")[0]
for line in open(device):
print(line.strip())
The first line ends crc=.. YES or NO; the second holds t= plus the temperature in thousandths of a degree. A 12-bit conversion takes 750 ms, and reading the file blocks for it.
Which bus should you use?
| Bus | Wires | Addressing | Good for |
|---|---|---|---|
| I²C | 2 | 7-bit addresses | many slow sensors and displays |
| SPI | 4 | one CS per device | ADCs, displays, fast or streaming data |
| UART | 2 | point to point | GPS, modems, another microcontroller |
| 1-Wire | 1 (+ pull-up) | unique ROM codes | chains of cheap temperature sensors |
Going deeper
- The simulator works at the transaction level. A program asks for an I²C read, an SPI transfer, UART bytes or a 1-Wire reading; the bus finds the parts wired to the board's bus pins and lets their behaviour answer at register level. That is why unmodified drivers work: the interesting mistakes are wiring mistakes, and those behave like hardware — a missing I²C device NACKs with Errno 121, an unwired MISO returns zeros, a wrong SPI mode rotates every bit of the reply and warns, a baud mismatch garbles the bytes.
- I²C in the simulator is bus 1 only. Two devices answering the same address raise a warning, and Lab mode's console can scan the bus with
i2cdetect -y 1. The BME280 sits at 0x76 unless its SDO pin is tied to VIN (0x77). - Clock speed is not simulated. Long wires, capacitance and 100 vs 400 kHz matter on real hardware; here every transaction either succeeds or fails electrically.
- UART details. Opening
/dev/serial0sets the board's baud rate; the serial monitor in the workspace injects bytes as if you had a USB–serial adapter on TXD/RXD; a device transmitting at one rate to a board listening at another has its bytes dropped and transformed. - 1-Wire details. The
/sysfolders are created when the program starts — the wiring cannot change mid-run — and every read re-samples the sensor and takes the conversion time (750 ms at 12 bits). The simulator occasionally returns a bad CRC (crc=.. NO) so retry logic gets exercised. - The bus sniffer. Every transaction the simulator services (I²C, SPI, UART, 1-Wire, DHT, NeoPixel) is recorded with its address, direction, bytes and success flag — in Lab mode you can watch your driver's traffic byte by byte.
- The canonical docs for the three libraries are smbus2, spidev and pySerial.