Skip to content

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.