Skip to content
Rob Dobson edited this page Oct 27, 2026 · 10 revisions

I2C Bus

Overview

Raft's I2C bus support is provided by the RaftI2C library. When a bus of type "I2C" is declared in the DevMan configuration, Raft creates a BusI2C instance that manages the complete lifecycle of I2C devices on that bus:

  • Automatic device detection through priority-based scanning, including devices behind bus multiplexers
  • Device type identification using WHO_AM_I registers and configurable detection sequences
  • Device initialization using register-write sequences defined per device type
  • Continuous polling of identified devices at configurable intervals
  • Data publishing via the devjson and devbin topics
  • Bus health management including stuck-bus detection and recovery, and device hot-swapping

The bus runs its own dedicated FreeRTOS task so that scanning and polling activity does not block the main application loop.

I2C Implementation Selection

The I2C hardware driver is selected automatically based on the target chip:

Chip Driver used
ESP32, ESP32-S3, ESP32-C3, … RaftI2CCentral (Raft native driver)
ESP32-C6 RaftI2CCentral_ESPIDF (ESP-IDF v5 driver)

The selection can be overridden at build time by defining I2C_USE_RAFT_I2C or I2C_USE_ESP_IDF_5.

Configuration

I2C buses are declared inside the Buses array of the DevMan SysType configuration. The type field must be "I2C". All other fields in the bus object are I2C-specific.

{
    "DevMan": {
        "Buses": [
            {
                "name": "I2CA",
                "type": "I2C",
                "sdaPin": "SDA",
                "sclPin": "SCL",
                "i2cFreq": 400000
            }
        ]
    }
}

Core Parameters

Parameter Type Default Description
name string "" Bus name used to identify this bus in REST API calls (e.g. "I2CA").
sdaPin string — SDA pin name (looked up via ConfigPinMap). Required.
sclPin string — SCL pin name (looked up via ConfigPinMap). Required.
i2cPort int 0 ESP32 I2C hardware port number (0 or 1).
i2cFreq int 100000 I2C bus clock frequency in Hz. Common values: 100000 (standard), 400000 (fast), 1000000 (fast-plus).
i2cFilter int driver default Glitch filter level for the I2C bus (passed directly to the I2C driver).

Task Parameters

The bus runs in a dedicated FreeRTOS task. These parameters control its scheduling:

Parameter Type Default Description
taskCore int 1 CPU core to pin the task to (0 or 1).
taskPriority int driver default FreeRTOS task priority.
taskStack int 5500 Task stack size in bytes (data subscribers decode on this task).

Loop Timing Parameters

The I2C task sleeps between passes until the next device poll is due, a request is queued, or loopYieldMs has passed (whichever is first). Each pass polls the devices that are due (earliest due first) within a time budget, then scans. The unyield budgets control how long scanning is allowed to run before yielding again; a scan also stops in time for the next poll that is due:

Parameter Type Default Description
loopYieldMs int 5 Longest the task sleeps between passes (milliseconds). Scanning runs at most once per this period.
pollBudgetUs int 2000 Time spent polling in each pass (microseconds): no further poll is started once this is used.
identBudgetUs int 5000 Time spent identifying devices in each pass (microseconds), beyond the first device: no further device is started once this is used (at most 8 per pass).
fastScanMaxUnyieldMs int driver default Maximum milliseconds spent on fast scanning before yielding (internally converted to microseconds).
slowScanMaxUnyieldMs int driver default Maximum milliseconds spent on slow scanning before yielding (internally converted to microseconds).

Bus Scanning Parameters

These parameters control how the bus is scanned for devices. They are read directly from the bus configuration object (no prefix):

Parameter Type Default Description
busScanPeriodMs int 5 Minimum time between slow-scan passes (milliseconds).
scanShareMinPct, scanShareMaxPct int 10, 30 Share of bus time used by slow scanning: the maximum on a quiet bus, falling with the poll load to the minimum. See I2C Device Scanning.
backgroundSweepMs int 10000 Time to probe every address nothing can claim (no record, hook or multiplexer) once, on the main bus and every slot.
fastScanMaxMs int 2000 Longest time in the fast scan mode (after startup or a scan request).
slotKnownWeight, slotChangedWeight int 2, 3 How often a multiplexer slot with an online device, or with a recent change, is scanned relative to an empty slot (1). See I2C Device Scanning.
slotChangedMs int 10000 How long a slot counts as recently changed.
muxCheckMs int 100 Interval between multiplexer supervision probes (one multiplexer address per probe, in turn); 0 turns supervision off.
generalCallRoundMs int 2000 Time for a round of general calls over every slot (one slot at a time, in turn), when a selected device type record has a generalCall - none are sent otherwise.
scanBoost array of strings [] I2C addresses to promote to the highest scan priority (hex strings, e.g. "0x6a"). These are scanned more frequently and detected faster. An identification hook can add more at runtime with setScanBoostAddrs() (RaftI2C 1.23.0) - see I2C Device Scanning.

Bus Multiplexer Parameters (mux)

Multiplexer support (e.g. TCA9548A, PCA9548A) is configured under the mux key:

{
    "mux": {
        "enable": true,
        "minAddr": "0x70",
        "maxAddr": "0x77",
        "rstPin": 5,
        "clearCascadeMux": false
    }
}
Parameter Type Default Description
enable bool false Enable multiplexer support.
minAddr string "0x70" Lowest I2C address to probe for multiplexers.
maxAddr string "0x77" Highest I2C address to probe for multiplexers.
rstPin int -1 GPIO pin connected to the multiplexer hardware reset line. -1 = not used.
clearCascadeMux bool false Clear cascaded multiplexer slots when a multiplexer is detected.

Devices behind multiplexers are addressed using a slot number. See I2C Device Scanning for slot numbering details.

IO Expander Parameters (ioExps)

IO expanders connected to the I2C bus (used for bus power switching) are configured under the ioExps key. Refer to the IO expander documentation for specific fields.

Bus Power Controller Parameters (pwr)

An optional power controller switches the power to the bus and/or individual multiplexer slots: it holds them off briefly at boot so devices start from a clean reset, powers them, and can power-cycle a slot later (stuck-bus recovery). It is only instantiated if the pwr key is present as a JSON object.

"pwr": {
    "voltageLevels": ["off", "3V3"],
    "slotGroups": [
        {
            "name": "STEMMA QT",
            "startSlotNum": 0,
            "numSlots": 1,
            "defaultLevelIdx": 1,
            "levelsExclOff": [ { "vPins": [21], "on": [1] } ]
        }
    ]
}
Key Meaning
voltageLevels Names of the power levels; the first is always "off"
slotGroups[] Groups of consecutive slots switched the same way
name Group name (diagnostics)
startSlotNum / numSlots The slots in the group. Slot 0 is the bus itself: with no multiplexer it is the only slot, so a board whose connector power is switched uses startSlotNum: 0, numSlots: 1
defaultLevelIdx Index into voltageLevels applied after boot (1 = the first level above off)
levelsExclOff[] One entry per level above off: vPins (one pin per slot, -1 for none) and on (the pin level that selects that level, per slot)

A vPins entry is a pin on an I/O expander when one owns it. Otherwise it is handed to RaftBusSystem::virtualPinsSet, which offers it to the other buses and then drives it as a plain GPIO - so a connector switched by a GPIO (an LDO enable, or Adafruit's TFT_I2C_POWER on GPIO 21) is configuration, with no board code. (The GPIO fallback is a 2026-10 RaftI2C change; with earlier RaftI2C a plain GPIO logs FAILED to set pins and stays unchanged.)

Sequence at boot: off for 100 ms, a 500 ms power-cycle off period, on, then 100 ms to stabilise before the slot counts as powered (~0.7 s). Devices on a powered slot appear on the next scan after that.

Complete Configuration Example

The following example configures an I2C bus with a 400 kHz clock, a TCA9548A multiplexer, scan boost for two known device addresses, and a 30-second slow scan period:

{
    "DevMan": {
        "Buses": [
            {
                "name": "I2CA",
                "type": "I2C",
                "sdaPin": "SDA",
                "sclPin": "SCL",
                "i2cPort": 0,
                "i2cFreq": 400000,
                "taskCore": 1,
                "busScanPeriodMs": 5,
                "scanBoost": ["0x6a", "0x36"],
                "mux": {
                    "enable": true,
                    "minAddr": "0x70",
                    "maxAddr": "0x77",
                    "rstPin": 5
                }
            }
        ]
    }
}

Bus Worker Task

The bus runs a background FreeRTOS task (I2CTask) that performs the following work each cycle, in order:

  1. Yield for loopYieldMs to allow other tasks to run
  2. Skip remaining work if a hiatus (deliberate pause) is active
  3. Bus scanning — if a scan is pending, service the scanner within the configured time budget
  4. Request queue — process any queued raw bus requests (from REST API calls etc.)
  5. Bus multiplexers — update multiplexer slot state
  6. Bus power controller — service power switching logic
  7. IO expanders — flush any pending output-state changes
  8. Device polling — poll all identified devices that are due
  9. Stuck-bus handler — check for and attempt recovery from stuck-bus conditions

The bus can be paused and resumed (e.g. during firmware update). While paused, scanning is suspended and polling is skipped, but queued requests are still processed.

Device Address Barring

Individual device addresses can be temporarily barred from access for a specified period (in milliseconds). This is used internally after commands that require a device to be left alone while processing (e.g. a soft reset). Barred addresses are skipped by both the scanner and the poller until the bar expires.

NACKed Writes and Bus Resynchronisation

A write with data bytes queued after the address (e.g. devman/cmdraw with hexWr, with or without a read) can be NACKed on the address, for example when the device is absent. On the ESP32-S3 the I2C engine then stops part way through its commands. The STOP is not sent, and the next transaction on the bus fails whatever its address. Previously that could be a poll of a present device, so repeated writes to an absent address could make present devices appear to go offline and come back.

In RaftI2C versions after 1.20.1 (with the Raft I2C central, I2C_USE_RAFT_I2C):

  • An access which fails without executing all of its commands is followed at once by an address-only probe of the same address. This completes a START...STOP sequence on the bus, and its result is ignored.
  • A NACK which ends in a software time-out is reported as BUS_ACK_ERROR rather than BUS_SW_TIME_OUT.

The ESP-IDF I2C central (I2C_USE_ESP_IDF) has not been checked for the same behaviour.

Related Documentation

Clone this wiki locally