ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
pca9554.cpp
Go to the documentation of this file.
1#include "pca9554.h"
3#include "esphome/core/log.h"
4
5namespace esphome::pca9554 {
6
7// for 16 bit expanders, these addresses will be doubled.
8static constexpr uint8_t INPUT_REG = 0;
9static constexpr uint8_t OUTPUT_REG = 1;
10static constexpr uint8_t INVERT_REG = 2;
11static constexpr uint8_t CONFIG_REG = 3;
12
13#ifdef USE_PCA9554_PCAL
14// PCAL registers, as offsets from PCAL_BASE spaced by the register width like the ones above.
15static constexpr uint8_t PCAL_BASE = 0x40;
16static constexpr uint8_t OUTPUT_DRIVE_0 = 0;
17static constexpr uint8_t OUTPUT_DRIVE_1 = 1;
18static constexpr uint8_t INPUT_LATCH = 2;
19static constexpr uint8_t PUPD_ENABLE = 3;
20static constexpr uint8_t PUPD_SEL = 4;
21static constexpr uint8_t INTERRUPT_MASK = 5;
22#endif
23
24ESPHOME_LOG_TAG(TAG, "pca9554");
25
27 // Test to see if device exists
28 if (!this->read_inputs_()) {
29 ESP_LOGE(TAG, "PCA95xx not detected at 0x%02X", this->address_);
30 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
31 return;
32 }
33
34 // No polarity inversion
35 this->write_register_(INVERT_REG, 0);
36 // All inputs at initialization
37 this->config_mask_ = 0;
38 // Invert mask as the part sees a 1 as an input
39 this->write_register_(CONFIG_REG, ~this->config_mask_);
40 // All outputs low
41 this->output_mask_ = 0;
42 this->write_register_(OUTPUT_REG, this->output_mask_);
43#ifdef USE_PCA9554_PCAL
44 // The chip keeps these across an MCU reboot
45 if (this->pcal_ && !this->write_pcal_shadows_()) {
46 this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
47 return;
48 }
49#endif
50 // Read the inputs
51 this->read_inputs_();
52 ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
53 this->status_has_error());
54
56}
57
59 ESP_LOGCONFIG(TAG,
60 "PCA9554:\n"
61 " I/O Pins: %d",
62 this->pin_count_);
63#ifdef USE_PCA9554_PCAL
64 ESP_LOGCONFIG(TAG, " PCAL registers: %s", YESNO(this->pcal_));
65#endif
66 LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
67 LOG_I2C_DEVICE(this)
68}
69
71 // Read all pins from hardware into input_mask_
72 return this->read_inputs_(); // Return true if I2C read succeeded, false on error
73}
74
76 // Return the cached pin state from input_mask_
77 return this->input_mask_ & (1 << pin);
78}
79
80void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
81 assign_bits(this->output_mask_, 1 << pin, value);
82#ifdef USE_PCA9554_PCAL
83 if (this->write_register_(OUTPUT_REG, this->output_mask_))
84 this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
85#else
86 this->write_register_(OUTPUT_REG, this->output_mask_);
87#endif
88}
89
91 if (flags & gpio::FLAG_INPUT) {
92 // Clear mode mask bit
93 this->config_mask_ &= ~(1 << pin);
95 } else if (flags & gpio::FLAG_OUTPUT) {
96 // Set mode mask bit
97 this->config_mask_ |= 1 << pin;
98 }
99 this->write_register_(CONFIG_REG, ~this->config_mask_);
100
101#ifdef USE_PCA9554_PCAL
102 if (this->pcal_) {
103 const uint16_t bit = 1U << pin;
104 // A masked pin never asserts INT, so only inputs are unmasked
105 this->update_pcal_register_(INTERRUPT_MASK, this->interrupt_mask_, bit, this->config_mask_ & bit);
106 const bool pull = flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN);
107 // The select bit means nothing while the pull is disabled
108 if (pull)
109 this->update_pcal_register_(PUPD_SEL, this->pupd_select_, bit, (flags & gpio::FLAG_PULLUP) ? bit : 0);
110 this->update_pcal_register_(PUPD_ENABLE, this->pupd_enable_, bit, pull ? bit : 0);
111 }
112#endif
113}
114
116 uint8_t inputs[2];
117
118 if (this->is_failed()) {
119 ESP_LOGD(TAG, "Device marked failed");
120 return false;
121 }
122
123 const i2c::ErrorCode err = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_);
124 if (!this->status_comm_result(err == i2c::ERROR_OK)) {
125 ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) err);
126 return false;
127 }
128 this->input_mask_ = inputs[0];
129 if (this->reg_width_ == 2) {
130 this->input_mask_ |= inputs[1] << 8;
131 }
132#ifdef USE_PCA9554_PCAL
133 // The bus answers again, so retry the settings a failed write left off the chip
134 this->retry_pcal_shadows_();
135#endif
136 return true;
137}
138
139bool PCA9554Component::write_address_(uint8_t address, uint16_t value) {
140 uint8_t outputs[2];
141 outputs[0] = (uint8_t) value;
142 outputs[1] = (uint8_t) (value >> 8);
143 const i2c::ErrorCode err = this->write_register(address, outputs, this->reg_width_);
144 if (!this->status_comm_result(err == i2c::ERROR_OK)) {
145 ESP_LOGE(TAG, "write_register(): I2C I/O error: %d", (int) err);
146 return false;
147 }
148 return true;
149}
150
151#ifdef USE_PCA9554_PCAL
152bool PCA9554Component::write_pcal_register_(uint8_t reg, uint16_t value) {
153 if (this->write_address_(PCAL_BASE + reg * this->reg_width_, value))
154 return true;
155 this->pcal_dirty_ = true;
156 return false;
157}
158
160 this->pcal_dirty_ = false;
161 // Every write runs so one failure does not hide the others; each failure sets pcal_dirty_ again
162 bool ok = this->write_pcal_register_(PUPD_ENABLE, this->pupd_enable_);
163 ok &= this->write_pcal_register_(PUPD_SEL, this->pupd_select_);
164 ok &= this->write_pcal_register_(INPUT_LATCH, this->latch_);
165 ok &= this->write_pcal_register_(INTERRUPT_MASK, this->interrupt_mask_);
166 ok &= this->write_pcal_register_(OUTPUT_DRIVE_0, static_cast<uint16_t>(this->drive_));
167 ok &= this->write_pcal_register_(OUTPUT_DRIVE_1, static_cast<uint16_t>(this->drive_ >> (8 * this->reg_width_)));
168 return ok;
169}
170
171void PCA9554Component::update_pcal_register_(uint8_t reg, uint16_t &shadow, uint16_t mask, uint16_t value) {
172 const uint16_t updated = (shadow & ~mask) | (value & mask);
173 // The shadows hold the wanted state; an unchanged value is already on the chip unless a write failed
174 if (updated == shadow && !this->pcal_dirty_)
175 return;
176 shadow = updated;
177 this->write_pcal_register_(reg, shadow);
178}
179#endif
180
181} // namespace esphome::pca9554
182
183ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pca9554::PCA9554Component, "%u via PCA9554")
uint8_t address
Definition bl0906.h:4
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
bool status_comm_result(bool ok)
Clears the warning when ok, else sets it with message (default "Communication failed"); returns ok.
bool status_has_warning() const
Definition component.h:280
bool status_has_error() const
Definition component.h:282
void enable_polling_loop_()
Call from pin_mode() for an input pin: without an interrupt pin the loop polls instead.
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) const
writes an array of bytes to a specific register in the I²C device
Definition i2c.cpp:37
uint8_t address_
store the address of the device on the bus
Definition i2c.h:327
I2CRegister reg(uint8_t a_register)
calls the I2CRegister constructor
Definition i2c.h:164
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len)
reads an array of bytes from a specific register in the I²C device
Definition i2c.cpp:28
bool write_pcal_shadows_()
Write every PCAL shadow to the chip; false when any write failed.
Definition pca9554.cpp:159
bool digital_read_cache(uint8_t pin) override
Definition pca9554.cpp:75
const uint8_t reg_width_
width of registers
Definition pca9554.h:77
bool write_register_(uint8_t reg, uint16_t value)
Definition pca9554.h:36
uint32_t drive_
Drive strength shadow, two bits per pin; the low reg_width_ bytes go to OUTPUT_DRIVE_0.
Definition pca9554.h:58
void retry_pcal_shadows_()
Retry the shadows when an earlier PCAL write failed.
Definition pca9554.h:43
uint16_t output_mask_
The mask to write as output state - 1 means HIGH, 0 means LOW.
Definition pca9554.h:63
void digital_write_hw(uint8_t pin, bool value) override
Definition pca9554.cpp:80
bool write_address_(uint8_t address, uint16_t value)
Definition pca9554.cpp:139
const uint8_t pin_count_
number of bits the expander has
Definition pca9554.h:75
void update_pcal_register_(uint8_t reg, uint16_t &shadow, uint16_t mask, uint16_t value)
Changes the masked bits of a shadow and writes the register only when it changed.
Definition pca9554.cpp:171
bool digital_read_hw(uint8_t pin) override
Definition pca9554.cpp:70
uint16_t input_mask_
The state of the actual input pins - 1 means HIGH, 0 means LOW.
Definition pca9554.h:65
void pin_mode(uint8_t pin, gpio::Flags flags)
Helper function to set the pin mode of a pin.
Definition pca9554.cpp:90
uint16_t config_mask_
Mask for the pin config - 1 means OUTPUT, 0 means INPUT.
Definition pca9554.h:61
bool write_pcal_register_(uint8_t reg, uint16_t value)
Definition pca9554.cpp:152
void setup() override
Check i2c availability and setup masks.
Definition pca9554.cpp:26
uint16_t flags
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition i2c_bus.h:13
@ ERROR_OK
No error found during execution of method.
Definition i2c_bus.h:15
ESPHOME_LOG_TAG(TAG, "pca9554")
ESPHOME_ALWAYS_INLINE constexpr void assign_bits(T &value, std::type_identity_t< T > mask, bool on)
Set the bits of mask in value when on is true, clear them otherwise.
Definition helpers.h:1072