mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Add tca9555 GPIO driver (#7146)
Co-authored-by: Michal Obrembski <michal@obrembski.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										0
									
								
								esphome/components/gpio_expander/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/gpio_expander/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										38
									
								
								esphome/components/gpio_expander/cached_gpio.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/gpio_expander/cached_gpio.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
| #include <cstdint> | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gpio_expander { | ||||
|  | ||||
| /// @brief A class to cache the read state of a GPIO expander. | ||||
| template<typename T, T N> class CachedGpioExpander { | ||||
|  public: | ||||
|   bool digital_read(T pin) { | ||||
|     if (!this->read_cache_invalidated_[pin]) { | ||||
|       this->read_cache_invalidated_[pin] = true; | ||||
|       return this->digital_read_cache(pin); | ||||
|     } | ||||
|     return this->digital_read_hw(pin); | ||||
|   } | ||||
|  | ||||
|   void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); } | ||||
|  | ||||
|  protected: | ||||
|   virtual bool digital_read_hw(T pin) = 0; | ||||
|   virtual bool digital_read_cache(T pin) = 0; | ||||
|   virtual void digital_write_hw(T pin, bool value) = 0; | ||||
|  | ||||
|   void reset_pin_cache_() { | ||||
|     for (T i = 0; i < N; i++) { | ||||
|       this->read_cache_invalidated_[i] = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   std::array<bool, N> read_cache_invalidated_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace gpio_expander | ||||
| }  // namespace esphome | ||||
							
								
								
									
										72
									
								
								esphome/components/tca9555/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								esphome/components/tca9555/__init__.py
									
									
									
									
									
										Normal 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 = ["@mobrembski"] | ||||
|  | ||||
| AUTO_LOAD = ["gpio_expander"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| tca9555_ns = cg.esphome_ns.namespace("tca9555") | ||||
|  | ||||
| TCA9555Component = tca9555_ns.class_("TCA9555Component", cg.Component, i2c.I2CDevice) | ||||
| TCA9555GPIOPin = tca9555_ns.class_("TCA9555GPIOPin", cg.GPIOPin) | ||||
|  | ||||
| CONF_TCA9555 = "tca9555" | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.declare_id(TCA9555Component), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
|     .extend(i2c.i2c_device_schema(0x21)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| 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 | ||||
|  | ||||
|  | ||||
| TCA9555_PIN_SCHEMA = pins.gpio_base_schema( | ||||
|     TCA9555GPIOPin, | ||||
|     cv.int_range(min=0, max=15), | ||||
|     modes=[CONF_INPUT, CONF_OUTPUT], | ||||
|     mode_validator=validate_mode, | ||||
|     invertable=True, | ||||
| ).extend( | ||||
|     { | ||||
|         cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| @pins.PIN_SCHEMA_REGISTRY.register(CONF_TCA9555, TCA9555_PIN_SCHEMA) | ||||
| async def tca9555_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_parented(var, config[CONF_TCA9555]) | ||||
|  | ||||
|     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 | ||||
							
								
								
									
										140
									
								
								esphome/components/tca9555/tca9555.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								esphome/components/tca9555/tca9555.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| #include "tca9555.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00; | ||||
| static const uint8_t TCA9555_INPUT_PORT_REGISTER_1 = 0x01; | ||||
| static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_0 = 0x02; | ||||
| static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_1 = 0x03; | ||||
| static const uint8_t TCA9555_POLARITY_REGISTER_0 = 0x04; | ||||
| static const uint8_t TCA9555_POLARITY_REGISTER_1 = 0x05; | ||||
| static const uint8_t TCA9555_CONFIGURATION_PORT_0 = 0x06; | ||||
| static const uint8_t TCA9555_CONFIGURATION_PORT_1 = 0x07; | ||||
|  | ||||
| namespace esphome { | ||||
| namespace tca9555 { | ||||
|  | ||||
| static const char *const TAG = "tca9555"; | ||||
|  | ||||
| void TCA9555Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up TCA9555..."); | ||||
|   if (!this->read_gpio_modes_()) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->read_gpio_outputs_()) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
| void TCA9555Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "TCA9555:"); | ||||
|   LOG_I2C_DEVICE(this) | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Communication with TCA9555 failed!"); | ||||
|   } | ||||
| } | ||||
| void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { | ||||
|   if (flags == gpio::FLAG_INPUT) { | ||||
|     // Set mode mask bit | ||||
|     this->mode_mask_ |= 1 << pin; | ||||
|   } else if (flags == gpio::FLAG_OUTPUT) { | ||||
|     // Clear mode mask bit | ||||
|     this->mode_mask_ &= ~(1 << pin); | ||||
|   } | ||||
|   // Write GPIO to enable input mode | ||||
|   this->write_gpio_modes_(); | ||||
| } | ||||
| void TCA9555Component::loop() { this->reset_pin_cache_(); } | ||||
|  | ||||
| bool TCA9555Component::read_gpio_outputs_() { | ||||
|   if (this->is_failed()) | ||||
|     return false; | ||||
|   uint8_t data[2]; | ||||
|   if (!this->read_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { | ||||
|     this->status_set_warning("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 TCA9555Component::read_gpio_modes_() { | ||||
|   if (this->is_failed()) | ||||
|     return false; | ||||
|   uint8_t data[2]; | ||||
|   bool success = this->read_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2); | ||||
|   if (!success) { | ||||
|     this->status_set_warning("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 TCA9555Component::digital_read_hw(uint8_t pin) { | ||||
|   if (this->is_failed()) | ||||
|     return false; | ||||
|   bool success; | ||||
|   uint8_t data[2]; | ||||
|   success = this->read_bytes(TCA9555_INPUT_PORT_REGISTER_0, data, 2); | ||||
|   this->input_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); | ||||
|  | ||||
|   if (!success) { | ||||
|     this->status_set_warning("Failed to read input register"); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   this->status_clear_warning(); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void TCA9555Component::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(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { | ||||
|     this->status_set_warning("Failed to write output register"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
|  | ||||
| bool TCA9555Component::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(TCA9555_CONFIGURATION_PORT_0, data, 2)) { | ||||
|     this->status_set_warning("Failed to write mode register"); | ||||
|     return false; | ||||
|   } | ||||
|   this->status_clear_warning(); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool TCA9555Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } | ||||
|  | ||||
| float TCA9555Component::get_setup_priority() const { return setup_priority::IO; } | ||||
|  | ||||
| void TCA9555GPIOPin::setup() { this->pin_mode(this->flags_); } | ||||
| void TCA9555GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } | ||||
| bool TCA9555GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } | ||||
| void TCA9555GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } | ||||
| std::string TCA9555GPIOPin::dump_summary() const { return str_sprintf("%u via TCA9555", this->pin_); } | ||||
|  | ||||
| }  // namespace tca9555 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										64
									
								
								esphome/components/tca9555/tca9555.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/tca9555/tca9555.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| #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 tca9555 { | ||||
|  | ||||
| class TCA9555Component : public Component, | ||||
|                          public i2c::I2CDevice, | ||||
|                          public gpio_expander::CachedGpioExpander<uint8_t, 16> { | ||||
|  public: | ||||
|   TCA9555Component() = 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 output, 0 means input | ||||
|   uint16_t mode_mask_{0x00}; | ||||
|   /// 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 TCA9555 pin as an internal input GPIO pin. | ||||
| class TCA9555GPIOPin : public GPIOPin, public Parented<TCA9555Component> { | ||||
|  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; } | ||||
|  | ||||
|  protected: | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace tca9555 | ||||
| }  // namespace esphome | ||||
		Reference in New Issue
	
	Block a user