Da C a Rust: un caso reale di sviluppo firmware bare-metal

In Develer amiamo sperimentare. Ogni anno formiamo dei gruppi di lavoro (GdL): piccoli team che si dedicano per alcuni giorni a studiare a fondo una tecnologia, analizzandone pro, contro e possibilità di adottarla in azienda.
L’obiettivo è condividere conoscenza e individuare tecnologie promettenti da portare nei progetti futuri.
Tra i GdL del 2025, uno in particolare ha affrontato un tema che sta facendo molto parlare di sé nel mondo dell’embedded: l’uso del linguaggio Rust in ambiente bare-metal.
Obiettivo: valutare Rust per lo sviluppo firmware
Il gruppo, composto da Pietro Lorefice, Luca Bennati, Giovan Battista Rolandi, Stefano Fedrigo, Marco Meloni, Mirko Banchi e Francesco Sacchi, si è posto una domanda concreta:
Quanto è maturo Rust per essere usato nello sviluppo firmware real-time, in alternativa al C?
Il focus principale è stato Embassy, un framework asincrono che si propone come riferimento per lo sviluppo embedded in Rust.
L’obiettivo era duplice:
- Analizzare pro e contro del linguaggio, con attenzione a performance, determinismo e sicurezza.
- Verificare sul campo la produttività e maturità di Embassy in un progetto realistico.
Rust e Embassy: un binomio interessante
Rust è un linguaggio general purpose che punta a unire memory safety, type safety e performance comparabili al C, grazie al backend LLVM e a un ecosistema in costante crescita.
Il compilatore supporta ufficialmente numerosi target embedded (inclusi Cortex-M e RISC-V), garantendo toolchain affidabili e cross-compilabili.
Embassy, invece, è un framework completo per applicazioni embedded asincrone:
- fornisce un HAL (Hardware Abstraction Layer) per varie architetture;
- include uno scheduler cooperativo o preemptive basato su
async/await; - offre driver integrati e componenti di rete;
- segue il principio delle zero-cost abstraction, cioè astrazioni ad alto livello senza overhead a runtime.
In altre parole, Embassy consente di scrivere codice di alto livello che gira su hardware di bassissimo livello, mantenendo il controllo tipico del C, ma con i benefici di Rust.
Il caso di studio: il firmware di un proiettore laser
Per il test pratico, il gruppo ha scelto di riscrivere in Rust il firmware di un proiettore laser utilizzato nelle macchine industriali per proiettare forme su piani di lavoro.
Il sistema si presta bene per l’esperimento perché è:
- eterogeneo (contiene logica real-time, rete, safety e controllo termico);
- conosciuto a fondo dal team (riducendo la curva di analisi del dominio);
- complesso ma finito, adatto per una riscrittura in tempi brevi.
Il proiettore laser funziona modulando un diodo laser che proietta un fascio su due specchietti galvanometrici controllati in posizione. In particolare, un fascio laser modulato incide su due specchietti XY controllati in posizione; gli specchietti deflettono il fascio e proiettano su un piano; la rotta da seguire viene inviata da un PC via rete ed eseguita ciclicamente.
Il sistema hardware
- Modulazione del diodo laser: segnale PWM.
- Controllo specchietti XY: due segnali analogici DAC.
- Lettura temperatura: ADC collegato al sensore del diodo.
- Controllo termico: cella di Peltier pilotata via PWM.
- Comunicazione: pacchetti UDP contenenti rotte, inviati dal PC tramite un protocollo custom chiamato TRPv2.
Il loop di controllo principale è real-time e gira a 50 kHz, quindi con vincoli stringenti di temporizzazione e jitter.
Architettura software e metodo di lavoro
Il firmware è stato riscritto da zero in Rust, mantenendo la stessa struttura modulare dell’originale in C.
Il progetto è stato suddiviso in più crate Rust, uno per ogni componente del firmware:
trpv2per il parsing del protocollo di rete proprietario;routesper la gestione delle rotte da proiettare (scompattamento, sicurezza, rotazione);sigprocper il processamento del segnale;firmwarecome crate principale che coordina tutto;- e un crate
logic-testper gli integration test su host.
L’organizzazione in crate e workspace Cargo ha permesso di sviluppare e testare i componenti in parallelo:
laser-v2/
├─ crc16/ # Calcolo CRC pacchetti
├─ trpv2/ # Parsing protocollo UDP
├─ routes/ # Gestione rotte laser
├─ sigproc/ # Filtri, PID, logica di controllo
├─ firmware/ # Applicazione principale (target)
├─ logic-test/ # Test logici su host
└─ xtask/ # Script CLI per build e deploy
Tutti i moduli sono stati scritti in Rust safe, senza uso diretto di unsafe, sfruttando le astrazioni del linguaggio per garantire sicurezza a compile-time.
La build è completamente riproducibile grazie alla toolchain e alle dipendenze vendorizzate.
Le principali componenti
TRPV2
Gestisce la ricezione dei pacchetti di rete contenenti le rotte:
- ricostruisce il payload dai chunk UDP (con controllo CRC);
- restituisce il pacchetto completo solo se integro;
- opera senza allocazione dinamica (buffer statici e
constgenerics).
Routes
Riceve le rotte e ne gestisce:
- lo scompattamento del payload;
- la sicurezza, generando una rotta circolare automatica in assenza di dati validi;
- la rotazione tra rotte pronte, libere e in proiezione, usando
StaticCelleRefCellper mantenere l’ownership in modo sicuro a runtime.
Firmware
È il collante tra tutti i moduli.
Divide il lavoro in task Embassy asincroni:
net_task→ gestisce lo stack di rete (embassy_net::Runner);route_net_task→ ricezione e parsing pacchetti UDP;peltier_control_task→ gestione termica;analog_out_task→ loop real-time del laser.
Lo scheduler di Embassy esegue due esecutori in parallelo:
uno cooperativo per i task di background, e uno preemptive ad alta priorità per il loop real-time.
Il loop principale del laser è scandito da un Ticker a 50 kHz, che legge i punti da un canale condiviso e aggiorna DAC e PWM:
let mut ticker = Ticker::every(Duration::from_micros(20));
loop {
ticker.next().await;
let p = POINTS_CHAN.receive().await;
dac_x.set(Value::Bit12Right(p.x() >> 2));
dac_y.set(Value::Bit12Right(p.y() >> 2));
diode_pwm.set_duty_cycle_percent(...);
}
Il risultato? Jitter di poche decine di nanosecondi – circa tre ordini di grandezza inferiore al periodo, ottimo per un’applicazione real-time.
Analisi del linguaggio Rust
Pro
- Sistema di build semplice ma potente, rispetto al classico caos di Makefile nel C.
- Tooling di alto livello (clippy, rustfmt, rustdoc, LSP).
- Type system ideale per modellare l’accesso all’hardware e prevenire errori di muxing o race condition.
- Ownership model che elimina intere classi di bug legati a concorrenza e race condition.
- Ecosistema no_std in forte crescita.
- Performance comparabili al C senza ottimizzazioni speciali.
- Codice dimezzato rispetto all’implementazione C.
- Nessun bug subdolo durante lo sviluppo.
Contro
- Curva di apprendimento molto ripida, specialmente per chi viene dal C.
- Dipendenza da crate esterni, spesso necessari per l’accesso sicuro all’hardware.
- Supporto architetturale ancora limitato (assenza di DSP, alcuni MCU).
- Workspace multi-target non ancora maturi e tooling IDE talvolta instabile.
Embassy: vantaggi e limiti
Pro
- Modello
async/awaitperfetto per scheduling cooperativo. - HAL condiviso tra più architetture (portabilità ~80%).
- Astrazioni zero-cost anche per operazioni complesse come DMA o interrupt.
- Facilita lo sviluppo di task real-time.
Contro
- Supporto ufficiale solo per poche architetture (STM32, nRF, ESP32, RPi).
- Parte del comportamento è poco trasparente.
- Stessa curva d’apprendimento ripida di Rust.
Le conclusioni del gruppo
“Abbiamo riscritto in tre giorni l’80% del firmware originale, con performance equivalenti al C e senza bug subdoli.”
Il gruppo ha concluso che Rust è assolutamente utilizzabile in ambiente bare-metal, anche per applicazioni real-time, purché si accetti l’iniziale complessità del linguaggio.
L’esperienza con Embassy è stata molto positiva: il framework semplifica lo sviluppo e consente un livello di astrazione elevato senza sacrificare efficienza.
Rust non è (ancora) il sostituto universale del C, ma in molti casi può già esserlo – in modo più sicuro, moderno e collaborativo.