Porting¶
Run the driver on a new MCU by writing one class: the I2C communication interface.
The driver never calls your SDK directly. It calls a few methods on a class you derive from
pcal95555::I2cInterface<Self> (in inc/pcal95555_i2c_interface.hpp).
The base class uses CRTP: the calls are resolved at compile time, so there are no virtual functions and no vtable.
Figure description and sources
From top to bottom. Your application talks to one PCAL95555
Drawn from: inc/pcal95555.hpp Pcal95555Reg; inc/pcal95555_i2c_interface.hpp I2cInterface; figure module docs/diagrams/figures/driver-layers.py.
What you implement¶
| Method | Required | Contract |
|---|---|---|
EnsureInitialized() |
yes | Bring the bus up on the first call, return true at once afterwards. false stops initialisation with I2CReadFail |
Write(addr, reg, data, len) |
yes | One transaction: START, addr + W, reg, len data bytes, STOP. The driver always passes len = 1. Return false on a NACK or timeout |
Read(addr, reg, data, len) |
yes | Write reg, repeated START, read len bytes, STOP. The driver always passes len = 1. Return false on a NACK or timeout |
SetAddressPins(a0, a1, a2) |
no | Drive A0-A2 from MCU GPIOs, if the board wires them; the default returns false |
RegisterInterruptHandler(handler) |
no | Keep handler and run it from a task when INT falls; the default returns false |
GpioSet(pin, signal), GpioRead(pin, signal) |
no | Control-pin hooks of the HardFOC interface model; this driver does not call them, since neither chip has a reset pin. CtrlPin::RSTN is deprecated for the same reason |
addr is the 7-bit address (0x20 to 0x27); your SDK may want it shifted left by one.
The port¶
class MyI2c : public pcal95555::I2cInterface<MyI2c> {
public:
// Bring the bus up once; the driver calls this before its first transfer.
bool EnsureInitialized() noexcept {
if (!ready_) {
ready_ = platform_i2c_init();
}
return ready_;
}
// One register write: [addr+W] [reg] [data...] STOP. Return false on a NACK.
bool Write(uint8_t addr, uint8_t reg, const uint8_t* data, size_t len) noexcept {
uint8_t frame[3] = {reg, 0, 0};
if (len > 2) {
return false; // the driver writes one register at a time
}
for (size_t i = 0; i < len; ++i) {
frame[1 + i] = data[i];
}
return platform_i2c_write(addr, frame, 1 + len);
}
// One register read: [addr+W] [reg] repeated START [addr+R] [data...] STOP.
bool Read(uint8_t addr, uint8_t reg, uint8_t* data, size_t len) noexcept {
return platform_i2c_write_read(addr, ®, 1, data, len);
}
// Optional: keep the driver's interrupt handler. Without it, poll instead.
bool RegisterInterruptHandler(std::function<void()> handler) noexcept {
handler_ = std::move(handler);
return true;
}
// Call from a task, not from the ISR, after INT (open drain, active low) falls:
// the handler reads the chip over I2C.
void ServiceInterrupt() {
if (handler_) {
handler_();
}
}
private:
bool ready_ = false;
std::function<void()> handler_;
};
Rules the transfers must follow:
- Return
falsefor a NACK. Variant detection depends on it: a PCA9555 NACKs command 0x4F, and a port that reports success anyway makes the driver believe it talks to a PCAL9555A. - Recover after a NACK. The next transfer must work. Some controllers need a bus reset after a NACK; do it
inside
Read()/Write(). - Repeated START for reads. The chips also accept a STOP between the command and the read, but a repeated start keeps another master from slipping in between.
- Bounded time. Use a timeout (a few milliseconds is plenty at 100 or 400 kHz); a stuck bus must not block forever.
Interrupt support¶
RegisterInterruptHandler() receives a closure that calls the driver's HandleInterrupt(), which does I2C. Run it
from a task, not from the ISR: the usual pattern is an ISR on the INT pin's falling edge that wakes a task, which
calls the closure. Configure the MCU pin as an input with a pull-up (or rely on an external one); INT is open drain.
Reference implementation¶
The ESP32 examples use esp32_pcal95555_bus.hpp: an ESP-IDF
i2c_master port at 400 kHz with internal pull-ups, GPIO-driven A0-A2 and an INT ISR that wakes a FreeRTOS task.
Start from it for any ESP-IDF target.
Check your port¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/, with the driver instantiated over it. - Call
EnsureInitialized()and printGetChipVariant(): it must match the part marking. - Toggle an output with
TogglePin()and watch it with a meter or a logic analyser. - Read
GetErrorFlags()after a few hundred calls: it should stay 0 on a healthy bus.
Threads¶
One driver object is not thread-safe: give each expander one owner task, or guard its calls with a mutex.
Driver objects share no state, so two expanders can be driven from two tasks without a common lock, as long as
your port's Read() / Write() serialise access to a shared bus. Callbacks run in the task that calls
HandleInterrupt().