Repository navigation
I2CBus
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
devjsonanddevbintopics - 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.
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.
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
}
]
}
}| 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). |
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). |
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). |
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. |
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 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.
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.
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
}
}
]
}
}The bus runs a background FreeRTOS task (I2CTask) that performs the following work each cycle, in order:
- Yield for
loopYieldMsto allow other tasks to run - Skip remaining work if a hiatus (deliberate pause) is active
- Bus scanning — if a scan is pending, service the scanner within the configured time budget
- Request queue — process any queued raw bus requests (from REST API calls etc.)
- Bus multiplexers — update multiplexer slot state
- Bus power controller — service power switching logic
- IO expanders — flush any pending output-state changes
- Device polling — poll all identified devices that are due
- 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.
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.
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_ERRORrather thanBUS_SW_TIME_OUT.
The ESP-IDF I2C central (I2C_USE_ESP_IDF) has not been checked for the same behaviour.
- I2C Device Scanning — priority-based scanning, multiplexer slot handling, scan modes, hot-swap, and stuck-bus recovery
- I2C Device Identification and Polling — device type detection, initialization sequences, polling configuration, and data decoding
-
Device Data Publishing —
devjsonanddevbinmessage formats and subscription - DeviceManager SysMod — DeviceManager features, device lifecycle, and REST API overview
-
DeviceManager Settings —
BusesandDevicesconfiguration reference - DeviceManager REST API — REST endpoints for raw commands, device config, and type info
Getting Started
- Quick Start
- Architecture at a Glance
- Writing Your First SysMod
- Adding a Comms Channel
- Adding an I2C Device Type
Scaffolding & Building
- Raft CLI
- Web Apps (Device Types, Flasher)
- SysTypes
- Top-Level SysType
- Build Process
- WebUI Build Pipeline
- File System
- Partitions & Flash
- Local Dev Libraries
- Library Developer Guide
Architecture
Built-in SysMods
- NetworkManager
- WiFi Troubleshooting
- BLEManager
- WebServer
- MQTTManager
- SerialConsole
- CommandSerial
- CommandSocket
- CommandFile
- FileManager
- LogManager
- ESPOTAUpdate
- StatePublisher
- Remote Logging
- Data Source Registration
Comms & Protocols
- Stack Overview
- Comms Channels
- ProtocolExchange
- RICREST Protocol
- Real-Time Streams
- Adding REST Endpoints
- Built-in REST Endpoints
- File Download (OKTO)
- OTA Update Flow
Devices & Buses
- DeviceManager
- Device Manager REST API
- Device Factory & Classes
- Device Type Records
- Adding an I2C Device Type
- Device Data Publishing
- Data Logger
- I2C Bus
- I2C Device Scanning
- I2C ID & Polling
- MotorControl Overview
- MotorControl Config
- MotorControl Commands
- Camera
ROS 2
Helpers
Reference