Skip to content

Quickstart

Talk to a PF1550 from your MCU: wire the I2C bus, write a small port class, identify the chip and read its rails.

Reading is safe; writing changes the board's supplies

This example only reads. The PF1550 powers the board it sits on, often including the MCU that talks to it: a wrong rail write can brown out the processor or over-volt a peripheral. Read the Portenta H7 profiles page before you apply a profile.

1. Wire it

MCU PF1550 Notes
I2C SDA SDA (pin 2) Pull up to VDDIO; up to 400 kHz
I2C SCL SCL (pin 3) Pull up to VDDIO
GPIO out (optional) STANDBY (pin 7) High moves RUN to STANDBY (default polarity)
GPIO in (optional) INTB (pin 9) Open drain, active low: pull it up
GND GND Common ground

The PF1550 answers on I2C once it has VSYS and its I/O supply VDDIO, so the host does not have to be the processor the PMIC supplies: an external jig can configure it first. Hardware has the full pin list and the electrical limits.

2. Add the driver

Add the repository as a submodule and link the hf::pf1550 CMake target (or add inc/ and the generated pf1550_version.h to your include path and build with C++20). Installation covers CMake and ESP-IDF.

git submodule add https://github.com/hardfoc/hf-pf1550-driver.git external/hf-pf1550-driver

3. Write the port

The port is the only platform code. It reads and writes one register, brings the bus up and sets the strap pins. Replace the platform_* calls with your SDK's.

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;
};

Porting explains each method and the strap-pin levels.

4. Read the rails

main.cpp
#include <cstdio>

#include "pf1550.hpp"

int bring_up_pmic(MyI2c& bus) {
  pf1550::PF1550<MyI2c> pmic(&bus);              // 7-bit address 0x08

  if (!pmic.EnsureInitialized()) {               // 1. bus up, DEVICE_ID == 0x7C
    return -1;                                   // wiring, pull-ups or address
  }

  pf1550::PmicState state{};                     // 2. where is the state machine?
  if (!pmic.ReadPmicState(state) || state != pf1550::PmicState::Run) {
    return -2;
  }

  pf1550::DiagnosticSnapshot snap{};             // 3. SW1..LDO3: set point and enable
  if (!pmic.ReadDiagnosticSnapshot(snap)) {
    return -3;
  }
  for (size_t i = 0; i <= static_cast<size_t>(pf1550::RailId::Ldo3); ++i) {
    const auto id = static_cast<pf1550::RailId>(i);
    std::printf("%s %u mV %s\n", pf1550::RailName(id), snap.rails[i].set_voltage_mv,
                snap.rails[i].enabled_run ? "on" : "off");
  }
  return 0;
}

What each step does:

  1. EnsureInitialized() calls your port's EnsureInitialized(), reads DEVICE_ID (register 0x00) and checks it is 0x7C. Later calls return at once.
  2. ReadPmicState() decodes STATE_INFO (0x67). Run means the start-up sequence has finished and the OTP-enabled regulators are on.
  3. ReadDiagnosticSnapshot() reads about 30 registers: identity, state, charger and VBUS sense, every rail's set point and enables, and the latched faults.

This example is built by the host CI, so it compiles against the current headers.

If it does not work

Symptom First thing to check
EnsureInitialized() returns false, DeviceNotFound set Pull-ups, SDA / SCL swapped, the 7-bit address (0x08, not 0x10)
WrongDeviceId set Another device answers at 0x08, or the part's OTP moved its address
Rails read 0 mV The snapshot read failed part-way: check read_ok and GetErrorFlags()
A rail's set point does not match its measured voltage SW1 / SW2 DVS mode (Rails and OTP)

Next

  • API guide: profiles, rail settings, status, faults and OTP.
  • Portenta H7 profiles: what the two built-in profiles write and in which order.
  • Examples: ESP32-C6 applications for probing, register dumps, diagnostics and provisioning.