In Review

This commit is contained in:
Dave Williams | DitroniX | G8PUO
2026-06-13 12:55:24 +01:00
parent 3a4371388f
commit 9d15088c02
4 changed files with 357 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
# TCA6408 ESPHome Component
Full-featured custom component for the **TCA6408** 8-bit I²C I/O Expander.
**This is work in progress and under test**
## Features
- 8 bidirectional GPIO pins
- Hardware polarity inversion support (inputs)
- Interrupt-driven inputs (active-low INT pin)
- Efficient cached GPIO expander
- Polling fallback mode
- Compatible with ESP32 (including C3/C5/C6), ESP8266, etc.
The INT pin on TCA6408 is open-drain active-low — connect a pull-up resistor (or use internal pull-up on ESP).
## Installation
1. Download or clone this component into your ESPHome project:
```bash
mkdir -p custom_components
cp -r TCA6408/custom_components/tca6408 custom_components/
## Basic Configuration
i2c:
sda: GPIO4
scl: GPIO5
scan: true
frequency: 400kHz
custom_components:
- source:
type: local
path: custom_components/tca6408
tca6408:
- id: tca6408_hub
address: 0x20 # Default: 0x20 (0x200x27 depending on ADDR pins)
interrupt_pin: GPIO6 # Recommended for fast input response
## Example: Outputs (Switches)
switch:
- platform: gpio
name: "TCA6408 Output 0"
pin:
tca6408: tca6408_hub
number: 0
mode: OUTPUT
- platform: gpio
name: "TCA6408 Relay (Active Low)"
pin:
tca6408: tca6408_hub
number: 1
mode: OUTPUT
inverted: true
## Example: Inputs (Binary Sensors) with Interrupt
binary_sensor:
- platform: gpio
name: "Door Sensor"
pin:
tca6408: tca6408_hub
number: 3
mode: INPUT
inverted: true # Uses hardware polarity inversion
- platform: gpio
name: "Push Button"
pin:
tca6408: tca6408_hub
number: 4
mode: INPUT
+27
View File
@@ -0,0 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID, CONF_ADDRESS
DEPENDENCIES = ["i2c"]
tca6408_ns = cg.esphome_ns.namespace("tca6408")
TCA6408Component = tca6408_ns.class_(
"TCA6408Component",
cg.Component,
i2c.I2CDevice
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(TCA6408Component),
cv.Optional(CONF_ADDRESS, default=0x20): cv.i2c_address,
}
).extend(cv.COMPONENT_SCHEMA)
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)
cg.add(var.set_address(config[CONF_ADDRESS]))
+179
View File
@@ -0,0 +1,179 @@
#include "tca6408.h"
#include "esphome/core/log.h"
namespace esphome {
namespace tca6408 {
static const char *const TAG = "tca6408";
// Registers
static const uint8_t REG_INPUT = 0x00;
static const uint8_t REG_OUTPUT = 0x01;
static const uint8_t REG_POLARITY = 0x02;
static const uint8_t REG_CONFIG = 0x03;
void TCA6408Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up TCA6408 @ 0x%02X", this->address_);
// Default safe state: all inputs
this->mode_mask_ = 0xFF;
this->output_mask_ = 0x00;
this->input_mask_ = 0x00;
this->polarity_mask_ = 0x00;
if (this->write_register(REG_POLARITY, 0x00) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to init polarity register");
this->mark_failed();
return;
}
if (this->write_register(REG_CONFIG, this->mode_mask_) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to init config register");
this->mark_failed();
return;
}
this->read_inputs_();
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->interrupt_pin_->attach_interrupt(
&TCA6408Component::gpio_intr,
this,
gpio::INTERRUPT_FALLING_EDGE);
this->enable_loop();
ESP_LOGCONFIG(TAG, "Interrupt mode enabled");
} else {
this->enable_loop();
ESP_LOGCONFIG(TAG, "Polling mode enabled");
}
ESP_LOGCONFIG(TAG, "TCA6408 setup complete");
}
void IRAM_ATTR TCA6408Component::gpio_intr(TCA6408Component *arg) {
arg->interrupt_triggered_ = true;
arg->enable_loop_soon_any_context();
}
void TCA6408Component::loop() {
if (this->interrupt_triggered_) {
this->interrupt_triggered_ = false;
this->read_inputs_();
this->reset_pin_cache_();
if (this->interrupt_pin_ && this->interrupt_pin_->digital_read()) {
// INT released -> stop looping until next interrupt
this->disable_loop();
}
}
}
void TCA6408Component::dump_config() {
ESP_LOGCONFIG(TAG, "TCA6408:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_);
LOG_PIN(" INT Pin: ", this->interrupt_pin_);
}
bool TCA6408Component::read_inputs_() {
uint8_t data = 0;
if (this->read_register(REG_INPUT, &data) != i2c::ERROR_OK) {
this->status_set_warning();
return false;
}
this->input_mask_ = data;
this->status_clear_warning();
return true;
}
bool TCA6408Component::digital_read_hw(uint8_t pin) {
// full refresh for accuracy
return this->read_inputs_();
}
bool TCA6408Component::digital_read_cache(uint8_t pin) {
bool value = (this->input_mask_ >> pin) & 0x01;
if (this->polarity_mask_ & (1 << pin)) {
value = !value;
}
return value;
}
void TCA6408Component::digital_write_hw(uint8_t pin, bool value) {
if (value)
this->output_mask_ |= (1 << pin);
else
this->output_mask_ &= ~(1 << pin);
if (this->write_register(REG_OUTPUT, this->output_mask_) != i2c::ERROR_OK) {
this->status_set_warning();
} else {
this->status_clear_warning();
}
}
void TCA6408Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags & gpio::FLAG_OUTPUT) {
this->mode_mask_ &= ~(1 << pin);
} else {
this->mode_mask_ |= (1 << pin);
}
this->write_register(REG_CONFIG, this->mode_mask_);
}
void TCA6408Component::set_polarity_inversion(uint8_t pin, bool inverted) {
if (inverted)
this->polarity_mask_ |= (1 << pin);
else
this->polarity_mask_ &= ~(1 << pin);
this->write_register(REG_POLARITY, this->polarity_mask_);
}
// ================= GPIO PIN WRAPPER =================
void TCA6408GPIOPin::set_parent(TCA6408Component *parent) {
this->parent_ = parent;
}
void TCA6408GPIOPin::set_pin(uint8_t pin) {
this->pin_ = pin;
}
void TCA6408GPIOPin::setup() {
this->pin_mode(this->flags_);
}
void TCA6408GPIOPin::pin_mode(gpio::Flags flags) {
this->flags_ = flags;
this->parent_->pin_mode(this->pin_, flags);
}
bool TCA6408GPIOPin::digital_read() {
return this->parent_->digital_read_cache(this->pin_) != this->inverted_;
}
void TCA6408GPIOPin::digital_write(bool value) {
this->parent_->digital_write_hw(this->pin_, value != this->inverted_);
}
void TCA6408GPIOPin::set_inverted(bool inverted) {
this->inverted_ = inverted;
this->parent_->set_polarity_inversion(this->pin_, inverted);
}
size_t TCA6408GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "TCA6408 pin %u", this->pin_);
}
} // namespace tca6408
} // namespace esphome
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace tca6408 {
/**
* TCA6408 8-bit I²C I/O Expander
*
* Supports:
* - 8 bidirectional pins
* - Hardware polarity inversion for inputs
* - Interrupt-driven inputs (active-low open-drain INT)
* - Efficient cached access
*/
class TCA6408Component : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 8> {
public:
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::IO; }
void set_address(uint8_t address) { this->address_ = address; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_polarity_inversion(uint8_t pin, bool inverted);
protected:
static void IRAM_ATTR gpio_intr(TCA6408Component *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;
void pin_mode(uint8_t pin, gpio::Flags flags) override;
uint8_t mode_mask_{0xFF}; // 1 = input, 0 = output
uint8_t output_mask_{0x00};
uint8_t input_mask_{0x00};
uint8_t polarity_mask_{0x00}; // Hardware polarity inversion
InternalGPIOPin *interrupt_pin_{nullptr};
bool interrupt_triggered_{false};
private:
bool read_gpio_outputs_();
};
class TCA6408GPIOPin : public gpio::GPIOPin, public Parented<TCA6408Component> {
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) { pin_ = pin; }
void set_inverted(bool inverted);
void set_flags(gpio::Flags flags) { flags_ = flags; }
gpio::Flags get_flags() const override { return flags_; }
protected:
uint8_t pin_{0};
bool inverted_{false};
gpio::Flags flags_{0};
};
} // namespace tca6408
} // namespace esphome