technique
I2C and SPI peripherals
The two buses most sensors and displays use: I2C addresses and pull-ups, SPI clock and chip selects, and how to find a device that will not answer.
Before this
This page assumes you are comfortable with:
- prerequisiteSerial communication basicsHow bytes travel one bit at a time over a wire: baud rate, start and stop bits, and the difference between a UART and USB.
- prerequisiteDigital logic and pull-up resistorsHow a pin decides between 0 and 1, why an unconnected input floats, and how pull-up and pull-down resistors fix it.
Why you need this
A bare ESP32 can blink a light and read a button. Almost everything else you would add to a gadget, a temperature sensor, a screen, a memory card, a touch panel, talks to the chip over one of two buses: I2C or SPI. This page is stage 4 of the pipeline on the hub, "Talk to hardware". It teaches enough of both buses to wire a part, prove it is alive, and read real numbers out of it.
The idea
A bus is a set of shared wires plus rules for taking turns on them. A peripheral is any chip outside the ESP32 that sits on a bus: a sensor, a display controller, a memory chip. The ESP32 is the controller: it starts every conversation and drives the clock. The peripheral only answers.
Both buses are synchronous, unlike the UART on Serial communication basics. A synchronous bus has a dedicated clock wire. The controller toggles it, and each tick says "a bit is ready now". Neither side needs to agree on a baud rate in advance.
I2C: two wires, many devices, addresses
I2C (said "I squared C") uses two wires:
| Wire | Name | Job |
|---|---|---|
| SDA | serial data | Bits in both directions, one way at a time |
| SCL | serial clock | Ticks driven by the controller |
Both wires are open drain: a chip can pull a wire down to 0 V, but nothing on the bus ever drives it up to 3.3 V. A pull-up resistor on each wire does that, as on Digital logic and pull-up resistors. Because nobody drives high, two chips talking at once cannot short each other out. Without pull-ups the wires never go high, and nothing works.
Every device has a 7-bit address, a number from 0x00 to 0x7F. A conversation goes like this:
- Start: the controller pulls SDA low while SCL is high.
- Address byte: the 7-bit address shifted left one place, with the lowest bit saying the direction: 0 for write (controller to device), 1 for read.
- Acknowledge (ACK): after every byte, the receiver pulls SDA low for one clock to say "got it". A device that is absent or dead leaves SDA high, which is a NACK.
- Data bytes, each followed by an ACK.
- Stop: SDA goes high while SCL is high.
Small numbers: the AHT10 temperature and humidity sensor sits at address 0x38 (from the author's code and its config). Shifted left, that is 0x70. So the address byte for a write is 0x70 and for a read is 0x71. Each byte costs 9 clocks: 8 data bits plus the ACK.
Scanning a bus means trying every address and listing the ones that ACK. MicroPython's documentation describes scan() as trying every address from 0x08 to 0x77 and returning those where a device "pulls the SDA line low after its address (including a write bit) is sent on the bus". It is the first thing to run after wiring anything.
SPI: four wires, one chip at a time, no addresses
SPI uses more wires and no addresses:
| Wire | Name | Job |
|---|---|---|
| SCK | serial clock | Ticks driven by the controller |
| MOSI | controller out, peripheral in | Bits from the ESP32 to the device |
| MISO | controller in, peripheral out | Bits from the device to the ESP32 |
| CS | chip select | One per device; pulled low to pick that device |
Several devices can share SCK, MOSI, and MISO. Each gets its own chip select (CS) line. The controller pulls exactly one CS low, talks, then lets it go high. A device whose CS is high ignores the clock and lets go of MISO. That is how one bus serves many chips without addresses.
SPI also has a mode: two settings that both sides must agree on. MicroPython's documentation defines polarity as "the level the idle clock line sits at" and phase as whether data is sampled "on the first or second clock edge". The author's CYD display code uses polarity 0, phase 0.
SPI is much faster than I2C because every wire is driven both ways (no pull-ups to slowly charge the line) and data flows both directions at once. The author's display runs SPI at 40 MHz, so one byte takes microseconds. The I2C sensor runs at 100 kHz, where one 9-clock byte takes 90 microseconds.
Hardware or software bus
MicroPython's I2C and SPI use the chip's bus hardware; SoftI2C and SoftSPI flip the pins from code ("bit-banging"). The documentation says the hardware version is "usually efficient and fast but may have restrictions on which pins can be used", and the software one "can be used on any pin but is not as efficient". The author's knowledge base adds a field note: on many ESP32 boards SoftI2C is more reliable than hardware I2C, so if a sensor reads intermittently, switch to SoftI2C first.
Worked example
The deep example is the ESP_32_SAI sensor dashboard, which reads an AHT10 on a CYD (ESP32-2432S028, the Cheap Yellow Display) and plots it on the screen. Its config puts I2C on the CYD's back header: SDA on GPIO 22, SCL on GPIO 27, which matches the author's CYD pinout.
Step 1: prove the sensor is there
This scan is excerpted from ESP_32_SAI's scan_i2c.py (author's project, hardware-proven). The pins come from the project's config file.
from machine import Pin, SoftI2C
from config import I2C_SDA, I2C_SCL
i2c = SoftI2C(sda=Pin(I2C_SDA), scl=Pin(I2C_SCL), freq=100000)
devices = i2c.scan()
...
for d in devices:
print(" 0x{:02X}".format(d), end="")
if d == 0x38:
print(" <-- AHT10", end="")
If the list is empty, the project's scan_all.py goes further: it tries every pair of free CYD header pins as SDA and SCL, 16 pins giving ordered pairs, and prints any pair that finds a device. That is how you find a sensor wired to the wrong header.
Step 2: ask for a measurement
This sequence is excerpted from ESP_32_SAI's aht10.py (author's project, hardware-proven). It writes three command bytes, waits, then polls a status byte until the busy bit clears.
def _trigger(self):
"""Trigger a measurement and wait for completion."""
self.i2c.writeto(self.addr, b'\xAC\x33\x00')
time.sleep_ms(80)
# Poll until busy bit clears (bit 7)
for _ in range(10):
status = self.i2c.readfrom(self.addr, 1)[0]
if not status & 0x80:
return
time.sleep_ms(10)
raise RuntimeError("AHT10 measurement timeout")
On the wire, writeto sends: start, 0x70 (address 0x38, write), 0xAC, 0x33, 0x00, stop. Before this, the driver's setup checks bit 3 of the status byte (status & 0x08) to confirm the sensor is calibrated.
Step 3: turn six bytes into degrees and percent
The driver then reads six bytes. The author's read() splits them like this (comments are the author's):
data = self.i2c.readfrom(self.addr, 6)
# Humidity: upper 4 bits of data[1], all of data[2], upper 4 bits of data[3]
hum_raw = ((data[1] << 12) | (data[2] << 4) | (data[3] >> 4))
# Temperature: lower 4 bits of data[3], all of data[4], all of data[5]
temp_raw = (((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5])
humidity = (hum_raw / 1048576.0) * 100.0
temperature = (temp_raw / 1048576.0) * 200.0 - 50.0
Each reading is a 20-bit number, so it runs from 0 to , and . The two formulas are:
Now with real bytes. Suppose the sensor returns 08 66 66 65 D7 0A (an illustrative reading, picked to land on round numbers):
| Step | Value |
|---|---|
| data[0] (status) | 0x08: bit 3 set (calibrated), bit 7 clear (not busy) |
| Humidity bits | 0x66, 0x66, then the top half of 0x65, which is 0x6 |
| 0x66666 = 419,430 | |
| Humidity | |
| Temperature bits | the bottom half of 0x65, which is 0x5, then 0xD7, 0x0A |
| 0x5D70A = 382,730 | |
| Temperature |
Notice that byte data[3] is split down the middle: its top 4 bits finish the humidity and its bottom 4 bits start the temperature. That is the one place this sensor's format bites people.
In an ESP32 project
The same CYD carries both buses. I2C is on the back header for add-on sensors. SPI drives the screen: in the author's notes the ILI9341 display uses hardware SPI with SCK on GPIO 14, MOSI on GPIO 13, MISO on GPIO 12, CS on GPIO 15, and a data/command pin on GPIO 2. The details are on Driving the CYD display.
The CYD is also the best lesson in why you check bus wiring rather than trusting a pinout article. Several of the author's older notes, and many guides, say the touch controller (an XPT2046) shares the display's SPI bus. The author's corrected notes, confirmed on real boards, say it does not: on this Sunton board touch has its own bus, SCK on GPIO 25, MOSI on GPIO 32, MISO on GPIO 39, CS on GPIO 33, driven with SoftSPI at about 1 MHz. Reading touch on the display bus "returns 0xFF forever", because the touch chip never drives the display's MISO wire. The author's pinout still lists the SD card slot on the display's clock and data pins with its own CS on GPIO 5, and a later note says to verify the SD wiring against the board before use. That is the classic shared-bus case: two devices, one clock and data set, separate chip selects.
The author's CYD programs also park the touch and SD chip selects high at startup, a cheap habit that keeps a forgotten device quiet.
Common mistakes
- No pull-ups on I2C. Symptom:
scan()returns an empty list, or a long run of addresses that are not really there (the author's scan-every-pair script ignores any pin pair that reports ten or more devices). Many breakout boards include pull-ups; a bare sensor chip does not. - SDA and SCL swapped. Symptom: empty scan. The author's own project documents disagreed once about which CYD header pin was which; the scan-every-pair script exists for exactly this.
- Hardware I2C flaky. Symptom: readings work, then
OSErrornow and then. TrySoftI2Con the same pins, per the author's notes. - Splitting the shared AHT10 byte wrong. Symptom: temperature jumps by tens of degrees between readings, or humidity sits above 100%. Check the
>> 4and& 0x0Fon data[3]. - Wrong SPI bus for a device. Symptom: a device reads 0xFF or 0x00 for every byte while its other signals (an interrupt pin, say) look alive. Trace the actual wires.
- Two CS lines low at once. Symptom: garbage on both devices. Exactly one CS low at a time.
Cost
The author's AHT10 driver is about 50 lines and a 6-byte buffer. Speed is set by the bus and the part. At 100 kHz an I2C byte with its ACK takes 90 microseconds, so the 7-byte read (address plus six data) takes about 0.63 ms, but the sensor's own 80 ms conversion wait is over 100 times longer. That wait is the real cost, so read a sensor like this every few seconds, not in a tight loop. SPI at 40 MHz moves a byte in 0.2 microseconds, which is why screens use it. The maker's time goes almost entirely into wiring: a scan that finds the device in one minute saves an evening of reading driver code. Software buses cost CPU time, which matters for a screen, rarely for a sensor.
Going further
- Driving the CYD display, which builds on the SPI half of this page.
- WiFi and MQTT, where ESP_32_SAI sends these readings to a broker.
- The AHT10 datasheet from its manufacturer, for the full command set and timing.
- The ESP32 Technical Reference Manual chapters on the I2C and SPI controllers, once you want to drive them from C or assembly.
- A logic analyzer on SDA and SCL: seeing the 0x70 address byte and its ACK makes the protocol concrete.
Leads to
Back to ESP32 development: assembly, C, MicroPython, and CircuitPython