# Taking a USB Power Delivery stack outside its silicon

> NXP's USB-PD middleware runs natively on NXP. We refactored it to run on ESP32-S3, STM32 and Arduino as well — with a clean HAL adapter and a single policy engine.

Published: 2026-04-07
Category: firmware
Tag: usb-pd, embedded, hal, stm32, esp32-s3, arduino, open-source

Page: <https://www.stline.it/en/blog/usbpd-stack-portable-cross-platform/>

---

USB Power Delivery is a mature, well-documented standard, but a complete free PD stack that runs outside its vendor's silicon is still hard to come by. For standard cases — chargers, power supplies, simple sink devices — the closed stacks shipped by TCPC manufacturers are perfectly fine. The trouble starts when you need things the closed stack won't let you touch.

## Why we started

We were working on a non-standard case: a custom alt mode with proprietary signalling, custom VDMs (Vendor Defined Messages), and device-to-device synchronisation that required fine control over the PD stack. None of the free stacks we found covered that perimeter. The vendor stack would have been perfect if we'd stayed on their silicon and inside their intended use cases — but neither was an option.

The TCPC choice mattered. We were evaluating two common options: ON Semi's FUSB302 and NXP's PTN5110. Opening the ON Semi code, we found a more intricate structure than we needed for our case and documentation that left too many gaps to fill on the bench. NXP's MCUXpresso middleware for the PTN5110, on the other hand, had clean layering: protocol engine, policy engine and TCPC driver separated with sharp boundaries. For someone who wants to take the code and modify it, that distinction is enormous.

We decided to start there: refactor the NXP PTN5110 middleware while keeping its policy engine intact — that's the part with value, the proven piece — and work below it, at the hardware interface, to make it portable.

## What we had to change

The main obstacle was predictable: the NXP middleware is written to run on NXP MCUs, and calls NXP's HAL directly wherever it touches registers, timers and GPIOs. To port it elsewhere, every low-level call had to be isolated behind a neutral interface and then implemented per platform.

The strategy was to keep the core untouched and introduce a **HAL adapter** for each of the three targets we cared about:

- **ESP32-S3** (Espressif): FreeRTOS task model, ESP-IDF
- **STM32** (STMicro): Cube HAL, FreeRTOS or bare-metal
- **Arduino**: cooperative, single-threaded loop

The adapter API is simple from the outside: register/unregister a PD instance, a high-resolution timer callback, microsecond busy-wait for tight timing. The complexity lives in respecting each runtime's constraints without asking the PD engine to know what it's running on.

On the ESP32-S3, for example, the PD protocol tick is generated by a 1 ms FreeRTOS software timer. The delicate point is that the PD engine has to be invoked for every registered instance, and that list can change at any moment — the callback has to be safe with respect to concurrent registrations:

```c
static void PD_PortEsp32S3_TimerCallback(TimerHandle_t timer) {
    pd_instance_t *snapshot[PD_CONFIG_MAX_PORT] = {0};

    taskENTER_CRITICAL();
    memcpy(snapshot, s_registeredInstances, sizeof(snapshot));
    taskEXIT_CRITICAL();

    for (size_t i = 0; i < PD_CONFIG_MAX_PORT; ++i) {
        if (snapshot[i] != NULL) {
            PD_TimerIsrFunction((pd_handle)snapshot[i]);
        }
    }
}
```

The snapshot inside the critical section prevents a concurrent registration or unregistration from leaving the list in an inconsistent state during iteration; outside the critical section, the time spent calling the PD engine doesn't block other tasks. On STM32 the pattern is equivalent, with either a hardware timer or a FreeRTOS software timer. On Arduino, where there is no task model, the tick is generated from `loop()` with a `millis()` check — simpler, but with the caveat that the PD engine can miss timing windows if the rest of the sketch takes too long. The Arduino port is in beta for this reason: it works for development scenarios and low-PD-traffic use cases, but it doesn't guarantee timing for high-load production applications.

## State of the repo and direction

The ESP32-S3 and STM32 ports are stable: they pass our full message-exchange tests on a USB-PD analyser built around Texas Instruments silicon, and handle PD 2.0 and 3.0 contract negotiation in both roles (source and sink). The Arduino port is marked beta — see above.

The stack is **compliant by design**: the core comes from middleware that was already USB-IF certified on NXP silicon. Our modifications are confined to the HAL adapter and do not touch protocol logic. That is not equivalent to a formal certification of the refactored repo — that would require an official USB-IF test — but it's a solid starting point.

Looking ahead: the 12-month roadmap has two concrete items. The first is **FUSB302** support as a second TCPC, because many devices already have hardware based on that controller and we don't want to force people starting from such hardware to change it just to use the stack. The second is a **C++ frontend** on top of the C core: a more modern API with RAII and strong types to reduce misuse, plus a TCPC abstraction exposed at the C++ level so the controller can be chosen independently of the rest of the stack. These are two distinct but complementary pieces of work — the first adds a platform, the second clears the way for arbitrary TCPCs without rewriting everything.

## Where the code comes from, and what was tested

The value of starting from a mature vendor middleware, even when the goal is to break out of it, is that it hands you the hard part — the certified PD policy engine — and leaves you the mechanical part: separating hardware from protocol. Porting is an exercise in discipline, not reinvention: every time it feels like the core needs to change, the right answer is usually one more interface in the HAL adapter.

---

USBPD-Stack is open source, dual-licensed under BSD-3-Clause (original NXP code) and MIT (ST-LINE additions), and lives on GitHub: [USBPD-Stack](https://github.com/stefanofante/USBPD-Stack). For the full technical documentation, see the [dedicated wiki page](/en/wiki/usbpd-stack/).

Working on custom USB-PD, proprietary alt modes, or use cases that fall outside the patterns the chip vendors had in mind? [Let's talk](/en/contact/).
