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

Laser projector project

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:

  1. Analyze the pros and cons of the language, with particular attention to performance, determinism, and safety.
  2. 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:

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:

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

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:

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:

Routes

Receives the paths and manages:

Firmware

Acts as the glue between all modules.
Divides the workload into asynchronous Embassy tasks:

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

Cons

Embassy: strengths and limitations

Pros

Cons

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.