PCA9555 / PCAL9555A Driver
API reference
Loading...
Searching...
No Matches
pcal95555.ipp
Go to the documentation of this file.
1
6#ifndef PCAL95555_IMPL
7#define PCAL95555_IMPL
8
9// Normally included by pcal95555.hpp (inside namespace pcal95555). Opened on its own (an IDE or a
10// linter), pull in the header first; the installed tree has no ../inc, so use the include path.
11#ifndef PCAL95555_HEADER_INCLUDED
12#include "pcal95555.hpp"
13#endif
14
15// Constructor: store I2C bus and pin levels (lazy initialization)
16template <typename I2cType>
17pcal95555::PCAL95555<I2cType>::PCAL95555(I2cType* bus, bool a0_level, bool a1_level, bool a2_level,
18 ChipVariant variant)
19 : i2c_(bus), previous_pin_states_(0), initialized_(false),
20 a0_level_(a0_level), a1_level_(a1_level), a2_level_(a2_level),
21 chip_variant_(ChipVariant::Unknown), user_variant_(variant) {
22 // Calculate address bits from pin levels (A0=bit0, A1=bit1, A2=bit2)
23 address_bits_ = (a0_level ? 1 : 0) | ((a1_level ? 1 : 0) << 1) | ((a2_level ? 1 : 0) << 2);
25
26 // Initialize pin callbacks array
27 for (auto& callback : pin_callbacks_) {
28 callback.registered = false;
29 callback.edge = InterruptEdge::Both;
30 }
31
32 // No initialization here - use EnsureInitialized() when ready
33}
34
35// Constructor: store I2C bus and calculate pin levels from address (lazy initialization)
36template <typename I2cType>
37pcal95555::PCAL95555<I2cType>::PCAL95555(I2cType* bus, uint8_t address, ChipVariant variant)
38 : i2c_(bus), previous_pin_states_(0), initialized_(false),
39 chip_variant_(ChipVariant::Unknown), user_variant_(variant) {
40 // Validate address range (0x20 to 0x27)
41 constexpr uint8_t BASE_ADDRESS = 0x20;
42 constexpr uint8_t MAX_ADDRESS = 0x27;
43 constexpr uint8_t MAX_BITS = 0x07;
44
45 if (address < BASE_ADDRESS || address > MAX_ADDRESS) {
46 // Address out of range - clamp to valid range
47 if (address < BASE_ADDRESS) {
48 address = BASE_ADDRESS;
49 } else {
50 address = MAX_ADDRESS;
51 }
52 // Note: We don't set an error flag here as the address is still valid, just clamped
53 }
54
55 // Calculate address bits from address: address_bits = address - BASE_ADDRESS
56 address_bits_ = (address - BASE_ADDRESS) & MAX_BITS;
58
59 // Calculate pin levels from address bits and store for lazy initialization
60 a0_level_ = (address_bits_ & 0x01) != 0;
61 a1_level_ = (address_bits_ & 0x02) != 0;
62 a2_level_ = (address_bits_ & 0x04) != 0;
63
64 // Initialize pin callbacks array
65 for (auto& callback : pin_callbacks_) {
66 callback.registered = false;
67 callback.edge = InterruptEdge::Both;
68 }
69
70 // No initialization here - use EnsureInitialized() when ready
71}
72
73// Ensure initialization (lazy initialization)
74template <typename I2cType>
76 if (initialized_) {
77 return true; // Already initialized
78 }
79 return initialize();
80}
81
82template <typename I2cType>
84 if (variant == ChipVariant::Unknown) {
85 return false;
86 }
87 user_variant_ = variant;
88 chip_variant_ = variant;
89 previous_pin_states_ = 0;
90 clearError(Error::I2CReadFail);
91 initialized_ = true;
92 return true;
93}
94
95// Perform actual initialization
96template <typename I2cType>
98 // Ensure I2C bus is initialized and ready
99 if (!i2c_->EnsureInitialized()) {
100 setError(Error::I2CReadFail);
101 initialized_ = false;
102 return false;
103 }
104
105 // Attempt to set address pins via I2C interface (if supported)
106 // This will return false if GPIO control is not implemented (hardwired pins)
107 i2c_->SetAddressPins(a0_level_, a1_level_, a2_level_);
108
109 // Verify communication by reading a register (INPUT_PORT_0)
110 // This ensures the device is accessible at the calculated address
111 uint8_t test_value = 0;
112 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0), test_value)) {
113 // Communication failed - this is expected if address pins are hardwired differently
114 // or device is not connected. We still store the address for potential retry.
115 setError(Error::I2CReadFail);
116 initialized_ = false;
117 return false;
118 }
119
120 // Communication successful - clear any previous errors
121 clearError(Error::I2CReadFail);
122
123 // Detect chip variant (PCA9555 vs PCAL9555A)
124 if (user_variant_ != ChipVariant::Unknown) {
125 // User specified variant - skip auto-detection
126 chip_variant_ = user_variant_;
127 } else {
128 // Auto-detect by probing an Agile I/O register. An inconclusive probe (the bus did not answer
129 // the read after it) leaves the variant unknown: fail so the next call retries instead of
130 // running with no feature set.
131 if (!detectChipVariant()) {
132 setError(Error::I2CReadFail);
133 initialized_ = false;
134 return false;
135 }
136 }
137
138 // Initialize previous pin states for edge detection
139 previous_pin_states_ = readPinStates();
140
141 // Mark as initialized
142 initialized_ = true;
143 return true;
144}
145
146// Configure retry count
147template <typename I2cType>
149 retries_ = retries;
150}
151
152// Low-level write with retries
153template <typename I2cType>
154bool pcal95555::PCAL95555<I2cType>::writeRegister(uint8_t reg, uint8_t value) noexcept {
155 /* Stage the payload in this driver object's transfer byte, not the caller's
156 * stack: on some MCU memory maps task stacks sit outside the memory the bus
157 * DMA can read. One byte per driver object (not a function-local static),
158 * so drivers for different chips do not overwrite each other's transfers. */
159 xfer_byte_ = value;
160 for (int attempt = 0; attempt <= retries_; ++attempt) {
161 if (i2c_->Write(dev_addr_, reg, &xfer_byte_, 1)) {
162 clearError(Error::I2CWriteFail);
163 return true;
164 }
165 }
166 setError(Error::I2CWriteFail);
167 return false;
168}
169// Low-level read with retries
170template <typename I2cType>
171bool pcal95555::PCAL95555<I2cType>::readRegister(uint8_t reg, uint8_t& value) noexcept {
172 for (int attempt = 0; attempt <= retries_; ++attempt) {
173 if (i2c_->Read(dev_addr_, reg, &xfer_byte_, 1)) {
174 clearError(Error::I2CReadFail);
175 value = xfer_byte_;
176 return true;
177 }
178 }
179 setError(Error::I2CReadFail);
180 return false;
181}
182
183// Reset all registers to defaults as per datasheet.
184template <typename I2cType>
186 if (!EnsureInitialized()) {
187 return;
188 }
189 // Standard PCA9555 registers (always available)
190 writeRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_0), 0xFF);
191 writeRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_1), 0xFF);
192 writeRegister(static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_0), 0x00);
193 writeRegister(static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_1), 0x00);
194 writeRegister(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_0), 0xFF);
195 writeRegister(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_1), 0xFF);
196
197 // PCAL9555A Agile I/O registers (only if chip supports them)
198 if (chip_variant_ == ChipVariant::PCAL9555A) {
199 // Drive strength = full (all bits 1)
200 writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_0), 0xFF);
201 writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_1), 0xFF);
202 writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_2), 0xFF);
203 writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_3), 0xFF);
204 // Latch disabled (default 0x00)
205 writeRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_0), 0x00);
206 writeRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_1), 0x00);
207 // Pull-up/down enabled (1), pull-up selected (1), interrupt masked (1)
208 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_0), 0xFF);
209 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_1), 0xFF);
210 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_0), 0xFF);
211 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_1), 0xFF);
212 writeRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_0), 0xFF);
213 writeRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_1), 0xFF);
214 // Output mode = push-pull (0)
215 writeRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_CONF), 0x00);
216 }
217}
218
219// Initialize using compile-time configuration
220template <typename I2cType>
222 if (!EnsureInitialized()) {
223 return;
224 }
225#if CONFIG_PCAL95555_INIT_FROM_KCONFIG
226 // Standard PCA9555 registers (always available)
227 writeRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_0), uint8_t(CONFIG_PCAL95555_INIT_OUTPUT & 0xFF));
228 writeRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_1), uint8_t((CONFIG_PCAL95555_INIT_OUTPUT >> 8) & 0xFF));
229
230 writeRegister(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_0), uint8_t(CONFIG_PCAL95555_INIT_DIRECTION & 0xFF));
231 writeRegister(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_1),
232 uint8_t((CONFIG_PCAL95555_INIT_DIRECTION >> 8) & 0xFF));
233
234 // PCAL9555A Agile I/O registers (only if chip supports them)
235 if (chip_variant_ == ChipVariant::PCAL9555A) {
236 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_0), uint8_t(CONFIG_PCAL95555_INIT_PULL_ENABLE & 0xFF));
237 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_1),
238 uint8_t((CONFIG_PCAL95555_INIT_PULL_ENABLE >> 8) & 0xFF));
239
240 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_0), uint8_t(CONFIG_PCAL95555_INIT_PULL_UP & 0xFF));
241 writeRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_1), uint8_t((CONFIG_PCAL95555_INIT_PULL_UP >> 8) & 0xFF));
242
243 uint8_t open_drain_config =
245 writeRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_CONF), open_drain_config);
246 }
247#endif
248}
249
250// Set or clear a bit in a register byte
251static inline uint8_t updateBit(uint8_t regVal, uint8_t bit, bool set) noexcept {
252 if (set) {
253 return regVal | (1 << bit);
254 }
255 return regVal & ~(1 << bit);
256}
257
258// ---- Internal R-M-W helpers ----
259
260template <typename I2cType>
261bool pcal95555::PCAL95555<I2cType>::readDualPort(uint8_t reg0, uint8_t reg1,
262 uint8_t& val0, uint8_t& val1) noexcept {
263 /* The bus transfer itself goes through xfer_byte_ (see readRegister). */
264 uint8_t port0 = 0;
265 uint8_t port1 = 0;
266 if (!readRegister(reg0, port0)) {
267 return false;
268 }
269 if (!readRegister(reg1, port1)) {
270 return false;
271 }
272 val0 = port0;
273 val1 = port1;
274 return true;
275}
276
277template <typename I2cType>
278bool pcal95555::PCAL95555<I2cType>::writeDualPort(uint8_t reg0, uint8_t reg1,
279 uint8_t val0, uint8_t val1) noexcept {
280 /* Two single-register writes. (A 3-byte sequential [cmd][v0][v1] frame was
281 * observed to leave CONFIG mismatched on Mid I2C0 while reads are fixed via
282 * sequential 2-byte Mem_Read below.) */
283 if (!writeRegister(reg0, val0)) {
284 return false;
285 }
286 return writeRegister(reg1, val1);
287}
288
289template <typename I2cType>
291 uint8_t pin, bool bit_value) noexcept {
292 uint8_t reg = (pin < 8) ? reg0 : reg1;
293 uint8_t bit = pin % 8;
294 uint8_t val = 0;
295 if (!readRegister(reg, val)) {
296 return false;
297 }
298 val = updateBit(val, bit, bit_value);
299 return writeRegister(reg, val);
300}
301
302template <typename I2cType>
304 uint16_t mask, bool bit_value) noexcept {
305 uint8_t val0 = 0;
306 uint8_t val1 = 0;
307 if (!readDualPort(reg0, reg1, val0, val1)) {
308 return false;
309 }
310 for (int bit = 0; bit < 8; ++bit) {
311 if ((mask & (1U << bit)) != 0U) {
312 val0 = updateBit(val0, bit, bit_value);
313 }
314 if ((mask & (1U << (bit + 8))) != 0U) {
315 val1 = updateBit(val1, bit, bit_value);
316 }
317 }
318 return writeDualPort(reg0, reg1, val0, val1);
319}
320
321// ---- Direction configuration ----
322
323template <typename I2cType>
325 if (!EnsureInitialized()) {
326 return false;
327 }
328 if (pin >= 16) {
329 setError(Error::InvalidPin);
330 return false;
331 }
332 clearError(Error::InvalidPin);
333 return modifySinglePinRegister(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_0),
334 static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_1),
335 pin, (dir == GPIODir::Input));
336}
337
338template <typename I2cType>
340 if (!EnsureInitialized()) {
341 return false;
342 }
343 if (mask == 0U) {
344 setError(Error::InvalidMask);
345 return false;
346 }
347 clearError(Error::InvalidMask);
348 return modifyDualPortByMask(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_0),
349 static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_1),
350 mask, (dir == GPIODir::Input));
351}
352
353// Configure direction for multiple pins with individual settings
354template <typename I2cType>
355bool pcal95555::PCAL95555<I2cType>::SetDirections(std::initializer_list<std::pair<uint8_t, GPIODir>> configs) noexcept {
356 if (!EnsureInitialized()) {
357 return false;
358 }
359 uint8_t port0 = 0;
360 uint8_t port1 = 0;
361 if (!readDualPort(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_0),
362 static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_1), port0, port1)) {
363 return false;
364 }
365 for (const auto& config : configs) {
366 if (config.first >= 16) {
367 setError(Error::InvalidPin);
368 return false;
369 }
370 uint8_t bit = config.first % 8;
371 if (config.first < 8) {
372 port0 = updateBit(port0, bit, (config.second == GPIODir::Input));
373 } else {
374 port1 = updateBit(port1, bit, (config.second == GPIODir::Input));
375 }
376 }
377 clearError(Error::InvalidPin);
378 return writeDualPort(static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_0),
379 static_cast<uint8_t>(Pcal95555Reg::CONFIG_PORT_1), port0, port1);
380}
381
382// Read input port registers and return bit
383template <typename I2cType>
384bool pcal95555::PCAL95555<I2cType>::ReadPin(uint8_t pin) noexcept {
385 bool level = false;
386 (void)ReadPin(pin, level);
387 return level;
388}
389
390// Read one pin, reporting success separately from the level
391template <typename I2cType>
392bool pcal95555::PCAL95555<I2cType>::ReadPin(uint8_t pin, bool& level) noexcept {
393 if (!EnsureInitialized()) {
394 return false;
395 }
396 if (pin >= 16) {
397 setError(Error::InvalidPin);
398 return false;
399 }
400 clearError(Error::InvalidPin);
401 uint8_t reg = (pin < 8) ? static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0) : static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_1);
402 uint8_t bit = pin % 8;
403 uint8_t val = 0;
404 if (!readRegister(reg, val)) {
405 return false;
406 }
407 level = (val & (1 << bit)) != 0;
408 return true;
409}
410
411// Write output port registers
412template <typename I2cType>
413bool pcal95555::PCAL95555<I2cType>::WritePin(uint8_t pin, bool value) noexcept {
414 if (!EnsureInitialized()) {
415 return false;
416 }
417 if (pin >= 16) {
418 setError(Error::InvalidPin);
419 return false;
420 }
421 clearError(Error::InvalidPin);
422 return modifySinglePinRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_0),
423 static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_1),
424 pin, value);
425}
426
427// Set multiple outputs via bitmask
428template <typename I2cType>
429bool pcal95555::PCAL95555<I2cType>::SetMultipleOutputs(uint16_t mask, bool value) noexcept {
430 if (!EnsureInitialized()) {
431 return false;
432 }
433 if (mask == 0U) {
434 setError(Error::InvalidMask);
435 return false;
436 }
437 clearError(Error::InvalidMask);
438 return modifyDualPortByMask(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_0),
439 static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_1),
440 mask, value);
441}
442
443template <typename I2cType>
445 if (!EnsureInitialized()) {
446 return false;
447 }
448 if (pin >= 16) {
449 setError(Error::InvalidPin);
450 return false;
451 }
452 clearError(Error::InvalidPin);
453 uint8_t reg = (pin < 8) ? static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_0)
454 : static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_1);
455 uint8_t bit = pin % 8;
456 uint8_t val = 0;
457 if (!readRegister(reg, val)) {
458 return false;
459 }
460 val ^= (1 << bit);
461 return writeRegister(reg, val);
462}
463
464// Write multiple pins
465template <typename I2cType>
466bool pcal95555::PCAL95555<I2cType>::WritePins(std::initializer_list<std::pair<uint8_t, bool>> configs) noexcept {
467 if (!EnsureInitialized()) {
468 return false;
469 }
470 uint8_t port0 = 0;
471 uint8_t port1 = 0;
472 if (!readDualPort(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_0),
473 static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_1), port0, port1)) {
474 return false;
475 }
476 for (const auto& config : configs) {
477 if (config.first >= 16) {
478 setError(Error::InvalidPin);
479 return false;
480 }
481 uint8_t bit = config.first % 8;
482 if (config.first < 8) {
483 port0 = updateBit(port0, bit, config.second);
484 } else {
485 port1 = updateBit(port1, bit, config.second);
486 }
487 }
488 clearError(Error::InvalidPin);
489 return writeDualPort(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_0),
490 static_cast<uint8_t>(Pcal95555Reg::OUTPUT_PORT_1), port0, port1);
491}
492
493// Read multiple pins (zero heap allocation — uses stack-based PinReadResult)
494template <typename I2cType>
495pcal95555::PinReadResult pcal95555::PCAL95555<I2cType>::ReadPins(std::initializer_list<uint8_t> pins) noexcept {
496 PinReadResult results;
497 if (!EnsureInitialized()) {
498 return results; // Return empty results if not initialized
499 }
500
501 // Read both input port registers once
502 uint8_t port0 = 0;
503 uint8_t port1 = 0;
504 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0), port0)) {
505 // On failure, return empty results
506 return results;
507 }
508 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_1), port1)) {
509 return results;
510 }
511
512 // Extract values for each requested pin (skip duplicates)
513 for (uint8_t pin : pins) {
514 if (results.contains(pin)) continue; // Deduplicate repeated pins
515
516 if (pin >= 16) {
517 // Invalid pin - add with false value
518 results.push_back(pin, false);
519 continue;
520 }
521
522 uint8_t bit = pin % 8;
523 bool value = false;
524 if (pin < 8) {
525 value = (port0 & (1 << bit)) != 0;
526 } else {
527 value = (port1 & (1 << bit)) != 0;
528 }
529 results.push_back(pin, value);
530 }
531
532 return results;
533}
534
535// Pull-up/down control
536template <typename I2cType>
537bool pcal95555::PCAL95555<I2cType>::SetPullEnable(uint8_t pin, bool enable) noexcept {
538 if (!EnsureInitialized() || !requireAgileIO()) {
539 return false;
540 }
541 if (pin >= 16) {
542 setError(Error::InvalidPin);
543 return false;
544 }
545 clearError(Error::InvalidPin);
546 return modifySinglePinRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_0),
547 static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_1),
548 pin, enable);
549}
550
551template <typename I2cType>
552bool pcal95555::PCAL95555<I2cType>::SetPullDirection(uint8_t pin, bool pull_up) noexcept {
553 if (!EnsureInitialized() || !requireAgileIO()) {
554 return false;
555 }
556 if (pin >= 16) {
557 setError(Error::InvalidPin);
558 return false;
559 }
560 clearError(Error::InvalidPin);
561 return modifySinglePinRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_0),
562 static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_1),
563 pin, pull_up);
564}
565
566// Configure pull enable for multiple pins
567template <typename I2cType>
568bool pcal95555::PCAL95555<I2cType>::SetPullEnables(std::initializer_list<std::pair<uint8_t, bool>> configs) noexcept {
569 if (!EnsureInitialized() || !requireAgileIO()) {
570 return false;
571 }
572 uint8_t port0 = 0;
573 uint8_t port1 = 0;
574 if (!readDualPort(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_0),
575 static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_1), port0, port1)) {
576 return false;
577 }
578 for (const auto& config : configs) {
579 if (config.first >= 16) {
580 setError(Error::InvalidPin);
581 return false;
582 }
583 uint8_t bit = config.first % 8;
584 if (config.first < 8) {
585 port0 = updateBit(port0, bit, config.second);
586 } else {
587 port1 = updateBit(port1, bit, config.second);
588 }
589 }
590 clearError(Error::InvalidPin);
591 return writeDualPort(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_0),
592 static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_1), port0, port1);
593}
594
595// Configure pull direction for multiple pins
596template <typename I2cType>
597bool pcal95555::PCAL95555<I2cType>::SetPullDirections(std::initializer_list<std::pair<uint8_t, bool>> configs) noexcept {
598 if (!EnsureInitialized() || !requireAgileIO()) {
599 return false;
600 }
601 uint8_t port0 = 0;
602 uint8_t port1 = 0;
603 if (!readDualPort(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_0),
604 static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_1), port0, port1)) {
605 return false;
606 }
607 for (const auto& config : configs) {
608 if (config.first >= 16) {
609 setError(Error::InvalidPin);
610 return false;
611 }
612 uint8_t bit = config.first % 8;
613 if (config.first < 8) {
614 port0 = updateBit(port0, bit, config.second);
615 } else {
616 port1 = updateBit(port1, bit, config.second);
617 }
618 }
619 clearError(Error::InvalidPin);
620 return writeDualPort(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_0),
621 static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_1), port0, port1);
622}
623
624// Read pull configuration from hardware
625template <typename I2cType>
627 uint16_t& direction_mask) noexcept {
628 if (!EnsureInitialized()) {
629 return false;
630 }
631 if (!requireAgileIO()) {
632 return false;
633 }
634
635 uint8_t en0 = 0, en1 = 0, sel0 = 0, sel1 = 0;
636 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_0), en0)) {
637 return false;
638 }
639 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_ENABLE_1), en1)) {
640 return false;
641 }
642 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_0), sel0)) {
643 return false;
644 }
645 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::PULL_SELECT_1), sel1)) {
646 return false;
647 }
648
649 enable_mask = static_cast<uint16_t>((en1 << 8) | en0);
650 direction_mask = static_cast<uint16_t>((sel1 << 8) | sel0);
651 return true;
652}
653
654// Drive strength (2 bits per pin)
655template <typename I2cType>
657 if (!EnsureInitialized()) {
658 return false;
659 }
660 if (!requireAgileIO()) {
661 return false;
662 }
663 if (pin >= 16) {
664 setError(Error::InvalidPin);
665 return false;
666 }
667 clearError(Error::InvalidPin);
668 uint8_t base = (pin < 8) ? static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_0) : static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_2);
669 uint8_t index = pin % 8;
670 uint8_t reg = base + ((index >= 4) ? 1 : 0);
671 uint8_t bit = (index % 4) * 2; // each pin uses 2 bits
672 uint8_t val = 0;
673 if (!readRegister(reg, val)) {
674 return false;
675 }
676 // Clear the two bits and set the new level
677 val &= ~(0x3 << bit);
678 val |= (uint8_t(level) << bit);
679 return writeRegister(reg, val);
680}
681
682// Configure drive strength for multiple pins
683template <typename I2cType>
684bool pcal95555::PCAL95555<I2cType>::SetDriveStrengths(std::initializer_list<std::pair<uint8_t, DriveStrength>> configs) noexcept {
685 if (!EnsureInitialized()) {
686 return false;
687 }
688 if (!requireAgileIO()) {
689 return false;
690 }
691 // Read all drive strength registers (4 registers total)
692 uint8_t ds0 = 0, ds1 = 0, ds2 = 0, ds3 = 0;
693 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_0), ds0)) {
694 return false;
695 }
696 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_1), ds1)) {
697 return false;
698 }
699 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_2), ds2)) {
700 return false;
701 }
702 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_3), ds3)) {
703 return false;
704 }
705
706 // Update values for each pin
707 for (const auto& config : configs) {
708 uint8_t pin = config.first;
709 DriveStrength level = config.second;
710
711 if (pin >= 16) {
712 setError(Error::InvalidPin);
713 return false;
714 }
715
716 uint8_t index = pin % 8;
717 uint8_t reg_offset = (index >= 4) ? 1 : 0;
718 uint8_t bit = (index % 4) * 2; // each pin uses 2 bits
719
720 uint8_t* reg_ptr = nullptr;
721 if (pin < 8) {
722 reg_ptr = (reg_offset == 0) ? &ds0 : &ds1;
723 } else {
724 reg_ptr = (reg_offset == 0) ? &ds2 : &ds3;
725 }
726
727 // Clear the two bits and set the new level
728 *reg_ptr &= ~(0x3 << bit);
729 *reg_ptr |= (uint8_t(level) << bit);
730 }
731
732 clearError(Error::InvalidPin);
733
734 // Write back all registers
735 if (!writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_0), ds0)) {
736 return false;
737 }
738 if (!writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_1), ds1)) {
739 return false;
740 }
741 if (!writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_2), ds2)) {
742 return false;
743 }
744 return writeRegister(static_cast<uint8_t>(Pcal95555Reg::DRIVE_STRENGTH_3), ds3);
745}
746
747// Configure interrupt for a single pin
748template <typename I2cType>
750 if (!EnsureInitialized()) {
751 return false;
752 }
753 if (!requireAgileIO()) {
754 return false;
755 }
756 if (pin >= 16) {
757 setError(Error::InvalidPin);
758 return false;
759 }
760 clearError(Error::InvalidPin);
761
762 // Read current mask
763 uint8_t mask0 = 0;
764 uint8_t mask1 = 0;
765 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_0), mask0)) {
766 return false;
767 }
768 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_1), mask1)) {
769 return false;
770 }
771
772 // Combine into 16-bit mask
773 uint16_t mask = (uint16_t(mask1) << 8) | mask0;
774
775 // Update the bit for this pin (0 = enabled, 1 = disabled)
776 if (state == InterruptState::Enabled) {
777 mask &= ~(1U << pin); // Clear bit (enable interrupt)
778 } else {
779 mask |= (1U << pin); // Set bit (disable interrupt)
780 }
781
782 // Write back
783 return ConfigureInterruptMask(mask);
784}
785
786// Configure interrupts for multiple pins
787template <typename I2cType>
788bool pcal95555::PCAL95555<I2cType>::ConfigureInterrupts(std::initializer_list<std::pair<uint8_t, InterruptState>> configs) noexcept {
789 if (!EnsureInitialized()) {
790 return false;
791 }
792 if (!requireAgileIO()) {
793 return false;
794 }
795 // Read current mask
796 uint8_t mask0 = 0;
797 uint8_t mask1 = 0;
798 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_0), mask0)) {
799 return false;
800 }
801 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_1), mask1)) {
802 return false;
803 }
804
805 // Combine into 16-bit mask
806 uint16_t mask = (uint16_t(mask1) << 8) | mask0;
807
808 // Update mask for each pin configuration
809 for (const auto& config : configs) {
810 uint8_t pin = config.first;
811 InterruptState state = config.second;
812
813 if (pin >= 16) {
814 setError(Error::InvalidPin);
815 return false;
816 }
817
818 // Update the bit for this pin (0 = enabled, 1 = disabled)
819 if (state == InterruptState::Enabled) {
820 mask &= ~(1U << pin); // Clear bit (enable interrupt)
821 } else {
822 mask |= (1U << pin); // Set bit (disable interrupt)
823 }
824 }
825
826 clearError(Error::InvalidPin);
827
828 // Write back once for all pins
829 return ConfigureInterruptMask(mask);
830}
831
832// Interrupt mask (low-level method)
833template <typename I2cType>
835 if (!EnsureInitialized()) {
836 return false;
837 }
838 if (!requireAgileIO()) {
839 return false;
840 }
841 if (!writeRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_0), uint8_t(mask & 0xFF))) {
842 return false;
843 }
844 return writeRegister(static_cast<uint8_t>(Pcal95555Reg::INT_MASK_1), uint8_t((mask >> 8) & 0xFF));
845}
846
847// Read interrupt status (and clear)
848template <typename I2cType>
850 if (!EnsureInitialized()) {
851 return 0;
852 }
853 if (!requireAgileIO()) {
854 return 0;
855 }
856 uint8_t low_byte = 0;
857 uint8_t high_byte = 0;
858 readRegister(static_cast<uint8_t>(Pcal95555Reg::INT_STATUS_0), low_byte);
859 readRegister(static_cast<uint8_t>(Pcal95555Reg::INT_STATUS_1), high_byte);
860 return uint16_t(high_byte) << 8 | low_byte;
861}
862
863// Output mode configuration (ODEN bits)
864template <typename I2cType>
865bool pcal95555::PCAL95555<I2cType>::SetOutputMode(bool port_0_open_drain, bool port_1_open_drain) noexcept {
866 if (!EnsureInitialized()) {
867 return false;
868 }
869 if (!requireAgileIO()) {
870 return false;
871 }
872 uint8_t val = (port_1_open_drain ? 1 : 0) << 1 | (port_0_open_drain ? 1 : 0);
873 return writeRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_CONF), val);
874}
875
876// Register pin interrupt callback
877template <typename I2cType>
879 std::function<void(uint8_t, bool)> callback) {
880 if (!EnsureInitialized()) {
881 return false;
882 }
883 if (pin >= 16) {
884 setError(Error::InvalidPin);
885 return false;
886 }
887 clearError(Error::InvalidPin);
888
889 if (!callback) {
890 return false; // Invalid callback
891 }
892
893 pin_callbacks_[pin].callback = callback;
894 pin_callbacks_[pin].edge = edge;
895 pin_callbacks_[pin].registered = true;
896
897 // Read current pin state for edge detection
898 uint16_t current_states = readPinStates();
899 previous_pin_states_ = current_states;
900
901 return true;
902}
903
904// Unregister pin interrupt callback
905template <typename I2cType>
907 // No I2C communication needed, but check initialization for consistency
908 if (!EnsureInitialized()) {
909 return false;
910 }
911 if (pin >= 16) {
912 setError(Error::InvalidPin);
913 return false;
914 }
915 clearError(Error::InvalidPin);
916
917 if (!pin_callbacks_[pin].registered) {
918 return false; // No callback registered
919 }
920
921 pin_callbacks_[pin].registered = false;
922 pin_callbacks_[pin].callback = nullptr;
923 return true;
924}
925
926// Set global interrupt callback
927template <typename I2cType>
928void pcal95555::PCAL95555<I2cType>::SetInterruptCallback(const std::function<void(uint16_t)>& callback) noexcept {
929 irq_callback_ = callback;
930}
931
932// Register interrupt handler with I2C interface
933template <typename I2cType>
935 if (!EnsureInitialized()) {
936 return false;
937 }
938 // Register this driver's HandleInterrupt method with the I2C interface
939 return i2c_->RegisterInterruptHandler([this]() {
940 HandleInterrupt();
941 });
942}
943
944// Read current pin states (private helper)
945template <typename I2cType>
947 uint8_t port0 = 0;
948 uint8_t port1 = 0;
949 readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0), port0);
950 readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_1), port1);
951 return static_cast<uint16_t>((uint16_t(port1) << 8) | port0);
952}
953
954// Read all 16 pin input states (public API)
955template <typename I2cType>
957 if (!EnsureInitialized()) {
958 return 0;
959 }
960 return readPinStates();
961}
962
963template <typename I2cType>
965 uint16_t& value) noexcept {
966 value = 0;
967 if (!EnsureInitialized()) {
968 return false;
969 }
970 uint8_t port0 = 0;
971 uint8_t port1 = 0;
972 if (!readDualPort(reg0, reg1, port0, port1)) {
973 return false;
974 }
975 value = static_cast<uint16_t>(port0) |
976 static_cast<uint16_t>(static_cast<uint16_t>(port1) << 8);
977 return true;
978}
979
980template <typename I2cType>
982 uint16_t value) noexcept {
983 if (!EnsureInitialized()) {
984 return false;
985 }
986 return writeDualPort(reg0, reg1, static_cast<uint8_t>(value & 0xFFU),
987 static_cast<uint8_t>((value >> 8) & 0xFFU));
988}
989
990// Handle interrupt - read status, check conditions, call callbacks
991template <typename I2cType>
993 if (!EnsureInitialized()) {
994 return;
995 }
996
997 uint16_t interrupt_status = 0;
998
999 if (chip_variant_ == ChipVariant::PCAL9555A) {
1000 // PCAL9555A: read hardware interrupt status register
1001 interrupt_status = GetInterruptStatus();
1002 } else {
1003 // PCA9555: No hardware interrupt status registers.
1004 // Fall back to change-detection by comparing current vs previous pin states.
1005 uint16_t current_states = readPinStates();
1006 interrupt_status = current_states ^ previous_pin_states_;
1007 // Note: previous_pin_states_ is updated at the end of this method
1008 }
1009
1010 // Read current pin states
1011 uint16_t current_states = readPinStates();
1012
1013 // Call global callback if registered
1014 if (irq_callback_) {
1015 irq_callback_(interrupt_status);
1016 }
1017
1018 // Process each pin that triggered an interrupt
1019 for (uint8_t pin = 0; pin < 16; ++pin) {
1020 // Check if this pin triggered an interrupt
1021 if ((interrupt_status & (1U << pin)) == 0) {
1022 continue; // Pin didn't trigger interrupt
1023 }
1024
1025 // Check if callback is registered for this pin
1026 if (!pin_callbacks_[pin].registered || !pin_callbacks_[pin].callback) {
1027 continue; // No callback registered
1028 }
1029
1030 // Determine edge type
1031 bool previous_state = (previous_pin_states_ & (1U << pin)) != 0;
1032 bool current_state = (current_states & (1U << pin)) != 0;
1033 bool rising_edge = !previous_state && current_state;
1034 bool falling_edge = previous_state && !current_state;
1035
1036 // Check if edge matches registered condition
1037 bool should_call = false;
1038 switch (pin_callbacks_[pin].edge) {
1039 case InterruptEdge::Rising:
1040 should_call = rising_edge;
1041 break;
1042 case InterruptEdge::Falling:
1043 should_call = falling_edge;
1044 break;
1045 case InterruptEdge::Both:
1046 should_call = rising_edge || falling_edge;
1047 break;
1048 }
1049
1050 // Call callback if condition matches
1051 if (should_call) {
1052 pin_callbacks_[pin].callback(pin, current_state);
1053 }
1054 }
1055
1056 // Update previous states for next interrupt
1057 previous_pin_states_ = current_states;
1058}
1059
1060template <typename I2cType>
1061bool pcal95555::PCAL95555<I2cType>::SetPinPolarity(uint8_t pin, Polarity polarity) noexcept {
1062 if (!EnsureInitialized()) {
1063 return false;
1064 }
1065 if (pin >= 16) {
1066 setError(Error::InvalidPin);
1067 return false;
1068 }
1069 clearError(Error::InvalidPin);
1070 return modifySinglePinRegister(static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_0),
1071 static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_1),
1072 pin, (polarity == Polarity::Inverted));
1073}
1074
1075template <typename I2cType>
1077 if (!EnsureInitialized()) {
1078 return false;
1079 }
1080 if (mask == 0U) {
1081 setError(Error::InvalidMask);
1082 return false;
1083 }
1084 clearError(Error::InvalidMask);
1085 return modifyDualPortByMask(static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_0),
1086 static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_1),
1087 mask, (polarity == Polarity::Inverted));
1088}
1089
1090// Configure polarity for multiple pins with individual settings
1091template <typename I2cType>
1092bool pcal95555::PCAL95555<I2cType>::SetPolarities(std::initializer_list<std::pair<uint8_t, Polarity>> configs) noexcept {
1093 if (!EnsureInitialized()) {
1094 return false;
1095 }
1096 uint8_t port0 = 0;
1097 uint8_t port1 = 0;
1098 if (!readDualPort(static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_0),
1099 static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_1), port0, port1)) {
1100 return false;
1101 }
1102 for (const auto& config : configs) {
1103 if (config.first >= 16) {
1104 setError(Error::InvalidPin);
1105 return false;
1106 }
1107 uint8_t bit = config.first % 8;
1108 if (config.first < 8) {
1109 port0 = updateBit(port0, bit, (config.second == Polarity::Inverted));
1110 } else {
1111 port1 = updateBit(port1, bit, (config.second == Polarity::Inverted));
1112 }
1113 }
1114 clearError(Error::InvalidPin);
1115 return writeDualPort(static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_0),
1116 static_cast<uint8_t>(Pcal95555Reg::POLARITY_INV_1), port0, port1);
1117}
1118
1119template <typename I2cType>
1120bool pcal95555::PCAL95555<I2cType>::EnableInputLatch(uint8_t pin, bool enable) noexcept {
1121 if (!EnsureInitialized() || !requireAgileIO()) {
1122 return false;
1123 }
1124 if (pin >= 16) {
1125 setError(Error::InvalidPin);
1126 return false;
1127 }
1128 clearError(Error::InvalidPin);
1129 return modifySinglePinRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_0),
1130 static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_1),
1131 pin, enable);
1132}
1133
1134template <typename I2cType>
1135bool pcal95555::PCAL95555<I2cType>::EnableMultipleInputLatches(uint16_t mask, bool enable) noexcept {
1136 if (!EnsureInitialized() || !requireAgileIO()) {
1137 return false;
1138 }
1139 if (mask == 0U) {
1140 setError(Error::InvalidMask);
1141 return false;
1142 }
1143 clearError(Error::InvalidMask);
1144 return modifyDualPortByMask(static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_0),
1145 static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_1),
1146 mask, enable);
1147}
1148
1149// Configure input latch for multiple pins with individual settings
1150template <typename I2cType>
1151bool pcal95555::PCAL95555<I2cType>::EnableInputLatches(std::initializer_list<std::pair<uint8_t, bool>> configs) noexcept {
1152 if (!EnsureInitialized() || !requireAgileIO()) {
1153 return false;
1154 }
1155 uint8_t port0 = 0;
1156 uint8_t port1 = 0;
1157 if (!readDualPort(static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_0),
1158 static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_1), port0, port1)) {
1159 return false;
1160 }
1161 for (const auto& config : configs) {
1162 if (config.first >= 16) {
1163 setError(Error::InvalidPin);
1164 return false;
1165 }
1166 uint8_t bit = config.first % 8;
1167 if (config.first < 8) {
1168 port0 = updateBit(port0, bit, config.second);
1169 } else {
1170 port1 = updateBit(port1, bit, config.second);
1171 }
1172 }
1173 clearError(Error::InvalidPin);
1174 return writeDualPort(static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_0),
1175 static_cast<uint8_t>(Pcal95555Reg::INPUT_LATCH_1), port0, port1);
1176}
1177
1178template <typename I2cType>
1180 return error_flags_;
1181}
1182
1183template <typename I2cType>
1185 return (error_flags_ & static_cast<uint16_t>(e)) != 0;
1186}
1187
1188template <typename I2cType>
1190 return error_flags_ != 0;
1191}
1192
1193template <typename I2cType>
1195 error_flags_ &= ~static_cast<uint16_t>(e);
1196}
1197
1198template <typename I2cType>
1200 error_flags_ &= ~mask;
1201}
1202
1203template <typename I2cType>
1205 error_flags_ |= static_cast<uint16_t>(error_code);
1206}
1207
1208template <typename I2cType>
1210 error_flags_ &= ~static_cast<uint16_t>(error_code);
1211}
1212
1213// Check if chip supports Agile I/O (PCAL9555A features)
1214template <typename I2cType>
1216 return chip_variant_ == ChipVariant::PCAL9555A;
1217}
1218
1219// Get detected chip variant
1220template <typename I2cType>
1222 return chip_variant_;
1223}
1224
1225// Guard helper: require Agile I/O support
1226template <typename I2cType>
1228 if (chip_variant_ != ChipVariant::PCAL9555A) {
1229 setError(Error::UnsupportedFeature);
1230 return false;
1231 }
1232 return true;
1233}
1234
1235// Detect chip variant by probing OUTPUT_CONF register (0x4F)
1236// Uses a 3-step sandwich: standard read -> probe -> standard read
1237template <typename I2cType>
1239 // Use single-shot (no retries) for the probe to avoid spamming the bus
1240 int saved_retries = retries_;
1241 retries_ = 0;
1242 chip_variant_ = ChipVariant::Unknown;
1243
1244 uint8_t dummy = 0;
1245
1246 // Step 1: Verify basic I2C communication with a standard register
1247 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0), dummy)) {
1248 // Bus is not working at all - leave variant as Unknown
1249 retries_ = saved_retries;
1250 return false;
1251 }
1252 clearError(Error::I2CReadFail);
1253
1254 // Step 2: Probe an Agile I/O register (OUTPUT_CONF = 0x4F)
1255 // PCAL9555A will ACK; PCA9555 will NACK (expected — do not treat as hard error)
1256 uint8_t probe_value = 0;
1257 if (readRegister(static_cast<uint8_t>(Pcal95555Reg::OUTPUT_CONF), probe_value)) {
1258 // Probe succeeded - chip has Agile I/O registers
1259
1260 // Step 3a: Confirm bus is still healthy after successful read
1261 if (readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0), dummy)) {
1262 chip_variant_ = ChipVariant::PCAL9555A;
1263 clearError(Error::I2CReadFail);
1264 } else {
1265 // Bus broke after successful probe - inconclusive, leave as Unknown
1266 clearError(Error::I2CReadFail);
1267 }
1268 } else {
1269 // Probe NACKed - expected for standard PCA9555
1270 clearError(Error::I2CReadFail);
1271
1272 // Step 3b: Verify the bus recovered after the NACK
1273 if (readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0), dummy)) {
1274 // Bus healthy after NACK - confidently a PCA9555
1275 chip_variant_ = ChipVariant::PCA9555;
1276 clearError(Error::I2CReadFail);
1277 } else {
1278 // Bus did not recover - detection inconclusive, leave as Unknown
1279 clearError(Error::I2CReadFail);
1280 }
1281 }
1282
1283 // Restore retries
1284 retries_ = saved_retries;
1285 return chip_variant_ != ChipVariant::Unknown;
1286}
1287
1288// Get current I2C address
1289template <typename I2cType>
1291 return dev_addr_;
1292}
1293
1294// Get current A2-A0 address bits
1295template <typename I2cType>
1297 return address_bits_;
1298}
1299
1300// ---- Shared ChangeAddress implementation ----
1301
1302template <typename I2cType>
1304 uint8_t new_addr = calculateAddress(new_bits);
1305 bool a0_level = (new_bits & 0x01) != 0;
1306 bool a1_level = (new_bits & 0x02) != 0;
1307 bool a2_level = (new_bits & 0x04) != 0;
1308
1309 // Try to set GPIO pins via I2C interface (returns false if hardwired)
1310 bool gpio_set = i2c_->SetAddressPins(a0_level, a1_level, a2_level);
1311
1312 // Update internal state
1313 address_bits_ = new_bits;
1314 dev_addr_ = new_addr;
1315 a0_level_ = a0_level;
1316 a1_level_ = a1_level;
1317 a2_level_ = a2_level;
1318
1319 // Reset initialization flag since address changed
1320 initialized_ = false;
1321
1322 // Verify communication at new address
1323 uint8_t test_value = 0;
1324 if (!readRegister(static_cast<uint8_t>(Pcal95555Reg::INPUT_PORT_0), test_value)) {
1325 setError(Error::I2CReadFail);
1326 (void)gpio_set; // gpio_set status is irrelevant - communication failed either way
1327 return false;
1328 }
1329
1330 // Communication successful
1331 clearError(Error::I2CReadFail);
1332 clearError(Error::InvalidAddress);
1333
1334 // Re-detect chip variant at new address
1335 chip_variant_ = ChipVariant::Unknown;
1336 if (user_variant_ != ChipVariant::Unknown) {
1337 chip_variant_ = user_variant_;
1338 } else if (!detectChipVariant()) {
1339 setError(Error::I2CReadFail);
1340 return false;
1341 }
1342
1343 initialized_ = true;
1344 return true;
1345}
1346
1347// Change address by setting A2-A0 pins via GPIO
1348template <typename I2cType>
1349bool pcal95555::PCAL95555<I2cType>::ChangeAddress(bool a0_level, bool a1_level, bool a2_level) noexcept {
1350 uint8_t new_bits = (a0_level ? 1U : 0U)
1351 | ((a1_level ? 1U : 0U) << 1)
1352 | ((a2_level ? 1U : 0U) << 2);
1353 return changeAddressImpl(static_cast<uint8_t>(new_bits));
1354}
1355
1356// Change address using address value directly
1357template <typename I2cType>
1358bool pcal95555::PCAL95555<I2cType>::ChangeAddress(uint8_t address) noexcept {
1359 constexpr uint8_t BASE_ADDRESS = 0x20;
1360 constexpr uint8_t MAX_ADDRESS = 0x27;
1361
1362 if (address < BASE_ADDRESS || address > MAX_ADDRESS) {
1363 setError(Error::InvalidAddress);
1364 return false;
1365 }
1366
1367 uint8_t new_bits = (address - BASE_ADDRESS) & 0x07;
1368 return changeAddressImpl(new_bits);
1369}
1370
1371#endif // PCAL95555_IMPL
bool SetPullEnables(std::initializer_list< std::pair< uint8_t, bool > > configs) noexcept
Definition pcal95555.ipp:568
bool EnableMultipleInputLatches(uint16_t mask, bool enable) noexcept
Enable or disable input latch for multiple pins using a bitmask.
Definition pcal95555.ipp:1135
bool readDualPort(uint8_t reg0, uint8_t reg1, uint8_t &val0, uint8_t &val1) noexcept
Read both port registers (port0 and port1) in one call.
Definition pcal95555.ipp:261
bool writeRegister(uint8_t reg, uint8_t value) noexcept
Write a single byte to a device register with retry logic.
Definition pcal95555.ipp:154
bool EnsureInitialized() noexcept
Definition pcal95555.ipp:75
bool RegisterPinInterrupt(uint8_t pin, InterruptEdge edge, std::function< void(uint8_t pin, bool state)> callback)
Definition pcal95555.ipp:878
bool ReadDualPortRegister(uint8_t reg0, uint8_t reg1, uint16_t &value) noexcept
Read a dual-port register pair into a 16-bit pin mask.
Definition pcal95555.ipp:964
void InitFromConfig() noexcept
Initialize the device using values from Kconfig.
Definition pcal95555.ipp:221
bool initialize() noexcept
Perform actual initialization of the driver.
Definition pcal95555.ipp:97
bool SetPullEnable(uint8_t pin, bool enable) noexcept
Enable or disable the pull-up/pull-down resistor on a pin.
Definition pcal95555.ipp:537
bool SetMultipleOutputs(uint16_t mask, bool value) noexcept
Definition pcal95555.ipp:429
bool SetDriveStrength(uint8_t pin, DriveStrength level) noexcept
Configure the output drive strength for a GPIO pin.
Definition pcal95555.ipp:656
bool RegisterInterruptHandler() noexcept
Definition pcal95555.ipp:934
bool a1_level_
Definition pcal95555.hpp:1130
bool SetPinPolarity(uint8_t pin, Polarity polarity) noexcept
Configure input polarity inversion for a single pin.
Definition pcal95555.ipp:1061
uint8_t dev_addr_
Definition pcal95555.hpp:1121
bool SetPullDirections(std::initializer_list< std::pair< uint8_t, bool > > configs) noexcept
Definition pcal95555.ipp:597
bool GetPullConfiguration(uint16_t &enable_mask, uint16_t &direction_mask) noexcept
Read the current pull resistor configuration from hardware registers.
Definition pcal95555.ipp:626
void SetRetries(int retries) noexcept
Configure retry mechanism for I2C transactions.
Definition pcal95555.ipp:148
bool EnableInputLatches(std::initializer_list< std::pair< uint8_t, bool > > configs) noexcept
Definition pcal95555.ipp:1151
PinReadResult ReadPins(std::initializer_list< uint8_t > pins) noexcept
Definition pcal95555.ipp:495
uint16_t readPinStates() noexcept
Read current pin states from input port registers.
Definition pcal95555.ipp:946
bool detectChipVariant() noexcept
Detect the chip variant by probing an Agile I/O register.
Definition pcal95555.ipp:1238
bool EnableInputLatch(uint8_t pin, bool enable) noexcept
Enable or disable the input latch for a single pin.
Definition pcal95555.ipp:1120
bool HasError(Error e) const noexcept
Check if a specific error flag is set.
Definition pcal95555.ipp:1184
void ClearErrorFlags(uint16_t mask=0xFFFF) noexcept
Clear specific error flags by raw bitmask.
Definition pcal95555.ipp:1199
bool SetOutputMode(bool port_0_open_drain, bool port_1_open_drain) noexcept
Configure per-port output mode (push-pull or open-drain).
Definition pcal95555.ipp:865
bool a0_level_
Definition pcal95555.hpp:1129
bool readRegister(uint8_t reg, uint8_t &value) noexcept
Read a device register with retry logic.
Definition pcal95555.ipp:171
bool modifyDualPortByMask(uint8_t reg0, uint8_t reg1, uint16_t mask, bool bit_value) noexcept
Batch mask read-modify-write on a dual-port register pair.
Definition pcal95555.ipp:303
uint16_t GetErrorFlags() const noexcept
Retrieve currently latched error flags.
Definition pcal95555.ipp:1179
bool SetDirections(std::initializer_list< std::pair< uint8_t, GPIODir > > configs) noexcept
Definition pcal95555.ipp:355
void ClearError(Error e) noexcept
Clear a single error flag.
Definition pcal95555.ipp:1194
bool WriteDualPortRegister(uint8_t reg0, uint8_t reg1, uint16_t value) noexcept
Write a dual-port register pair from a 16-bit pin mask (no readback).
Definition pcal95555.ipp:981
bool ConfigureInterruptMask(uint16_t mask) noexcept
Definition pcal95555.ipp:834
bool a2_level_
Definition pcal95555.hpp:1131
uint8_t GetAddress() const noexcept
Get the current I2C address of the device.
Definition pcal95555.ipp:1290
bool HasAgileIO() const noexcept
Definition pcal95555.ipp:1215
bool SetDriveStrengths(std::initializer_list< std::pair< uint8_t, DriveStrength > > configs) noexcept
Definition pcal95555.ipp:684
void clearError(Error error_code) noexcept
Definition pcal95555.ipp:1209
bool SetPolarities(std::initializer_list< std::pair< uint8_t, Polarity > > configs) noexcept
Definition pcal95555.ipp:1092
uint8_t address_bits_
Definition pcal95555.hpp:1122
bool ConfigureInterrupts(std::initializer_list< std::pair< uint8_t, InterruptState > > configs) noexcept
Definition pcal95555.ipp:788
uint8_t GetAddressBits() const noexcept
Get the current A2-A0 address bits.
Definition pcal95555.ipp:1296
bool changeAddressImpl(uint8_t new_bits) noexcept
Shared implementation for ChangeAddress overloads.
Definition pcal95555.ipp:1303
ChipVariant GetChipVariant() const noexcept
Get the detected chip variant.
Definition pcal95555.ipp:1221
bool WritePin(uint8_t pin, bool value) noexcept
Write a logical level to a GPIO output pin.
Definition pcal95555.ipp:413
bool ForceMarkInitialized(ChipVariant variant) noexcept
Mark the driver initialized without the probe read.
Definition pcal95555.ipp:83
bool SetPullDirection(uint8_t pin, bool pull_up) noexcept
Select internal pull-up or pull-down resistor direction.
Definition pcal95555.ipp:552
static constexpr uint8_t calculateAddress(uint8_t bits)
Calculate I2C address from A2-A0 bits.
Definition pcal95555.hpp:1142
void SetInterruptCallback(const std::function< void(uint16_t)> &callback) noexcept
Definition pcal95555.ipp:928
bool ChangeAddress(bool a0_level, bool a1_level, bool a2_level) noexcept
Definition pcal95555.ipp:1349
PinInterruptCallback pin_callbacks_[16]
Definition pcal95555.hpp:1126
bool ReadPin(uint8_t pin) noexcept
Read the current logical level of a GPIO pin.
Definition pcal95555.ipp:384
bool SetMultipleDirections(uint16_t mask, GPIODir dir) noexcept
Set the direction for multiple GPIO pins at once using a bitmask.
Definition pcal95555.ipp:339
bool ConfigureInterrupt(uint8_t pin, InterruptState state) noexcept
Definition pcal95555.ipp:749
void setError(Error error_code) noexcept
Definition pcal95555.ipp:1204
bool SetMultiplePolarities(uint16_t mask, Polarity polarity) noexcept
Configure input polarity for multiple pins using a bitmask.
Definition pcal95555.ipp:1076
bool requireAgileIO() noexcept
Check that the chip supports Agile I/O, setting error if not.
Definition pcal95555.ipp:1227
void HandleInterrupt() noexcept
Internal handler to process an interrupt event.
Definition pcal95555.ipp:992
bool SetPinDirection(uint8_t pin, GPIODir dir) noexcept
Set the direction of a single GPIO pin.
Definition pcal95555.ipp:324
bool modifySinglePinRegister(uint8_t reg0, uint8_t reg1, uint8_t pin, bool bit_value) noexcept
Single-pin read-modify-write on a dual-port register pair.
Definition pcal95555.ipp:290
bool WritePins(std::initializer_list< std::pair< uint8_t, bool > > configs) noexcept
Definition pcal95555.ipp:466
uint16_t GetInterruptStatus() noexcept
Retrieve and clear the interrupt status.
Definition pcal95555.ipp:849
bool TogglePin(uint8_t pin) noexcept
Toggle the output state of a GPIO pin.
Definition pcal95555.ipp:444
PCAL95555(I2cType *bus, bool a0_level, bool a1_level, bool a2_level, ChipVariant variant=ChipVariant::Unknown)
Definition pcal95555.ipp:17
bool HasAnyError() const noexcept
Check if any error flag is set.
Definition pcal95555.ipp:1189
bool UnregisterPinInterrupt(uint8_t pin) noexcept
Unregister callback for a specific pin interrupt.
Definition pcal95555.ipp:906
void ResetToDefault() noexcept
Reset all registers to their power-on default state.
Definition pcal95555.ipp:185
bool writeDualPort(uint8_t reg0, uint8_t reg1, uint8_t val0, uint8_t val1) noexcept
Write both port registers (port0 and port1) in one call.
Definition pcal95555.ipp:278
uint16_t ReadAllInputs() noexcept
Definition pcal95555.ipp:956
InterruptState
Definition pcal95555.hpp:96
GPIODir
Definition pcal95555.hpp:77
DriveStrength
Definition pcal95555.hpp:83
ChipVariant
Identifies the detected or user-specified chip variant.
Definition pcal95555.hpp:203
@ PCAL9555A
NXP PCAL9555A with Agile I/O (registers 0x00-0x07 + 0x40-0x4F)
@ Unknown
Not yet detected (pre-initialization)
@ PCA9555
Standard PCA9555 (registers 0x00-0x07 only)
Polarity
Definition pcal95555.hpp:80
Error
Definition pcal95555.hpp:103
InterruptEdge
Definition pcal95555.hpp:89
Driver for the PCA9555 and PCAL9555A 16-bit I/O expanders with I²C interface.
#define CONFIG_PCAL95555_INIT_DIRECTION
Definition pcal95555_kconfig.hpp:298
#define CONFIG_PCAL95555_INIT_PULL_ENABLE
Definition pcal95555_kconfig.hpp:302
#define CONFIG_PCAL95555_INIT_OUTPUT
Definition pcal95555_kconfig.hpp:310
#define CONFIG_PCAL95555_INIT_OD_PORT1
Definition pcal95555_kconfig.hpp:317
#define CONFIG_PCAL95555_INIT_OD_PORT0
Definition pcal95555_kconfig.hpp:314
#define CONFIG_PCAL95555_INIT_PULL_UP
Definition pcal95555_kconfig.hpp:306
Fixed-capacity result container for multi-pin read operations.
Definition pcal95555.hpp:140
bool contains(uint8_t pin) const noexcept
Check whether a pin number is already stored in the result set.
Definition pcal95555.hpp:173
void push_back(uint8_t pin, bool value) noexcept
Append a pin/value pair. Silently drops entries beyond kMaxPins.
Definition pcal95555.hpp:164