[PCA9505] Add PCA9505/PCA9506 40-channel GPIO expander (#20667)

This commit is contained in:
Andrej Friesen
2026-10-13 12:50:43 -10:00
committed by GitHub
parent 7d735f0677
commit f265c46c21
9 changed files with 693 additions and 0 deletions
+1
View File
@@ -439,6 +439,7 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
+79
View File
@@ -0,0 +1,79 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
AUTO_LOAD = ["gpio_expander"]
CODEOWNERS = ["@ssieb", "@ajfriesen"]
DEPENDENCIES = ["i2c"]
DOMAIN = "pca9505"
MULTI_CONF = True
pca9505_ns = cg.esphome_ns.namespace("pca9505")
PCA9505Component = pca9505_ns.class_("PCA9505Component", cg.Component, i2c.I2CDevice)
PCA9505GPIOPin = pca9505_ns.class_(
"PCA9505GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9505Component)
)
CONF_PCA9505 = "pca9505"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9505Component),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
async def to_code(config: ConfigType) -> None:
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)))
def validate_mode(value: ConfigType) -> ConfigType:
if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
raise cv.Invalid("Mode must be either input or output")
if value[CONF_INPUT] and value[CONF_OUTPUT]:
raise cv.Invalid("Mode must be either input or output")
return value
PCA9505_PIN_SCHEMA = pins.gpio_base_schema(
PCA9505GPIOPin,
cv.int_range(min=0, max=39),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
).extend(
{
cv.Required(CONF_PCA9505): cv.use_id(PCA9505Component),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_PCA9505, PCA9505_PIN_SCHEMA)
async def pca9505_pin_to_code(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_PCA9505])
cg.add(var.set_pin(config[CONF_NUMBER]))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var
+165
View File
@@ -0,0 +1,165 @@
#include "pca9505.h"
#include "esphome/core/log.h"
namespace esphome::pca9505 {
// Each register block holds one byte per port. Add the port number to reach a single
// port, or set the auto-increment bit to transfer the whole block in one go.
static constexpr uint8_t INPUT_REG = 0x00;
static constexpr uint8_t OUTPUT_REG = 0x08;
static constexpr uint8_t INVERT_REG = 0x10;
static constexpr uint8_t CONFIG_REG = 0x18;
static constexpr uint8_t MASK_REG = 0x20;
static constexpr uint8_t AUTO_INCREMENT = 0x80;
ESPHOME_LOG_TAG(TAG, "pca9505");
void PCA9505Component::setup() {
// Test to see if device exists
if (!this->read_bank_(0)) {
ESP_LOGE(TAG, "PCA9505 not detected at 0x%02X", this->address_);
this->mark_failed();
return;
}
// No polarity inversion
this->write_all_banks_(INVERT_REG, 0x00);
// All outputs low, before anything is switched to being an output
this->write_all_banks_(OUTPUT_REG, 0x00);
// All inputs at initialization. The part sees a 1 as an input, so this is the
// inverse of config_, which is all zeroes here.
this->write_all_banks_(CONFIG_REG, 0xFF);
// Mask every interrupt. This must be written, not assumed: the chip keeps its
// registers through an MCU reboot, so a previous run may have left pins unmasked.
for (uint8_t &bank : this->mask_) {
bank = 0xFF;
}
this->write_all_banks_(MASK_REG, 0xFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA9505Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
// Don't invalidate cache on read - only invalidate when interrupt fires
this->set_invalidate_on_read_(false);
}
// Nothing to poll until pin_mode() registers an input pin.
this->disable_loop();
}
void IRAM_ATTR PCA9505Component::gpio_intr(PCA9505Component *arg) { arg->enable_loop_soon_any_context(); }
void PCA9505Component::loop() {
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
}
void PCA9505Component::dump_config() {
ESP_LOGCONFIG(TAG, "PCA9505:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
bool PCA9505Component::digital_read_hw(uint8_t pin) { return this->read_bank_(pin / 8); }
bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin / 8] & (1 << (pin % 8)); }
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
if (value) {
this->output_[bank] |= mask;
} else {
this->output_[bank] &= ~mask;
}
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
}
void PCA9505Component::pin_mode(uint8_t pin, gpio::Flags flags) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
if (flags & gpio::FLAG_INPUT) {
this->config_[bank] &= ~mask;
// An input may raise INT; the polling loop is only needed when there is no interrupt pin.
this->mask_[bank] &= ~mask;
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags & gpio::FLAG_OUTPUT) {
this->config_[bank] |= mask;
// An output must never raise INT.
this->mask_[bank] |= mask;
}
// Invert as the part sees a 1 as an input
this->write_bank_(CONFIG_REG, bank, ~this->config_[bank]);
// Without an interrupt pin INT is unconnected, so the masks written at setup can stay
if (this->interrupt_pin_ != nullptr) {
this->write_bank_(MASK_REG, bank, this->mask_[bank]);
}
}
bool PCA9505Component::read_bank_(uint8_t bank) {
if (this->is_failed())
return false;
if (this->read_register(INPUT_REG + bank, &this->input_[bank], 1) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to read input register"));
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9505Component::write_bank_(uint8_t reg, uint8_t bank, uint8_t value) {
if (this->is_failed())
return false;
if (this->write_register(reg + bank, &value, 1) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to write register"));
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9505Component::write_all_banks_(uint8_t reg, uint8_t value) {
if (this->is_failed())
return false;
uint8_t values[BANK_COUNT];
for (uint8_t &bank : values) {
bank = value;
}
if (this->write_register(reg | AUTO_INCREMENT, values, sizeof(values)) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to write register"));
return false;
}
this->status_clear_warning();
return true;
}
float PCA9505Component::get_setup_priority() const { return setup_priority::IO; }
void PCA9505GPIOPin::setup() { this->pin_mode(this->flags_); }
void PCA9505GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool PCA9505GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA9505GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCA9505GPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "%u via PCA9505", this->pin_);
}
} // namespace esphome::pca9505
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::pca9505 {
/// Number of I/O pins on the PCA9505 and PCA9506.
static constexpr uint8_t PIN_COUNT = 40;
/// The pins are grouped into five ports of eight, each with its own set of registers.
static constexpr uint8_t BANK_COUNT = PIN_COUNT / 8;
class PCA9505Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, PIN_COUNT> {
public:
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void setup() override;
void loop() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
protected:
static void IRAM_ATTR gpio_intr(PCA9505Component *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;
bool read_bank_(uint8_t bank);
bool write_bank_(uint8_t reg, uint8_t bank, uint8_t value);
bool write_all_banks_(uint8_t reg, uint8_t value);
/// The pin config per bank - 1 means OUTPUT, 0 means INPUT
uint8_t config_[BANK_COUNT]{};
/// The output state per bank - 1 means HIGH, 0 means LOW
uint8_t output_[BANK_COUNT]{};
/// The input pin states per bank - 1 means HIGH, 0 means LOW
uint8_t input_[BANK_COUNT]{};
/// Which pins may pull INT low - 1 means masked, matching the chip
uint8_t mask_[BANK_COUNT]{};
InternalGPIOPin *interrupt_pin_{nullptr};
};
class PCA9505GPIOPin final : public GPIOPin, public Parented<PCA9505Component> {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
gpio::Flags get_flags() const override { return this->flags_; }
protected:
uint8_t pin_{0};
bool inverted_{false};
gpio::Flags flags_{};
};
} // namespace esphome::pca9505
+46
View File
@@ -0,0 +1,46 @@
pca9505:
- id: pca9505_hub
i2c_id: i2c_bus
address: 0x26
- id: pca9505_hub_second
interrupt_pin: ${interrupt_pin}
i2c_id: i2c_bus
address: 0x27
binary_sensor:
# Pins 0 and 39 are the first and last pins, so between them they cover every
# bank index the five-port register maths has to get right.
- platform: gpio
id: pca9505_input_first
name: PCA9505 Binary Sensor First
pin:
pca9505: pca9505_hub
number: 0
mode: INPUT
inverted: true
- platform: gpio
id: pca9505_input_last
name: PCA9505 Binary Sensor Last
pin:
pca9505: pca9505_hub_second
number: 39
mode: INPUT
inverted: false
switch:
- platform: gpio
id: pca9505_output_low
name: PCA9505 Switch Low
pin:
pca9505: pca9505_hub
number: 7
mode: OUTPUT
inverted: false
- platform: gpio
id: pca9505_output_high
name: PCA9505 Switch High
pin:
pca9505: pca9505_hub
number: 32
mode: OUTPUT
inverted: true
@@ -0,0 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
pca9505: !include common.yaml
@@ -0,0 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
pca9505: !include common.yaml
@@ -0,0 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
pca9505: !include common.yaml
+312
View File
@@ -0,0 +1,312 @@
#include <gtest/gtest.h>
#include "esphome/components/pca9505/pca9505.h"
namespace esphome::pca9505::testing {
// Models the PCA9505 register map: five ports, each with its own input, output,
// polarity inversion and configuration byte. The command byte selects a register
// and its top bit asks the part to auto-increment through the block.
class FakeBus : public i2c::I2CBus {
public:
static constexpr uint8_t ADDRESS = 0x26;
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
size_t read_count) override {
EXPECT_EQ(address, ADDRESS);
if (this->fail)
return i2c::ERROR_TIMEOUT;
// Every transfer starts with the command byte.
EXPECT_GE(write_count, 1u);
const uint8_t command = write_buffer[0];
const bool auto_increment = (command & 0x80) != 0;
uint8_t *block = this->block_for(command & 0x7F);
const uint8_t index = (command & 0x7F) % 8;
if (read_count != 0) {
this->reads++;
// Without auto-increment the part keeps returning the same register.
EXPECT_TRUE(auto_increment || read_count == 1);
for (size_t i = 0; i < read_count; i++)
read_buffer[i] = block[auto_increment ? index + i : index];
return i2c::ERROR_OK;
}
this->writes++;
EXPECT_TRUE(auto_increment || write_count == 2);
for (size_t i = 1; i < write_count; i++)
block[auto_increment ? index + i - 1 : index] = write_buffer[i];
return i2c::ERROR_OK;
}
uint8_t *block_for(uint8_t reg) {
if (reg < 0x08)
return this->input;
if (reg < 0x10)
return this->output;
if (reg < 0x18)
return this->invert;
if (reg < 0x20)
return this->config;
return this->mask;
}
// Power-on reset defaults from the datasheet.
uint8_t input[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t output[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t invert[BANK_COUNT]{0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t config[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t mask[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
unsigned reads{0};
unsigned writes{0};
bool fail{false};
};
class PCA9505Test : public ::testing::Test {
protected:
void SetUp() override {
this->component_.set_i2c_bus(&this->bus_);
this->component_.set_i2c_address(FakeBus::ADDRESS);
}
PCA9505GPIOPin make_pin_(uint8_t number, gpio::Flags flags, bool inverted = false) {
PCA9505GPIOPin pin;
pin.set_parent(&this->component_);
pin.set_pin(number);
pin.set_flags(flags);
pin.set_inverted(inverted);
return pin;
}
FakeBus bus_;
PCA9505Component component_;
InternalGPIOPin int_pin_; // digital_read() returns the inverted flag on the host, so it doubles as the level
};
TEST_F(PCA9505Test, SetupLeavesEveryPinAnInputWithOutputsLow) {
this->component_.setup();
ASSERT_FALSE(this->component_.is_failed());
for (uint8_t bank = 0; bank < BANK_COUNT; bank++) {
EXPECT_EQ(this->bus_.config[bank], 0xFF) << "bank " << int(bank) << " should still be an input";
EXPECT_EQ(this->bus_.output[bank], 0x00) << "bank " << int(bank) << " should be driven low";
EXPECT_EQ(this->bus_.invert[bank], 0x00) << "bank " << int(bank) << " should not be inverted";
}
}
TEST_F(PCA9505Test, SetupFailsWhenTheDeviceIsAbsent) {
this->bus_.fail = true;
this->component_.setup();
EXPECT_TRUE(this->component_.is_failed());
}
// The chip has 40 pins, so pin numbers reach past the width of an int shift.
// Every pin must land in its own bank and bit.
TEST_F(PCA9505Test, EachPinMapsToItsOwnBankAndBit) {
this->component_.setup();
for (uint8_t number = 0; number < PIN_COUNT; number++) {
PCA9505GPIOPin pin = this->make_pin_(number, gpio::FLAG_OUTPUT);
pin.setup();
pin.digital_write(true);
for (uint8_t bank = 0; bank < BANK_COUNT; bank++) {
const uint8_t want = (bank == number / 8) ? uint8_t(1 << (number % 8)) : 0x00;
ASSERT_EQ(this->bus_.output[bank], want) << "pin " << int(number) << " wrote outside its own bit";
}
pin.digital_write(false);
ASSERT_EQ(this->bus_.output[number / 8], 0x00) << "pin " << int(number) << " did not clear";
}
}
TEST_F(PCA9505Test, ConfiguringAnOutputOnlyTouchesItsOwnBank) {
this->component_.setup();
PCA9505GPIOPin pin = this->make_pin_(35, gpio::FLAG_OUTPUT);
pin.setup();
// Pin 35 is bit 3 of bank 4. The part reads a 1 as an input, so that bit clears.
EXPECT_EQ(this->bus_.config[4], 0xFF & ~(1 << 3));
for (uint8_t bank = 0; bank < 4; bank++)
EXPECT_EQ(this->bus_.config[bank], 0xFF) << "bank " << int(bank) << " should be untouched";
}
TEST_F(PCA9505Test, ReadsReportTheLevelOnTheHighestPin) {
this->component_.setup();
PCA9505GPIOPin pin = this->make_pin_(39, gpio::FLAG_INPUT);
pin.setup();
this->bus_.input[4] = 0x80; // pin 39 high
EXPECT_TRUE(pin.digital_read());
this->component_.loop(); // clears the cache so the next read hits the bus
this->bus_.input[4] = 0x00;
EXPECT_FALSE(pin.digital_read());
}
TEST_F(PCA9505Test, InvertedPinFlipsTheReportedLevel) {
this->component_.setup();
PCA9505GPIOPin pin = this->make_pin_(39, gpio::FLAG_INPUT, /*inverted=*/true);
pin.setup();
this->bus_.input[4] = 0x80;
EXPECT_FALSE(pin.digital_read());
this->component_.loop();
this->bus_.input[4] = 0x00;
EXPECT_TRUE(pin.digital_read());
}
// Eight pins sharing a port must cost one transfer per loop, not eight.
TEST_F(PCA9505Test, PinsInOneBankShareASingleRead) {
this->component_.setup();
PCA9505GPIOPin pins[8];
for (uint8_t i = 0; i < 8; i++) {
pins[i] = this->make_pin_(8 + i, gpio::FLAG_INPUT);
pins[i].setup();
}
this->bus_.input[1] = 0xA5;
const unsigned before = this->bus_.reads;
for (uint8_t i = 0; i < 8; i++)
EXPECT_EQ(pins[i].digital_read(), ((0xA5 >> i) & 1) != 0) << "pin " << int(8 + i);
EXPECT_EQ(this->bus_.reads - before, 1u);
}
// Pins in different ports are cached separately, so each port costs its own read.
TEST_F(PCA9505Test, EachBankIsReadSeparately) {
this->component_.setup();
PCA9505GPIOPin pins[BANK_COUNT];
for (uint8_t bank = 0; bank < BANK_COUNT; bank++) {
pins[bank] = this->make_pin_(bank * 8, gpio::FLAG_INPUT);
pins[bank].setup();
}
const unsigned before = this->bus_.reads;
for (auto &pin : pins)
pin.digital_read();
EXPECT_EQ(this->bus_.reads - before, BANK_COUNT);
}
TEST_F(PCA9505Test, WritesAreSkippedOnceSetupHasFailed) {
this->bus_.fail = true;
this->component_.setup();
ASSERT_TRUE(this->component_.is_failed());
this->bus_.fail = false;
const unsigned before = this->bus_.writes;
PCA9505GPIOPin pin = this->make_pin_(0, gpio::FLAG_OUTPUT);
pin.setup();
pin.digital_write(true);
EXPECT_EQ(this->bus_.writes, before);
}
// A 1 in a mask register blocks that pin from pulling INT low.
TEST_F(PCA9505Test, SetupMasksEveryInterrupt) {
// Pretend a previous run left port 2 unmasked. The chip keeps its registers
// through an MCU reboot, so setup must write them rather than assume the reset state.
this->bus_.mask[2] = 0x00;
this->component_.setup();
for (uint8_t bank = 0; bank < BANK_COUNT; bank++)
EXPECT_EQ(this->bus_.mask[bank], 0xFF) << "bank " << int(bank) << " should be masked";
}
TEST_F(PCA9505Test, ConfiguringAnInputUnmasksOnlyThatPin) {
this->component_.set_interrupt_pin(&this->int_pin_);
this->component_.setup();
PCA9505GPIOPin pin = this->make_pin_(34, gpio::FLAG_INPUT);
pin.setup();
// Pin 34 is bit 2 of bank 4, and a 0 lets it through.
EXPECT_EQ(this->bus_.mask[4], 0xFF & ~(1 << 2));
for (uint8_t bank = 0; bank < 4; bank++)
EXPECT_EQ(this->bus_.mask[bank], 0xFF) << "bank " << int(bank) << " should be untouched";
}
TEST_F(PCA9505Test, ConfiguringAnOutputLeavesItMasked) {
this->component_.set_interrupt_pin(&this->int_pin_);
this->component_.setup();
PCA9505GPIOPin pin = this->make_pin_(34, gpio::FLAG_OUTPUT);
pin.setup();
// An output must not be able to pull INT low.
EXPECT_EQ(this->bus_.mask[4], 0xFF);
}
TEST_F(PCA9505Test, WithoutAnInterruptPinTheMasksAreNotRewritten) {
this->component_.setup();
const unsigned before = this->bus_.writes;
PCA9505GPIOPin pin = this->make_pin_(34, gpio::FLAG_INPUT);
pin.setup();
EXPECT_EQ(this->bus_.writes, before + 1); // the configuration register only
EXPECT_EQ(this->bus_.mask[4], 0xFF);
}
// Without an interrupt pin an input turns the polling loop on; with one it stays off.
TEST_F(PCA9505Test, AnInputEnablesPollingOnlyWithoutAnInterruptPin) {
this->component_.setup();
ASSERT_TRUE(this->component_.is_idle());
PCA9505GPIOPin polled = this->make_pin_(3, gpio::FLAG_INPUT);
polled.setup();
EXPECT_TRUE(this->component_.is_in_loop_state());
PCA9505Component with_interrupt;
with_interrupt.set_i2c_bus(&this->bus_);
with_interrupt.set_i2c_address(FakeBus::ADDRESS);
with_interrupt.set_interrupt_pin(&this->int_pin_);
with_interrupt.setup();
ASSERT_TRUE(with_interrupt.is_idle());
PCA9505GPIOPin pin;
pin.set_parent(&with_interrupt);
pin.set_pin(3);
pin.set_flags(gpio::FLAG_INPUT);
pin.setup();
EXPECT_TRUE(with_interrupt.is_idle());
}
// With an interrupt pin a read is served from the cache until loop() clears it.
TEST_F(PCA9505Test, InterruptPinKeepsTheCacheUntilTheLoopRuns) {
this->component_.set_interrupt_pin(&this->int_pin_);
this->component_.setup();
PCA9505GPIOPin pin = this->make_pin_(3, gpio::FLAG_INPUT);
pin.setup();
const unsigned before = this->bus_.reads;
pin.digital_read();
pin.digital_read();
EXPECT_EQ(this->bus_.reads, before + 1);
this->component_.loop();
pin.digital_read();
EXPECT_EQ(this->bus_.reads, before + 2);
}
// The loop only turns itself off once INT has gone high again, so a change that
// lands during the read cannot leave the cache stale with no edge to wake it.
TEST_F(PCA9505Test, LoopStaysOnUntilIntIsReleased) {
this->component_.set_interrupt_pin(&this->int_pin_);
this->component_.setup();
this->component_.enable_loop();
ASSERT_TRUE(this->component_.is_in_loop_state());
this->int_pin_.set_inverted(false); // the host pin reads back its inverted flag: INT still low
this->component_.loop();
EXPECT_TRUE(this->component_.is_in_loop_state());
this->int_pin_.set_inverted(true); // INT released
this->component_.loop();
EXPECT_TRUE(this->component_.is_idle());
}
} // namespace esphome::pca9505::testing