Embedded Rust: no_std e microcontroladores
1. Introdução ao Embedded Rust
Rust é uma linguagem que promete segurança de memória sem garbage collector, mas em sistemas embarcados típicos — microcontroladores com poucos KB de RAM e sem sistema operacional — não temos acesso ao runtime completo da biblioteca padrão (std). É aqui que entra o no_std.
O atributo #![no_std] diz ao compilador para usar apenas o crate core, que contém tipos, traits e funções independentes de alocação dinâmica e de um sistema operacional. Sem std, não há Vec, String, HashMap, File, ou threads do SO. Em compensação, ganhamos previsibilidade, tamanho reduzido de binário e controle total sobre o hardware.
No ecossistema Embedded Rust, trabalhamos com três camadas principais:
- PACs (Peripheral Access Crates): gerados a partir de SVD (System View Description), expõem registros de hardware de forma crua e unsafe.
- HALs (Hardware Abstraction Layers): constroem sobre PACs com APIs seguras e ergonômicas (ex: stm32f4xx-hal, rp2040-hal).
- BSPs (Board Support Packages): integram HALs para placas específicas (ex: nucleo-f401re, raspberry-pi-pico).
2. Configuração do Ambiente e Primeiro Projeto
Para começar, instale o target para Cortex-M3 (exemplo comum):
rustup target add thumbv7m-none-eabi
Crie um projeto library:
cargo init --lib meu_projeto
cd meu_projeto
No arquivo .cargo/config.toml, configure o target e o linker:
[build]
target = "thumbv7m-none-eabi"
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]
Crie um arquivo memory.x para definir as regiões de memória do microcontrolador (ex: STM32F103):
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
Adicione as dependências no Cargo.toml:
[dependencies]
cortex-m-rt = "0.7"
cortex-m-semihosting = "0.5"
panic-halt = "0.2"
3. O Atributo #![no_std] e o Entry Point
No src/lib.rs, escrevemos o programa mínimo:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // trata panics com loop infinito
#[entry]
fn main() -> ! {
loop {}
}
Explicação:
- #![no_std]: remove a biblioteca padrão.
- #![no_main]: desabilita o entry point padrão do Rust.
- #[entry]: macro do cortex-m-rt que gera o vetor de reset adequado.
- panic_halt: implementa #[panic_handler] com um loop infinito.
Sem panic_handler, o compilador reclama. Podemos usar alternativas como panic_abort (chama abort()) ou panic_semihosting (envia mensagem via depurador).
Exemplo prático: piscar LED (conceitual, sem HAL ainda)
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
// Endereço do registo de saída do GPIOA (exemplo STM32F1)
const GPIOA_ODR: *mut u32 = 0x4001_080C as *mut u32;
#[entry]
fn main() -> ! {
// Configuração omitida (RCC, MODER, etc.)
loop {
unsafe {
*GPIOA_ODR ^= 1 << 5; // toggle bit 5 (PA5)
}
// delay aproximado
for _ in 0..1_000_000 {}
}
}
4. Acesso a Periféricos com PACs e HALs
Trabalhar com registros brutos é propenso a erros. Usamos PACs e HALs. Exemplo com stm32f1xx-hal:
[dependencies.stm32f1xx-hal]
features = ["stm32f103", "rt"]
version = "0.10"
#![no_std]
#![no_main]
use panic_halt as _;
use stm32f1xx_hal::{
pac,
prelude::*,
timer::Timer,
};
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let mut flash = dp.FLASH.constrain();
let mut gpioc = dp.GPIOC.split(&mut rcc.apb2);
let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
let mut timer = Timer::syst(dp.SYST, 1.hz(), &mut rcc.clocks);
loop {
led.toggle();
timer.wait();
}
}
Aqui:
- Peripherals::take() acessa os registros de forma segura (uma única vez).
- constrain() configura clocks e pinos.
- Timer::syst usa o SysTick para gerar delays precisos.
- led.toggle() alterna o pino PC13 (LED da Blue Pill).
5. Interrupções e Concorrência sem OS
Interrupções são essenciais em sistemas embarcados. Com cortex-m-rt, usamos #[interrupt]:
use cortex_m::peripheral::NVIC;
use cortex_m_rt::entry;
// Recurso compartilhado
static COUNTER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
#[interrupt]
fn TIM2() {
COUNTER.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
}
#[entry]
fn main() -> ! {
// Configurar TIM2 para gerar interrupção a cada 1s (omitido)
unsafe {
NVIC::unmask(pac::Interrupt::TIM2);
}
loop {
// Pode ler COUNTER com critical_section
let val = cortex_m::interrupt::free(|_| COUNTER.load(core::sync::atomic::Ordering::SeqCst));
// ...
}
}
Para dados maiores que AtomicU32, usamos Mutex do crate cortex-m:
use cortex_m::interrupt::Mutex;
use core::cell::RefCell;
static SHARED: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
#[interrupt]
fn TIM2() {
cortex_m::interrupt::free(|cs| {
*SHARED.borrow(cs).borrow_mut() += 1;
});
}
6. Alocação Dinâmica e Vec sem std
Às vezes precisamos de Vec ou String. Para isso, precisamos de um allocador global:
[dependencies]
alloc-cortex-m = "0.4"
#![no_std]
#![no_main]
#![feature(alloc_error_handler)]
extern crate alloc;
use alloc::vec::Vec;
use alloc_cortex_m::CortexMHeap;
#[global_allocator]
static ALLOCATOR: CortexMHeap = CortexMHeap::empty();
#[entry]
fn main() -> ! {
unsafe { ALLOCATOR.init(cortex_m_rt::heap_start() as usize, 1024); } // 1KB heap
let mut v = Vec::new();
v.push(42);
// ...
loop {}
}
Limitações importantes:
- Heap pequeno e fixo.
- Fragmentação é um problema real.
- HashMap não está disponível (precisa de std ou crate específico).
Alternativas recomendadas:
- heapless: coleções de tamanho fixo (sem alocação dinâmica).
- tinyvec: Vec baseado em array estático.
7. Comunicação Serial e Debugging
Depurar sem println! é desafiador. Usamos UART ou RTT.
Exemplo com UART (STM32F1):
use stm32f1xx_hal::serial::{Config, Serial};
use nb::block;
let serial = Serial::usart1(
dp.USART1,
(gpioc.pc10, gpioc.pc11), // TX, RX
Config::default().baudrate(115200.bps()),
&mut rcc.apb2,
);
let (mut tx, _rx) = serial.split();
block!(tx.write(b'H')).ok();
block!(tx.write(b'i')).ok();
Debugging com RTT (Real-Time Transfer):
[dependencies]
rtt-target = { version = "0.4", features = ["cortex-m"] }
use rtt_target::{rtt_init_print, rprintln};
#[entry]
fn main() -> ! {
rtt_init_print!();
rprintln!("Hello from RTT!");
loop {}
}
Para monitorar: probe-rs run --chip STM32F103C8 target/thumbv7m-none-eabi/debug/meu_projeto
Ferramentas úteis:
- probe-rs: debugger moderno com suporte a RTT.
- openocd + itmdump: para semihosting.
- screen /dev/ttyUSB0 115200: para UART.
8. Próximos Passos e Boas Práticas
- Gerenciamento de energia: use
asm::wfi()(wait for interrupt) para reduzir consumo. - Simulação: QEMU com
cortex-m-semihostingpermite testar sem hardware. - Segurança: minimize
unsafe; prefira HALs e PACs testados. - RTIC (Real-Time Interrupt-driven Concurrency): framework para concorrência determinística em sistemas de tempo real.
Boas práticas finais:
- Sempre defina um panic_handler explícito.
- Use critical_section para acesso a recursos compartilhados.
- Prefira heapless a alocação dinâmica sempre que possível.
- Documente o layout de memória (memory.x) e o consumo de stack.
Referências
- The Embedded Rust Book — Guia oficial da organização Rust Embedded, cobrindo
no_std, toolchain e conceitos fundamentais. - cortex-m-rt crate documentation — Documentação do runtime para Cortex-M, incluindo
#[entry]e#[interrupt]. - stm32f1xx-hal crate — HAL para a família STM32F1, com exemplos de GPIO, timer, UART e mais.
- RTIC: Real-Time Interrupt-driven Concurrency — Framework para concorrência em sistemas embarcados sem
std, com foco em segurança e tempo real. - Embedded Rust on Raspberry Pi Pico (RP2040) — Repositório oficial com HAL e exemplos para RP2040, demonstrando
no_stdna prática. - The
heaplesscrate — Coleções de tamanho fixo (Vec, String, HashMap) sem alocação dinâmica, ideais parano_std. - Probe-rs: A modern embedded debugger — Ferramenta de debugging e flashing com suporte a RTT, SWD e múltiplos chips.