Skip to content

The I²C Bus

TWO WIRES · ADDRESSES 0x08–0x77 · SCAN BEFORE USE · WIRE AND WIRE1

I²C is the bus for everything that does not need to be fast: the display, the real-time clock, the sensors, sometimes the GPS. Two wires, one address per device, and a firmware that does not know in advance what is out there. This chapter describes how MeshCore scans the bus, why that happens before any sensor library runs, and when a node uses a second bus.

[!NOTE] Source. This page has been verified against the firmware itself: MeshCore v1.16.0, commit 03b6ef4, 28 July 2026 — files src/helpers/sensors/EnvironmentSensorManager.cpp, src/helpers/AutoDiscoverRTCClock.cpp and the PIN_BOARD_SDA flags in variants/.

Diagram of the I²C bus: SDA and SCL with pull-ups, the SoC as master and
display, clock and sensors as slaves, each with its own
address

Two wires, two pins

The bus has one data line (SDA) and one clock line (SCL). Which pins those are is set by the variant:

variants/lilygo_tbeam_1w/platformio.ini

  ; I2C pins
  -D PIN_BOARD_SDA=8
  -D PIN_BOARD_SCL=9

53 of the 79 variant directories set PIN_BOARD_SDA; counted per directory across variants/, commented-out lines excluded. The rest use the Arduino board's default pins or have no I²C devices on board.

Wire is the name of that bus in the Arduino world. It is a framework library and not a PlatformIO package — see Wire and SPI.

Scanning before anything is addressed

The most striking thing about I²C handling in MeshCore is the order. The firmware first probes the whole bus and only afterwards starts the libraries of the devices that answered:

src/helpers/sensors/EnvironmentSensorManager.cpp r.220-225

static void scanI2CBus(TwoWire* wire, bool found[128]) {
  for (uint8_t addr = 0x08; addr < 0x78; addr++) {
    wire->beginTransmission(addr);
    found[addr] = (wire->endTransmission() == 0);
  }
}

Addresses 0x08 through 0x77. Addresses outside that range are reserved in the I²C standard and are not tried. A device that acknowledges (endTransmission() returns zero) is noted; nothing else happens yet.

The comment above it says why that order matters:

src/helpers/sensors/EnvironmentSensorManager.cpp r.215-218

// Probes every valid address and records which ones ACK.
// This runs before any sensor library is touched, so a missing
// or misbehaving device cannot stall or crash the boot sequence.

A sensor library addressing a device that is not there can hang or crash. By scanning first and initialising only the addresses that were found, a missing or broken sensor cannot hold up the boot. Which sensors those are and which libraries belong to them is in Sensor libraries.

The clock does not scan, it tries four addresses

For the real-time clock it works differently. Four addresses are hard-coded and each is tried separately:

src/helpers/AutoDiscoverRTCClock.cpp r.18-27

#define DS3231_ADDRESS   0x68
#define RV3028_ADDRESS   0x52
#define PCF8563_ADDRESS  0x51
#define RX8130CE_ADDRESS 0x32

bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) {
  wire.beginTransmission(addr);
  uint8_t error = wire.endTransmission();
  return (error == 0);
}
Chip Address
DS3231 0x68
RV3028 0x52
PCF8563 0x51
RX8130CE 0x32

The same technique — begin a transmission and see whether it is acknowledged — but targeted instead of broad. There are only four supported clocks, so a full scan would add nothing. The DS3231 probe can also be disabled with DISABLE_DS3231_PROBE, because on some boards something else sits at 0x68.

Two buses on some boards

Sensors do not have to sit on the same bus as the display:

src/helpers/sensors/EnvironmentSensorManager.cpp r.6-8

#define TELEM_WIRE &Wire1  // Use Wire1 as the I2C bus for Environment Sensors
// ...
#define TELEM_WIRE &Wire  // Use default I2C bus for Environment Sensors

TELEM_WIRE points at Wire1 on boards that have a second bus and at Wire otherwise. Every sensor library is handed that reference on construction. A second bus is useful when the display keeps the first bus busy, or when a sensor uses an address that is already taken.

[!NOTE] An address collision cannot be solved in software on I²C. Two devices at the same address on the same bus both answer and the result is unusable. The second bus is then the only way out — or a sensor with a selectable address.

Sources

Firmware, commit 03b6ef4 (v1.16.0, 28 July 2026):

Related in this documentation:

Translated from Dutch by Anthropic Claude