TCA6408 in lowercase
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
"""
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user