[mcp23xxx][pi4ioe5v6408] Add optional interrupt pin to eliminate polling

Extend interrupt_pin support to MCP23008, MCP23017, MCP23S08, MCP23S17,
and PI4IOE5V6408 GPIO expander components. Same approach as PCF8574/PCA9554:
when configured, the component disables its loop and only wakes on interrupt,
and the cache stays valid between interrupts so binary sensors return from
cache instead of doing I2C/SPI reads every loop iteration.

For MCP23xxx, the interrupt_pin config and C++ logic lives in the shared
MCP23XXXBase template class, so all four variants (I2C 8/16-pin, SPI
8/16-pin) inherit it automatically.

Inspired by jesserockz's work in #11959 which proposed a more comprehensive
interrupt-driven approach with per-pin interrupt status register reading.
This implementation takes a simpler path by leveraging the existing
CachedGpioExpander cache invalidation mechanism.
This commit is contained in:
J. Nick Koston
2026-04-04 09:12:09 -10:00
parent 9f3fb37d12
commit 41e09c1e6c
28 changed files with 101 additions and 9 deletions
+2
View File
@@ -22,6 +22,8 @@ void MCP23008::setup() {
// enable open-drain interrupt pins, 3.3V-safe
this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR);
}
this->setup_interrupt_pin_();
}
void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); }
+2
View File
@@ -24,6 +24,8 @@ void MCP23017::setup() {
this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | IOCON_ODR);
this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR);
}
this->setup_interrupt_pin_();
}
void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); }
+2
View File
@@ -34,6 +34,8 @@ void MCP23S08::setup() {
// enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip)
this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR);
}
this->setup_interrupt_pin_();
}
void MCP23S08::dump_config() {
+2
View File
@@ -42,6 +42,8 @@ void MCP23S17::setup() {
this->write_reg(mcp23x17_base::MCP23X17_IOCONA, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR);
this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR);
}
this->setup_interrupt_pin_();
}
void MCP23S17::dump_config() {
@@ -5,6 +5,7 @@ from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
@@ -32,6 +33,7 @@ MCP23XXX_INTERRUPT_MODES = {
MCP23XXX_CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
).extend(cv.COMPONENT_SCHEMA)
@@ -43,6 +45,8 @@ async def register_mcp23xxx(config, num_pins):
await cg.register_component(var, config)
CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins
cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT]))
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
return var
@@ -15,11 +15,28 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode);
void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
float get_setup_priority() const override { return setup_priority::IO; }
void loop() override { this->reset_pin_cache_(); }
void setup_interrupt_pin_() {
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
this->disable_loop();
}
}
void loop() override {
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
protected:
static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); }
// read a given register
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
// write a value to a given register
@@ -28,6 +45,7 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0;
bool open_drain_ints_;
InternalGPIOPin *interrupt_pin_{nullptr};
};
template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin {
@@ -5,6 +5,7 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
@@ -33,6 +34,7 @@ CONFIG_SCHEMA = (
{
cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component),
cv.Optional(CONF_RESET, default=True): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -46,6 +48,8 @@ async def to_code(config):
await i2c.register_i2c_device(var, config)
cg.add(var.set_reset(config[CONF_RESET]))
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
def validate_mode(value):
@@ -33,9 +33,18 @@ void PI4IOE5V6408Component::setup() {
return;
}
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
this->disable_loop();
}
}
void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); }
void PI4IOE5V6408Component::dump_config() {
ESP_LOGCONFIG(TAG, "PI4IOE5V6408:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
@@ -60,7 +69,12 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) {
this->write_gpio_modes_();
}
void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); }
void PI4IOE5V6408Component::loop() {
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
bool PI4IOE5V6408Component::read_gpio_outputs_() {
if (this->is_failed())
@@ -22,8 +22,11 @@ class PI4IOE5V6408Component : public Component,
/// Indicate if the component should reset the state during setup
void set_reset(bool reset) { this->reset_ = reset; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
@@ -40,6 +43,7 @@ class PI4IOE5V6408Component : public Component,
uint8_t pull_up_down_mask_{0x00};
bool reset_{true};
InternalGPIOPin *interrupt_pin_{nullptr};
bool read_gpio_modes_();
bool write_gpio_modes_();
+6 -2
View File
@@ -1,6 +1,10 @@
mcp23008:
i2c_id: i2c_bus
id: mcp23008_hub
- i2c_id: i2c_bus
id: mcp23008_hub
- i2c_id: i2c_bus
id: mcp23008_hub_int
address: 0x21
interrupt_pin: ${interrupt_pin}
binary_sensor:
- platform: gpio
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
+6 -2
View File
@@ -1,6 +1,10 @@
mcp23017:
i2c_id: i2c_bus
id: mcp23017_hub
- i2c_id: i2c_bus
id: mcp23017_hub
- i2c_id: i2c_bus
id: mcp23017_hub_int
address: 0x21
interrupt_pin: ${interrupt_pin}
binary_sensor:
- platform: gpio
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
+1
View File
@@ -2,3 +2,4 @@ mcp23s08:
- id: mcp23s08_hub
cs_pin: ${cs_pin}
deviceaddress: 0
interrupt_pin: ${interrupt_pin}
@@ -1,5 +1,6 @@
substitutions:
cs_pin: GPIO5
interrupt_pin: GPIO15
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
@@ -1,5 +1,6 @@
substitutions:
cs_pin: GPIO15
interrupt_pin: GPIO13
packages:
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
@@ -1,5 +1,6 @@
substitutions:
cs_pin: GPIO5
interrupt_pin: GPIO2
packages:
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
+1
View File
@@ -2,3 +2,4 @@ mcp23s17:
- id: mcp23s17_hub
cs_pin: ${cs_pin}
deviceaddress: 0
interrupt_pin: ${interrupt_pin}
@@ -1,5 +1,6 @@
substitutions:
cs_pin: GPIO5
interrupt_pin: GPIO15
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
@@ -1,5 +1,6 @@
substitutions:
cs_pin: GPIO15
interrupt_pin: GPIO13
packages:
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
@@ -1,5 +1,6 @@
substitutions:
cs_pin: GPIO5
interrupt_pin: GPIO2
packages:
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
+7 -3
View File
@@ -1,7 +1,11 @@
pi4ioe5v6408:
i2c_id: i2c_bus
id: pi4ioe1
address: 0x44
- i2c_id: i2c_bus
id: pi4ioe1
address: 0x44
- i2c_id: i2c_bus
id: pi4ioe1_int
address: 0x45
interrupt_pin: ${interrupt_pin}
switch:
- platform: gpio
@@ -1,6 +1,7 @@
substitutions:
i2c_sda: GPIO21
i2c_scl: GPIO22
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
@@ -1,6 +1,7 @@
substitutions:
i2c_sda: GPIO4
i2c_scl: GPIO5
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml