Rust é uma das linguagens mais empolgantes para sistemas embarcados. Enquanto C dominou esse espaço por décadas, as garantias de memória de Rust eliminam classes inteiras de bugs que são especialmente devastadores em sistemas onde não há sistema operacional para conter danos, depurar é difícil, e reinicializar pode ser impossível.
Neste artigo descemos ao nível mais baixo: microcontroladores sem sistema operacional, acesso direto a registradores de hardware, interrupts, e o ecossistema embedded-hal que torna o código Rust portável entre diferentes plataformas de hardware.
O que significa "bare metal"
Em um computador normal, o sistema operacional isola seu programa do hardware. Quando você abre um arquivo, o kernel lida com os detalhes do controlador de disco. Em sistemas embarcados bare metal:
- Não há kernel nem sistema operacional
- Seu programa controla o hardware diretamente
- Não há alocador de memória por padrão — você gerencia a stack e a heap manualmente
- Não há threads do OS — você implementa concorrência com interrupts
- Um bug pode danificar hardware permanentemente
O Rust resolve o problema mais crítico dessa categoria: corrupção de memória. As garantias de ownership e borrowing são verificadas em tempo de compilação — sem runtime, sem GC, perfeito para sistemas com kilobytes de RAM.
O ecossistema embarcado Rust
# Camadas do ecossistema embarcado Rust:
#
# ┌─────────────────────────────────────┐
# │ Aplicação (seu código) │
# ├─────────────────────────────────────┤
# │ Board Support Crate (BSP) │ ex: rp-pico, stm32f4xx-hal
# ├─────────────────────────────────────┤
# │ embedded-hal (traits abstratos) │ interface portável
# ├─────────────────────────────────────┤
# │ HAL específico do chip │ ex: stm32f4xx-hal
# ├─────────────────────────────────────┤
# │ PAC (Peripheral Access Crate) │ registradores gerados por svd2rust
# ├─────────────────────────────────────┤
# │ cortex-m / riscv │ intrínsecos do processador
# ├─────────────────────────────────────┤
# │ Hardware │
# └─────────────────────────────────────┘
Configurando para o Raspberry Pi Pico (RP2040)
O RP2040 é um microcontrolador acessível e popular. Vamos usá-lo como exemplo:
# Instalar toolchain para ARM Cortex-M0+
rustup target add thumbv6m-none-eabi
# Instalar ferramenta de debug/flash
cargo install probe-rs --features cli
Cargo.toml:
[package]
name = "pico_embarcado"
version = "0.1.0"
edition = "2021"
[dependencies]
# HAL do RP2040
rp2040-hal = { version = "0.10", features = ["rt"] }
# Suporte ao board Raspberry Pi Pico
rp-pico = "0.9"
# Traits abstratos — código portável entre chips
embedded-hal = "1.0"
# Runtime para Cortex-M
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
# Sem alocação dinâmica por padrão
# Mas podemos habilitar um alocador estático
embedded-alloc = "0.5"
# Formatação sem std
ufmt = "0.2"
# Panic handler para sistemas sem OS
panic-halt = "0.2"
[profile.release]
opt-level = "s" # menor binário possível
lto = true
debug = false
codegen-units = 1
# Configuração do target
[build]
target = "thumbv6m-none-eabi"
memory.x — mapa de memória do RP2040:
MEMORY {
BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
RAM : ORIGIN = 0x20000000, LENGTH = 256K
}
EXTERN(BOOT2_FIRMWARE)
SECTIONS {
.boot2 ORIGIN(BOOT2) :
{
KEEP(*(.boot2));
} > BOOT2
} INSERT BEFORE .text;
O atributo #![no_std]
Código bare metal não tem acesso à biblioteca padrão — não há sistema de arquivos, não há heap por padrão, não há threads do OS:
// Declara que não usamos std
#![no_std]
// Declara que fornecemos nosso próprio ponto de entrada
#![no_main]
use panic_halt as _; // panic handler: trava o processador em pânico
use rp_pico::entry;
use rp_pico::hal;
use hal::pac;
use embedded_hal::digital::OutputPin;
// Ponto de entrada — o runtime cortex-m-rt chama esta função
#[entry]
fn main() -> ! { // ! significa que nunca retorna
// Obtém acesso aos periféricos
let mut pac = pac::Peripherals::take().unwrap();
// Inicializa o watchdog (necessário para inicialização)
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
// Inicializa os clocks
let clocks = hal::clocks::init_clocks_and_plls(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
// Delay baseado em SysTick
let mut delay = cortex_m::delay::Delay::new(
unsafe { pac::CorePeripherals::steal() }.SYST,
clocks.system_clock.freq().to_Hz(),
);
// Configura os pinos GPIO
let sio = hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
// LED onboard do Pico está no pino 25
let mut led_pin = pins.led.into_push_pull_output();
// Loop principal — pisca o LED
loop {
led_pin.set_high().unwrap();
delay.delay_ms(500);
led_pin.set_low().unwrap();
delay.delay_ms(500);
}
}
embedded-hal — traits portáveis
O poder do ecossistema embarcado Rust está nos traits do embedded-hal. Um driver escrito contra esses traits funciona em qualquer chip que os implemente:
// Driver para sensor de temperatura DS18B20
// Funciona em qualquer chip que implemente embedded-hal
use embedded_hal::digital::{InputPin, OutputPin};
use embedded_hal::delay::DelayNs;
#[derive(Debug)]
pub enum ErroBus {
SemPresenca,
CrcInvalido,
Timeout,
}
pub struct Ds18b20<P, D>
where
P: InputPin + OutputPin,
D: DelayNs,
{
pino: P,
delay: D,
}
impl<P, D> Ds18b20<P, D>
where
P: InputPin + OutputPin,
D: DelayNs,
{
pub fn novo(pino: P, delay: D) -> Self {
Ds18b20 { pino, delay }
}
fn reset(&mut self) -> Result<(), ErroBus> {
// Sinalização de reset do protocolo 1-Wire
self.pino.set_low().ok();
self.delay.delay_us(480);
self.pino.set_high().ok();
self.delay.delay_us(70);
// Verifica pulso de presença
let presenca = self.pino.is_low()
.map_err(|_| ErroBus::Timeout)?;
self.delay.delay_us(410);
if presenca {
Ok(())
} else {
Err(ErroBus::SemPresenca)
}
}
fn escrever_bit(&mut self, bit: bool) {
self.pino.set_low().ok();
self.delay.delay_us(if bit { 1 } else { 60 });
self.pino.set_high().ok();
self.delay.delay_us(if bit { 60 } else { 1 });
}
fn ler_bit(&mut self) -> bool {
self.pino.set_low().ok();
self.delay.delay_us(1);
self.pino.set_high().ok();
self.delay.delay_us(14);
let bit = self.pino.is_high().unwrap_or(false);
self.delay.delay_us(45);
bit
}
fn escrever_byte(&mut self, byte: u8) {
for i in 0..8 {
self.escrever_bit((byte >> i) & 1 == 1);
}
}
fn ler_byte(&mut self) -> u8 {
let mut byte = 0u8;
for i in 0..8 {
if self.ler_bit() {
byte |= 1 << i;
}
}
byte
}
pub fn ler_temperatura(&mut self) -> Result<f32, ErroBus> {
// Inicia conversão
self.reset()?;
self.escrever_byte(0xCC); // Skip ROM (único dispositivo)
self.escrever_byte(0x44); // Convert T
// Aguarda conversão (750ms para 12-bit)
self.delay.delay_ms(750);
// Lê scratchpad
self.reset()?;
self.escrever_byte(0xCC);
self.escrever_byte(0xBE); // Read Scratchpad
let byte_lsb = self.ler_byte();
let byte_msb = self.ler_byte();
// Verifica CRC (simplificado)
let raw = u16::from_le_bytes([byte_lsb, byte_msb]) as i16;
// Converte para Celsius (resolução 0.0625°C)
Ok(raw as f32 * 0.0625)
}
}
Este driver compila para qualquer plataforma — RP2040, STM32, AVR (Arduino), ESP32, ou até seu desktop para testes.
Interrupts
Interrupts são a forma de responder a eventos de hardware sem polling constante:
#![no_std]
#![no_main]
use cortex_m::interrupt::Mutex;
use core::cell::Cell;
use rp_pico::entry;
use rp_pico::hal::{self, pac, pac::interrupt};
use embedded_hal::digital::OutputPin;
// Estado compartilhado entre código principal e interrupt
// Mutex de Cortex-M — desabilita interrupts durante acesso
static CONTADOR_BOTAO: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
static LED_ESTADO: Mutex<Cell<bool>> = Mutex::new(Cell::new(false));
// Handler de interrupt para o pino GPIO
#[interrupt]
fn IO_IRQ_BANK0() {
// Acesso seguro — interrupts desabilitados durante critical section
cortex_m::interrupt::free(|cs| {
let contador = CONTADOR_BOTAO.borrow(cs);
contador.set(contador.get() + 1);
let led = LED_ESTADO.borrow(cs);
led.set(!led.get()); // toggle
// Limpa o interrupt (necessário para receber próximo)
// Na implementação real: limpar flag no periférico
});
}
#[entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let sio = hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let mut led = pins.led.into_push_pull_output();
// Configura pino do botão com interrupt na borda de descida
let botao = pins.gpio15.into_pull_up_input();
botao.set_interrupt_enabled(
hal::gpio::Interrupt::EdgeLow, true
);
// Habilita interrupts globalmente
unsafe { pac::NVIC::unmask(pac::Interrupt::IO_IRQ_BANK0) };
let mut ultimo_contador = 0u32;
loop {
// Lê estado de forma segura
let (contador, estado_led) = cortex_m::interrupt::free(|cs| {
(
CONTADOR_BOTAO.borrow(cs).get(),
LED_ESTADO.borrow(cs).get(),
)
});
// Atualiza LED apenas quando estado mudou
if contador != ultimo_contador {
if estado_led {
led.set_high().ok();
} else {
led.set_low().ok();
}
ultimo_contador = contador;
}
// WFI — Wait For Interrupt, economiza energia
cortex_m::asm::wfi();
}
}
RTIC — Real-Time Interrupt-driven Concurrency
RTIC é um framework para sistemas embarcados que usa o sistema de tipos de Rust para garantir ausência de data races em código concorrente com interrupts:
[dependencies]
rtic = { version = "2", features = ["thumbv6-backend"] }
rtic-monotonics = { version = "2", features = ["rp2040"] }
#![no_std]
#![no_main]
#[rtic::app(device = rp_pico::hal::pac, peripherals = true)]
mod app {
use rp_pico::hal::{self, pac, clocks, gpio};
use embedded_hal::digital::OutputPin;
use rtic_monotonics::rp2040::Timer;
use rtic_monotonics::Monotonic;
// Recursos compartilhados entre tasks
#[shared]
struct Shared {
contador: u32,
}
// Recursos locais — exclusivos de uma task
#[local]
struct Local {
led: gpio::Pin<gpio::bank0::Gpio25, gpio::FunctionSio<gpio::SioOutput>, gpio::PullNone>,
botao: gpio::Pin<gpio::bank0::Gpio15, gpio::FunctionSio<gpio::SioInput>, gpio::PullUp>,
}
#[init]
fn init(ctx: init::Context) -> (Shared, Local) {
let mut pac = ctx.device;
// Inicializa clocks
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
let clocks = clocks::init_clocks_and_plls(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC, pac.CLOCKS,
pac.PLL_SYS, pac.PLL_USB,
&mut pac.RESETS, &mut watchdog,
).ok().unwrap();
// Inicializa timer
let timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
Timer::start(timer, &mut pac.RESETS);
// Configura pinos
let sio = hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0, pac.PADS_BANK0,
sio.gpio_bank0, &mut pac.RESETS,
);
let led = pins.led.into_push_pull_output();
let botao = pins.gpio15.into_pull_up_input();
// Agenda task periódica
piscar_led::spawn().ok();
(
Shared { contador: 0 },
Local { led, botao },
)
}
// Task periódica — pisca o LED a cada 500ms
#[task(local = [led], shared = [contador])]
async fn piscar_led(mut ctx: piscar_led::Context) {
loop {
ctx.local.led.set_high().ok();
Timer::delay(500u32.millis()).await;
ctx.local.led.set_low().ok();
Timer::delay(500u32.millis()).await;
// Acessa recurso compartilhado com lock automático
ctx.shared.contador.lock(|c| {
*c += 1;
});
}
}
// Handler de interrupt para botão
#[task(binds = IO_IRQ_BANK0, local = [botao], shared = [contador])]
fn botao_pressionado(mut ctx: botao_pressionado::Context) {
ctx.shared.contador.lock(|c| {
*c += 100; // Botão incrementa por 100
});
// Limpa interrupt
ctx.local.botao.clear_interrupt(gpio::Interrupt::EdgeLow);
}
}
RTIC garante em tempo de compilação que não há data races — você não precisa de Mutex explícito para recursos que só uma task acessa.
Comunicação serial — UART
UART é a interface de comunicação mais simples entre microcontrolador e computador:
#![no_std]
#![no_main]
use panic_halt as _;
use rp_pico::entry;
use rp_pico::hal::{self, pac, clocks, uart::{self, UartConfig}};
use fugit::RateExtU32;
use core::fmt::Write;
#[entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
let clocks = clocks::init_clocks_and_plls(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC, pac.CLOCKS,
pac.PLL_SYS, pac.PLL_USB,
&mut pac.RESETS, &mut watchdog,
).ok().unwrap();
let sio = hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0, pac.PADS_BANK0,
sio.gpio_bank0, &mut pac.RESETS,
);
// Configura UART0 em 115200 baud
let uart_pins = (
pins.gpio0.into_function::<hal::gpio::FunctionUart>(),
pins.gpio1.into_function::<hal::gpio::FunctionUart>(),
);
let mut uart = hal::uart::UartPeripheral::new(
pac.UART0,
uart_pins,
&mut pac.RESETS,
)
.enable(
UartConfig::new(115_200u32.Hz(), uart::DataBits::Eight, None, uart::StopBits::One),
clocks.peripheral_clock.freq(),
)
.unwrap();
let mut delay = cortex_m::delay::Delay::new(
core.SYST,
clocks.system_clock.freq().to_Hz(),
);
// Envia mensagens pela serial
writeln!(uart, "Sistema iniciado!").ok();
writeln!(uart, "RP2040 com Rust embarcado").ok();
let mut contador = 0u32;
loop {
writeln!(uart, "Tick {contador}").ok();
contador += 1;
delay.delay_ms(1000);
}
}
Alocação dinâmica com embedded-alloc
Quando você precisa de Vec ou String em sistemas embarcados:
#![no_std]
#![no_main]
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
use embedded_alloc::LlffHeap as Heap;
// Define o alocador global com 8KB de heap
#[global_allocator]
static HEAP: Heap = Heap::empty();
use panic_halt as _;
use rp_pico::entry;
#[entry]
fn main() -> ! {
// Inicializa o heap — deve ser feito antes de qualquer alocação
{
use core::mem::MaybeUninit;
const HEAP_SIZE: usize = 8192;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] =
[MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
}
// Agora podemos usar Vec, String, Box, etc.
let mut buffer: Vec<u8> = Vec::new();
for i in 0..10u8 {
buffer.push(i * i);
}
// String formatada sem std
let mut msg = String::from("Valores: ");
for &v in &buffer {
// ufmt para formatação sem std (mais leve que core::fmt)
// msg.push_str(&format!("{} ", v)); // requer std
}
loop {
cortex_m::asm::wfi();
}
}
Testando código embarcado no desktop
Uma vantagem enorme do Rust embarcado é que código escrito contra embedded-hal pode ser testado no desktop usando implementações mock:
[dev-dependencies]
embedded-hal-mock = "0.10"
#[cfg(test)]
mod testes {
use super::*;
use embedded_hal_mock::eh1::{
digital::{Mock as PinoMock, State, Transaction as PinoTx},
delay::NoopDelay,
};
#[test]
fn ds18b20_responde_presenca() {
// Define expectativas: o pino vai ser lido como LOW (presença)
let expectativas = [
PinoTx::set(State::Low), // reset: puxar para baixo
PinoTx::set(State::High), // liberar
PinoTx::get(State::Low), // sensor puxa para baixo (presença)
];
let pino = PinoMock::new(&expectativas);
let delay = NoopDelay::new();
let mut sensor = Ds18b20::novo(pino, delay);
// O reset deve detectar presença
// assert!(sensor.reset().is_ok());
// Verifica que todas as expectativas foram cumpridas
// sensor.pino.done();
}
#[test]
fn temperatura_converte_corretamente() {
// 0x0190 = 400 em decimal
// 400 * 0.0625 = 25.0°C
let raw: i16 = 0x0190;
let temp = raw as f32 * 0.0625;
assert!((temp - 25.0).abs() < 0.001);
}
#[test]
fn temperatura_negativa() {
// 0xFF5E = -162 em complemento de dois
// -162 * 0.0625 = -10.125°C
let raw: i16 = -162i16;
let temp = raw as f32 * 0.0625;
assert!((temp - (-10.125)).abs() < 0.001);
}
}
Esses testes rodam com cargo test no seu computador — sem necessidade de hardware.
Depuração com probe-rs
# Flash e executa no hardware
cargo run --release
# Debug interativo com GDB
cargo embed --gdb
# Monitora saída RTT (Real-Time Transfer — debug sem serial)
probe-rs run --chip RP2040 target/thumbv6m-none-eabi/release/pico_embarcado
Embed.toml — configuração para probe-rs:
[default.general]
chip = "RP2040"
[default.reset]
halt_afterwards = false
[default.rtt]
enabled = true
[default.gdb]
enabled = false
Um projeto completo: termômetro com display
Combinando sensor de temperatura, display OLED e UART:
#![no_std]
#![no_main]
use panic_halt as _;
use rp_pico::entry;
use rp_pico::hal::{self, pac, clocks, i2c::I2C};
use embedded_hal::digital::OutputPin;
use core::fmt::Write;
use fugit::RateExtU32;
// Driver OLED SSD1306 via I2C
// (requer crate ssd1306)
#[entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
let clocks = clocks::init_clocks_and_plls(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC, pac.CLOCKS,
pac.PLL_SYS, pac.PLL_USB,
&mut pac.RESETS, &mut watchdog,
).ok().unwrap();
let sio = hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(
pac.IO_BANK0, pac.PADS_BANK0,
sio.gpio_bank0, &mut pac.RESETS,
);
// I2C para o display OLED
let i2c = I2C::i2c0(
pac.I2C0,
pins.gpio4.into_function(), // SDA
pins.gpio5.into_function(), // SCL
400u32.kHz(),
&mut pac.RESETS,
&clocks.system_clock,
);
let mut delay = cortex_m::delay::Delay::new(
core.SYST,
clocks.system_clock.freq().to_Hz(),
);
// Interface I2C
let interface = ssd1306::I2CDisplayInterface::new(i2c);
let mut display = ssd1306::Ssd1306::new(
interface,
ssd1306::size::DisplaySize128x64,
ssd1306::rotation::DisplayRotation::Rotate0,
)
.into_buffered_graphics_mode();
display.init().unwrap();
// Sensor de temperatura interno do RP2040
let mut adc = hal::Adc::new(pac.ADC, &mut pac.RESETS);
let mut sensor_temp = hal::adc::AdcPin::new(
hal::adc::TempSense::new(&mut adc)
).unwrap();
loop {
// Lê ADC do sensor interno
let leitura: u16 = adc.read(&mut sensor_temp).unwrap();
// Converte ADC para temperatura
// Fórmula do datasheet RP2040:
// T = 27 - (ADC_voltage - 0.706) / 0.001721
let tensao = leitura as f32 * 3.3 / 4096.0;
let temperatura = 27.0 - (tensao - 0.706) / 0.001721;
// Exibe no display (implementação simplificada)
display.clear_buffer();
// Em produção: use embedded-graphics para renderizar texto
// Por simplicidade, apenas demonstramos a estrutura
display.flush().unwrap();
delay.delay_ms(1000);
}
}
Adicione ao Cargo.toml:
ssd1306 = "0.8"
embedded-graphics = "0.8"
O estado do Rust embarcado em 2024
O ecossistema embarcado Rust amadureceu rapidamente:
Plataformas bem suportadas: RP2040 (Raspberry Pi Pico), STM32 (toda a família), nRF52 (Bluetooth), ESP32 com suporte crescente, RISC-V.
Embassy — framework async para embarcados — permite escrever código assíncrono em microcontroladores sem OS, usando async/await com executores zero-cost.
Knurling — ferramentas de developer experience para embarcados: defmt para logging eficiente, flip-link para detecção de stack overflow.
Produção — empresas como Oxide Computer, Tweede Golf, e Ferrous Systems usam Rust em hardware crítico em produção.
Fontes e leituras recomendadas
- "The Embedded Rust Book" — guia oficial, essencial — https://docs.rust-embedded.org/book/
- "The Discovery Book" — tutorial prático com STM32 — https://docs.rust-embedded.org/discovery/
- Embassy — framework async para embarcados — https://embassy.dev
- RTIC — concorrência baseada em interrupts — https://rtic.rs
probe-rs— ferramenta de debug e flash — https://probe.rsdefmt— logging eficiente para embarcados — https://defmt.ferrous-systems.com- "Awesome Embedded Rust" — curadoria do ecossistema — https://github.com/rust-embedded/awesome-embedded-rust
- Ferrous Systems Trainings — treinamentos profissionais em Rust embarcado — https://ferrous-systems.com/training/