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.
Figure description and sources
From top to bottom. Your application talks to one PF1550
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¶
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, ®, 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
falsefor a NACK.EnsureInitialized()reportsDeviceNotFoundfrom 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¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/, with the driver instantiated over it. - Call
EnsureInitialized(): it reads DEVICE_ID and must returntrue. - Read STATE_INFO with
ReadPmicState():Runon a powered board. - Take a
ReadDiagnosticSnapshot()and compare the set points with a meter on each rail. - 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.