1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-19 16:25:50 +00:00

[tcal6416] Add support for TCAL6416 I2C I/O expander

Implements support for the TI TCAL6416 16-bit I2C I/O expander with the following features:
- 16 GPIO pins (2 banks of 8)
- Bidirectional I/O with configurable input/output modes
- Support for inverted pins
- Compatible with ESP32, ESP8266, and RP2040 platforms
- I2C interface with configurable address (default: 0x20)

The TCAL6416 provides higher current latched outputs suitable for directly
driving LEDs or keypads. It operates at voltages from 1.08V to 3.6V and
supports I2C speeds up to 1MHz.

Addresses: https://github.com/orgs/esphome/discussions/3233
This commit is contained in:
Claude
2025-11-17 22:43:46 +00:00
parent 23f85162d0
commit b85c98abb4
7 changed files with 328 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
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_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
CODEOWNERS = ["@crgarcia12"]
AUTO_LOAD = ["gpio_expander"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
tcal6416_ns = cg.esphome_ns.namespace("tcal6416")
TCAL6416Component = tcal6416_ns.class_("TCAL6416Component", cg.Component, i2c.I2CDevice)
TCAL6416GPIOPin = tcal6416_ns.class_("TCAL6416GPIOPin", cg.GPIOPin)
CONF_TCAL6416 = "tcal6416"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(TCAL6416Component),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
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)
def validate_mode(value):
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
TCAL6416_PIN_SCHEMA = pins.gpio_base_schema(
TCAL6416GPIOPin,
cv.int_range(min=0, max=15),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
invertible=True,
).extend(
{
cv.Required(CONF_TCAL6416): cv.use_id(TCAL6416Component),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_TCAL6416, TCAL6416_PIN_SCHEMA)
async def tcal6416_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_TCAL6416])
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

View File

@@ -0,0 +1,155 @@
#include "tcal6416.h"
#include "esphome/core/log.h"
static const uint8_t TCAL6416_INPUT_PORT_REGISTER_0 = 0x00;
static const uint8_t TCAL6416_INPUT_PORT_REGISTER_1 = 0x01;
static const uint8_t TCAL6416_OUTPUT_PORT_REGISTER_0 = 0x02;
static const uint8_t TCAL6416_OUTPUT_PORT_REGISTER_1 = 0x03;
static const uint8_t TCAL6416_POLARITY_REGISTER_0 = 0x04;
static const uint8_t TCAL6416_POLARITY_REGISTER_1 = 0x05;
static const uint8_t TCAL6416_CONFIGURATION_PORT_0 = 0x06;
static const uint8_t TCAL6416_CONFIGURATION_PORT_1 = 0x07;
namespace esphome {
namespace tcal6416 {
static const char *const TAG = "tcal6416";
void TCAL6416Component::setup() {
if (!this->read_gpio_modes_()) {
this->mark_failed();
return;
}
if (!this->read_gpio_outputs_()) {
this->mark_failed();
return;
}
// Disable polarity inversion
uint8_t data[2] = {0x00, 0x00};
if (!this->write_bytes(TCAL6416_POLARITY_REGISTER_0, data, 2)) {
this->mark_failed();
return;
}
}
void TCAL6416Component::dump_config() {
ESP_LOGCONFIG(TAG, "TCAL6416:");
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
void TCAL6416Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
// Set mode mask bit (1 = input)
this->mode_mask_ |= 1 << pin;
} else if (flags == gpio::FLAG_OUTPUT) {
// Clear mode mask bit (0 = output)
this->mode_mask_ &= ~(1 << pin);
}
// Write GPIO to enable input mode
this->write_gpio_modes_();
}
void TCAL6416Component::loop() { this->reset_pin_cache_(); }
bool TCAL6416Component::read_gpio_outputs_() {
if (this->is_failed())
return false;
uint8_t data[2];
if (!this->read_bytes(TCAL6416_OUTPUT_PORT_REGISTER_0, data, 2)) {
this->status_set_warning(LOG_STR("Failed to read output register"));
return false;
}
this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0);
this->status_clear_warning();
return true;
}
bool TCAL6416Component::read_gpio_modes_() {
if (this->is_failed())
return false;
uint8_t data[2];
bool success = this->read_bytes(TCAL6416_CONFIGURATION_PORT_0, data, 2);
if (!success) {
this->status_set_warning(LOG_STR("Failed to read mode register"));
return false;
}
this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0);
this->status_clear_warning();
return true;
}
bool TCAL6416Component::digital_read_hw(uint8_t pin) {
if (this->is_failed())
return false;
uint8_t data;
uint8_t bank_number = pin < 8 ? 0 : 1;
uint8_t register_to_read = bank_number ? TCAL6416_INPUT_PORT_REGISTER_1 : TCAL6416_INPUT_PORT_REGISTER_0;
if (!this->read_bytes(register_to_read, &data, 1)) {
this->status_set_warning(LOG_STR("Failed to read input register"));
return false;
}
uint8_t second_half = this->input_mask_ >> 8;
uint8_t first_half = this->input_mask_;
if (bank_number) {
this->input_mask_ = (data << 8) | (uint16_t(first_half) << 0);
} else {
this->input_mask_ = (uint16_t(second_half) << 8) | (data << 0);
}
this->status_clear_warning();
return true;
}
void TCAL6416Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
uint8_t data[2];
data[0] = this->output_mask_;
data[1] = this->output_mask_ >> 8;
if (!this->write_bytes(TCAL6416_OUTPUT_PORT_REGISTER_0, data, 2)) {
this->status_set_warning(LOG_STR("Failed to write output register"));
return;
}
this->status_clear_warning();
}
bool TCAL6416Component::write_gpio_modes_() {
if (this->is_failed())
return false;
uint8_t data[2];
data[0] = this->mode_mask_;
data[1] = this->mode_mask_ >> 8;
if (!this->write_bytes(TCAL6416_CONFIGURATION_PORT_0, data, 2)) {
this->status_set_warning(LOG_STR("Failed to write mode register"));
return false;
}
this->status_clear_warning();
return true;
}
bool TCAL6416Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
float TCAL6416Component::get_setup_priority() const { return setup_priority::IO; }
void TCAL6416GPIOPin::setup() { this->pin_mode(this->flags_); }
void TCAL6416GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool TCAL6416GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void TCAL6416GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
std::string TCAL6416GPIOPin::dump_summary() const { return str_sprintf("%u via TCAL6416", this->pin_); }
} // namespace tcal6416
} // namespace esphome

View File

@@ -0,0 +1,66 @@
#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 tcal6416 {
class TCAL6416Component : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
public:
TCAL6416Component() = default;
/// Check i2c availability and setup masks
void setup() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
void loop() override;
protected:
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;
/// Mask for the pin mode - 1 means input, 0 means output
uint16_t mode_mask_{0xFFFF};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint16_t output_mask_{0x00};
/// The state read in digital_read_hw - 1 means HIGH, 0 means LOW
uint16_t input_mask_{0x00};
bool read_gpio_modes_();
bool write_gpio_modes_();
bool read_gpio_outputs_();
};
/// Helper class to expose a TCAL6416 pin as an internal input GPIO pin.
class TCAL6416GPIOPin : public GPIOPin, public Parented<TCAL6416Component> {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() 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_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace tcal6416
} // namespace esphome

View File

@@ -0,0 +1,23 @@
tcal6416:
- id: tcal6416_hub
i2c_id: i2c_bus
address: 0x20
binary_sensor:
- platform: gpio
id: tcal6416_binary_sensor
name: TCAL6416 Binary Sensor
pin:
tcal6416: tcal6416_hub
number: 1
mode: INPUT
inverted: true
output:
- platform: gpio
id: tcal6416_output
pin:
tcal6416: tcal6416_hub
number: 0
mode: OUTPUT
inverted: false

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
<<: !include common.yaml