mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[PCA9505] Add PCA9505/PCA9506 40-channel GPIO expander (#20667)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user