From 896c176e2f3db9b934f9c64fa427639e39b081e8 Mon Sep 17 00:00:00 2001 From: Francesco Mulassano Date: Sat, 25 Jul 2026 16:18:35 +0200 Subject: [PATCH 1/2] Add TCA9534 I2C GPIO expander driver and example. Header-only device driver following the Mcp23x17 pattern, with both owned-bus and shared-bus Init for multi-device I2C boards. Includes example and changelog entry. Co-authored-by: Cursor --- CHANGELOG.md | 4 + examples/CMakeLists.txt | 1 + examples/TCA9534_GPIO/CMakeLists.txt | 3 + examples/TCA9534_GPIO/Makefile | 12 ++ examples/TCA9534_GPIO/main.cpp | 65 ++++++++ src/daisy.h | 1 + src/dev/tca9534.h | 222 +++++++++++++++++++++++++++ 7 files changed, 308 insertions(+) create mode 100644 examples/TCA9534_GPIO/CMakeLists.txt create mode 100644 examples/TCA9534_GPIO/Makefile create mode 100644 examples/TCA9534_GPIO/main.cpp create mode 100644 src/dev/tca9534.h diff --git a/CHANGELOG.md b/CHANGELOG.md index d534c27ed..6ef94add5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- TCA9534: Added I2C GPIO expander driver (`src/dev/tca9534.h`) with shared-bus and owned-bus init, plus `TCA9534_GPIO` example. + ## v8.1.0 ### Features diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9e8d78cc7..704cee329 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(SerialRead) add_subdirectory(SR_4021) add_subdirectory(Switch) add_subdirectory(Switch3) +add_subdirectory(TCA9534_GPIO) add_subdirectory(TIM_SingleCallback) # Folders diff --git a/examples/TCA9534_GPIO/CMakeLists.txt b/examples/TCA9534_GPIO/CMakeLists.txt new file mode 100644 index 000000000..0390a4614 --- /dev/null +++ b/examples/TCA9534_GPIO/CMakeLists.txt @@ -0,0 +1,3 @@ +set(FIRMWARE_NAME TCA9534_GPIO) +set(FIRMWARE_SOURCES main.cpp) +include(DaisyProject) diff --git a/examples/TCA9534_GPIO/Makefile b/examples/TCA9534_GPIO/Makefile new file mode 100644 index 000000000..048411cec --- /dev/null +++ b/examples/TCA9534_GPIO/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = TCA9534_GPIO + +# Sources +CPP_SOURCES = main.cpp + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/TCA9534_GPIO/main.cpp b/examples/TCA9534_GPIO/main.cpp new file mode 100644 index 000000000..6fbfa1350 --- /dev/null +++ b/examples/TCA9534_GPIO/main.cpp @@ -0,0 +1,65 @@ +/** TCA9534 GPIO expander demo + * + * Wiring (Daisy Seed I2C1 defaults): + * - SCL = D11 (PB8), SDA = D12 (PB9), +3V3, GND + * - TCA9534 at address 0x20 (A0/A1/A2 = GND), or change kAddress + * - External pull-ups on SDA/SCL (typically 2.2k–4.7k to 3V3) + * - External pull-ups on input pins (chip has no internal pull-ups) + * + * P0 is driven as an output (blink). P1 is read as an input and printed over USB serial. + */ +#include "daisy_seed.h" + +using namespace daisy; + +DaisySeed hw; +Tca9534 expander; + +// 7-bit I2C address — do not left-shift (libDaisy v8+). +static constexpr uint8_t kAddress = 0x20; + +int main(void) +{ + hw.Init(true); + hw.StartLog(false); + + Tca9534::Config cfg; + cfg.Defaults(); + cfg.i2c_address = kAddress; + expander.Init(cfg); + + // P0 = output, P1–P7 = inputs + expander.SetConfig(0xFE); + expander.WritePin(0, false); + + uint32_t last_print = System::GetNow(); + bool led_on = false; + + while(1) + { + const uint32_t now = System::GetNow(); + if(now - last_print > 250) + { + last_print = now; + led_on = !led_on; + expander.WritePin(0, led_on); + + uint8_t port = 0; + bool p1 = false; + if(expander.ReadInput(port)) + { + expander.ReadPin(1, p1); + hw.PrintLine("OUT0:%d IN:%02X P1:%d ok:%d", + led_on ? 1 : 0, + port, + p1 ? 1 : 0, + expander.LastTransferOk() ? 1 : 0); + } + else + { + hw.PrintLine("TCA9534 read failed (check wiring / address)"); + } + } + hw.SetLed((now & 511) < 255); + } +} diff --git a/src/daisy.h b/src/daisy.h index 26c23b426..599648053 100644 --- a/src/daisy.h +++ b/src/daisy.h @@ -51,6 +51,7 @@ #include "dev/dps310.h" #include "dev/lcd_hd44780.h" #include "dev/mcp23x17.h" +#include "dev/tca9534.h" #include "dev/max11300.h" #include "dev/tlv493d.h" #include "dev/dotstar.h" diff --git a/src/dev/tca9534.h b/src/dev/tca9534.h new file mode 100644 index 000000000..4ca6eabfa --- /dev/null +++ b/src/dev/tca9534.h @@ -0,0 +1,222 @@ +#pragma once +#ifndef DSY_TCA9534_H +#define DSY_TCA9534_H + +#include "per/gpio.h" +#include "per/i2c.h" + +namespace daisy +{ +/** @addtogroup external + @{ +*/ + +/** + * Registers for the TCA9534 8-bit I2C GPIO expander. + * See TI datasheet SCPS208 (TCA9534). + * + * Note: unlike the MCP23017, the TCA9534 has **no** internal pull-ups. + * Use external resistors on input pins. + */ +enum class Tca9534Register : uint8_t +{ + INPUT = 0x00, /**< Input port */ + OUTPUT = 0x01, /**< Output port */ + POLARITY = 0x02, /**< Polarity inversion */ + CONFIG = 0x03, /**< Configuration (1 = input, 0 = output) */ +}; + +/** Pin direction for a single TCA9534 pin. */ +enum class Tca9534Mode : uint8_t +{ + INPUT, + OUTPUT, +}; + +/** + * Driver for the Texas Instruments TCA9534 (and TCA9534A) 8-bit I2C GPIO expander. + * + * Address range is typically 0x20–0x27 depending on A0/A1/A2 (TCA9534A uses 0x38–0x3F). + * + * Two init styles are supported: + * - Owned bus: `Init(Config)` creates/initializes an I2C peripheral (like Mcp23017). + * - Shared bus: `Init(I2CHandle&, address)` reuses an already-configured I2CHandle + * (useful when LEDs / codecs already own the same bus). + * + * Usage (shared bus): + * \code + * I2CHandle i2c; + * i2c.Init(...); + * Tca9534 exp; + * exp.Init(i2c, 0x21); + * exp.SetConfig(0xFE); // P0 output, P1–P7 inputs + * exp.WritePin(0, true); + * uint8_t port = 0; + * exp.ReadInput(port); + * \endcode + */ +class Tca9534 +{ + public: + struct Config + { + I2CHandle::Config i2c_config; + uint8_t i2c_address; + + void Defaults() + { + i2c_config.periph = I2CHandle::Config::Peripheral::I2C_1; + i2c_config.speed = I2CHandle::Config::Speed::I2C_400KHZ; + i2c_config.mode = I2CHandle::Config::Mode::I2C_MASTER; + i2c_config.pin_config.scl = Pin(PORTB, 8); + i2c_config.pin_config.sda = Pin(PORTB, 9); + // TCA9534 default (A2/A1/A0 = 000). Do not left-shift: libDaisy + // Read/WriteDataAtAddress expect the 7-bit address. + i2c_address = 0x20; + } + }; + + Tca9534() + : i2c_(nullptr), + address_(0x20), + output_state_(0x00), + config_(0xFF), + last_ok_(false), + owns_i2c_(false) + { + } + + /** Initialize with default Seed I2C1 pins and address 0x20. */ + void Init() + { + Config cfg; + cfg.Defaults(); + Init(cfg); + } + + /** Initialize and own a dedicated I2C peripheral. */ + void Init(const Config& config) + { + address_ = config.i2c_address; + owns_i2c_ = true; + owned_i2c_.Init(config.i2c_config); + i2c_ = &owned_i2c_; + // Datasheet power-on: all pins inputs (0xFF), outputs low. + config_ = 0xFF; + output_state_ = 0x00; + WriteReg(Tca9534Register::CONFIG, config_); + WriteReg(Tca9534Register::OUTPUT, output_state_); + } + + /** + * Attach to an already-initialized I2CHandle (shared bus). + * Does not re-init the peripheral. + */ + void Init(I2CHandle& i2c, uint8_t address = 0x20) + { + i2c_ = &i2c; + address_ = address; + owns_i2c_ = false; + config_ = 0xFF; + output_state_ = 0x00; + } + + /** 1 = input, 0 = output for each bit. */ + bool SetConfig(uint8_t config) + { + config_ = config; + return WriteReg(Tca9534Register::CONFIG, config_); + } + + bool SetOutputState(uint8_t output) + { + output_state_ = output; + return WriteReg(Tca9534Register::OUTPUT, output_state_); + } + + bool PinMode(uint8_t pin, Tca9534Mode mode) + { + if(pin > 7) + return false; + if(mode == Tca9534Mode::INPUT) + config_ |= static_cast(1u << pin); + else + config_ &= static_cast(~(1u << pin)); + return WriteReg(Tca9534Register::CONFIG, config_); + } + + bool WritePin(uint8_t pin, bool high) + { + if(pin > 7) + return false; + if(high) + output_state_ |= static_cast(1u << pin); + else + output_state_ &= static_cast(~(1u << pin)); + return WriteReg(Tca9534Register::OUTPUT, output_state_); + } + + bool ReadInput(uint8_t& value) + { + last_ok_ = ReadReg(Tca9534Register::INPUT, value); + return last_ok_; + } + + bool ReadPin(uint8_t pin, bool& high) + { + uint8_t value = 0; + if(!ReadInput(value) || pin > 7) + return false; + high = (value & static_cast(1u << pin)) != 0; + return true; + } + + bool LastTransferOk() const { return last_ok_; } + /** Alias matching early Cosmolab naming. */ + bool LastReadOk() const { return last_ok_; } + + uint8_t OutputState() const { return output_state_; } + uint8_t ConfigRegister() const { return config_; } + uint8_t Address() const { return address_; } + + private: + bool WriteReg(Tca9534Register reg, uint8_t value) + { + if(i2c_ == nullptr) + { + last_ok_ = false; + return false; + } + last_ok_ = i2c_->WriteDataAtAddress( + address_, static_cast(reg), 1, &value, 1, 10) + == I2CHandle::Result::OK; + return last_ok_; + } + + bool ReadReg(Tca9534Register reg, uint8_t& value) + { + if(i2c_ == nullptr) + { + last_ok_ = false; + return false; + } + last_ok_ = i2c_->ReadDataAtAddress( + address_, static_cast(reg), 1, &value, 1, 10) + == I2CHandle::Result::OK; + return last_ok_; + } + + I2CHandle* i2c_; + I2CHandle owned_i2c_; + uint8_t address_; + uint8_t output_state_; + uint8_t config_; + bool last_ok_; + bool owns_i2c_; +}; + +/** @} */ + +} // namespace daisy + +#endif // DSY_TCA9534_H From a98953ee8d3c9e6d80ba5f5065c62fc532f58b95 Mon Sep 17 00:00:00 2001 From: Francesco Mulassano Date: Tue, 28 Jul 2026 09:46:36 +0200 Subject: [PATCH 2/2] Fix clang-format spacing in TCA9534 Init assignments. Co-authored-by: Cursor --- src/dev/tca9534.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev/tca9534.h b/src/dev/tca9534.h index 4ca6eabfa..8a2dd1b51 100644 --- a/src/dev/tca9534.h +++ b/src/dev/tca9534.h @@ -97,8 +97,8 @@ class Tca9534 /** Initialize and own a dedicated I2C peripheral. */ void Init(const Config& config) { - address_ = config.i2c_address; - owns_i2c_ = true; + address_ = config.i2c_address; + owns_i2c_ = true; owned_i2c_.Init(config.i2c_config); i2c_ = &owned_i2c_; // Datasheet power-on: all pins inputs (0xFF), outputs low.