Ho passato sei mesi su un progetto firmware in cui il workflow Git era il singolo più grande killer di produttività. Non il compilatore, non il debugger, non l'hardware — il processo di version control. I merge richiedevano ore. I file binari gonfiavano il repository. La CI impiegava quarantacinque minuti a ogni push. Avevamo quattro branch long-lived divergenti al punto che un merge pulito era statisticamente improbabile.
Da allora ho lavorato su oltre una dozzina di progetti firmware con team di dimensioni variabili, famiglie di MCU e toolchain diverse. I workflow Git che vedo più spesso nel mondo embedded o sono copiati dal web development (GitFlow con release branch long-lived che non hanno senso per firmware) o completamente ad-hoc ("pusha su main quando compila"). Nessuno dei due funziona bene nella pratica.
Ecco il workflow a cui sono arrivato dopo anni di tentativi ed errori. È costruito per i vincoli specifici del firmware: asset binari, dipendenze hardware, cicli CI lunghi, e la necessità di riprodurre una build esatta per certificazione o debug sul campo.
Il Modello di Branch: Trunk-Based con Feature Branch Brevissimi
Il cuore del workflow è un modello trunk-based semplificato con un singolo branch long-lived (main) e feature branch molto brevi. Niente develop, niente release, niente hotfix — tutti questi creano overhead di merge senza benefici significativi per progetti firmware.
main ──●─────●─────────●──────────●────────●──
\ / / / /
f1●──● f2●────────● /
\ / /
f3●─────────●───────────●
Ogni feature branch vive al massimo due o tre giorni. Se una feature richiede più tempo, viene scomposta in unità più piccole. Questa è la regola singola più impattante: nessun branch vive più di 48 ore. I branch brevi si mergiano pulitamente, si revisionano velocemente, e mantengono il modello mentale del codice accurato per tutto il team.
La regola che impongo in ogni team in cui entro: se il tuo branch ha più di tre giorni, lo rebasi su main, lo schiacci in un singolo commit, e lo pushi per review entro un'ora. Nessuna eccezione.
Gestione dei File Binari: Git LFS con una Politica Rigorosa
I repository firmware sono eccezionalmente inclini a una cattiva gestione dei file binari. File hex, librerie compilate, modelli CAD, datasheet PDF e immagini di bootloader finiscono nel repo e trasformano ogni clone e fetch in un'operazione lenta e dolorosa. Ho visto un repo firmware crescere fino a 4.7 GB in meno di un anno perché qualcuno aveva committato pacchetti STM32Cube compilati.
Ecco la politica che uso:
- Binari che cambiano regolarmente (binari di release, artifact di test compilati) — conservarli in Git LFS con una regola
.gitattributesper ogni estensione rilevante:*.hex filter=lfs diff=lfs merge=lfs -text,*.bin filter=lfs diff=lfs merge=lfs -text. - Binari che non dovrebbero stare nel repo (datasheet, manuali di riferimento, modelli CAD, librerie compilate dai vendor) — conservarli fuori dal repo in un drive condiviso o artifact store. Aggiungere una riga
.gitignoreesplicita con un commento:# Librerie vendor — scaricare dal sito ST. - Toolchain precompilate — mai committarle. Bloccare la versione della toolchain in un
CMakeLists.txt,Makefileo Dockerfile.
Aggiungo anche un job CI che fallisce se un commit introduce un file binario oltre 1 MB non tracciato da LFS. Questo previene il problema "qualcuno ha committato un PDF per sbaglio" che ogni team firmware incontra almeno una volta.
Convenzione dei Commit: Un Singolo Cambiamento per Commit
I team firmware tendono a committare tutto insieme — "aggiunta feature X, fixato bug Y, pulita formattazione, aggiornato linker script." Questo rende il bisecting quasi inutile e la code review estenuante.
La convenzione che spingo è semplice: un cambiamento logico per commit. Se tocchi un file driver e un linker script nello stesso commit, devono far parte dello stesso cambiamento logico. Se non è così, dividi il commit.
Il formato del messaggio di commit che uso:
componente: breve descrizione della modifica Spiegazione più lunga se necessario — che problema risolve, perché questo approccio è stato scelto rispetto ad alternative. Fixes: #ID_ISSUE (se applicabile) See also: SHA_DEL_COMMIT_CORRELATO (se applicabile)
Esempi concreti da un recente progetto STM32U5:
adc: aggiunta configurazione oversampling per serie U5 L'STM32U5 supporta l'oversampling hardware fino a 256x. Questo commit espone il rapporto e lo shift di oversampling tramite LL_ADC_Init. La dimensione del buffer DMA viene aggiustata automaticamente quando l'oversampling è attivo. See also: a3f8e21 linker: aumento SRAM2 a 64 KB per U5A5ZJ Il linker script predefinito allocava solo 32 KB a SRAM2, causando esaurimento heap durante l'avvio di FreeRTOS su schede con configurazioni da 2 MB di flash.
Pipeline CI: Trigger Intelligenti, Non Build-Tutto
Nel web development, la CI gira in meno di un minuto. Nel firmware, una build completa di un progetto multi-target può richiedere quindici-trenta minuti. Eseguire la suite completa a ogni push è dispendioso e insegna al team a ignorare i fallimenti della CI.
Strutturo la pipeline CI in stadi con trigger crescenti:
Stage 1 — Analisi statica (sempre, ~2 minuti) ├── cppcheck sui file modificati ├── clang-format diff sui file modificati └── controllo file binari >1 MB senza LFS Stage 2 — Compila target modificati (push su feature branch, ~5-10 min) ├── build solo dei target affetti dai file sorgente modificati └── usando uno script di analisi dipendenze Stage 3 — Build completa + test (merge su main, ~20-30 min) ├── build di tutti i target ├── test unitari su host (Ceedling/CMock) └── test di integrazione su hardware se disponibile
Il trucco: lo Stage 2 usa un semplice script Python che mappa i file modificati ai target di build. Se hai cambiato solo un driver UART per STM32G4, la CI non ricostruisce il target STM32U5. Questo taglia il tempo medio di CI sui feature branch da venticinque minuti a meno di otto.
Tag di Rilascio: Tag Riproducibili
I rilasci firmware non sono come i deployments web. Non puoi fare rollback a un'immagine container — hai bisogno della fonte esatta + toolchain + configurazione che ha prodotto il binario in esecuzione sul campo. Questo è critico per applicazioni medicali, automotive e industriali dove la tracciabilità di certificazione è obbligatoria.
La mia convenzione per i tag:
v1.2.3+build20260705-gcc12.3-cm4 │ │ │ │ │ │ │ └─ architettura target │ │ └─────────── versione toolchain │ └───────────────────────────── data build └──────────────────────────────────── versione semantica
Ogni tag di release è accompagnato da un file release-notes-v1.2.3.md che documenta:
- Git hash e tag esatti
- Versione toolchain (inclusi GCC ARM, newlib e patch)
- Configurazione MCU (dimensione flash, RAM, option bytes)
- SHA256 dei file .hex e .bin prodotti
- Problemi noti e copertura dei test
Taggo anche gli artifact di build in Git LFS con lo stesso tag di release: git fetch --tags && git checkout v1.2.3+build20260705-gcc12.3-cm4 -- artifacts/.
Checklist pratica
- Mantieni i feature branch sotto le 48 ore — rebasa e squash immediatamente i branch più vecchi
- Configura Git LFS per file .hex, .bin, .elf con regole .gitattributes esplicite
- Aggiungi un gate CI che rifiuti binari non tracciati oltre 1 MB
- Scrivi messaggi di commit in formato "componente: azione" — un cambiamento per commit
- Dividi la CI in analisi statica, build solo-target-modificati e build completa
- Usa tag di release riproducibili con info su toolchain e target
- Documenta gli artifact con hash, toolchain e configurazione MCU
- Blocca esplicitamente la versione della toolchain — mai assumere "l'ultimo GCC"
- Esegui bisect automatici sulle regressioni:
git bisect start --first-parentevita il rumore dei merge commit
Il framework che uso
Ho codificato questo workflow in uno script firmware-git-init che eseguo all'inizio di ogni progetto. Crea .gitattributes, .gitignore, lo scheletro della configurazione CI, e un CONTRIBUTING.md che spiega le convenzioni di branching e commit a chiunque si unisca al team. Ci vogliono dieci minuti per impostarlo; il tempo risparmiato solo in conflitti di merge prevenuti si ripaga nella prima settimana.
Cosa ha funzionato per me
La regola del branch breve è quella a cui i team resistono di più e che offre il maggior valore. Ogni volta che mi sono unito a un team che diceva "i nostri branch durano due settimane perché la feature è complessa", la ragione vera era che la feature non era scomposta in unità indipendenti. Una volta imposto il limite di 48 ore, il team ha imparato naturalmente a suddividere meglio il lavoro — e la qualità del codice è migliorata perché le review erano più piccole, più veloci e più mirate.
La politica sui binari è al secondo posto. Rimuovere i blob vendor dal repository ha ridotto il tempo di clone su un progetto da ventidue minuti a meno di due. Il team è passato dall'evitare git clone all'eseguirlo regolarmente per il setup CI, migliorando la riproducibilità delle build su tutta la linea.
Il firmware embedded ha vincoli unici, ma Git non è uno di questi. I workflow progettati per team web e mobile funzionano anche nel firmware — devi solo tenere conto dei cicli CI più lunghi e dei file binari. I principi sono gli stessi: branch brevi, commit chiari, gate di qualità automatici e rilasci riproducibili.
📬 Commenti / discussione
Scrivimi a: comments@carrese.eu — includi l'URL dell'articolo così posso seguire. Per correzioni o domande più approfondite, di solito rispondo entro 48 ore.