Most small sensors used in DIY home projects talk I²C: temperature and humidity sensors, pressure sensors, CO₂ sensors, light sensors, small OLED displays and I/O expanders. Two wires connect them all to an ESP32. When it works, it's easy. When it doesn't, the usual causes are a missing or wrong pull-up resistor, two devices with the same address, too much wire or the wrong pins.

This guide explains what you need to know to get a bus working and keep it working. The theory comes from NXP's I²C-bus specification (UM10204, rev. 7.0, October 2021), the practical part from the ESPHome and MicroPython documentation.

How the bus works

An I²C bus has two lines: SDA (serial data) and SCL (serial clock). Both lines connect to a positive supply through a pull-up resistor (or a current source). When the bus is idle, both lines are HIGH.

Devices never drive a line HIGH. Their outputs are open-drain (or open-collector): they can only pull the line LOW or let go of it. The resistor pulls it back up. That is why the bus doesn't work at all without pull-ups. The MicroPython documentation says that without them the behavior is undefined: anything from a hang or a watchdog reset to wrong values.

The controller (your ESP32) starts each transfer and addresses one target device. The specification defines these speeds:

  • Standard-mode: up to 100 kbit/s
  • Fast-mode: up to 400 kbit/s
  • Fast-mode Plus: up to 1 Mbit/s
  • High-speed mode: up to 3.4 Mbit/s

Home sensors almost always run in Standard-mode or Fast-mode.

Some devices hold SCL LOW to pause a transfer while they prepare data. This is called clock stretching, and it is optional in the specification.

Addresses: why scans show 0x08 to 0x77

Most devices use a 7-bit address, written in hex, for example 0x44 or 0x76. The specification reserves two groups of eight addresses, 0000XXX and 1111XXX, for special purposes such as the general call address and 10-bit addressing. That leaves 0x08 to 0x77 for ordinary devices, and that is exactly the range the MicroPython scan() method checks.

Each device on a bus needs a unique address. A few defaults from the ESPHome documentation:

  • BME280 (temperature, humidity, pressure): 0x77 by default, 0x76 as the alternative.
  • SHT4x (temperature, humidity): 0x44.
  • TCA9548A multiplexer: 0x70.

The usual case is two identical sensors on one bus. You have three options:

  1. An alternative address. Some chips have an address pin or solder jumper. The BME280 can use 0x76 or 0x77, so two of them can share a bus.
  2. A second bus. The ESP32 has two hardware I²C peripherals (0 and 1, according to the MicroPython quick reference), and ESPHome supports multiple buses with separate ids.
  3. A multiplexer. A TCA9548A has 8 channels. ESPHome treats each channel as a separate virtual bus, so the same address can appear once per channel.

Choose the pull-up resistors

The pull-up value is a compromise.

  • Too large, and the line rises too slowly. Every wire, connector and pin adds capacitance to the bus. Together with the resistor, that capacitance sets how fast the line can rise.
  • Too small, and a device can't pull the line LOW enough. The specification requires devices to sink at least 3 mA in Standard-mode and Fast-mode, at a LOW level of no more than 0.4 V when the supply is above 2 V.

The specification gives a formula for each limit.

Maximum: Rp(max) = tr / (0.8473 × Cb)

  • tr is the maximum rise time: 1000 ns in Standard-mode and 300 ns in Fast-mode.
  • Cb is the bus capacitance. The limit is 400 pF in both modes.

Minimum: Rp(min) = (VDD - VOL(max)) / IOL

Worked example for a 3.3 V bus with an estimated 100 pF of capacitance:

  • Rp(min) = (3.3 - 0.4) / 0.003 ≈ 0.97 kΩ
  • Rp(max) in Standard-mode = 1000 ns / (0.8473 × 100 pF) ≈ 11.8 kΩ
  • Rp(max) in Fast-mode = 300 ns / (0.8473 × 100 pF) ≈ 3.5 kΩ

Any value between the minimum and the maximum works. If the bus has 200 pF, the Fast-mode maximum drops to about 1.8 kΩ. The MicroPython documentation mentions 1-10 kΩ as the usual range.

You rarely know the real capacitance, so treat these numbers as a sanity check. If a long or crowded bus misbehaves, lower the speed or the resistor value, within the limits above.

Pull-ups that are already there

The MicroPython documentation notes that pull-ups are often built into the microcontroller board or the sensor breakout board, but there is no rule. Check the schematic of your modules. If several breakout boards each have their own pull-ups, they work in parallel, and the combined resistance is lower than any single one. With many modules, it can fall below the minimum. Some boards have a solder jumper to disconnect their pull-ups.

The ESPHome documentation describes the internal pull-ups:

  • On the ESP32, the internal pull-ups on SDA and SCL are enabled by default. They can be turned off with sda_pullup_enabled: false and scl_pullup_enabled: false.
  • On the ESP8266, internal 10 kΩ pull-ups are always enabled.
  • External pull-ups are still recommended for longer wires or multiple devices.

Mixed voltages

Pull-ups connect to the supply voltage of whatever module they sit on. A module whose pull-ups go to 5 V raises SDA and SCL to 5 V on your ESP32's pins. Before you connect such a module, check its schematic and your microcontroller's datasheet for the maximum pin voltage. The simplest approach is to power I²C modules from 3.3 V when they support it.

Keep the bus short and calm

Bus capacitance limits the number and length of everything you connect. The specification sets the limit at 400 pF per line in Standard-mode and Fast-mode, counting wires, connections and pins. If you need more, the specification describes two approaches:

  • Stronger pull-ups (within the Rp(min) limit) and therefore faster edges.
  • A buffer, such as the PCA9515, that splits the bus into segments. Each segment can then have up to 400 pF.

In practice, the first thing to try on a long or unreliable bus is a lower clock. ESPHome's default is already a cautious 50 kHz.

Set up the bus in ESPHome

This configuration for a classic ESP32 uses the usual default pins (GPIO21 for SDA, GPIO22 for SCL), scans the bus at startup and reads a BME280:

i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true
  frequency: 100kHz
  id: bus_a

sensor:
  - platform: bme280_i2c
    i2c_id: bus_a
    address: 0x76
    temperature:
      name: "BME280 Temperature"
    pressure:
      name: "BME280 Pressure"
    humidity:
      name: "BME280 Humidity"

Notes from the ESPHome I²C and BME280 pages:

  • scan defaults to true. The results of the startup scan appear in the device logs, so this is the first place to look when a sensor isn't found.
  • frequency defaults to 50 kHz. Valid values run from 10 kHz to 800 kHz. 100 kHz is Standard-mode speed.
  • timeout defaults to 100 µs on the ESP32 (maximum 13 ms) and 1 s on the ESP8266.
  • If the sensor is at the default address 0x77, leave out address.
  • For the BME280 in I²C mode, ESPHome recommends pulling the CSB pin to 3 V to avoid the "Communication with BME280 failed!" error.

Scan the bus from MicroPython

On a board with MicroPython, a scan takes three lines:

from machine import Pin, I2C

i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
print([hex(a) for a in i2c.scan()])

According to the MicroPython documentation:

  • scan() checks addresses 0x08 to 0x77 and returns those that respond.
  • A device counts as responding when it pulls SDA LOW after its address is sent.
  • The ESP32's hardware peripheral 0 defaults to SCL 18 and SDA 19, but any output-capable pins can be used.
  • machine.SoftI2C is a software implementation that works on any output-capable pin.
  • The hardware clock may be lower than requested. Printing the i2c object shows the actual configuration.

An empty list means no device acknowledged its address. Check power, ground, the SDA/SCL order and the pull-ups.

Troubleshooting checklist

  1. Nothing found in the scan. Check that the sensor has power and a common ground with the ESP32, that SDA and SCL aren't swapped and that pull-ups exist somewhere on the bus.
  2. Found at a different address than expected. Use the address from the scan. Many modules come with the alternative address set.
  3. Two devices, one address. Use an alternative address, a second bus or a TCA9548A.
  4. Works on the desk, fails in place. A longer wire adds capacitance. Lower the frequency, shorten the wires or adjust the pull-ups within the limits above.
  5. Occasional timeouts with some sensors. Some devices use clock stretching. ESPHome's timeout option sets the bus timeout. In MicroPython, the timeout parameter limits how long a transaction may take, and for SoftI2C it is documented as the wait for clock stretching.