Hardware-agnostic C++ driver for the NXP PCA9555 and PCAL9555A 16-bit I/O expanders

Overview
📖 📚🌐 Live Complete Documentation - Interactive guides, examples, and step-by-step tutorials
The PCA9555 and PCAL9555A are 16-bit I/O expanders from NXP Semiconductors that communicate via I2C. They provide 16 GPIO pins organized into two 8-bit ports (PORT_0: pins 0-7, PORT_1: pins 8-15), expanding your microcontroller's I/O capabilities over a simple two-wire bus.
This driver provides a single, unified C++ class that supports both chip variants. The chip type is auto-detected during initialization by probing the extended register bank. Features exclusive to the PCAL9555A degrade gracefully when a standard PCA9555 is detected.
The driver is hardware-agnostic – it uses the CRTP (Curiously Recurring Template Pattern) for zero-overhead I2C abstraction. You only need to implement a small I2cInterface class for your platform.
Features
- Dual-chip support: Auto-detects PCA9555 vs PCAL9555A at runtime
- 16 GPIO Pins: Two 8-bit ports (PORT_0: pins 0-7, PORT_1: pins 8-15)
- Per-pin configuration: Direction, pull-up/pull-down, drive strength, polarity
- Interrupt support: Per-pin interrupt masking with edge-detection callbacks
- Hardware agnostic: CRTP-based I2C interface for platform independence
- Modern C++17: Template-based design with
std::initializer_list multi-pin APIs
- Zero overhead: CRTP for compile-time polymorphism – no virtual calls
- Lazy initialization: No I2C traffic in the constructor; init on first use
- Kconfig integration: Optional compile-time configuration via ESP-IDF Kconfig
- Robust error handling: Bitmask error flags, configurable I2C retries
- Graceful degradation: PCAL9555A features fail cleanly on PCA9555
Quick Start
1. Implement the I2C interface for your platform
#include "pcal95555.hpp"
class MyI2c : public pcal95555::I2cInterface<MyI2c> {
public:
bool Write(uint8_t addr, uint8_t reg, const uint8_t* data, size_t len) {
}
bool Read(uint8_t addr, uint8_t reg, uint8_t* data, size_t len) {
}
bool SetAddressPins(bool a0, bool a1, bool a2) {
return false;
}
};
2. Create the driver and use it
{% raw %}
MyI2c i2c;
pcal95555::PCAL95555<MyI2c> gpio(&i2c, false, false, false);
pcal95555::PCAL95555<MyI2c> gpio(&i2c, 0x20);
pcal95555::PCAL95555<MyI2c> gpio(&i2c, 0x20, pcal95555::ChipVariant::PCA9555);
gpio.SetPinDirection(0, GPIODir::Output);
gpio.WritePin(0, true);
bool state = gpio.ReadPin(1);
if (gpio.HasAgileIO()) {
gpio.SetDriveStrength(0, DriveStrength::Level3);
gpio.SetPullEnable(1, true);
gpio.SetPullDirection(1, true);
}
gpio.WritePins({{0, true}, {1, false}, {2, true}});
gpio.SetDirections({{0, GPIODir::Output}, {1, GPIODir::Input}});
{% endraw %}
Building and Flashing (ESP32)
Prerequisites
- ESP-IDF v5.5+
- ESP32-S3 development board with PCA9555 or PCAL9555A connected via I2C
Build
cd examples/esp32
# List available apps
./scripts/build_app.sh list
# Build the comprehensive test suite
./scripts/build_app.sh pcal95555_comprehensive_test Debug
# Build the LED animation demo
./scripts/build_app.sh pcal95555_led_animation Debug
Flash and Monitor
# Flash and open serial monitor
./scripts/flash_app.sh flash_monitor pcal95555_comprehensive_test Debug
# Flash only
./scripts/flash_app.sh flash pcal95555_comprehensive_test Debug
# Monitor only (if already flashed)
./scripts/flash_app.sh monitor
Note (ESP32-S3 native USB): If using the USB-SERIAL-JTAG port (/dev/ttyACM0), you may need to manually enter download mode: Hold BOOT, press RESET, release BOOT, then immediately run the flash command.
Examples
Comprehensive Test Suite (pcal95555_comprehensive_test)
A thorough test of every public API method, organized into 17 test sections:
- Initialization (I2C bus, driver, chip variant detection)
- GPIO direction (single pin, multi-pin mask, initializer list)
- GPIO read/write (ReadPin, WritePin, TogglePin, WritePins, ReadPins)
- Pull resistor configuration (single + multi-pin, PCAL9555A)
- Drive strength (single + multi-pin, PCAL9555A)
- Output mode (push-pull / open-drain, PCAL9555A)
- Polarity inversion (single, mask, initializer list)
- Input latch (single, mask, initializer list, PCAL9555A)
- Interrupt configuration (mask, status, callbacks, handler, PCAL9555A)
- Port-level operations
- Multi-pin API tests (WritePins, ReadPins, SetDirections, SetPolarities)
- Address management (ChangeAddress, address-based constructor)
- Configuration (SetRetries, EnsureInitialized)
- Multi-pin PCAL9555A APIs (SetPullEnables, SetDriveStrengths, ConfigureInterrupts, etc.)
- Interactive input (button press test, disabled by default)
- Error handling (invalid pins, UnsupportedFeature, selective flag clearing)
- Stress tests (rapid pin toggling)
PCAL9555A-only tests are automatically skipped (not failed) on a standard PCA9555.
To enable the interactive input test (requires a physical button on pin 0):
static constexpr bool ENABLE_INTERACTIVE_INPUT_TESTS = true;
LED Animation Demo (pcal95555_led_animation)
A visual demo driving 16 LEDs through 10 animation patterns:
- Sequential Chase – single LED scans left/right
- Bounce – LED bounces between endpoints
- Binary Counter – counts 0-65535 in binary on LEDs
- Breathing (PWM) – software PWM fade-in/fade-out
- Wave / Comet Tail – 4-LED comet sweeps back and forth
- Random Sparkle – random LED patterns
- Build-up / Teardown – LEDs light up then extinguish one by one
- Accelerating Scan – speed ramps from 120ms to 1ms and back
- Center Expand / Contract – symmetric outward/inward animation
- Alternating Flash – port-vs-port and even-vs-odd flashing
Configure LEDS_ACTIVE_LOW for your wiring (common-anode vs common-cathode).
Hardware Setup
Minimal wiring (ESP32-S3 + PCA9555/PCAL9555A)
ESP32-S3 PCA9555 / PCAL9555A
───────── ──────────────────
GPIO4 (SDA) ────────── SDA
GPIO5 (SCL) ────────── SCL
3.3V ───────────────── VDD
GND ────────────────── VSS (GND)
A0 ─── GND (or GPIO45)
A1 ─── GND (or GPIO48)
A2 ─── GND (or GPIO47)
Pull-up resistors: 4.7kΩ on SDA and SCL to 3.3V (or enable internal pull-ups).
Address: The I2C address is 0x20 + (A2<<2 | A1<<1 | A0). Default with all address pins LOW is 0x20.
LED animation demo wiring
Connect LEDs with current-limiting resistors (220Ω-1kΩ) to each I/O pin. For active-HIGH: LED anode to IO pin, cathode to GND. For active-LOW: LED cathode to IO pin, anode to VDD through resistor.
Interactive test wiring
Connect a momentary push-button between PCA9555 pin IO0_0 and GND. For PCAL9555A, the internal pull-up is enabled automatically. For PCA9555, add an external 10kΩ pull-up resistor to VDD.
Contributing
Pull requests and suggestions are welcome! Please:
- Follow the existing code style (clang-format, Doxygen comments)
- Add tests for new features in the comprehensive test suite
- Update documentation for any API changes
- Ensure the build passes for both
Debug and Release configurations