Summary: I2C Issue and Fix
The Problem
The MCP23017 I/O expander (for the keypad) and the ES8311 audio codec both needed to share the same I2C bus (GPIO 18/23) on the LyraT Mini board.
What We Discovered
MCP23017 worked perfectly BEFORE audio_board_init() using the native ESP-IDF I2C driver
After audio_board_init(), using ESP-ADF's i2c_bus_read_bytes/i2c_bus_write_bytes gave garbage data (always read 0xB2 regardless of what was written)
Root Cause
We dug into ESP-ADF's i2c_bus_v2.c and found the bug on line 92:
.device_address = addr >> 1, // ESP-ADF shifts the address RIGHT
ESP-ADF expects an 8-bit I2C address (left-shifted with R/W bit), but we were passing a 7-bit address (0x21). So:
We passed: 0x21
ESP-ADF calculated: 0x21 >> 1 = 0x10 (WRONG device!)
The Solution
We found that ESP-ADF exposes i2c_bus_get_master_handle(port) which returns the native i2c_master_bus_handle_t.
The fix:
Let audio_board_init() create the I2C bus normally
Get the native handle: i2c_bus_get_master_handle(I2C_NUM_0)
Add MCP23017 using native API: i2c_master_bus_add_device(bus, &cfg, &mcp23017_dev)
Use native functions: i2c_master_transmit() / i2c_master_receive()
This allows both the audio codec (via ESP-ADF) and MCP23017 (via native driver) to share the same I2C bus without conflicts, each using their preferred API.
We are writing our own code - not using the UncleRus library.
We initially attempted to integrate the UncleRus mcp23x17 component, but it caused conflicts with ESP-ADF's i2c_bus library (both tried to manage the I2C bus, leading to crashes).
Our current implementation is minimal custom code:
Code: Select all
// MCP23017 device handle (native ESP-IDF)
static i2c_master_dev_handle_t mcp23017_dev = NULL;
// Simple register write
static esp_err_t mcp23017_write_reg(uint8_t reg, uint8_t value)
{
uint8_t data[2] = {reg, value};
return i2c_master_transmit(mcp23017_dev, data, 2, 100);
}
// Simple register read
static esp_err_t mcp23017_read_reg(uint8_t reg, uint8_t *value)
{
esp_err_t err = i2c_master_transmit(mcp23017_dev, ®, 1, 100);
if (err != ESP_OK) return err;
return i2c_master_receive(mcp23017_dev, value, 1, 100);
}
This is just ~20 lines of code that directly uses the native ESP-IDF I2C master API, which integrates cleanly with ESP-ADF's audio board without any library conflicts.
Here's how mcp23017_dev is initialized in mcp23017_init():
Code: Select all
static esp_err_t mcp23017_init(void)
{
// Step 1: Get the native I2C bus handle from ESP-ADF
// (audio_board_init already created this bus for the codec)
i2c_master_bus_handle_t bus = i2c_bus_get_master_handle(I2C_NUM_0);
// Step 2: Configure the MCP23017 device
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = MCP23017_ADDR, // 0x21 (7-bit address)
.scl_speed_hz = 100000, // 100 kHz
};
// Step 3: Add device to the bus - this creates the device handle
esp_err_t err = i2c_master_bus_add_device(bus, &dev_cfg, &mcp23017_dev);
// Step 4: Configure MCP23017 registers for keypad
mcp23017_write_reg(MCP23017_IODIRA, 0xF0); // GPA0-3 outputs, GPA4-7 inputs
mcp23017_write_reg(MCP23017_GPPUA, 0xF0); // Pull-ups on inputs
mcp23017_write_reg(MCP23017_OLATA, 0x0F); // Columns HIGH initially
...
}
The key insight is:
i2c_bus_get_master_handle(I2C_NUM_0) - ESP-ADF function that returns the native bus handle created by audio_board_init()
i2c_master_bus_add_device() - Native ESP-IDF function that adds our MCP23017 to the existing bus and gives us a device handle
This way we "piggyback" on the I2C bus that the audio board already created, rather than trying to create our own conflicting bus.