mirror of
https://github.com/esphome/esphome.git
synced 2026-10-21 13:34:12 +00:00
[pca9554] Add a device option with the PCA953x/PCA955x/TCA955x names and the PCAL9538 and PCAL9555 expanders (#15449)
This commit is contained in:
@@ -1,13 +1,23 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import gpio_expander, i2c
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_DRIVE_STRENGTH
|
||||
from esphome.components.gpio_expander import validate_interrupt_pin
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_DEVICE,
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
@@ -17,46 +27,106 @@ AUTO_LOAD = ["gpio_expander"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "pca9554"
|
||||
MULTI_CONF = True
|
||||
CONF_PIN_COUNT = "pin_count"
|
||||
pca9554_ns = cg.esphome_ns.namespace("pca9554")
|
||||
|
||||
CONF_LATCH = "latch"
|
||||
CONF_PCA9554 = "pca9554"
|
||||
CONF_PIN_COUNT = "pin_count"
|
||||
|
||||
pca9554_ns = cg.esphome_ns.namespace("pca9554")
|
||||
PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice)
|
||||
PCA9554GPIOPin = pca9554_ns.class_(
|
||||
"PCA9554GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9554Component)
|
||||
)
|
||||
|
||||
CONF_PCA9554 = "pca9554"
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
class _Device(NamedTuple):
|
||||
pins: int
|
||||
# Lowest I2C address, with all address pins low
|
||||
address: int
|
||||
# Has the PCAL registers: pull resistors, input latch, drive strength, interrupt mask
|
||||
pcal: bool = False
|
||||
|
||||
|
||||
DEVICES: dict[str, _Device] = {
|
||||
"PCA9536": _Device(4, 0x41),
|
||||
"PCA9534": _Device(8, 0x20),
|
||||
"PCA9534A": _Device(8, 0x38),
|
||||
"PCA9538": _Device(8, 0x70),
|
||||
"PCA9554": _Device(8, 0x20),
|
||||
"PCA9554A": _Device(8, 0x38),
|
||||
"TCA9554": _Device(8, 0x20),
|
||||
"PCA9535": _Device(16, 0x20),
|
||||
"PCA9555": _Device(16, 0x20),
|
||||
"TCA9535": _Device(16, 0x20),
|
||||
"TCA9555": _Device(16, 0x20),
|
||||
"PCAL9538": _Device(8, 0x70, True),
|
||||
"PCAL9555": _Device(16, 0x20, True),
|
||||
}
|
||||
# The device a legacy `pin_count:` stands for
|
||||
_LEGACY_DEVICES = {4: "PCA9536", 8: "PCA9554", 16: "PCA9555"}
|
||||
# Datasheet drive levels and their register codes
|
||||
DRIVE_STRENGTHS = {0.25: 0, 0.5: 1, 0.75: 2, 1.0: 3}
|
||||
|
||||
|
||||
def _validate_hub(config: ConfigType) -> ConfigType:
|
||||
count = config.get(CONF_PIN_COUNT)
|
||||
if (device := config.get(CONF_DEVICE)) is None:
|
||||
device = config[CONF_DEVICE] = _LEGACY_DEVICES[8 if count is None else count]
|
||||
elif count is not None and count != DEVICES[device].pins:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_PIN_COUNT}' ({count}) does not match the {DEVICES[device].pins} "
|
||||
f"pins of device {device}; remove '{CONF_PIN_COUNT}'"
|
||||
)
|
||||
config.setdefault(CONF_ADDRESS, DEVICES[device].address)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCA9554Component),
|
||||
cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
cv.GenerateID(CONF_I2C_ID): cv.use_id(i2c.I2CBus),
|
||||
# The default depends on the device, so _validate_hub fills it in
|
||||
cv.Optional(CONF_ADDRESS): cv.i2c_address,
|
||||
cv.Optional(CONF_DEVICE): cv.one_of(*DEVICES, upper=True),
|
||||
cv.Optional(CONF_PIN_COUNT): cv.one_of(4, 8, 16),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): validate_interrupt_pin,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
i2c.i2c_device_schema(0x20)
|
||||
) # Note: 0x20 for the non-A part. The PCA9554A parts start at addess 0x38
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_hub,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_pin_count(config[CONF_PIN_COUNT]))
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
device = DEVICES[config[CONF_DEVICE]]
|
||||
var = await i2c.new_i2c_component(config, device.pins)
|
||||
if device.pcal:
|
||||
# Only a build with a PCAL part carries the code for its extra registers
|
||||
cg.add_define("USE_PCA9554_PCAL")
|
||||
cg.add(var.set_pcal())
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
|
||||
|
||||
def validate_mode(value: ConfigType) -> ConfigType:
|
||||
"""Reject contradictory modes; whether the device supports a mode is checked in final validation."""
|
||||
pins.gpio_validate_input_xor_output(value)
|
||||
if value[CONF_PULLDOWN] and value[CONF_PULLUP]:
|
||||
raise cv.Invalid("Pull-up and pull-down are mutually exclusive")
|
||||
return value
|
||||
|
||||
|
||||
PCA9554_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
PCA9554GPIOPin,
|
||||
cv.int_range(min=0, max=15),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT],
|
||||
mode_validator=pins.gpio_validate_input_xor_output,
|
||||
modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP, CONF_PULLDOWN],
|
||||
mode_validator=validate_mode,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_PCA9554): pins.use_id_or_address(PCA9554Component),
|
||||
cv.Optional(CONF_LATCH, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DRIVE_STRENGTH): cv.All(
|
||||
cv.percentage, cv.one_of(*DRIVE_STRENGTHS)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -64,13 +134,36 @@ PCA9554_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
def pca9554_pin_final_validate(
|
||||
pin_config: ConfigType, parent_config: ConfigType
|
||||
) -> None:
|
||||
count = parent_config[CONF_PIN_COUNT]
|
||||
if pin_config[CONF_NUMBER] >= count:
|
||||
raise cv.Invalid(f"Pin number must be in range 0-{count - 1}")
|
||||
device_name = parent_config[CONF_DEVICE]
|
||||
device = DEVICES[device_name]
|
||||
if pin_config[CONF_NUMBER] >= device.pins:
|
||||
raise cv.Invalid(f"Pin number must be in range 0-{device.pins - 1}")
|
||||
|
||||
mode = pin_config[CONF_MODE]
|
||||
if pin_config[CONF_LATCH] and mode[CONF_OUTPUT]:
|
||||
raise cv.Invalid(f"'{CONF_LATCH}' is not valid for an output pin")
|
||||
if CONF_DRIVE_STRENGTH in pin_config and mode[CONF_INPUT]:
|
||||
raise cv.Invalid(f"'{CONF_DRIVE_STRENGTH}' is not valid for an input pin")
|
||||
if device.pcal:
|
||||
return
|
||||
for name in (CONF_PULLUP, CONF_PULLDOWN):
|
||||
if mode[name]:
|
||||
raise cv.Invalid(f"Pin mode '{name}' is not valid for device {device_name}")
|
||||
for key in (CONF_LATCH, CONF_DRIVE_STRENGTH):
|
||||
if pin_config.get(key):
|
||||
raise cv.Invalid(f"'{key}' is not supported by device {device_name}")
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(
|
||||
CONF_PCA9554, PCA9554_PIN_SCHEMA, pca9554_pin_final_validate
|
||||
)
|
||||
async def pca9554_pin_to_code(config: ConfigType) -> MockObj:
|
||||
return await pins.new_parented_pin(config, CONF_PCA9554)
|
||||
var = await pins.new_parented_pin(config, CONF_PCA9554)
|
||||
parent = await cg.get_variable(config[CONF_PCA9554])
|
||||
num = config[CONF_NUMBER]
|
||||
# Final validation only lets these through on a PCAL hub
|
||||
if config[CONF_LATCH]:
|
||||
cg.add(parent.set_latch(num))
|
||||
if (drive_strength := config.get(CONF_DRIVE_STRENGTH)) is not None:
|
||||
cg.add(parent.set_drive_strength(num, DRIVE_STRENGTHS[drive_strength]))
|
||||
return var
|
||||
|
||||
@@ -4,15 +4,25 @@
|
||||
namespace esphome::pca9554 {
|
||||
|
||||
// for 16 bit expanders, these addresses will be doubled.
|
||||
const uint8_t INPUT_REG = 0;
|
||||
const uint8_t OUTPUT_REG = 1;
|
||||
const uint8_t INVERT_REG = 2;
|
||||
const uint8_t CONFIG_REG = 3;
|
||||
static constexpr uint8_t INPUT_REG = 0;
|
||||
static constexpr uint8_t OUTPUT_REG = 1;
|
||||
static constexpr uint8_t INVERT_REG = 2;
|
||||
static constexpr uint8_t CONFIG_REG = 3;
|
||||
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
// PCAL registers, as offsets from PCAL_BASE spaced by the register width like the ones above.
|
||||
static constexpr uint8_t PCAL_BASE = 0x40;
|
||||
static constexpr uint8_t OUTPUT_DRIVE_0 = 0;
|
||||
static constexpr uint8_t OUTPUT_DRIVE_1 = 1;
|
||||
static constexpr uint8_t INPUT_LATCH = 2;
|
||||
static constexpr uint8_t PUPD_ENABLE = 3;
|
||||
static constexpr uint8_t PUPD_SEL = 4;
|
||||
static constexpr uint8_t INTERRUPT_MASK = 5;
|
||||
#endif
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "pca9554");
|
||||
|
||||
void PCA9554Component::setup() {
|
||||
this->reg_width_ = (this->pin_count_ + 7) / 8;
|
||||
// Test to see if device exists
|
||||
if (!this->read_inputs_()) {
|
||||
ESP_LOGE(TAG, "PCA95xx not detected at 0x%02X", this->address_);
|
||||
@@ -29,6 +39,13 @@ void PCA9554Component::setup() {
|
||||
// All outputs low
|
||||
this->output_mask_ = 0;
|
||||
this->write_register_(OUTPUT_REG, this->output_mask_);
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
// The chip keeps these across an MCU reboot
|
||||
if (this->pcal_ && !this->write_pcal_shadows_()) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Read the inputs
|
||||
this->read_inputs_();
|
||||
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||
@@ -45,7 +62,9 @@ void PCA9554Component::setup() {
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void PCA9554Component::loop() {
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
@@ -62,6 +81,9 @@ void PCA9554Component::dump_config() {
|
||||
"PCA9554:\n"
|
||||
" I/O Pins: %d",
|
||||
this->pin_count_);
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
ESP_LOGCONFIG(TAG, " PCAL registers: %s", YESNO(this->pcal_));
|
||||
#endif
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
}
|
||||
@@ -82,11 +104,16 @@ void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
if (this->write_register_(OUTPUT_REG, this->output_mask_))
|
||||
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
|
||||
#else
|
||||
this->write_register_(OUTPUT_REG, this->output_mask_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (flags == gpio::FLAG_INPUT) {
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
// Clear mode mask bit
|
||||
this->config_mask_ &= ~(1 << pin);
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
@@ -94,11 +121,24 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
} else if (flags & gpio::FLAG_OUTPUT) {
|
||||
// Set mode mask bit
|
||||
this->config_mask_ |= 1 << pin;
|
||||
}
|
||||
this->write_register_(CONFIG_REG, ~this->config_mask_);
|
||||
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
if (this->pcal_) {
|
||||
const uint16_t bit = 1U << pin;
|
||||
// A masked pin never asserts INT, so only inputs are unmasked
|
||||
this->update_pcal_register_(INTERRUPT_MASK, this->interrupt_mask_, bit, this->config_mask_ & bit);
|
||||
const bool pull = flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN);
|
||||
// The select bit means nothing while the pull is disabled
|
||||
if (pull)
|
||||
this->update_pcal_register_(PUPD_SEL, this->pupd_select_, bit, (flags & gpio::FLAG_PULLUP) ? bit : 0);
|
||||
this->update_pcal_register_(PUPD_ENABLE, this->pupd_enable_, bit, pull ? bit : 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PCA9554Component::read_inputs_() {
|
||||
@@ -109,30 +149,64 @@ bool PCA9554Component::read_inputs_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_);
|
||||
if (!this->status_comm_result(this->last_error_ == i2c::ERROR_OK)) {
|
||||
ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_);
|
||||
const i2c::ErrorCode err = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_);
|
||||
if (!this->status_comm_result(err == i2c::ERROR_OK)) {
|
||||
ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) err);
|
||||
return false;
|
||||
}
|
||||
this->input_mask_ = inputs[0];
|
||||
if (this->reg_width_ == 2) {
|
||||
this->input_mask_ |= inputs[1] << 8;
|
||||
}
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
// The bus answers again, so retry the settings a failed write left off the chip
|
||||
this->retry_pcal_shadows_();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) {
|
||||
bool PCA9554Component::write_address_(uint8_t address, uint16_t value) {
|
||||
uint8_t outputs[2];
|
||||
outputs[0] = (uint8_t) value;
|
||||
outputs[1] = (uint8_t) (value >> 8);
|
||||
this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_);
|
||||
if (!this->status_comm_result(this->last_error_ == i2c::ERROR_OK)) {
|
||||
ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_);
|
||||
const i2c::ErrorCode err = this->write_register(address, outputs, this->reg_width_);
|
||||
if (!this->status_comm_result(err == i2c::ERROR_OK)) {
|
||||
ESP_LOGE(TAG, "write_register(): I2C I/O error: %d", (int) err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
bool PCA9554Component::write_pcal_register_(uint8_t reg, uint16_t value) {
|
||||
if (this->write_address_(PCAL_BASE + reg * this->reg_width_, value))
|
||||
return true;
|
||||
this->pcal_dirty_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PCA9554Component::write_pcal_shadows_() {
|
||||
this->pcal_dirty_ = false;
|
||||
// Every write runs so one failure does not hide the others; each failure sets pcal_dirty_ again
|
||||
bool ok = this->write_pcal_register_(PUPD_ENABLE, this->pupd_enable_);
|
||||
ok &= this->write_pcal_register_(PUPD_SEL, this->pupd_select_);
|
||||
ok &= this->write_pcal_register_(INPUT_LATCH, this->latch_);
|
||||
ok &= this->write_pcal_register_(INTERRUPT_MASK, this->interrupt_mask_);
|
||||
ok &= this->write_pcal_register_(OUTPUT_DRIVE_0, static_cast<uint16_t>(this->drive_));
|
||||
ok &= this->write_pcal_register_(OUTPUT_DRIVE_1, static_cast<uint16_t>(this->drive_ >> (8 * this->reg_width_)));
|
||||
return ok;
|
||||
}
|
||||
|
||||
void PCA9554Component::update_pcal_register_(uint8_t reg, uint16_t &shadow, uint16_t mask, uint16_t value) {
|
||||
const uint16_t updated = (shadow & ~mask) | (value & mask);
|
||||
// The shadows hold the wanted state; an unchanged value is already on the chip unless a write failed
|
||||
if (updated == shadow && !this->pcal_dirty_)
|
||||
return;
|
||||
shadow = updated;
|
||||
this->write_pcal_register_(reg, shadow);
|
||||
}
|
||||
#endif
|
||||
|
||||
float PCA9554Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
} // namespace esphome::pca9554
|
||||
|
||||
@@ -12,7 +12,7 @@ class PCA9554Component final : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
|
||||
public:
|
||||
PCA9554Component() = default;
|
||||
explicit PCA9554Component(uint8_t pin_count) : pin_count_(pin_count), reg_width_((pin_count + 7) / 8) {}
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
@@ -24,33 +24,70 @@ class PCA9554Component final : public Component,
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
/// The PCAL parts add pull resistors, input latches, an interrupt mask and drive strength.
|
||||
void set_pcal() { this->pcal_ = true; }
|
||||
/// Only updates the shadow, which setup() writes.
|
||||
void set_latch(uint8_t pin) { this->latch_ |= 1U << pin; }
|
||||
/// Only updates the shadow, which setup() writes. 0 is a quarter of the full drive strength, 3 the full strength.
|
||||
void set_drive_strength(uint8_t pin, uint8_t strength) {
|
||||
const uint8_t shift = 2 * pin;
|
||||
this->drive_ = (this->drive_ & ~(3UL << shift)) | (uint32_t(strength) << shift);
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
|
||||
|
||||
bool read_inputs_();
|
||||
bool write_register_(uint8_t reg, uint16_t value);
|
||||
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
|
||||
bool write_address_(uint8_t address, uint16_t value);
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
bool write_pcal_register_(uint8_t reg, uint16_t value);
|
||||
/// Write every PCAL shadow to the chip; false when any write failed
|
||||
bool write_pcal_shadows_();
|
||||
/// Retry the shadows when an earlier PCAL write failed
|
||||
void retry_pcal_shadows_() {
|
||||
if (this->pcal_dirty_)
|
||||
this->write_pcal_shadows_();
|
||||
}
|
||||
/// Changes the masked bits of a shadow and writes the register only when it changed.
|
||||
void update_pcal_register_(uint8_t reg, uint16_t &shadow, uint16_t mask, uint16_t value);
|
||||
#endif
|
||||
|
||||
// Virtual methods from CachedGpioExpander
|
||||
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;
|
||||
|
||||
/// number of bits the expander has
|
||||
size_t pin_count_{8};
|
||||
/// width of registers
|
||||
size_t reg_width_{1};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
/// Drive strength shadow, two bits per pin; the low reg_width_ bytes go to OUTPUT_DRIVE_0
|
||||
uint32_t drive_{0xFFFFFFFF};
|
||||
#endif
|
||||
/// Mask for the pin config - 1 means OUTPUT, 0 means INPUT
|
||||
uint16_t config_mask_{0x00};
|
||||
/// The mask to write as output state - 1 means HIGH, 0 means LOW
|
||||
uint16_t output_mask_{0x00};
|
||||
/// The state of the actual input pin states - 1 means HIGH, 0 means LOW
|
||||
/// The state of the actual input pins - 1 means HIGH, 0 means LOW
|
||||
uint16_t input_mask_{0x00};
|
||||
/// Storage for last I2C error seen
|
||||
esphome::i2c::ErrorCode last_error_;
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
// Shadows of the PCAL registers. setup() writes them, so every part starts with the pulls disconnected
|
||||
// whatever its power-on state is (the PCAL9555 powers up with pull-ups enabled).
|
||||
uint16_t pupd_enable_{0x0000};
|
||||
uint16_t pupd_select_{0xFFFF};
|
||||
uint16_t latch_{0x0000};
|
||||
uint16_t interrupt_mask_{0xFFFF};
|
||||
#endif
|
||||
/// number of bits the expander has
|
||||
const uint8_t pin_count_;
|
||||
/// width of registers
|
||||
const uint8_t reg_width_;
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
bool pcal_{false};
|
||||
bool pcal_dirty_{false}; // a PCAL write failed; retried after the next good read or output write
|
||||
#endif
|
||||
};
|
||||
|
||||
using PCA9554GPIOPin = gpio_expander::ExpanderGPIOPin<PCA9554Component>;
|
||||
|
||||
@@ -257,6 +257,7 @@
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
#define USE_OUTPUT
|
||||
#define USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
#define USE_PCA9554_PCAL
|
||||
#define USE_POWER_SUPPLY
|
||||
// Only defined by key-lookup preference backends; the slot-based platforms
|
||||
// (esp8266, rp2040) never set it in generated builds, and their preferences
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
id: i2c_bus
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
pca9554:
|
||||
- id: plain_hub
|
||||
address: 0x20
|
||||
device: PCA9554
|
||||
- id: pcal_hub
|
||||
address: 0x70
|
||||
device: PCAL9555
|
||||
- id: pca9538_hub
|
||||
device: PCA9538
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: plain_input
|
||||
name: Plain input
|
||||
pin:
|
||||
pca9554: plain_hub
|
||||
number: 1
|
||||
mode: INPUT
|
||||
- platform: gpio
|
||||
id: pcal_input
|
||||
name: PCAL input
|
||||
pin:
|
||||
pca9554: pcal_hub
|
||||
number: 9
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
latch: true
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: pcal_weak_output
|
||||
pin:
|
||||
pca9554: pcal_hub
|
||||
number: 10
|
||||
mode: OUTPUT
|
||||
drive_strength: 25%
|
||||
- platform: gpio
|
||||
id: pcal_full_output
|
||||
pin:
|
||||
pca9554: pcal_hub
|
||||
number: 11
|
||||
mode: OUTPUT
|
||||
drive_strength: 100%
|
||||
- platform: gpio
|
||||
id: pcal_mid_output
|
||||
pin:
|
||||
pca9554: pcal_hub
|
||||
number: 12
|
||||
mode: OUTPUT
|
||||
drive_strength: 75%
|
||||
- platform: gpio
|
||||
id: pca9538_output
|
||||
pin:
|
||||
pca9554: pca9538_hub
|
||||
number: 7
|
||||
mode: OUTPUT
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Tests for the PCA9554 device table and its pin validation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.pca9554 import (
|
||||
CONFIG_SCHEMA,
|
||||
PCA9554_PIN_SCHEMA,
|
||||
pca9554_pin_final_validate,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def _hub(**overrides: object) -> ConfigType:
|
||||
return CONFIG_SCHEMA({"id": "hub", **overrides})
|
||||
|
||||
|
||||
def _pin(number: int, mode: dict, **overrides: object) -> ConfigType:
|
||||
return PCA9554_PIN_SCHEMA(
|
||||
{"pca9554": "hub", "number": number, "mode": mode, **overrides}
|
||||
)
|
||||
|
||||
|
||||
def test_pin_count_must_match_the_device() -> None:
|
||||
"""A stale pin_count next to a device would address every register wrongly."""
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="does not match the 8 pins of device PCAL9538"
|
||||
):
|
||||
_hub(device="PCAL9538", pin_count=16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("overrides", "device"),
|
||||
[
|
||||
({}, "PCA9554"),
|
||||
({"pin_count": 4}, "PCA9536"),
|
||||
({"pin_count": 8}, "PCA9554"),
|
||||
({"pin_count": 16}, "PCA9555"),
|
||||
],
|
||||
)
|
||||
def test_legacy_pin_count_maps_to_a_device(overrides: dict, device: str) -> None:
|
||||
assert _hub(**overrides)["device"] == device
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device", "address"),
|
||||
[
|
||||
("PCA9554", 0x20),
|
||||
("PCA9554A", 0x38),
|
||||
("PCA9536", 0x41),
|
||||
("PCA9538", 0x70),
|
||||
("PCAL9538", 0x70),
|
||||
],
|
||||
)
|
||||
def test_address_defaults_to_the_device_base(device: str, address: int) -> None:
|
||||
assert _hub(device=device)["address"] == address
|
||||
assert _hub(device=device, address=0x27)["address"] == 0x27
|
||||
|
||||
|
||||
def test_pin_number_follows_the_device() -> None:
|
||||
with pytest.raises(cv.Invalid, match="range 0-3"):
|
||||
pca9554_pin_final_validate(_pin(4, {"input": True}), _hub(device="PCA9536"))
|
||||
pca9554_pin_final_validate(_pin(15, {"input": True}), _hub(device="PCAL9555"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "overrides", "message"),
|
||||
[
|
||||
({"input": True, "pullup": True}, {}, "Pin mode 'pullup' is not valid"),
|
||||
({"input": True, "pulldown": True}, {}, "Pin mode 'pulldown' is not valid"),
|
||||
({"input": True}, {"latch": True}, "'latch' is not supported"),
|
||||
({"output": True}, {"drive_strength": "100%"}, "'drive_strength' is not"),
|
||||
],
|
||||
)
|
||||
def test_pcal_only_settings_are_rejected_on_a_plain_part(
|
||||
mode: dict, overrides: dict, message: str
|
||||
) -> None:
|
||||
pin = _pin(1, mode, **overrides)
|
||||
with pytest.raises(cv.Invalid, match=f"{message}.*device PCA9554"):
|
||||
pca9554_pin_final_validate(pin, _hub(device="PCA9554"))
|
||||
pca9554_pin_final_validate(pin, _hub(device="PCAL9538"))
|
||||
|
||||
|
||||
def test_outputs_cannot_be_latched() -> None:
|
||||
with pytest.raises(cv.Invalid, match="'latch' is not valid for an output pin"):
|
||||
pca9554_pin_final_validate(
|
||||
_pin(1, {"output": True}, latch=True), _hub(device="PCAL9538")
|
||||
)
|
||||
|
||||
|
||||
def test_inputs_take_no_drive_strength() -> None:
|
||||
with pytest.raises(cv.Invalid, match="'drive_strength' is not valid for an input"):
|
||||
pca9554_pin_final_validate(
|
||||
_pin(1, {"input": True}, drive_strength="50%"), _hub(device="PCAL9538")
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["30%", "0%", 2])
|
||||
def test_drive_strength_must_be_a_datasheet_level(value: object) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
_pin(1, {"output": True}, drive_strength=value)
|
||||
|
||||
|
||||
def test_pcal_code_is_only_generated_for_pcal_hubs(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The define and the hub setters exist only where a PCAL part is configured."""
|
||||
main_cpp = generate_main(component_config_path("pca9554.yaml"))
|
||||
|
||||
for call in (
|
||||
"pcal_hub->set_pcal();",
|
||||
"pcal_hub->set_latch(9);",
|
||||
"pcal_hub->set_drive_strength(12, 2);",
|
||||
"pcal_hub->set_drive_strength(10, 0);",
|
||||
"pcal_hub->set_drive_strength(11, 3);",
|
||||
):
|
||||
assert main_cpp.count(call) == 1
|
||||
for call in ("set_pcal(", "set_latch(", "set_drive_strength("):
|
||||
assert f"plain_hub->{call}" not in main_cpp
|
||||
assert f"pca9538_hub->{call}" not in main_cpp
|
||||
# 0x70, the PCA9538 default
|
||||
assert "pca9538_hub->set_i2c_address(112);" in main_cpp
|
||||
@@ -8,6 +8,14 @@ pca9554:
|
||||
pin_count: 8
|
||||
address: 0x3E
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
- id: pcal9538
|
||||
i2c_id: i2c_bus
|
||||
address: 0x70
|
||||
device: PCAL9538
|
||||
- id: pcal9555
|
||||
i2c_id: i2c_bus
|
||||
address: 0x71
|
||||
device: PCAL9555
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
@@ -25,3 +33,20 @@ binary_sensor:
|
||||
number: 0
|
||||
mode: OUTPUT
|
||||
inverted: false
|
||||
- platform: gpio
|
||||
id: pcal9538_input
|
||||
pin:
|
||||
pca9554: pcal9538
|
||||
number: 3
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
latch: true
|
||||
# A pin on the second port of a 16 bit PCAL part, with a non-default drive strength
|
||||
- platform: gpio
|
||||
id: pcal9555_output
|
||||
pin:
|
||||
pca9554: pcal9555
|
||||
number: 12
|
||||
mode: OUTPUT
|
||||
drive_strength: 50%
|
||||
|
||||
Reference in New Issue
Block a user