Compare commits

...

11 Commits

Author SHA1 Message Date
J. Nick Koston
32f05f50f5 [sx1509] Extract clear_interrupt_ inline helper 2026-04-09 17:01:51 -10:00
J. Nick Koston
37796887f0 [sx1509] Remove unnecessary interrupt source clear in enable_pin_interrupt_ 2026-04-09 17:00:36 -10:00
J. Nick Koston
dd2543a2d6 [sx1509] Reorder fields to minimize padding 2026-04-09 16:58:28 -10:00
J. Nick Koston
dce6d5ab0c [sx1509] Only clear interrupt source when interrupt actually fired 2026-04-09 16:57:18 -10:00
J. Nick Koston
867df4225a [sx1509] Clear interrupt source before resetting cache 2026-04-09 16:55:41 -10:00
J. Nick Koston
801f385896 [sx1509] Move SENSE_REGS array to registers header 2026-04-09 16:54:44 -10:00
J. Nick Koston
010717afa2 [sx1509] Replace magic numbers with array lookup, add comment 2026-04-09 16:53:47 -10:00
J. Nick Koston
7841ef7067 [sx1509] Move SENSE_BOTH_EDGES constant to registers header 2026-04-09 16:52:23 -10:00
J. Nick Koston
9691d5b381 [sx1509] Extract enable_pin_interrupt_ helper, use named constant 2026-04-09 16:51:08 -10:00
J. Nick Koston
043d05cd12 [sx1509] Configure interrupt mask and sense registers for input pins
Unmask per-pin interrupts and set both-edge sense detection when
interrupt_pin is configured. Clear interrupt source in loop() to
deassert INT line after servicing.
2026-04-09 16:49:15 -10:00
J. Nick Koston
59065c71a8 [sx1509] Add interrupt pin support
Add optional interrupt_pin configuration to eliminate I2C polling
for GPIO reads. When configured, the component disables its loop
and only reads the I2C bus when the interrupt fires, matching
the pattern used by PCF8574, PCA9554, and other GPIO expanders.

When keypad mode is active, the loop continues running for key
scanning regardless of interrupt configuration.
2026-04-09 13:28:35 -10:00
8 changed files with 78 additions and 1 deletions

View File

@@ -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,
@@ -75,6 +76,7 @@ CONFIG_SCHEMA = (
{
cv.GenerateID(): cv.declare_id(SX1509Component),
cv.Optional(CONF_KEYPAD): cv.Schema(KEYPAD_SCHEMA),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -86,6 +88,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
if conf := config.get(CONF_KEYPAD):
cg.add(var.set_rows_cols(conf[CONF_KEY_ROWS], conf[CONF_KEY_COLUMNS]))
if (

View File

@@ -28,10 +28,26 @@ void SX1509Component::setup() {
delayMicroseconds(500);
if (this->has_keypad_)
this->setup_keypad_();
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&SX1509Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
// Disable loop until an input pin is configured via pin_mode()
// or keypad is active. For interrupt-driven mode, loop is re-enabled by the ISR.
if (!this->has_keypad_) {
this->disable_loop();
}
}
void IRAM_ATTR SX1509Component::gpio_intr(SX1509Component *arg) {
arg->interrupt_pending_ = true;
arg->enable_loop_soon_any_context();
}
void SX1509Component::dump_config() {
ESP_LOGCONFIG(TAG, "SX1509:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
if (this->is_failed()) {
ESP_LOGE(TAG, "Setting up SX1509 failed!");
}
@@ -39,7 +55,17 @@ void SX1509Component::dump_config() {
}
void SX1509Component::loop() {
// Reset cache at the start of each loop
if (this->interrupt_pending_) {
this->interrupt_pending_ = false;
// Clear interrupt source before resetting cache to avoid losing
// pin changes that occur between cache reset and interrupt clear
this->clear_interrupt_();
this->reset_pin_cache_();
if (!this->has_keypad_) {
this->disable_loop();
}
return;
}
this->reset_pin_cache_();
if (this->has_keypad_) {
@@ -169,6 +195,11 @@ void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
// Set direction to input
this->ddr_mask_ |= (1 << pin);
this->write_byte_16(REG_DIR_B, this->ddr_mask_);
if (this->interrupt_pin_ != nullptr) {
this->enable_pin_interrupt_(pin);
} else {
this->enable_loop();
}
}
}
@@ -313,5 +344,23 @@ void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8
set_debounce_pin_(i + 8);
}
void SX1509Component::enable_pin_interrupt_(uint8_t pin) {
// Unmask this pin's interrupt (clear bit = enabled)
uint16_t mask = 0;
this->read_byte_16(REG_INTERRUPT_MASK_B, &mask);
mask &= ~(1 << pin);
this->write_byte_16(REG_INTERRUPT_MASK_B, mask);
// Configure sense to trigger on both edges
uint8_t sense_reg = SENSE_REGS[pin / 4];
uint8_t sense_val = 0;
this->read_byte(sense_reg, &sense_val);
// 2-bit field position within the sense register (4 pins per register, 2 bits each)
uint8_t shift = (pin % 4) * 2;
sense_val &= ~(SENSE_BOTH_EDGES << shift);
sense_val |= (SENSE_BOTH_EDGES << shift);
this->write_byte(sense_reg, sense_val);
}
} // namespace sx1509
} // namespace esphome

View File

@@ -46,6 +46,7 @@ class SX1509Component : public Component,
uint16_t read_key_data();
void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); };
void pin_mode(uint8_t pin, gpio::Flags flags);
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
uint32_t get_clock() { return this->clk_x_; };
void set_rows_cols(uint8_t rows, uint8_t cols) {
this->rows_ = rows;
@@ -63,6 +64,8 @@ class SX1509Component : public Component,
void setup_led_driver(uint8_t pin);
protected:
static void IRAM_ATTR gpio_intr(SX1509Component *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -75,6 +78,7 @@ class SX1509Component : public Component,
uint16_t port_mask_ = 0x00;
uint16_t output_state_ = 0x00;
bool has_keypad_ = false;
volatile bool interrupt_pending_{false};
uint8_t rows_ = 0;
uint8_t cols_ = 0;
std::string keys_;
@@ -85,9 +89,15 @@ class SX1509Component : public Component,
std::vector<SX1509Processor *> keypad_binary_sensors_;
std::vector<SX1509KeyTrigger *> key_triggers_;
InternalGPIOPin *interrupt_pin_{nullptr};
uint32_t last_loop_timestamp_ = 0;
const uint32_t min_loop_period_ = 15; // ms
void enable_pin_interrupt_(uint8_t pin);
void clear_interrupt_() {
uint16_t interrupt_source = 0;
this->read_byte_16(REG_INTERRUPT_SOURCE_B, &interrupt_source);
}
void setup_keypad_();
void set_debounce_config_(uint8_t config_value);
void set_debounce_time_(uint8_t time);

View File

@@ -57,6 +57,10 @@ const uint8_t REG_SENSE_HIGH_B = 0x14; // RegSenseHighB Sense register for I
const uint8_t REG_SENSE_LOW_B = 0x15; // RegSenseLowB Sense register for I/O[11:8] 0000 0000
const uint8_t REG_SENSE_HIGH_A = 0x16; // RegSenseHighA Sense register for I/O[7:4] 0000 0000
const uint8_t REG_SENSE_LOW_A = 0x17; // RegSenseLowA Sense register for I/O[3:0] 0000 0000
// Sense register values (2 bits per pin): 00=None, 01=Rising, 10=Falling, 11=Both
const uint8_t SENSE_BOTH_EDGES = 0x03;
// Sense register lookup: each register covers 4 pins (pins 0-3, 4-7, 8-11, 12-15)
const uint8_t SENSE_REGS[] = {REG_SENSE_LOW_A, REG_SENSE_HIGH_A, REG_SENSE_LOW_B, REG_SENSE_HIGH_B};
const uint8_t REG_INTERRUPT_SOURCE_B =
0x18; // RegInterruptSourceB Interrupt source register _ I/O[15_8] (Bank B) 0000 0000
const uint8_t REG_INTERRUPT_SOURCE_A =

View File

@@ -2,6 +2,7 @@ sx1509:
- id: sx1509_hub
i2c_id: i2c_bus
address: 0x3E
interrupt_pin: ${interrupt_pin}
keypad:
key_rows: 2
key_columns: 2

View File

@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml

View File

@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml

View File

@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml