USB Power Delivery è uno standard maturo e ben documentato, ma uno stack PD libero, completo e fuori dal silicon del vendor che lo ha scritto è ancora difficile da trovare. Per casi standard — caricatori, alimentatori, semplici dispositivi sink — gli stack chiusi forniti dai produttori di TCPC bastano e funzionano. Il problema arriva quando ti servono cose che lo stack chiuso non ti permette di toccare.
Perché siamo partiti
Stavamo lavorando a un caso d’uso non standard: un alt mode custom con signaling proprietario, VDM (Vendor Defined Messages) personalizzati e sincronizzazione fra dispositivi che richiedevano controllo fine sullo stack PD. Nessuno stack libero che avevamo trovato copriva questo perimetro. Lo stack vendor sarebbe stato perfetto se fossimo rimasti sul loro silicon e dentro i loro casi previsti — ma né l’una né l’altra cosa era opzione.
La scelta del TCPC ha pesato. Stavamo valutando due opzioni diffuse, il FUSB302 di ON Semi e il PTN5110 di NXP. Aperto il codice ON Semi, abbiamo trovato una struttura più intricata di quanto serviva al nostro caso e una documentazione che lasciava troppi vuoti da riempire sul banco. Il middleware NXP MCUXpresso per il PTN5110, al contrario, aveva un layering pulito: protocol engine, policy engine e driver TCPC separati con confini chiari. Per chi vuole prendere il codice e modificarlo, è una distinzione enorme.
Abbiamo deciso di partire da lì: refattorizzare il middleware NXP PTN5110 mantenendone intatto il policy engine — è quello che ha valore, è il pezzo collaudato — e lavorare sotto, all’interfaccia hardware, per renderlo portabile.
Cosa abbiamo dovuto cambiare
Il principale ostacolo era prevedibile: il middleware NXP è scritto per girare su MCU NXP, e chiama direttamente l’HAL NXP in tutti i posti dove tocca registri, timer e GPIO. Per portarlo altrove, bisognava isolare ogni chiamata di basso livello dietro un’interfaccia neutra e poi fornire implementazioni specifiche per platform.
La strategia è stata mantenere il core invariato e introdurre un HAL adapter per ognuno dei tre target che ci interessavano:
- ESP32-S3 (Espressif): FreeRTOS task model, ESP-IDF
- STM32 (STMicro): Cube HAL, FreeRTOS o bare-metal
- Arduino: loop cooperativo single-threaded
L’API dell’adapter è semplice da fuori: registrazione/deregistrazione di un’istanza PD, callback di timer ad alta risoluzione, busy-wait microsecondi per i timing critici. La complessità sta nel rispettare i constraint di ogni runtime senza chiedere al PD engine di sapere su cosa sta girando.
Sull’ESP32-S3, per esempio, il tick del protocollo PD viene generato da un timer software FreeRTOS a 1ms. Il punto delicato è che il PD engine va invocato per ogni istanza registrata, e la lista delle istanze può cambiare in qualsiasi momento — la callback deve essere safe rispetto a registrazioni concorrenti:
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]);
}
}
}
Lo snapshot in critical section evita che una registrazione o deregistrazione concorrente lasci la lista in stato inconsistente durante l’iterazione; fuori dalla sezione critica, il tempo speso a chiamare il PD engine non blocca le altre task. Su STM32 il pattern è equivalente con un timer hardware o un software timer FreeRTOS. Su Arduino, dove non c’è un task model, il tick viene generato dal loop() con un controllo millis() — più semplice ma con il caveat che il PD engine può perdere finestre di timing se il resto dello sketch impiega troppo. La porta Arduino è per questo in beta: funziona per scenari di sviluppo e per casi d’uso a basso traffico PD, ma non garantisce il timing per applicazioni di produzione ad alto carico.
Stato del repo e direzione
Le porte ESP32-S3 e STM32 sono stabili: passano i nostri test di message exchange completi su un analizzatore USB-PD basato su silicon Texas Instruments, e gestiscono negoziazione contratto PD 2.0 e 3.0 in entrambi i ruoli (source e sink). La porta Arduino è marcata beta — vedi sopra.
Lo stack è compliant by design: il core viene da un middleware che era già USB-IF certificato sui silicon NXP. Le nostre modifiche sono confinate all’HAL adapter e non toccano la logica protocollare. Non è equivalente a una certificazione formale del repo refattorizzato — quella richiederebbe un test ufficiale USB-IF — ma è una posizione di partenza solida.
Sul futuro: la roadmap a 12 mesi ha due voci concrete. La prima è il supporto FUSB302 come secondo TCPC, perché molti dispositivi hanno già hardware basato su quel controller e non vogliamo costringere chi parte da quell’hardware a cambiarlo per usare lo stack. La seconda è un frontend C++ sopra il core C: API più moderna con RAII e tipi forti per ridurre l’uso scorretto, più un’astrazione TCPC esposta al livello C++ che consenta di scegliere il controller indipendentemente dal resto dello stack. Sono due lavori distinti ma complementari — il primo aggiunge una piattaforma, il secondo apre la strada perché possiamo accogliere TCPC arbitrari senza riscrivere ogni volta.
Da dove viene il codice, e cosa è stato provato
Il valore di partire da un middleware vendor maturo, anche quando l’obiettivo è uscirne, è che ti regala il pezzo difficile — il policy engine PD certificato — e ti lascia il pezzo meccanico: separare l’hardware dal protocollo. Il porting è un esercizio di disciplina, non di reinvenzione: ogni volta che ti sembra che serva modificare il core, è probabile che la risposta giusta sia un’interfaccia in più nell’HAL adapter.
USBPD-Stack è open source, doppia licenza BSD-3-Clause (codice NXP originale) + MIT (aggiunte ST-LINE), e vive su GitHub: USBPD-Stack. Per la documentazione tecnica completa c’è la pagina wiki dedicata.
Lavori su USB-PD custom, alt mode proprietari, o casi d’uso che escono dai pattern dei chip vendor? Parliamone.