Skip to content

BSP IO Expanders

The bsp_io_expanders module provides thin, board-agnostic I2C IO expander drivers used within the Hardware Abstraction Layer (HAL). It currently covers two chips — CH422G and TCA9554 — both of which extend the number of available GPIO lines over the I2C bus. These drivers are shared across all supported board variants and are located under hardware/common/drivers/.

IO expanders are used wherever the ESP32 or ESP32-S3 does not have enough native GPIO pins to drive display backlight signals, reset lines, enable pins, or other board-level control signals. The board-specific BSP code in bsp_bsp_board_initialization instantiates and configures these drivers at boot; they sit on top of the I2C bus provided by bsp_bus_drivers.


Hardware Abstraction Layer

bsp_bus_drivers

bsp_io_expanders (this module)

Board Support Package

io_ch422g_configure()

io_tca9554_configure()

i2c_master_write_to_device()

i2c_master_write_to_device()

i2c_master_write_read_device()

I2C bus transactions

bsp_board_initialization

(board_init)

CH422G Driver

io_ch422g.c

io_ch422g_config.h

TCA9554 Driver

io_tca9554.c

io_tca9554_config.h

bus_i2c

(ESP-IDF i2c driver)

Physical IO Expander Chips

Both drivers follow the same lifecycle pattern:

  1. The BSP calls *_configure() once during board_init.
  2. The BSP (or display/touch initialization code) calls write/read functions as needed at runtime.
  3. *_deinit() marks the driver as uninitialised; the I2C bus itself is managed separately by bsp_bus_drivers.

Files:

  • hardware/common/drivers/io_ch422g/io_ch422g.c
  • hardware/common/drivers/io_ch422g/io_ch422g_config.h

The CH422G is a pure output-only IO expander (EXIO0–EXIO7, push-pull) with an unconventional I2C addressing model: instead of a single device address combined with a register-select byte, the chip exposes each register as a distinct, fixed I2C bus address. This means:

RegisterFixed I2C AddressPurpose
System/Mode0x24Set EXIO0-7 to push-pull output mode (write 0x01)
Output data0x38Drive EXIO0-7 high/low (one byte, one bit per pin)

The driver maintains a singleton state (_is_initialized, _i2c_port) so it can be called from anywhere in the BSP without passing a handle.

Configuration Structure — io_ch422g_config_t

Section titled “Configuration Structure — io_ch422g_config_t”
FieldTypeDescription
i2c_porti2c_port_tESP-IDF I2C port (I2C_NUM_0 or I2C_NUM_1)
i2c_clk_speedintI2C clock frequency in Hz
initial_outputuint8_tBit-mask written to EXIO0-7 at init time
FunctionReturnsDescription
io_ch422g_configure(const io_ch422g_config_t *config)esp_err_tPuts chip in output mode and writes the initial output value. Must be called once before any write.
io_ch422g_write_output_pins(uint8_t pin_val)esp_err_tWrites all 8 output bits simultaneously.
io_ch422g_deinit(void)voidResets the initialized flag; does not touch the I2C bus.

Note: The I2C addresses 0x24 and 0x38 are hardcoded constants inside the driver — they are chip-defined, not board policy. Only initial_output and port settings come from the board BSP.


Files:

  • hardware/common/drivers/io_tca9554/io_tca9554.c
  • hardware/common/drivers/io_tca9554/io_tca9554_config.h

The TCA9554 is a full-featured 8-bit bidirectional IO expander using a conventional I2C model: a single device address with a register-offset byte. It supports per-pin direction control, polarity inversion, and both read and write operations.

RegisterOffsetDescription
Input Port0x00Reads the actual logic level on each pin (regardless of direction)
Output Port0x01Sets the output latch for output-configured pins
Polarity Inversion0x02Inverts the polarity seen on input reads (1 = inverted per bit)
Configuration0x03Pin direction: 1 = input, 0 = output

Because the chip’s I2C address is set by hardware address pins (A0, A1, A2), the driver supports address auto-discovery: the board BSP supplies up to three candidate addresses in i2c_addr[]; the driver probes each one at configure time and uses the first that responds successfully.

Configuration Structure — io_tca9554_config_t

Section titled “Configuration Structure — io_tca9554_config_t”
FieldTypeDescription
i2c_addr[3]uint8_t[]Candidate I2C addresses, zero-terminated
i2c_porti2c_port_tESP-IDF I2C port
i2c_clk_speedintI2C clock frequency in Hz
direction_maskuint8_tPer-bit direction (1 = input, 0 = output)
initial_outputuint8_tInitial output latch value written at configure time
FunctionReturnsDescription
io_tca9554_configure(const io_tca9554_config_t *config)esp_err_tProbes candidate addresses, sets direction, writes initial output.
io_tca9554_deinit(void)voidResets state flags; does not touch the I2C bus.
io_tca9554_set_configuration(uint8_t val)esp_err_tUpdates the direction register at runtime (1=input, 0=output per bit).
io_tca9554_write_output_pins(uint8_t pin_val)esp_err_tWrites all 8 output bits simultaneously via the Output Port register.
io_tca9554_read_output_pins(uint8_t *pin_val)esp_err_tReads back the output latch register (not the live pin level).
io_tca9554_read_input_pins(uint8_t *pin_val)esp_err_tReads the actual logic level on all 8 pins via the Input Port register.
io_tca9554_set_polarity_inversion(uint8_t val)esp_err_tSets the polarity inversion register (1 = invert per bit).

FeatureCH422GTCA9554
Pin count8 (EXIO0-7)8 (P0-P7)
DirectionOutput-onlyPer-bit configurable
I2C address schemeTwo separate fixed bus addresses per registerSingle device address + register offset byte
Address probingNot needed (fixed by chip)Yes — up to 3 candidate addresses scanned
Input readNoYes (io_tca9554_read_input_pins)
Output latch read-backNoYes (io_tca9554_read_output_pins)
Polarity inversionNoYes (io_tca9554_set_polarity_inversion)
Runtime direction changeNoYes (io_tca9554_set_configuration)
I2C timeout guard50 ms50 ms

Physical ChipESP-IDF I2CIO Expander Driverboard_init (BSP)Physical ChipESP-IDF I2CIO Expander Driverboard_init (BSP)*_configure(&config)Write mode/direction registerI2C transactionACKESP_OKWrite initial output registerI2C transactionACKESP_OKESP_OK (_is_initialized = true) Physical ChipESP-IDF I2CIO Expander DriverCaller (BSP / Display Init)Physical ChipESP-IDF I2CIO Expander DriverCaller (BSP / Display Init)*_write_output_pins(val)Guard: _is_initialized checki2c_master_write_to_device(port, addr, &val, 1, timeout)I2C write transactionACKESP_OKESP_OK TCA9554 ChipESP-IDF I2CTCA9554 DriverCaller (BSP)TCA9554 ChipESP-IDF I2CTCA9554 DriverCaller (BSP)io_tca9554_read_input_pins(&pin_val)Guard: _is_initialized + pin_val != NULLi2c_master_write_read_device(port, addr, &reg, 1, &data, 1, timeout)Write register address byte (0x00)Data byte (pin states) + ACK1 byte in dataESP_OK + *pin_val populated

No

Yes

No

Yes

io_tca9554_configure() called

Copy config to static _config

i = 0, probe_ret = ESP_FAIL

i2c_addr[i] != 0

AND i < 3?

No device found

Return probe_ret

(ESP_ERR_*)

Set _i2c_addr = i2c_addr[i]

Write CONFIGURATION_REG

with direction_mask

ESP_OK?

_i2c_addr = 0, i++

Device confirmed at address

Write OUTPUT_PORT_REG

with initial_output

_is_initialized = true

Return ESP_OK


Both drivers use module-level static state (_is_initialized, _i2c_port/_config, _i2c_addr). This means only one instance of each chip type can be active per build. This is a deliberate design choice to avoid heap allocation on the memory-constrained ESP32 — see CLAUDE.md memory constraints.

Both drivers hardcode a 50 ms I2C timeout (I2C_TIMEOUT_MS 50). This is intentionally short to avoid blocking the LVGL task running on Core 1. The comment in the source files explicitly states: “keep short to avoid blocking LVGL polling”. See bsp_bus_drivers for I2C bus initialization.

All public functions return esp_err_t. Guard-against-use-before-init checks are present in every write/read function — they return ESP_ERR_INVALID_STATE immediately if the driver has not been configured. Failures during configure are logged with esp3d_log_e() and propagated back to board_init.

The CH422G has no register-address byte in its I2C protocol. Each register is a separate I2C device address on the bus. When writing to the chip, only a single data byte is sent (no preceding register-address byte). This is the opposite of the conventional pattern used by TCA9554 and most other I2C peripherals.


bsp_io_expanders

bsp_bus_drivers

(bus_i2c)

esp3d_log

(logging macros)

ESP-IDF

driver/i2c.h

bsp_bsp_board_initialization

DependencyRole
bsp_bus_driversProvides the initialized i2c_port_t used in all transactions. The I2C bus must be initialized before calling any *_configure() function.
esp3d_logDiagnostic logging via esp3d_log() and esp3d_log_e() macros
ESP-IDF driver/i2c.hi2c_master_write_to_device, i2c_master_write_read_device

The drivers do not initialize the I2C bus themselves. The BSP is responsible for calling the I2C bus initialization before calling io_ch422g_configure() or io_tca9554_configure().


ModuleRelationship
bsp_bus_driversProvides the underlying I2C bus shared by IO expanders, touch controllers, and other peripherals
bsp_bsp_board_initializationCalls *_configure() at boot; owns the lifecycle of IO expander instances
bsp_touch_controllersTouch controllers (FT5x06, GT911) share the same I2C bus as the IO expanders
bspParent BSP module grouping all hardware drivers in the HAL