USBPD-Stack
Distribuzione cross-platform dello stack USB Power Delivery NXP per PTN5110.
Codice su GitHub ↗USBPD-Stack è una distribuzione open source dello stack USB Power Delivery NXP MCUXpresso (derivato dal middleware MCUXpresso 25.06.00): mantiene il collaudato driver TCPC per il PTN5110 e aggiunge un build system moderno, un layer HAL portabile e port multi-piattaforma. Il valore aggiunto ST-LINE è la portabilità: la stessa policy engine PD gira su ESP32-S3, STM32 e Arduino cambiando solo gli adapter. Questa pagina sintetizza README, guida d’uso e wiki del repository; il riferimento autoritativo resta GitHub.
Origine e valore aggiunto
Il cuore PD (policy engine e driver PTN5110) proviene dallo stack USB PD di NXP MCUXpresso, codice già provato in compliance. ST-LINE non riscrive il protocollo: lo riconfeziona in una struttura pulita, con separazione netta fra codice NXP ereditato (third_party/nxp_pd/) e aggiunte proprie.
L’apporto originale è il layer di portabilità: adapter HAL header-only per GPIO, I²C e primitive di sistema operativo, port di scheduling per ogni piattaforma, layout PlatformIO-ready e selezione del target a compile-time. Doppia licenza: BSD-3-Clause per i sorgenti NXP, MIT per le parti ST-LINE.
Cosa fa
Policy engine USB Power Delivery 2.0 / 3.0 con supporto al Type-C Port Controller PTN5110. Gestisce ruoli Source, Sink e DRP, scoperta delle capability, negoziazione dei contratti PDO/RDO e transizioni di alimentazione.
Funzioni opzionali abilitabili da configurazione: alt mode DisplayPort, Programmable Power Supply (PPS) di PD 3.0 e Fast Role Swap (opt-in).
I concetti USB Power Delivery in breve
USB Power Delivery (PD) è il protocollo che permette a due dispositivi su Type-C di negoziare tensione e corrente ben oltre i 5 V / 3 A del bus USB classico — fino a 240 W con la Extended Power Range. Pochi termini ricorrono in tutta la documentazione:
- Source / Sink — chi eroga e chi assorbe potenza. Un DRP (Dual-Role Power) può fare entrambi e scambiarsi il ruolo a runtime.
- PDO / RDO — la Source pubblicizza le proprie capability come lista di Power Data Object (es. “5 V 3 A”, “9 V 3 A”, “20 V 5 A”); il Sink ne sceglie uno e risponde con un Request Data Object che fissa il contratto di alimentazione.
- TCPC — Type-C Port Controller, il chip (qui il PTN5110) che implementa il PHY PD e l’interfaccia fisica del cavo; la policy engine software, che gira sull’MCU, decide ruoli, capability e transizioni dialogando col TCPC via I²C secondo l’interfaccia TCPCI.
- PPS — Programmable Power Supply di PD 3.0: tensione regolabile a passi fini (20 mV) per la ricarica diretta delle batterie.
La policy engine di USBPD-Stack è esattamente la macchina a stati che orchestra questo dialogo; il driver PTN5110 è il ponte verso il silicio.
Target supportati
ESP32-S3 (ESP-IDF) — FreeRTOS + HAL, stato Stable: tick a 1 kHz tramite software timer FreeRTOS condiviso.
STM32 F3/F4/H7 — FreeRTOS + Cube HAL, stato Stable: gestione ALERT via EXTI, timeout I²C configurabile.
Arduino (AVR/ESP/NXP) — loop cooperativo, stato Beta: usa TwoWire per l’I²C; PD_PortArduino_TaskTick() va chiamata dal loop principale.
Si seleziona esattamente una macro PD_CONFIG_TARGET_* prima della compilazione; ogni piattaforma ha la propria struttura pd_phy_<platform>_config_t che descrive le risorse MCU richieste.
Architettura
include/ — header pubblici del core (policy engine, configurazioni, tipi). src/ — sorgenti C del core: Policy Engine (usb_pd_policy.c, macchina a stati PD), Device Policy Manager (usb_pd_interface.c, crea le istanze PD e dispaccia gli eventi), timer di protocollo (usb_pd_timer.c) e alt mode.
third_party/nxp_pd/ — driver PTN5110 e codice NXP ereditato (accesso ai registri TCPC, interrupt ALERT, recovery del bus). support/ — astrazione HAL (GPIO, I²C, primitive OS) e relative implementazioni per piattaforma. port/ — glue dello scheduler che invoca periodicamente PD_TimerIsrFunction.
Per portare lo stack su una nuova MCU si implementa essenzialmente solo il layer support/ (più il port di scheduling): il core e il driver PTN5110 restano invariati.

Configurazione
Tutte le opzioni vivono in include/usb_pd_config.h. Oltre alla selezione del target (PD_CONFIG_TARGET_ESP32S3 / _STM32 / _ARDUINO; default ESP32-S3 se nessuna definita) i flag principali sono: PD_CONFIG_MAX_PORT (numero di porte PD simultanee), PD_CONFIG_ALT_MODE_SUPPORT / _DP_SUPPORT (alt mode DisplayPort), PD_CONFIG_PD3_PPS_ENABLE (PPS), PD_CONFIG_SOURCE_ROLE_ENABLE / _SINK_ROLE_ENABLE (limita i ruoli annunciati), PD_CONFIG_PD3_FAST_ROLE_SWAP_ENABLE.
Le macro si impostano dal build system (build_flags in PlatformIO, add_compile_definitions in CMake). Include path da aggiungere: include/, port/include, support/include, third_party/nxp_pd/include; sorgenti da compilare: src/, port/src, support/src, third_party/nxp_pd/src.
Integrazione
Prerequisiti: un PTN5110 collegato via I²C con il pin ALERT su un GPIO capace di interrupt sul fronte, pull-up corretti su SDA/SCL e un toolchain C per il target (ESP-IDF, STM32Cube, Arduino, PlatformIO).
Flusso tipico: (1) descrivere l’hardware in pd_phy_<platform>_config_t (GPIO SDA/SCL e priorità per ESP32-S3; I2C_HandleTypeDef*, pin ALERT e IRQ per STM32; TwoWire* e pull-up per Arduino); (2) compilare pd_instance_config_t con ruoli e tabelle PDO e phyType = kPD_PhyPTN5110; (3) implementare i callback pd_stack_callback_t e pd_power_handle_callback_t; (4) chiamare PD_InstanceInit; (5) eseguire PD_InstanceTask in un task FreeRTOS o nel loop Arduino, assicurando il tick condiviso da 1 ms.
Perché il tick è da 1 ms, e non da 10
Il tick condiviso da 1 ms non è una convenzione comoda: è il numero che esce imponendo che i timer di protocollo cadano dentro le loro finestre. Il PD non definisce scadenze puntuali ma intervalli — il più stretto è il tSenderResponse, fra 24 e 30 ms: rispondere prima di 24 ms o dopo 30 ms viola il protocollo allo stesso modo.
La finestra utile è quindi larga 6 ms, e un tick di periodo T introduce fino a T di errore di quantizzazione sulla scadenza, perché la macchina a stati può accorgersi del tempo trascorso solo al tick successivo:
| Periodo del tick | Errore massimo | % della finestra | Tick nella finestra | Verdetto |
|---|---|---|---|---|
| 0,5 ms | 0,5 ms | 8,3 % | 12 | utilizzabile |
| 1 ms | 1 ms | 16,7 % | 6 | utilizzabile |
| 2 ms | 2 ms | 33,3 % | 3 | utilizzabile |
| 5 ms | 5 ms | 83,3 % | 1 | al limite |
| 10 ms | 10 ms | 166,7 % | 0 | fuori specifica |
| 20 ms | 20 ms | 333,3 % | 0 | fuori specifica |
Con 1 ms l’incertezza vale il 17 % della finestra e ci stanno sei tick dentro: c’è margine sia per l’errore di quantizzazione sia per la latenza di risposta dell’I²C verso il TCPC. Con 5 ms la quantizzazione da sola mangia l’83 % della finestra e non resta niente per il resto. Con 10 ms la finestra viene sfondata dal solo tick: non si tratta di prestazioni marginali, la scadenza può essere mancata.
È anche il motivo per cui il tick va condiviso e non moltiplicato: due timer software indipendenti che scandiscono lo stesso stack sommano i propri errori di quantizzazione, e su una finestra da 6 ms la somma si nota. Sulle piattaforme FreeRTOS conviene inoltre verificare che configTICK_RATE_HZ sia almeno 1000, perché un tick di sistema più lento fa da pavimento a qualunque timer software costruito sopra.
Cosa serve davvero per un port nuovo
«Si implementa solo il layer support/» è vero ma non dice quanto lavoro sia. In concreto il layer deve fornire quattro cose, e sono queste che decidono se una piattaforma è adatta:
Cosa deve fornire il layer support/ |
Perché lo stack lo chiede |
|---|---|
| I²C: lettura e scrittura di registri a 8 bit, bloccanti, con timeout | è l’unico canale verso il TCPC: ogni transizione della macchina a stati diventa un accesso a registro |
| GPIO: un ingresso con interrupt sul fronte di discesa (ALERT) | il TCPC non è interrogabile a polling senza perdere eventi: segnala e va servito |
Tick periodico da 1 ms che invochi PD_TimerIsrFunction |
è la base tempi di tutti i timer di protocollo — vedi il conto sopra |
| Primitive OS: un task o un loop, e una mutua esclusione sull’I²C | il bus è condiviso con il resto dell’applicazione; l’ISR di ALERT e il task non devono accedervi insieme |
Il criterio di ammissibilità di una MCU si riduce quindi a due domande: ha un GPIO con interrupt su fronte (non solo su livello) e può garantire un tick da 1 ms? Se sì, il port è meccanico. Se la risposta alla seconda è no — tipico dei loop cooperativi molto carichi — il problema non si risolve nel layer support/: si risolve alleggerendo il loop, ed è la ragione per cui il target Arduino è dichiarato Beta e non Stable.
Le resistenze sulle CC: perché non compaiono nel software
Una domanda che ricorre nell’integrazione: dove si impostano i 3 A da annunciare? La risposta è che non si impostano nel software dello stack. Prima che il PD entri in gioco, il Type-C dichiara le proprie intenzioni con delle resistenze sulle linee CC, e chi le legge è il TCPC:
| Resistore | Valore | Chi lo mette | Cosa dichiara |
|---|---|---|---|
| Rp | 56 kΩ | Source | solo USB di default (500/900 mA) |
| Rp | 22 kΩ | Source | 1,5 A a 5 V |
| Rp | 10 kΩ | Source | 3 A a 5 V |
| Rd | 5,1 kΩ | Sink | «sono un sink» |
| Ra | ≈ 1 kΩ | cavo attivo | «sono un cavo, non un dispositivo» |
Questo livello — chiamato Type-C e non Power Delivery — funziona anche senza alcun PD: è il motivo per cui un caricatore Type-C senza PD eroga 3 A a un telefono senza scambiare un solo messaggio. Il PD entra sopra, per andare oltre i 5 V. Nel PTN5110 le Rp e Rd sono interne e commutate via registro, quindi lo stack le governa indirettamente attraverso il ruolo che dichiara: non c’è nulla da montare sul PCB oltre alla Rd se il dispositivo è solo sink senza TCPC.
Diagnostica e note di piattaforma
Diagnostica: si abilitano le macro di debug in usb_pd_config.h per tracciare lo scambio di messaggi; PD_Control(..., PD_CONTROL_GET_PD_STATE, ...) restituisce il nodo corrente della macchina a stati; il driver PTN5110 espone helper per leggere lo stato di fault e regolare la VBUS.
ESP32-S3: l’adapter installa un timer da 1 ms e usa l’istanza driver I²C i2c_instance di pd_phy_esp32s3_config_t; PD_InstanceTask gira in un task dedicato e il callback del timer è registrato non appena è presente un’istanza.
STM32 (F3/F4/H7): richiede handle Cube HAL e FreeRTOS; si forniscono puntatore I²C HAL, info GPIO ALERT e priorità IRQ in pd_phy_stm32_config_t, e l’adapter gestisce il dispatch EXTI tramite PD_PortStm32_DispatchExti. Arduino: design cooperativo, si chiamano PD_PortArduino_TaskTick() e PD_InstanceTask() dentro loop(), I²C via TwoWire.
Conformità, stato e roadmap
docs/library_usage.md contiene la guida d’integrazione passo-passo; docs/compliance_test_report/ raccoglie catture Ellisys dello stack NXP originale. Release iniziale 0.1.0 (2025-11-06); sviluppo sul branch develop.
Roadmap dichiarata: port di un layer TCPC FUSB302B con helper di gestione ALERT, refresh dei report di compliance Ellisys sulle nuove piattaforme e progetti di esempio pronti al build per ogni target. Riferimenti: scheda prodotto PTN5110 (NXP) e USB Type-C Port Controller Interface Specification (UM11056).
Un progetto simile?
Acustica, embedded, strumenti di calcolo: se hai un caso d’uso vicino, parliamone.