Why chips need buses

A microcontroller rarely works alone. On the same board, or on a few wires away, sit a temperature sensor, a display, flash memory, a GPS module or a radio chip. They need a way to exchange bytes. Running eight parallel data lines to every chip would use too many pins, so embedded systems use serial buses: bits go one after another over a few wires.

Three serial buses cover most of what you meet in IoT devices. UART connects two devices and needs no clock line. I2C connects many slow chips with only two wires. SPI is the fast option for chips that move a lot of data. All three work at the voltage of the chips themselves (usually 3.3 V or 5 V, see GPIO, PWM and Interrupts), over centimeters to at most a few meters. For longer cables in buildings and factories, industrial links such as RS-485 take over.

The examples on this page follow Lotte Kortenhoeve, a Kestrel Energy customer in Utrecht who builds her own reader for her smart meter: an ESP32 development board on the meter's P1 port, with a temperature sensor in the cabinet of her home battery. Both use a bus from this page.

UART: two wires, no clock

A UART (universal asynchronous receiver-transmitter) is the hardware block that turns bytes into a serial bit stream on one wire (TX, transmit) and back into bytes from another wire (RX, receive). "Asynchronous" means there is no shared clock line: the receiver knows when to read each bit only because both sides agreed on the speed in advance.

That speed is the baud rate: the number of signal changes (symbols) per second. On a UART each symbol is one bit, so 115200 baud is 115,200 bits per second on the wire. Common values are 9600 and 115200. Both sides must use the same baud rate and the same frame format; there is no automatic negotiation.

The UART frame

When nothing is sent, the line rests at the high level (idle). Each byte then travels as one frame:

  • a start bit (low), which tells the receiver "a byte begins now" and lets it time the rest;
  • usually eight data bits, least significant bit first;
  • an optional parity bit for simple error detection (see parity bits);
  • one or two stop bits (high), which return the line to idle.

The shorthand 8N1 means 8 data bits, no parity, 1 stop bit: 10 bits on the wire for every byte. "7E1" (7 data bits, even parity, 1 stop bit) is also 10 bits; older Dutch smart meters used it at 9600 baud.

The UART frame for the character slash, hexadecimal 2F, in 8N1. The line idles high, drops low for the start bit, then sends the data bits least significant first: 1, 1, 1, 1, 0, 1, 0, 0. A high stop bit returns the line to idle. At 115200 baud each bit lasts 8.68 microseconds and the whole frame of ten bits 86.8 microseconds. 1 0 idle idle start D0 D1 D2 D3 D4 D5 D6 D7 stop 0 1 1 1 1 0 1 0 0 1 one frame: 10 bits, 86.8 µs at 115200 baud (8.68 µs per bit)
The character "/" (0x2F) in 8N1: start bit, eight data bits least significant first, stop bit.

The example builds the same frame in Python and works out how long a frame takes. The telegram is the 631-byte message that a Dutch smart meter sends on its P1 port (see Smart Meters and the P1 Port); it starts with "/".

def uart_frame(byte):
    """Line levels for one byte in 8N1: start bit 0, eight data bits LSB first, stop bit 1."""
    data_bits = [(byte >> i) & 1 for i in range(8)]
    return [0] + data_bits + [1]


slash = ord("/")
frame = uart_frame(slash)
print(f"'/' = 0x{slash:02X} = {slash:08b} (most significant bit first)")
print("data bits as sent, LSB first:", "".join(map(str, frame[1:9])))
print("whole frame on the wire:     ", "".join(map(str, frame)))
print()

BITS_PER_BYTE = 10  # 8N1: 1 start + 8 data + 1 stop bit
TELEGRAM_BYTES = 631
for baud in (9600, 115200):
    bytes_per_second = baud / BITS_PER_BYTE
    ms = TELEGRAM_BYTES * BITS_PER_BYTE / baud * 1000
    print(f"{baud:>6} baud: {bytes_per_second:>6,.0f} bytes/s, "
          f"{TELEGRAM_BYTES}-byte telegram in {ms:5.1f} ms")
'/' = 0x2F = 00101111 (most significant bit first)
data bits as sent, LSB first: 11110100
whole frame on the wire:      0111101001

  9600 baud:    960 bytes/s, 631-byte telegram in 657.3 ms
115200 baud: 11,520 bytes/s, 631-byte telegram in  54.8 ms

At 115200 baud the whole telegram takes about 55 ms, comfortably inside the one second between telegrams on a DSMR 5 meter. The same arithmetic tells you whether a UART link can carry what you plan to send: bytes per second is the baud rate divided by 10 for 8N1. See bit rates and transfer times for the general method.

Wiring a UART

Three wires connect two UART devices: TX of one side to RX of the other, RX to TX, and a common ground (GND). Without the shared ground the receiver has no reference for "high" and "low" and reads noise.

The levels on a microcontroller UART are "TTL-level" logic: 0 V for low and 3.3 V or 5 V for high, depending on the chip. The old PC serial port (RS-232) uses different and much larger voltages, roughly ±3 to ±25 V and typically around ±13 V, with the meaning inverted. Never connect an RS-232 port directly to a microcontroller pin; use a level converter. A USB-to-UART adapter for 3.3 V logic is the usual way to talk to a board from a laptop, and many ESP32 development boards have one built in behind their USB port.

The P1 port is a UART with a twist

The P1 port of a Dutch smart meter is a one-way UART at 115200 baud, 8N1 (DSMR 4 and 5). Two details make it different from a plain UART:

  • The data output is open collector: the meter can only pull the line low; something on the reader's side must pull it up (see open-drain outputs below). Pull it up to the 3.3 V of the ESP32, not to the meter's 5 V: with a 5 V pull-up the high pulses exceed 4 V, too much for a 3.3 V input.
  • The signal is logically inverted: the idle line is low and the start bit is high, the opposite of the frame above. The reader must invert it, in hardware with a transistor or in software. On an ESP32 the UART can invert its RX input (in MicroPython invert=UART.INV_RX).

Pin-out, the Data Request line, power and the telegram format are on Smart Meters and the P1 Port.

I2C: many chips on two wires

I2C (Inter-Integrated Circuit, also written I²C) is a bus on which one controller and many target chips share two wires: SDA (serial data) and SCL (serial clock). The controller, usually the microcontroller, drives the clock and starts every transfer; targets such as sensors, small displays and memory chips only answer when they are addressed. The current specification (NXP UM10204, Rev. 7.0 of 2021) uses the words controller and target instead of the older master and slave.

Speed classes in the specification:

Mode Bit rate Notes
Standard-mode 100 kbit/s supported by practically every I2C chip
Fast-mode 400 kbit/s the common choice for sensors
Fast-mode Plus 1 Mbit/s less common
High-speed mode 3.4 Mbit/s rare in IoT devices
Ultra Fast-mode 5 Mbit/s one direction only, for chips that only receive

Open-drain outputs and pull-up resistors

Every chip on an I2C bus drives SDA and SCL with an open-drain output (open collector on bipolar transistors): an output that can only pull the line down to ground or let go of it. It never drives the line high. A pull-up resistor to the supply voltage makes the line high whenever nobody pulls it down.

This "wired-AND" design is why many chips can share one wire safely: if two chips disagree, the one pulling low wins, and nothing is short-circuited. It also means that without pull-ups the bus does not work at all. Many breakout boards include pull-up resistors; with several such boards on one bus, the resistors end up in parallel, which can make the pull-up too strong. The total capacitance of wires and chips (at most 400 pF in Standard- and Fast-mode, 550 pF in Fast-mode Plus) limits how long the bus can be; I2C is meant for one board or short cables.

An I2C bus. An ESP32 controller on the left drives two shared lines, SDA and SCL. Each line has a pull-up resistor to 3.3 volts. Two targets hang on both lines: a temperature sensor at address 0x48 and a pressure sensor at address 0x76. All devices share ground. 3.3 V pull-up resistors Controller ESP32 SDA SCL Temperature 0x48 Pressure 0x76 common GND (not drawn)
One controller, two targets, two shared lines with one pull-up each. Any chip can pull a line low; the resistors pull it high.

Addresses

Each target on the bus has an I2C address: a 7-bit number, usually written in hexadecimal, that the controller sends to say which chip it wants to talk to. The address is set by the chip's manufacturer, often with one or two pins on the chip that select between a few alternatives, so that two identical sensors can share a bus. Two chips with the same address on one bus cannot both work.

On the wire, the address travels in the first byte after the start condition: the seven address bits shifted one place to the left (see bit shifts), followed by one R/W bit: 0 to write, 1 to read. That is why a data sheet may give the same sensor as "0x48" (the 7-bit address) or as "0x90/0x91" (the write and read bytes). Mixing up the two is a classic reason for "no device found". Of the 128 possible 7-bit addresses, two groups of eight (0000xxx and 1111xxx) are reserved for special purposes, which leaves 112 for targets.

def address_byte(address, read):
    """The first byte after START: 7-bit address shifted left, R/W bit last (1 = read)."""
    return (address << 1) | (1 if read else 0)


for name, address in (("temperature sensor", 0x48), ("pressure sensor", 0x76)):
    write, read = address_byte(address, False), address_byte(address, True)
    print(f"{name:<18} 0x{address:02X}: write byte 0x{write:02X}, read byte 0x{read:02X}")

# Addresses 0000xxx and 1111xxx are reserved by the I2C specification.
reserved = [a for a in range(128) if a >> 3 in (0b0000, 0b1111)]
print(f"reserved: {len(reserved)}, usable 7-bit addresses: {128 - len(reserved)}")
temperature sensor 0x48: write byte 0x90, read byte 0x91
pressure sensor    0x76: write byte 0xEC, read byte 0xED
reserved: 16, usable 7-bit addresses: 112

The first test on any new I2C setup is a bus scan: the controller tries every address and lists the ones that answer. In MicroPython on Lotte's ESP32 that is two lines (pin numbers are examples; they depend on the board):

from machine import I2C, Pin

i2c = I2C(1, scl=Pin(5), sda=Pin(4), freq=400000)
print(i2c.scan())
[72, 118]

The scan prints decimal numbers: 72 is 0x48 and 118 is 0x76, the two sensors. An empty list means no chip answered: check the pull-ups, the power and ground of the sensor, and whether SDA and SCL are swapped.

One read, step by step

Reading a register from a sensor takes two steps in one transaction. The controller first writes the register number it wants, then, with a repeated start instead of a stop, reads the bytes. After every byte the receiving side answers with an acknowledgment bit (ACK); the controller ends the read with a "not acknowledged" (NACK) to say it needs no more bytes, and then a STOP.

The example decodes such a transaction as a logic analyzer would list it and turns the two data bytes into a temperature. The sensor is a generic 12-bit model of a common kind: the reading is a signed two's complement number in the top 12 bits of two bytes, with 0.0625 °C per step. Your own sensor's data sheet gives the exact format.

# What a logic analyzer's I2C decoder reported for one read (values in hex).
capture = [
    ("START", None), ("ADDRESS", 0x90), ("ACK", None),       # write to 0x48 ...
    ("DATA", 0x00), ("ACK", None),                           # ... register 0x00
    ("START", None), ("ADDRESS", 0x91), ("ACK", None),       # repeated START, read
    ("DATA", 0x19), ("ACK", None),
    ("DATA", 0x40), ("NACK", None),                          # controller: "enough"
    ("STOP", None),
]


def temperature(msb, lsb):
    """Generic 12-bit sensor: two's complement in the top 12 bits, 0.0625 °C per step."""
    raw = ((msb << 8) | lsb) >> 4
    if raw & 0x800:          # sign bit of a 12-bit number set: negative
        raw -= 1 << 12
    return raw, raw * 0.0625


reading = False
data = []
for event, value in capture:
    if event == "ADDRESS":
        reading = bool(value & 1)
        print(f"address 0x{value >> 1:02X}, {'read' if reading else 'write'}")
    elif event == "DATA" and reading:
        data.append(value)
    elif event == "DATA":
        print(f"register 0x{value:02X} selected")

raw, celsius = temperature(*data)
print("bytes read:", " ".join(f"{b:02X}" for b in data))
print(f"raw {raw} -> {celsius} °C")
raw, celsius = temperature(0xFF, 0x00)
print(f"FF 00: raw {raw} -> {celsius} °C")
address 0x48, write
register 0x00 selected
address 0x48, read
bytes read: 19 40
raw 404 -> 25.25 °C
FF 00: raw -16 -> -1.0 °C

Firmware often avoids floating-point numbers and works in integers, such as thousandths of a degree. The same conversion in C, the usual language of firmware (see Firmware and Embedded Programming):

#include <stdint.h>
#include <stdio.h>

/* Convert two bytes from a generic 12-bit temperature sensor to
   thousandths of a degree, with integers only (no floating point). */
static int32_t millidegrees(uint8_t msb, uint8_t lsb)
{
    int32_t raw = (int32_t)((((uint16_t)msb << 8) | lsb) >> 4); /* top 12 bits */
    if (raw & 0x800) {          /* sign bit of a 12-bit two's complement number */
        raw -= 0x1000;
    }
    return raw * 625 / 10;      /* 0.0625 degrees = 62.5 millidegrees per step */
}

int main(void)
{
    const uint8_t samples[][2] = {{0x19, 0x40}, {0x00, 0x10}, {0xFF, 0x00}, {0xE7, 0x00}};

    for (unsigned i = 0; i < sizeof samples / sizeof samples[0]; i++) {
        printf("%02X %02X -> %ld millidegrees\n",
               samples[i][0], samples[i][1],
               (long)millidegrees(samples[i][0], samples[i][1]));
    }
    return 0;
}
19 40 -> 25250 millidegrees
00 10 -> 62 millidegrees
FF 00 -> -1000 millidegrees
E7 00 -> -25000 millidegrees

Note the second line: one step is 62.5 millidegrees, and integer division drops the half. Choose the unit so that the rounding does not matter for the job; for a battery cabinet, tenths of a degree are plenty.

SPI: the fast bus

SPI (Serial Peripheral Interface) is a synchronous bus in which the controller drives a clock line and data moves in both directions at the same time (full duplex), one bit per clock tick in each direction. It has no addresses and no acknowledgments, and its lines are driven actively high and low instead of through pull-ups. That makes it simpler and much faster than I2C, which is why flash memory, SD cards, displays and radio chips use it. There is no single formal SPI standard; each chip's data sheet describes what it expects.

The signals have new names. The Open Source Hardware Association's resolution replaced the old master/slave names, and many newer data sheets follow it:

Signal Meaning Older name
SCK serial clock, driven by the controller SCK, SCLK
SDO serial data out (from the point of view of the chip it is printed on) MOSI on a controller, MISO on a peripheral
SDI serial data in MISO on a controller, MOSI on a peripheral
CS chip select, one per peripheral SS (slave select)

For pins that can work in both roles, the resolution uses PICO (peripheral in, controller out) and POCI (peripheral out, controller in). The wiring rule is the same as for a UART: the controller's SDO goes to the peripheral's SDI and the other way round.

Chip select instead of addresses

Because SPI has no addresses, the controller picks a peripheral with a separate wire: the chip select (CS) line. Each peripheral has its own CS line, normally active low: the controller pulls the line of one chip low, clocks the bytes, and lets the line go high again. All other chips keep their data outputs released and ignore the clock. The price is one extra pin per peripheral, which is why boards with many SPI chips run out of pins faster than with I2C.

SPI modes

Chips differ in two clock details: the level of the clock when idle (CPOL, clock polarity) and on which clock edge the data is read (CPHA, clock phase). The four combinations are numbered mode 0 to mode 3. Controller and peripheral must use the same mode, or every byte arrives shifted by one bit.

# SPI mode = (CPOL, CPHA). CPOL: clock level when idle. CPHA: sample on the first
# (leading) or second (trailing) clock edge of each bit.
for mode in range(4):
    cpol, cpha = mode >> 1, mode & 1
    idle = "high" if cpol else "low"
    leading = "falling" if cpol else "rising"
    trailing = "rising" if cpol else "falling"
    sample = trailing if cpha else leading
    print(f"mode {mode}: CPOL={cpol} CPHA={cpha}  clock idles {idle:<4}  sample on {sample} edge")
mode 0: CPOL=0 CPHA=0  clock idles low   sample on rising edge
mode 1: CPOL=0 CPHA=1  clock idles low   sample on falling edge
mode 2: CPOL=1 CPHA=0  clock idles high  sample on falling edge
mode 3: CPOL=1 CPHA=1  clock idles high  sample on rising edge

Mode 0 is the most common. The data sheet of the peripheral tells you which mode it needs; the controller adapts.

Choosing a bus

Usually the peripheral chooses for you: a sensor is sold with an I2C interface, a display with SPI, a GPS module with a UART. When a chip offers more than one, this comparison helps:

UART I2C SPI
Signal wires TX, RX (+ GND) SDA, SCL (+ GND) SCK, SDO, SDI, one CS per chip (+ GND)
Clock none; agreed baud rate from the controller from the controller
Devices two (point to point) many, by 7-bit address many, by chip select line
Direction both at once one at a time both at once
Speed 9600 to 115200 baud typical 100 kbit/s to 1 Mbit/s typical usually far faster than I2C
Typical use GPS and modem modules, debug console, the P1 port sensors, small displays, small memory chips flash memory, SD cards, displays, radio chips

The protocols on top differ too. A UART only delivers bytes; how a message starts and ends is up to the application (the P1 telegram starts with "/" and ends with "!" and a checksum). I2C and SPI define the transfer, and the chip's data sheet defines the registers. When the distance grows beyond a board or a short cable, the same ideas reappear on sturdier physical layers: a UART over RS-485 carries Modbus RTU in industry (see Modbus RTU and TCP).

Debugging with a logic analyzer

A multimeter shows an average voltage, which tells you a line is alive but not what it says. A logic analyzer records digital lines many times faster than the bus runs and shows them as high and low levels over time; its software decodes the levels into the bytes, addresses, ACKs and frames of UART, I2C or SPI. Small USB logic analyzers cost little and work with open-source software, which makes them one of the most useful tools for anyone who builds or supports devices.

Lotte connected one to the P1 data line (after her pull-up resistor) on Saturday 12 September 2026, because her reader received nothing useful. With the decoder set to 115200 baud, 8N1, it showed only errors:

  time (µs)  decoded
      0.0     0x00  framing error
     86.8     0x00  framing error
    173.6     0x00  framing error
    260.4     0x00  framing error

A stream of framing errors means the receiver does not find a stop bit where it expects one: wrong baud rate, or a signal that is upside down. Because the P1 signal is inverted, the decoder saw the idle line as one long run of start bits. With the decoder's "invert" option switched on, the telegram appeared:

  time (µs)  decoded
      0.0     0x2F  '/'
     86.8     0x58  'X'
    173.6     0x59  'Y'
    260.4     0x5A  'Z'
    347.2     0x35  '5'
    434.0     0x5C  '\'

That told her exactly what to change in her firmware: invert the RX input of the UART. Ten bits per byte at 115200 baud gives the 86.8 µs between bytes; if the decoded bytes look right at a different baud rate, the baud rate was the problem instead.

Troubleshooting bus problems

Most problems are found by checking, in this order:

  1. Power and ground. Is the peripheral powered at the right voltage, and do both sides share ground?
  2. Voltage levels. A 5 V module on a 3.3 V microcontroller (ESP32, Raspberry Pi, Pico) can damage the inputs. Use a level shifter or a module made for 3.3 V.
  3. Wiring. UART: TX to RX. I2C: SDA to SDA, SCL to SCL, pull-ups present. SPI: SDO to SDI, the right CS pin.
  4. Settings. UART baud rate and frame format; the I2C address (7-bit, not the shifted byte); the SPI mode and clock speed.
  5. Look at the signal. A logic analyzer shows which of the above is wrong in minutes.