From 18779cd550ab8e5da4229259f213d070ce3de757 Mon Sep 17 00:00:00 2001 From: Dave Williams | DitroniX | G8PUO <20687856+DitroniX@users.noreply.github.com> Date: Sat, 13 Jun 2026 10:09:33 +0100 Subject: [PATCH] TCA6408 in lowercase --- components/tca6408/README.md | 77 +++++++++++++ components/tca6408/__init__.py | 97 ++++++++++++++++ components/tca6408/tca6408.cpp | 203 +++++++++++++++++++++++++++++++++ components/tca6408/tca6408.h | 74 ++++++++++++ 4 files changed, 451 insertions(+) create mode 100644 components/tca6408/README.md create mode 100644 components/tca6408/__init__.py create mode 100644 components/tca6408/tca6408.cpp create mode 100644 components/tca6408/tca6408.h diff --git a/components/tca6408/README.md b/components/tca6408/README.md new file mode 100644 index 0000000..217331b --- /dev/null +++ b/components/tca6408/README.md @@ -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 (0x20–0x27 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 + diff --git a/components/tca6408/__init__.py b/components/tca6408/__init__.py new file mode 100644 index 0000000..3cf72d7 --- /dev/null +++ b/components/tca6408/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, gpio +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_INTERRUPT_PIN + +# This component depends on the I2C bus +DEPENDENCIES = ["i2c"] + +# Namespace for the C++ component +tca6408_ns = cg.esphome_ns.namespace("tca6408") + +# Reference to the main C++ class +TCA6408Component = tca6408_ns.class_("TCA6408Component", cg.Component, i2c.I2CDevice) + +# Configuration schema for the component +CONFIG_SCHEMA = ( + cv.Schema( + { + # Unique ID for this component instance + cv.GenerateID(): cv.declare_id(TCA6408Component), + + # I²C address of the TCA6408 (default 0x20 when ADDR pins are grounded) + cv.Optional(CONF_ADDRESS, default=0x20): cv.i2c_address, + + # Optional ESP GPIO connected to the TCA6408 INT pin (recommended) + # cv.Optional(CONF_INTERRUPT_PIN): gpio.validate_gpio_pin("internal"), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema()) +) + +async def to_code(config): + """Generate C++ code from the YAML configuration.""" + + 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])) + + if CONF_INTERRUPT_PIN in config: + pin = await gpio.register_gpio_pin(var, config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(pin)) + + +# ================================================ +# YAML USAGE EXAMPLES +# ================================================ + +""" +# Example 1: Basic Outputs Only +tca6408: + - id: tca6408_hub + address: 0x20 + +switch: + - platform: gpio + name: "TCA6408 Output 0" + pin: + tca6408: tca6408_hub + number: 0 + mode: OUTPUT + + +# Example 2: Full Configuration with Interrupt (Recommended) +i2c: + sda: GPIO4 + scl: GPIO5 + scan: true + +tca6408: + - id: tca6408_hub + address: 0x20 + interrupt_pin: GPIO6 + +# Outputs +switch: + - platform: gpio + name: "Relay (Active Low)" + pin: + tca6408: tca6408_hub + number: 1 + mode: OUTPUT + inverted: true + +# Inputs +binary_sensor: + - platform: gpio + name: "Door Sensor" + pin: + tca6408: tca6408_hub + number: 3 + mode: INPUT + inverted: true +""" diff --git a/components/tca6408/tca6408.cpp b/components/tca6408/tca6408.cpp new file mode 100644 index 0000000..fc27923 --- /dev/null +++ b/components/tca6408/tca6408.cpp @@ -0,0 +1,203 @@ +#include "tca6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tca6408 { + +static const char *const TAG = "tca6408"; + +// TCA6408 Register Addresses (from datasheet) +static const uint8_t TCA6408_INPUT_PORT = 0x00; // Input Port Register +static const uint8_t TCA6408_OUTPUT_PORT = 0x01; // Output Port Register +static const uint8_t TCA6408_POLARITY_PORT = 0x02; // Polarity Inversion Register +static const uint8_t TCA6408_CONFIG_PORT = 0x03; // Configuration Register (1 = input, 0 = output) + +void TCA6408Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TCA6408 at address 0x%02X...", this->address_); + + // Initialize chip to a known safe state + if (this->write_register(TCA6408_POLARITY_PORT, 0x00) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to write polarity register"); + this->mark_failed(); + return; + } + + if (this->write_register(TCA6408_CONFIG_PORT, this->mode_mask_) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to write config register"); + this->mark_failed(); + return; + } + + if (!this->read_gpio_outputs_()) { + ESP_LOGE(TAG, "Failed to read initial output state"); + this->mark_failed(); + return; + } + + // Configure interrupt mode if requested + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); // Required for open-drain INT + this->interrupt_pin_->attach_interrupt(&TCA6408Component::gpio_intr, this, + gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); // We manage cache manually for efficiency + ESP_LOGCONFIG(TAG, " Interrupt mode enabled (active-low INT pin)"); + } else { + this->enable_loop(); // Fallback to polling + ESP_LOGCONFIG(TAG, " Polling mode enabled"); + } + + ESP_LOGCONFIG(TAG, " TCA6408 setup complete"); +} + +/** + * Interrupt Service Routine (ISR) + * Runs in interrupt context - must be very lightweight + */ +void IRAM_ATTR TCA6408Component::gpio_intr(TCA6408Component *arg) { + arg->interrupt_triggered_ = true; + arg->enable_loop_soon_any_context(); // Schedule loop() to run soon +} + +/** + * Main loop - handles interrupt processing + * Only runs when an interrupt occurs (in interrupt mode) + */ +void TCA6408Component::loop() { + if (this->interrupt_triggered_) { + this->interrupt_triggered_ = false; + + // Reading Input Port automatically clears the INT pin on TCA6408 + this->digital_read_hw(0); // pin argument is ignored - reads all 8 bits + this->reset_pin_cache_(); // Notify ESPHome that inputs may have changed + + // Check if INT line has returned high (no more pending interrupts) + if (this->interrupt_pin_ && this->interrupt_pin_->digital_read()) { + this->disable_loop(); // Stop looping until next interrupt + } + } +} + +void TCA6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "TCA6408:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + ESP_LOGCONFIG(TAG, " Mode Mask: 0x%02X (1 = input)", this->mode_mask_); + ESP_LOGCONFIG(TAG, " Polarity Mask: 0x%02X", this->polarity_mask_); +} + +bool TCA6408Component::read_gpio_outputs_() { + if (this->is_failed()) return false; + uint8_t data = 0; + if (this->read_register(TCA6408_OUTPUT_PORT, &data) != i2c::ERROR_OK) { + this->status_set_warning(); + return false; + } + this->output_mask_ = data; + this->status_clear_warning(); + return true; +} + +/** Read current state of all input pins from hardware */ +bool TCA6408Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) return false; + + uint8_t data = 0; + if (this->read_register(TCA6408_INPUT_PORT, &data) != i2c::ERROR_OK) { + this->status_set_warning(); + return false; + } + + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +/** Write to a specific output pin */ +void TCA6408Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) return; + + if (value) { + this->output_mask_ |= (1U << pin); + } else { + this->output_mask_ &= ~(1U << pin); + } + + if (this->write_register(TCA6408_OUTPUT_PORT, this->output_mask_) != i2c::ERROR_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } +} + +/** Configure pin direction (input/output) */ +void TCA6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + this->mode_mask_ &= ~(1U << pin); // 0 = output + } else { + this->mode_mask_ |= (1U << pin); // 1 = input + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); // Enable polling if no interrupt pin + } + } + this->write_register(TCA6408_CONFIG_PORT, this->mode_mask_); +} + +/** Set hardware polarity inversion for inputs */ +void TCA6408Component::set_polarity_inversion(uint8_t pin, bool inverted) { + if (inverted) { + this->polarity_mask_ |= (1U << pin); + } else { + this->polarity_mask_ &= ~(1U << pin); + } + this->write_register(TCA6408_POLARITY_PORT, this->polarity_mask_); +} + +/** Read cached value with polarity inversion applied */ +bool TCA6408Component::digital_read_cache(uint8_t pin) { + bool value = (this->input_mask_ & (1U << pin)) != 0; + + // Apply hardware polarity inversion if enabled for this pin + if (this->polarity_mask_ & (1U << pin)) { + value = !value; + } + return value; +} + +// ====================== TCA6408GPIOPin Wrapper ====================== + +void TCA6408GPIOPin::setup() { + this->pin_mode(this->flags_); + if (this->inverted_) { + this->parent_->set_polarity_inversion(this->pin_, true); + } +} + +void TCA6408GPIOPin::set_inverted(bool inverted) { + this->inverted_ = inverted; + if (this->parent_) { + this->parent_->set_polarity_inversion(this->pin_, inverted); + } +} + +void TCA6408GPIOPin::pin_mode(gpio::Flags flags) { + this->flags_ = flags; + this->parent_->pin_mode(this->pin_, flags); +} + +bool TCA6408GPIOPin::digital_read() { + // Note: inversion handled in digital_read_cache() for inputs + return this->parent_->digital_read(this->pin_) != this->inverted_; +} + +void TCA6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} + +size_t TCA6408GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via TCA6408", this->pin_); +} + +} // namespace tca6408 +} // namespace esphome diff --git a/components/tca6408/tca6408.h b/components/tca6408/tca6408.h new file mode 100644 index 0000000..eae3cfd --- /dev/null +++ b/components/tca6408/tca6408.h @@ -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 { + 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 { + 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