API guide¶
How to do the common jobs with the driver: bring it up, configure pins, write, read, use Agile I/O, handle interrupts and errors.
Every snippet on this page is compiled by the host CI from
tests/docs/api_tasks.cpp, with Gpio standing for
pcal95555::PCAL95555<MyI2c>. The API reference lists every member.
The shape of the API¶
One PCAL95555<I2c> object per chip. Pins are numbered 0 to 15: 0-7 are P0_0 to P0_7 (port 0), 8-15 are P1_0 to
P1_7 (port 1). Most settings come in three forms:
| Form | Example | Use it for |
|---|---|---|
| One pin | SetPinDirection(3, dir) |
One change: one register read and one write |
| Mask | SetMultipleDirections(0x00FF, dir) |
Many pins, one value: each port read and written once |
| List | SetDirections({{0, d0}, {8, d1}}) |
Many pins, different values, one pass |
| Group | Members |
|---|---|
| Set-up | EnsureInitialized(), GetChipVariant(), HasAgileIO(), SetRetries(), ResetToDefault(), InitFromConfig() |
| Direction and polarity | SetPinDirection(), SetMultipleDirections(), SetDirections(), SetPinPolarity(), SetMultiplePolarities(), SetPolarities() |
| Outputs | WritePin(), WritePins(), SetMultipleOutputs(), TogglePin() |
| Inputs | ReadPin(), ReadPins(), ReadAllInputs() |
| Agile I/O (PCAL9555A) | pull, drive strength, input latch, interrupt mask, SetOutputMode() |
| Interrupts | RegisterPinInterrupt(), UnregisterPinInterrupt(), SetInterruptCallback(), RegisterInterruptHandler(), HandleInterrupt() |
| Address | GetAddress(), GetAddressBits(), ChangeAddress() |
| Raw registers | ReadDualPortRegister(), WriteDualPortRegister() |
| Errors | GetErrorFlags(), HasError(), HasAnyError(), ClearError(), ClearErrorFlags() |
Every type is in namespace pcal95555: PCAL95555, ChipVariant, PinReadResult, I2cInterface, the pin enums
(GPIODir, Polarity, DriveStrength, InterruptState, InterruptEdge), Error and the register map
Pcal95555Reg; the snippets bring them in with using pcal95555::GPIODir; and so on. Up to 1.0.0 the pin enums,
Error and Pcal95555Reg were global. Deprecated global aliases still accept the old spelling; define
HF_PCAL95555_NO_GLOBAL_ALIASES to drop them (they go in the next major version).
Bring the chip up¶
bool bring_up(Gpio& gpio) {
// Bus up, probe INPUT_PORT_0, detect the variant.
if (!gpio.EnsureInitialized()) {
return false;
}
const bool pcal = gpio.GetChipVariant() == ChipVariant::PCAL9555A;
std::printf("0x%02X: %s\n", gpio.GetAddress(), pcal ? "PCAL9555A" : "PCA9555");
return true;
}
Every call runs EnsureInitialized() on first use, so an explicit call is optional. It is the one place where a
missing chip shows up as a single false instead of a failure in whichever call came first.
Architecture lists what it does.
Configure pins¶
bool configure_pins(Gpio& gpio) {
// Port 0 (pins 0-7) outputs, port 1 (pins 8-15) inputs.
bool ok = gpio.SetMultipleDirections(0x00FF, GPIODir::Output);
ok = ok && gpio.SetMultipleDirections(0xFF00, GPIODir::Input);
// Pins 8 and 9 are active-low buttons: invert them so a press reads as true.
ok = ok && gpio.SetPolarities({{8, Polarity::Inverted}, {9, Polarity::Inverted}});
return ok;
}
All pins are inputs after power-up. Polarity inversion applies to the Input Port register only: an inverted pin
reads true when it is low. It does not change what an output drives.
Write outputs¶
bool drive_outputs(Gpio& gpio) {
bool ok = gpio.WritePin(0, true); // read-modify-write
ok = ok && gpio.WritePins({{1, true}, {2, false}}); // each port once
ok = ok && gpio.SetMultipleOutputs(0x00F0, false); // pins 4-7 low
ok = ok && gpio.TogglePin(3);
return ok;
}
WritePin() writes the Output Port register, which holds its value while the pin is an input: set the level first
and then switch the direction to output to avoid a glitch. TogglePin() toggles the Output Port bit, not the
measured level.
Read inputs¶
void read_inputs(Gpio& gpio) {
bool button = false;
if (!gpio.ReadPin(8, button)) { // false: the read failed, button untouched
std::printf("pin 8: read failed\n");
}
const bool led = gpio.ReadPin(0); // shorthand: false on a bus error too
const uint16_t all = gpio.ReadAllInputs(); // bit N = pin N
const auto some = gpio.ReadPins({8, 9, 10});
for (const auto& [pin, level] : some) {
std::printf("pin %u: %d\n", pin, level);
}
if (gpio.HasError(Error::I2CReadFail)) {
std::printf("read failed\n");
}
(void)button;
(void)led;
(void)all;
}
ReadPin(pin, level) returns whether the read worked and puts the level in level. The one-argument ReadPin(pin)
returns the level alone, so a failed read looks like a low pin; ReadAllInputs() returns 0 on failure. After those,
check HasError(Error::I2CReadFail) when it matters. ReadPins() returns a PinReadResult: up to 16 (pin, level)
pairs in the order you asked for them, iterable, with contains() and find(). Reading an output pin returns the level
on the pin.
Use Agile I/O (PCAL9555A)¶
bool configure_agile_io(Gpio& gpio) {
if (!gpio.HasAgileIO()) {
return true; // PCA9555: none of this exists
}
// 100 kOhm (typ.) resistors: pin 10 pull-up, pin 11 pull-down.
bool ok = gpio.SetPullEnables({{10, true}, {11, true}});
ok = ok && gpio.SetPullDirections({{10, true}, {11, false}});
// Half drive on the LED pin; port 0 open drain, port 1 push-pull.
ok = ok && gpio.SetDriveStrength(0, DriveStrength::Level1);
ok = ok && gpio.SetOutputMode(true, false);
// Keep a short pulse on pin 12 until the input is read.
ok = ok && gpio.EnableInputLatch(12, true);
return ok;
}
| Feature | Call | Register | Notes |
|---|---|---|---|
| Pull enable | SetPullEnable() |
0x46, 0x47 |
On at power-up; off while the port is open drain |
| Pull direction | SetPullDirection() |
0x48, 0x49 |
true = 100 kΩ pull-up, false = pull-down |
| Drive strength | SetDriveStrength() |
0x40 to 0x43 |
Level0 to Level3 = 0.25 to 1 × full drive |
| Input latch | EnableInputLatch() |
0x44, 0x45 |
Holds a change (and its interrupt) until the read |
| Output stage | SetOutputMode() |
0x4F |
Per port; write it before making pins outputs |
| Interrupt mask | ConfigureInterrupt() |
0x4A, 0x4B |
All masked at power-up |
Check HasAgileIO() once after initialisation: on a PCA9555 these calls return false and set
Error::UnsupportedFeature. PCA9555 and PCAL9555A has the full comparison.
Handle interrupts¶
bool watch_button(Gpio& gpio) {
bool ok = gpio.SetPinDirection(8, GPIODir::Input);
ok = ok && gpio.ConfigureInterrupt(8, InterruptState::Enabled); // PCAL9555A
auto on_fall = [](uint8_t pin, bool level) {
std::printf("pin %u fell, now %d\n", pin, level);
};
ok = ok && gpio.RegisterPinInterrupt(8, InterruptEdge::Falling, on_fall);
gpio.SetInterruptCallback([](uint16_t src) { std::printf("INT 0x%04X\n", src); });
// The port now calls HandleInterrupt() when INT falls.
return ok && gpio.RegisterInterruptHandler();
}
The callbacks run in whatever context calls HandleInterrupt(): your port's interrupt task, or your polling loop.
Interrupts explains the chip behaviour, the edge decoding and the pitfalls.
Use several chips¶
bool two_expanders(MyI2c& bus) {
Gpio left(&bus, uint8_t{0x20}); // A2 A1 A0 = 000
Gpio right(&bus, uint8_t{0x21}); // A2 A1 A0 = 001
return left.EnsureInitialized() && right.EnsureInitialized();
}
One driver object per chip, all on the same port object. Mixed PCA9555 and PCAL9555A parts on one bus are fine: each object detects its own variant.
Access registers directly¶
bool raw_registers(Gpio& gpio) {
auto r = [](Pcal95555Reg reg) { return static_cast<uint8_t>(reg); };
uint16_t config = 0;
if (!gpio.ReadDualPortRegister(r(Pcal95555Reg::CONFIG_PORT_0),
r(Pcal95555Reg::CONFIG_PORT_1), config)) {
return false;
}
// Absolute write from a shadow you keep: no read-modify-write.
return gpio.WriteDualPortRegister(r(Pcal95555Reg::OUTPUT_PORT_0),
r(Pcal95555Reg::OUTPUT_PORT_1), 0xA55A);
}
ReadDualPortRegister() and WriteDualPortRegister() take the port-0 and port-1 command bytes and move a 16-bit
value with bit N = pin N. WriteDualPortRegister() writes without reading first, which suits code that keeps its
own shadow of the Output or Configuration registers. They do not check the Agile I/O guard: on a PCA9555 a command
from 0x40 up NACKs and sets the I2C error flag.
Handle errors¶
void report_errors(Gpio& gpio) {
if (!gpio.HasAnyError()) {
return;
}
if (gpio.HasError(Error::I2CReadFail) || gpio.HasError(Error::I2CWriteFail)) {
std::printf("bus: NACK or timeout at 0x%02X\n", gpio.GetAddress());
}
if (gpio.HasError(Error::UnsupportedFeature)) {
std::printf("Agile I/O call on a PCA9555\n");
}
if (gpio.HasError(Error::InvalidPin)) {
std::printf("pin index above 15\n");
}
// Sticky until cleared, or until a later success clears its own flag.
gpio.ClearErrorFlags();
}
| Flag | Set when | Cleared by |
|---|---|---|
Error::I2CReadFail |
A register read failed after all retries, or initialisation found no chip | The next successful read |
Error::I2CWriteFail |
A register write failed after all retries | The next successful write |
Error::InvalidPin |
A pin above 15 | The next call with a valid pin |
Error::UnsupportedFeature |
An Agile I/O call on a PCA9555 (or an undetected variant) | ClearError() / ClearErrorFlags() |
Error::InvalidAddress |
ChangeAddress(address) outside 0x20 to 0x27 |
A successful ChangeAddress() |
Error::InvalidMask |
A mask-form call (SetMultipleDirections(), SetMultipleOutputs(), SetMultiplePolarities(), EnableMultipleInputLatches()) got a mask of 0; nothing is sent |
The next of those calls with a non-zero mask |
GetErrorFlags() returns the flags as a uint16_t; HasError() tests one, ClearErrorFlags(mask) clears the
bits in mask (all by default).
Restore defaults¶
ResetToDefault() writes the power-up values of every register (the Agile I/O bank too on a PCAL9555A), since the
chips have no reset pin. Configuration covers InitFromConfig() and the retry count.