From C to Rust: a real case of bare-metal firmware development

At Develer, we love to experiment. Every year we set up working groups – small teams that spend a few days diving deep into a specific technology, analyzing its pros, cons, and potential adoption within the company.
The goal is to share knowledge and identify promising technologies that could make their way into future projects.
Among the 2025 working groups, one in particular tackled a topic that’s been generating a lot of buzz in the embedded world: using the Rust programming language in bare-metal environments.
Goal: evaluating Rust for firmware development
The group – composed of Pietro Lorefice, Luca Bennati, Gianbattista Rolandi, Stefano Fedrigo, Marco Meloni, Mirko Banchi, and Francesco Sacchi – started from a concrete question:
How mature is Rust for real-time firmware development as an alternative to C?
Their main focus was Embassy, an asynchronous framework that aims to be a reference point for embedded development in Rust.
The goal was twofold:
- Analyze the pros and cons of the language, with particular attention to performance, determinism, and safety.
- Evaluate Embassy’s productivity and maturity through hands-on testing in a realistic project.
Rust and Embassy: an interesting pairing
Rust is a general purpose language that aims to combine memory safety, type safety, and C-like performance, thanks to its LLVM backend and a steadily growing ecosystem.
The compiler officially supports several embedded targets (including Cortex-M and RISC-V), providing reliable, cross-compilable toolchains.
Embassy, on the other hand, is a comprehensive framework for asynchronous embedded applications:
- it provides a HAL (Hardware Abstraction Layer) for multiple architectures;
- includes a cooperative or preemptive scheduler based on
async/await; - offers integrated drivers and networking components;
- and follows the principle of zero-cost abstractions, meaning high-level APIs without runtime overhead.
In other words, Embassy lets you write high-level code that runs on low-level hardware, maintaining C-like control while gaining the safety and expressiveness of Rust.
The case study: the firmware of a laser projector
For the practical test, the group chose to rewrite in Rust the firmware of a laser projector used in industrial machines to project shapes onto work surfaces.
The system was well suited for the experiment because it is:
- heterogeneous (it includes real-time logic, networking, safety, and thermal control);
- well known to the team (reducing the domain learning curve);
- complex but contained, making it suitable for a full rewrite in a short time.
The laser projector works by modulating a laser diode that projects a beam onto two galvanometric mirrors controlled in position. In particular, a modulated laser beam strikes two XY mirrors whose positions are precisely controlled; the mirrors deflect the beam and project it onto a plane; the path to follow is sent from a PC over the network and executed cyclically.
The hardware system
- Laser diode modulation: PWM signal.
- XY mirror control: two analog DAC signals.
- Temperature reading: ADC connected to the diode sensor.
- Thermal control: Peltier cell driven via PWM.
- Communication: UDP packets containing paths, sent from the PC through a custom protocol called TRPv2.
The main control loop is real-time and runs at 50 kHz, with strict timing and jitter constraints.
Software architecture and workflow
The firmware was rewritten from scratch in Rust, keeping the same modular structure as the original C version.
The project was divided into multiple Rust crates, one for each firmware component:
trpv2for parsing the network protocol;routesfor managing the projection paths (unpacking, safety, rotation);sigprocfor signal processing;firmwareas the main crate that coordinates all components;- and a
logic-testcrate for integration tests on the host.
The organization into crates and a Cargo workspace made it possible to develop and test components in parallel:
laser-v2/
├─ crc16/ # Packet CRC calculation
├─ trpv2/ # UDP protocol parsing
├─ routes/ # Laser path management
├─ sigproc/ # Filters, PID, control logic
├─ firmware/ # Main application (target)
├─ logic-test/ # Logic tests on host
└─ xtask/ # CLI scripts for build and deploy
All modules were written in safe Rust, with no direct use of unsafe, leveraging the language’s abstractions to ensure compile-time safety.
The build is fully reproducible thanks to the toolchain and vendored dependencies.
The main components
TRPV2
Handles the reception of network packets containing the paths:
- reconstructs the payload from UDP chunks (with CRC checking);
- returns the complete packet only if it is intact;
- operates without dynamic memory allocation (using static buffer and
constgenerics).
Routes
Receives the paths and manages:
- unpacking of the payload;
- safety, by generating an automatic circular path when no valid data is available;
- rotation among ready, free, and active paths, using
StaticCelleRefCellto maintain ownership safely at runtime.
Firmware
Acts as the glue between all modules.
Divides the workload into asynchronous Embassy tasks:
net_task→ manages the network stack (embassy_net::Runner);route_net_task→ receives and parses UDP packets;peltier_control_task→ handles thermal management;analog_out_task→ real-time laser control loop.
The Embassy scheduler runs two executors in parallel: a cooperative one for background tasks, and a high-priority preemptive one for the real-time loop.
The main laser loop is driven by a 50 kHz Ticker, which reads points from a shared channel and updates DAC and PWM outputs:
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(...);
}
The result? Jitter of just a few tens of nanoseconds – about three orders of magnitude lower than the loop period, an excellent outcome for a real-time application.
Analysis of the Rust language
Pros
- A simple yet powerful build system compared to the usual Makefile chaos in C.
- High-quality tooling (clippy, rustfmt, rustdoc, LSP).
- A type system well-suited for modeling hardware access and preventing muxing errors or race conditions.
- Ownership model that eliminates entire classes of bugs related to concurrency and race conditions.
- A rapidly growing no_std ecosystem.
- Performance comparable to C without special optimizations.
- About half the amount of code compared to the C implementation.
- No hidden or hard-to-trace bugs encountered during development.
Cons
- A very steep learning curve, especially for developers coming from C.
- Dependence on external crates, often required for safe hardware access.
- Still-limited architectural support (no DSPs, some MCUs unsupported).
- Multi-target workspaces not yet mature and IDE tooling sometimes unstable.
Embassy: strengths and limitations
Pros
- An
async/awaitmodel that’s perfect for cooperative scheduling. - A HAL shared across architectures (about 80% code portability).
- Zero-cost abstractions even for complex operations such as DMA or interrupts.
- Simplifies the development of real-time tasks.
Cons
- Official support for only a few architectures (STM32, nRF, ESP32, RPi).
- Some internal behavior is not very transparent.
- Same steep learning curve as Rust itself.
Conclusions
“We managed to rewrite 80% of the original firmware in just three days, achieving C-level performance without any hidden bugs.”
The group concluded that Rust is absolutely viable for bare-metal environments, even for real-time applications, as long as one accepts the language’s initial complexity.
The experience with Embassy was very positive: the framework simplifies development and allows for a high level of abstraction without sacrificing efficiency.
Rust is not (yet) a universal replacement for C, but in many cases it can already serve as one – in a safer, more modern, and more collaborative way.