Skip to content

Porting

Run the driver on a new MCU by writing one class: the bus interface for I2C and the strap pins.

The driver never calls your SDK directly. It calls four methods (and an optional fifth) on a class you derive from pf1550::BusInterface<Self> (in inc/pf1550_i2c_interface.hpp). The base class uses CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.

Layer diagram: the application calls the hardware-agnostic PF1550 driver, whose operations read and write the PMIC's registers, which uses the CRTP bus interface, which calls the platform port you write, which drives the I2C bus and the STANDBY and USB strap pins of the PF1550.Layer diagram: the application calls the hardware-agnostic PF1550 driver, whose operations read and write the PMIC's registers, which uses the CRTP bus interface, which calls the platform port you write, which drives the I2C bus and the STANDBY and USB strap pins of the PF1550.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application talks to one PF1550 object per PMIC. Its public operations (listed from inc/pf1550.hpp) read and write the PF1550's registers: identification, named register sequences (profiles), rail set points, strap pins, OTP, interrupts and diagnostics. The communication interface (pf1550::BusInterface in inc/pf1550_i2c_interface.hpp) is a CRTP base: four methods are required, DelayUs() has a default. The platform port is the only code you write. Below it is the chip.

Drawn from: inc/pf1550.hpp PF1550 public members; inc/pf1550_i2c_interface.hpp BusInterface; figure module docs/diagrams/figures/driver-layers.py.

What you implement

Method Required Contract
EnsureInitialized() yes Bring the bus up once; return true when it is ready. Called by the driver's EnsureInitialized() and before every profile
Write(addr, reg, data, len) yes [addr+W] [reg] [data...] STOP. The driver always sends len = 1. Return false on a NACK or timeout
Read(addr, reg, data, len) yes [addr+W] [reg], repeated START, [addr+R], len bytes, STOP. The driver always reads one byte
GpioSet(pin, signal) yes Drive CtrlPin::Standby, UsbVbusEn or UsbOtgEn to Active / Inactive; do nothing for a pin your board does not wire
DelayUs(us) no Block for at least us microseconds. The base version returns at once, so profile waits and the 2 ms OTP wait are skipped without it

addr is the 7-bit address; your SDK may want it shifted. The base class also provides GpioSetActive(pin) and GpioSetInactive(pin), which call your GpioSet().

The port

my_i2c.hpp
class MyI2c : public pf1550::BusInterface<MyI2c> {
public:
  // Bring the bus up once; the driver calls this before it first talks to the chip.
  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 {
    if (len != 1) {
      return false;  // the driver writes one register at a time
    }
    const uint8_t frame[2] = {reg, data[0]};
    return platform_i2c_write(addr, frame, sizeof frame);
  }

  // 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, &reg, 1, data, len);
  }

  // Strap pins wired to the MCU. Active means: STANDBY high (standby), USB enables high.
  void GpioSet(pf1550::CtrlPin pin, pf1550::GpioSignal signal) noexcept {
    const int gpio = gpioFor(pin);
    if (gpio >= 0) {
      platform_gpio_write(gpio, signal == pf1550::GpioSignal::Active);
    }
  }

  // Optional: profiles wait between some writes. Without it, the waits are skipped.
  void DelayUs(uint32_t us) noexcept {
    platform_delay_us(us);
  }

private:
  static int gpioFor(pf1550::CtrlPin pin) noexcept {
    switch (pin) {
    case pf1550::CtrlPin::Standby:   return 10;  // your board's GPIO numbers; -1 = not wired
    case pf1550::CtrlPin::UsbVbusEn: return 11;
    case pf1550::CtrlPin::UsbOtgEn:  return 12;
    default:                         return -1;
    }
  }

  bool ready_ = false;
};

Rules the transfers must follow:

  • Return false for a NACK. EnsureInitialized() reports DeviceNotFound from it, and a profile stops at the first failed write only if the port tells the truth.
  • Recover after a NACK. The next transfer must work. Some controllers need a bus reset after a NACK; do it inside Read() / Write().
  • Bounded time. Use a timeout; a stuck bus must not block forever. Ten milliseconds is generous at 400 kHz.
  • At most 400 kHz. The PF1550 implements fast mode. Keep SCL and SDA edges slower than 20 ns.

Strap-pin levels

GpioSet() turns a logical state into a level. With the PF1550's default STANDBYINV = 0:

CtrlPin Active Inactive
Standby High (standby) Low (RUN)
UsbVbusEn, UsbOtgEn (Portenta) High (enabled) Low

If your board inverts a signal, or a profile sets STANDBYINV = 1, swap the levels in GpioSet(); the driver code stays the same.

Reference implementation

The ESP32 examples use esp32_pf1550_bus.hpp: an ESP-IDF i2c_master port (400 kHz by default, internal pull-ups on, 1 s transfer timeout) with optional strap GPIOs from Kconfig, and delays through vTaskDelay() (1 ms and longer) or esp_rom_delay_us(). Start from it for any ESP-IDF target.

Through hf-core

In the HardFOC HAL, hf-core wraps this driver in Pf1550Handler (handlers/pf1550/, enabled with -DHF_CORE_ENABLE_PF1550=ON). The handler builds the port from a BaseI2c and optional BaseGpio strap pins and serialises calls with a mutex; use it there instead of writing a port.

Check your port

  1. Build it: the port on this page is compiled by the host CI from tests/docs/, with the driver instantiated over it.
  2. Call EnsureInitialized(): it reads DEVICE_ID and must return true.
  3. Read STATE_INFO with ReadPmicState(): Run on a powered board.
  4. Take a ReadDiagnosticSnapshot() and compare the set points with a meter on each rail.
  5. Read GetErrorFlags() after a few hundred calls: it should stay 0 on a healthy bus.

Threads

The driver is not thread-safe. Give each PF1550 one owner task, or guard every call on a driver object (and its port) with one mutex. A boot sequence and a monitor task that share one PMIC need that mutex.