Rust embarcado: microcontroladores, no_std e Embassy
Rust embarcado (embedded Rust) é escrever Rust para computadores pequenos que vivem dentro das coisas: um termostato, um teclado, um drone, um sensor que rega as plantas. Esses computadores se chamam microcontroladores. Um único chip tem o processador, algumas centenas de kilobytes de memória e os pinos que conversam com LEDs, botões e sensores. Normalmente não existe sistema operacional nenhum. O seu programa é a única coisa rodando, desde que a energia liga até ela desligar.
Parece assustador, mas também é muito divertido: você escreve algumas linhas e uma luz de verdade pisca na sua mesa. Esta página mostra o que é possível, qual placa e quais crates (pacotes) escolher e como colocar o seu primeiro programa em um chip. No caminho, você vai praticar o “jeito embarcado de pensar” com exemplos que rodam direto no navegador.
Por que o Rust combina tão bem com sistemas embarcados
Seção intitulada “Por que o Rust combina tão bem com sistemas embarcados”Por décadas, o código embarcado foi escrito principalmente em C. O Rust traz algumas coisas que importam muito em um chip minúsculo:
- Sem garbage collector. Nada pausa o seu programa para limpar a memória, então o tempo de resposta fica previsível. Isso é importante quando você precisa reagir a um sinal em microssegundos.
- Segurança de memória sem runtime. As regras de ownership (posse) e de borrowing (empréstimo) são verificadas na compilação, então não custam nada no chip. Classes inteiras de falhas (ponteiros pendurados, data races entre uma interrupção e o seu laço principal) são pegas antes de você gravar qualquer coisa no chip.
- Abstrações de custo zero. Uma HAL deixa você escrever
led.set_high()em vez de mexer em endereços de memória, e isso compila para a mesma escrita única no registrador que você faria à mão. - O ownership modela o hardware. Um pino é um valor. Depois que ele é movido para um
Output, ninguém mais consegue pegá-lo e usá-lo como entrada por engano. O compilador garante o que antes era só um comentário em um arquivo C. - Ótimas ferramentas.
cargo, os targets dorustupe oprobe-rssubstituem muitas IDEs de fabricantes e muitos makefiles.
Sendo honesto sobre as arestas: os fabricantes de chips ainda lançam os seus SDKs e exemplos primeiro em C, então para alguns chips o suporte a Rust é mais fraco ou só da comunidade. Você ainda precisa ler datasheets. E alguns crates estão antes da versão 1.0 e mudam a API entre versões, então siga sempre os exemplos da versão que você usa.
O que você pode construir
Seção intitulada “O que você pode construir”- Sensores e gadgets: uma estação meteorológica, um alarme de umidade do solo, um contador de passos.
- Dispositivos USB: teclados personalizados, controles de videogame, controladores MIDI.
- Dispositivos conectados (IoT): sensores com Wi-Fi ou Bluetooth em um ESP32 que mandam leituras para um servidor.
- Robôs e motores: seguidores de linha, controle de motores de passo e servos, fitas de LED e displays.
- Sistemas sérios: a Oxide Computer escreveu o Hubris, um pequeno sistema operacional para microcontroladores, inteiramente em Rust. A Espressif, a empresa por trás do ESP32, mantém o seu próprio suporte a Rust e publica o The Rust on ESP Book (em inglês).
Placas fáceis para começar
Seção intitulada “Placas fáceis para começar”Qualquer uma destas é uma boa primeira placa. Se não conseguir decidir, pegue um Raspberry Pi Pico: é barato, muito bem documentado, e o exemplo com Embassy mais abaixo foi escrito para ele.
| Placa | Chip | Target do Rust | Por que é legal | Comece com |
|---|---|---|---|---|
| Raspberry Pi Pico / Pico W | RP2040 (Arm Cortex-M0+) | thumbv6m-none-eabi |
Barato, popular, excelente suporte no Embassy | embassy-rp, ou rp2040-hal + rp-pico |
| Raspberry Pi Pico 2 | RP2350 (Arm Cortex-M33, ou RISC-V) | thumbv8m.main-none-eabihf |
Mais novo e mais rápido, tem unidade de ponto flutuante | embassy-rp (feature rp235xa), rp235x-hal |
| Placas ESP32-C3 / ESP32-C6 | Chips RISC-V da Espressif | riscv32imc-unknown-none-elf / riscv32imac-unknown-none-elf |
Wi-Fi e Bluetooth embutidos, suporte oficial a Rust | esp-hal e a ferramenta esp-generate |
| STM32 Nucleo, “Black Pill” | STM32 (por exemplo, STM32F4) | thumbv7em-none-eabihf |
Família enorme de chips usada na indústria; as placas Nucleo têm um debug probe embutido | embassy-stm32, stm32f4xx-hal |
| BBC micro:bit v2 | nRF52833 (Arm Cortex-M4F) | thumbv7em-none-eabihf |
Debug probe, LEDs, botões e sensores na própria placa | microbit-v2, mais o livro Discovery |
O micro:bit é a placa usada pelo Embedded Rust Discovery book (em inglês), um curso oficial, gratuito e passo a passo para iniciantes. O ESP32 original e o ESP32-S3 usam núcleos Xtensa em vez de RISC-V; para eles, a ferramenta espup da Espressif instala uma toolchain especial do Rust. O LED do Pico W está ligado ao chip de Wi-Fi, e não a um pino normal, então ele precisa de um blinky diferente (o Embassy tem um exemplo wifi_blinky para ele).
no_std e no_main explicados
Seção intitulada “no_std e no_main explicados”Um programa Rust normal usa a biblioteca padrão (std). A std espera um sistema operacional: arquivos, threads, um heap, um terminal para o println!. Um microcontrolador não tem nada disso. Por isso, os programas embarcados começam com duas linhas:
#![no_std] // não liga a biblioteca padrão, só a `core`#![no_main] // não usa o ponto de entrada habitual `fn main()`#![no_std]significa que você só tem acore(em inglês): inteiros,Option,Result, iteradores, slices, traits, formatação. Nada deVec,String,Box, arquivos ou threads. (Com um alocador, você pode voltar a usar oVece companhia pelo cratealloc, mas muitos projetos nunca precisam disso.)#![no_main]é necessário porque, em um chip, não existe sistema operacional para chamar omain. Um crate de runtime como ocortex-m-rtprepara a memória depois do reset e então pula para a sua função, que você marca com um atributo como#[cortex_m_rt::entry]ou, com Embassy,#[embassy_executor::main].- Você também precisa dizer o que acontece em um panic, porque não há terminal para imprimir. Crates como
panic-probeoupanic-haltcuidam disso; você acrescenta um comuse panic_probe as _;.
Sem Vec, como guardar uma lista? O crate heapless (em inglês) oferece Vec, String e filas com capacidade fixa, escolhida na compilação. Eles moram na pilha ou em um static, então não há heap nem surpresas de falta de memória. Um buffer cheio simplesmente devolve o seu valor:
use core::fmt::Write;use heapless::{String, Vec};
fn main() { // Espaço para exatamente 4 leituras. A memória faz parte da variável: // sem heap, sem alocador, e o tamanho é conhecido na compilação. let mut readings: Vec<i32, 4> = Vec::new();
for value in [215, 218, 221, 224, 227] { match readings.push(value) { Ok(()) => println!("stored {value}"), Err(rejected) => println!("buffer full, dropped {rejected}"), } }
// Uma String com capacidade fixa de 16 bytes let mut line: String<16> = String::new(); write!(line, "avg={}", readings.iter().sum::<i32>() / readings.len() as i32).unwrap(); println!("{line} (capacity {})", line.capacity());}O heapless também funciona em um PC, então você pode experimentar com cargo add [email protected] e cargo run (ele não está no Playground). As mensagens dizem que cada valor foi guardado (“stored”), que o buffer encheu e o último valor foi descartado, e mostram a média (avg):
stored 215stored 218stored 221stored 224buffer full, dropped 227avg=219 (capacity 16)O ecossistema em camadas
Seção intitulada “O ecossistema em camadas”Os crates de Rust embarcado se empilham uns sobre os outros. Na maior parte do tempo você vai trabalhar no nível da HAL ou do framework, mas ajuda saber o que tem embaixo:
| Camada | O que é | Exemplos |
|---|---|---|
| PAC (Peripheral Access Crate) | Gerado a partir do arquivo de descrição dos registradores do chip. Uma função mais ou menos segura para cada registrador e campo. Bem baixo nível. | rp2040-pac, stm32f4, esp32c3 |
| HAL (Hardware Abstraction Layer, camada de abstração de hardware) | Tipos amigáveis para uma família de chips: Output, Uart, I2c, timers. Construída sobre um PAC. |
rp2040-hal, esp-hal, stm32f4xx-hal, embassy-rp, embassy-stm32 |
Traits do embedded-hal |
Traits compartilhadas, como OutputPin, I2c, SpiDevice e DelayNs. As HALs as implementam; os drivers as usam. A versão 1.0 é estável. |
embedded-hal, embedded-hal-async |
| BSP (Board Support Package, pacote de suporte à placa) | Uma HAL mais nomes para os pinos e as peças de uma placa específica (“o LED”, “o botão A”). | rp-pico, microbit-v2 |
| RTIC | Um framework para apps guiados por interrupções. As tarefas ficam ligadas a interrupções e prioridades, e o compilador verifica os dados compartilhados na hora do build. | rtic |
| Embassy | Um framework async: um executor, timers, rede e USB, além das suas próprias HALs. Você escreve tarefas async fn e faz .await em eventos do hardware. |
embassy-executor, embassy-time, embassy-rp |
A mágica está na linha do meio. Um driver de sensor de temperatura escrito com a trait I2c do embedded-hal funciona em um Pico, em um ESP32 e em um STM32 sem mudanças, porque cada HAL implementa a mesma trait. Você mesmo vai escrever um driver assim mais abaixo.
Instale um target e o probe-rs
Seção intitulada “Instale um target e o probe-rs”O seu PC tem um processador de desktop x86 ou Arm; o Pico tem um Cortex-M0+. Então você faz compilação cruzada: compila em uma máquina, para outra. O rustup baixa a biblioteca core já compilada para o chip:
rustup target add thumbv6m-none-eabi # RP2040 (Pico)rustup target add thumbv8m.main-none-eabihf # RP2350 (Pico 2)rustup target add thumbv7em-none-eabihf # micro:bit v2, STM32F4Para copiar o programa para o chip e ver os logs, você precisa do probe-rs e de um debug probe (sonda de depuração): um pequeno dispositivo USB que se conecta aos pinos de depuração do chip. As placas micro:bit e STM32 Nucleo já têm um embutido. Para um Pico, use um Raspberry Pi Debug Probe, ou um segundo Pico gravado com o firmware gratuito debugprobe. Instale o probe-rs com o script instalador (veja a página de instalação, em inglês, para o Windows) ou com o cargo:
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh# ou, mais devagar, a partir do código-fonte:cargo install probe-rs-tools --locked
probe-rs list # mostra os probes que ele encontraAinda não tem um probe? Segure o botão BOOTSEL do Pico enquanto o conecta, e ele aparece como um pendrive. Ferramentas como picotool ou elf2uf2-rs conseguem copiar o seu programa para ele desse jeito. Mas, sem um probe, você não vê os logs do defmt.
Em um ESP32-C3 ou C6, normalmente você nem precisa de um probe: o chip tem USB embutido, e a ferramenta espflash grava o programa pelo cabo USB normal. Rode cargo install esp-generate espflash --locked, depois esp-generate, escolha o seu chip e as opções, e cargo run --release compila, grava e mostra os logs.
Blinky com Embassy em um Raspberry Pi Pico
Seção intitulada “Blinky com Embassy em um Raspberry Pi Pico”O “blinky” é o “Olá, mundo!” dos sistemas embarcados: acende o LED, espera, apaga, espera, para sempre. Este é o exemplo oficial de blinky do Embassy para o RP2040 (com comentários acrescentados). Ele precisa de uma placa de verdade e não roda no Playground.
#![no_std]#![no_main]
use defmt::*;use defmt_rtt as _; // manda os logs do defmt para o probeuse embassy_executor::Spawner;use embassy_rp::gpio;use embassy_time::Timer;use gpio::{Level, Output};use panic_probe as _; // em um panic: imprime a mensagem pelo probe e para
#[embassy_executor::main(executor = "embassy_rp::executor::Executor", entry = "cortex_m_rt::entry")]async fn main(_spawner: Spawner) { // Toma posse de todos os periféricos do chip, exatamente uma vez let p = embassy_rp::init(Default::default()); // O LED do Pico fica no pino 25. PIN_25 é movido para `led`. let mut led = Output::new(p.PIN_25, Level::Low);
loop { info!("led on!"); led.set_high(); Timer::after_millis(500).await;
info!("led off!"); led.set_low(); Timer::after_millis(500).await; }}Algumas coisas para reparar:
- O
mainé umaasync fn.Timer::after_millis(500).awaitnão fica girando em um laço ocupado; o executor coloca o chip para dormir até o timer disparar. Com mais tarefas (criadas peloSpawner), as outras rodam enquanto esta espera. Se async/await é novidade para você, a ideia é a mesma do Tokio em um PC, só que sem sistema operacional. p.PIN_25é movido para dentro deOutput::new. Tente usarp.PIN_25uma segunda vez e o compilador impede: é o ownership aplicado ao hardware.use defmt_rtt as _;euse panic_probe as _;importam crates só pelo que eles configuram (o canal de logs e o tratador de panic), e não por algum nome.
O jeito mais rápido de ver o LED piscar é usar a própria pasta de exemplos oficial: clone o repositório do Embassy e, em examples/rp, rode cargo run --bin blinky --release. Para o seu próprio projeto, as dependências vêm do Cargo.toml desse exemplo:
[package]name = "blinky"version = "0.1.0"edition = "2024"
[dependencies]embassy-executor = { version = "0.10.0", features = ["defmt"] }embassy-time = { version = "0.5.1", features = ["defmt", "defmt-timestamp-uptime"] }embassy-rp = { version = "0.10.0", features = ["defmt", "unstable-pac", "time-driver", "critical-section-impl", "rp2040", "executor-thread"] }defmt = "1.0.1"defmt-rtt = "1.0.0"panic-probe = { version = "1.0.0", features = ["print-defmt"] }cortex-m = { version = "0.7.6", features = ["inline-asm"] }cortex-m-rt = "0.7.0"
[profile.release]debug = true # mantém as informações de debug para o probe-rs; elas não vão para o chipMais dois arquivos dizem ao cargo o que compilar e como rodar. O .cargo/config.toml define o target e faz o cargo run gravar o chip pelo probe-rs:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']runner = "probe-rs run --chip RP2040"
[build]target = "thumbv6m-none-eabi"
[env]DEFMT_LOG = "debug"Por fim, copie o memory.x (onde ficam a flash e a RAM no chip) e o build.rs (que passa os scripts do linker) da mesma pasta examples/rp. Depois conecte o seu probe e rode:
cargo run --releaseO cargo compila para thumbv6m-none-eabi, o probe-rs grava o chip, o LED começa a piscar e as mensagens led on! / led off! (“LED aceso!” / “LED apagado!”) aparecem no seu terminal, com horário.
Logs com defmt
Seção intitulada “Logs com defmt”Um microcontrolador não tem tela, e formatar texto com core::fmt ocupa muito espaço na flash. O defmt (“deferred formatting”, formatação adiada) resolve as duas coisas. As strings de formatação ficam guardadas no seu PC, e não no chip; o chip só manda um ID minúsculo mais os valores brutos pelo probe, e o probe-rs os transforma de volta em texto legível.
use defmt::{info, warn, Format};
#[derive(Format)] // como #[derive(Debug)], mas para o defmtpub struct Reading { pub sensor_id: u8, pub milli_celsius: i32,}
pub fn report(r: &Reading) { info!("new reading: {}", r); if r.milli_celsius > 60_000 { // `=u8` e `=i32` dizem ao defmt o tipo exato, o que economiza ainda mais espaço warn!("sensor {=u8} is too hot: {=i32} mC", r.sensor_id, r.milli_celsius); }}Os níveis são trace!, debug!, info!, warn! e error!, iguais aos do crate log. A configuração DEFMT_LOG no .cargo/config.toml escolhe quais níveis são compilados; tudo o que fica abaixo dela custa zero bytes.
Pensando em registradores: bits e máscaras
Seção intitulada “Pensando em registradores: bits e máscaras”Por baixo de toda HAL, controlar o hardware significa escrever números em registradores: endereços de memória especiais em que cada bit liga ou desliga algo no chip. Em um chip típico, um registrador de saída GPIO de 32 bits tem um bit por pino. Aqui, um u32 comum faz o papel desse registrador, para você praticar os quatro movimentos que todo desenvolvedor de sistemas embarcados usa. Os textos impressos ficaram em inglês porque estão alinhados à mão com espaços (“LED on” é “LED aceso”, “toggled” é “invertido”, “pin 3 off” é “pino 3 desligado”):
const LED: u32 = 25; // o LED da placa do Pico fica no pino 25
fn main() { // Faz de conta que este u32 é o registrador de saída GPIO do chip. // O bit 0 controla o pino 0, o bit 1 controla o pino 1, e assim por diante. let mut gpio_out: u32 = 0b1000; // o pino 3 já está ligado
// Ligar um bit: OR com uma máscara gpio_out |= 1 << LED; println!("LED on: {gpio_out:#034b}");
// Ler um bit: AND com uma máscara e depois comparar com zero let led_is_on = (gpio_out & (1 << LED)) != 0; let pin2_is_on = (gpio_out & (1 << 2)) != 0; println!("LED on? {led_is_on}, pin 2 on? {pin2_is_on}");
// Inverter um bit: XOR com uma máscara gpio_out ^= 1 << LED; println!("toggled: {gpio_out:#034b}");
// Desligar um bit: AND com a máscara invertida gpio_out |= 1 << LED; gpio_out &= !(1 << 3); println!("pin 3 off: {gpio_out:#034b}"); println!("as hex: {gpio_out:#010x}");}Saída:
LED on: 0b00000010000000000000000000001000LED on? true, pin 2 on? falsetoggled: 0b00000000000000000000000000001000pin 3 off: 0b00000010000000000000000000000000as hex: 0x02000000Em Rust, ! aplicado a um inteiro inverte todos os bits (o C usa ~ para isso). {:#034b} imprime o valor em binário, com o prefixo 0b e preenchido até 32 dígitos, para você ver o bit 25 aparecer e sumir. Veja Operadores em Rust para a lista completa de operadores bit a bit.
Muitos registradores juntam vários campos em um só número: alguns bits para uma configuração de velocidade, um bit para “ativado”. Para mudar um campo, você faz um read-modify-write (ler, modificar, escrever): limpa os bits do campo e depois faz OR com o novo valor, sem tocar em nenhum outro bit:
// Um registrador de controle de UART inventado:// bit 0 = ativar// bits 4..=6 = configuração de velocidade (um número de 3 bits, de 0 a 7)const ENABLE: u32 = 1 << 0;const SPEED_SHIFT: u32 = 4;const SPEED_MASK: u32 = 0b111 << SPEED_SHIFT;
fn read_speed(reg: u32) -> u32 { (reg & SPEED_MASK) >> SPEED_SHIFT}
fn write_speed(reg: u32, speed: u32) -> u32 { // Limpa o campo antigo e depois coloca o novo valor. // Todos os outros bits ficam exatamente como estavam. (reg & !SPEED_MASK) | ((speed << SPEED_SHIFT) & SPEED_MASK)}
fn main() { let mut ctrl: u32 = ENABLE | (2 << SPEED_SHIFT); println!("start: {ctrl:#010b}, speed = {}", read_speed(ctrl));
ctrl = write_speed(ctrl, 5); println!("speed 5: {ctrl:#010b}, speed = {}", read_speed(ctrl));
// 9 não cabe em 3 bits. A máscara corta o valor em vez de // deixá-lo vazar para os bits vizinhos. ctrl = write_speed(ctrl, 9); println!("speed 9?: {ctrl:#010b}, speed = {}", read_speed(ctrl));
let still_enabled = (ctrl & ENABLE) != 0; println!("still enabled: {still_enabled}");}Saída:
start: 0b00100001, speed = 2speed 5: 0b01010001, speed = 5speed 9?: 0b00010001, speed = 1still enabled: trueRepare que o 9 virou 1 em silêncio. É exatamente o tipo de bug de que os PACs e as HALs protegem você: eles dão a cada campo um método com nome próprio (muitas vezes com valores nomeados também), então você raramente escreve máscaras à mão. No hardware de verdade, o registrador é um endereço de memória fixo, escrito com core::ptr::write_volatile dentro de código unsafe; o PAC faz essa parte por você.
Escreva um driver que você pode testar no PC
Seção intitulada “Escreva um driver que você pode testar no PC”O melhor hábito embarcado que você pode aprender: escreva a sua lógica usando traits, e não um chip específico. Assim o mesmo código roda na placa com a HAL de verdade e no seu PC com peças falsas (“mocks”) em um teste normal. Aqui, StatusLed só precisa de “algo que eu consiga ligar e desligar” e de “algo que consiga esperar”:
use std::cell::RefCell;use std::rc::Rc;
// As duas habilidades de que um driver de pisca-pisca precisa. Elas imitam as// traits OutputPin e DelayNs do embedded-hal 1.0, simplificadas para rodar no Playground.trait OutputPin { fn set_high(&mut self); fn set_low(&mut self);}
trait Delay { fn delay_ms(&mut self, ms: u32);}
// O driver não sabe nada sobre nenhum chip. Ele só usa as traits.struct StatusLed<P: OutputPin, D: Delay> { pin: P, delay: D,}
impl<P: OutputPin, D: Delay> StatusLed<P, D> { // Pisca `times` vezes: aceso por `on_ms`, depois apagado por `off_ms`. fn blink(&mut self, times: u32, on_ms: u32, off_ms: u32) { for _ in 0..times { self.pin.set_high(); self.delay.delay_ms(on_ms); self.pin.set_low(); self.delay.delay_ms(off_ms); } }}
// --- Peças falsas para testar no PC ---// As duas escrevem no mesmo log compartilhado, então dá para ver a ordem.type Log = Rc<RefCell<Vec<String>>>;
struct MockPin { log: Log,}
impl OutputPin for MockPin { fn set_high(&mut self) { self.log.borrow_mut().push("liga".to_string()); } fn set_low(&mut self) { self.log.borrow_mut().push("desliga".to_string()); }}
struct MockDelay { log: Log,}
impl Delay for MockDelay { fn delay_ms(&mut self, ms: u32) { self.log.borrow_mut().push(format!("espera {ms}")); }}
fn main() { let log: Log = Rc::new(RefCell::new(Vec::new())); let pin = MockPin { log: Rc::clone(&log) }; let delay = MockDelay { log: Rc::clone(&log) };
let mut led = StatusLed { pin, delay }; led.blink(2, 100, 400);
let events = log.borrow(); println!("{}", events.join(", "));
assert_eq!(events.len(), 8); assert_eq!(events[0], "liga"); assert_eq!(events[1], "espera 100"); println!("todas as verificações passaram");}Saída:
liga, espera 100, desliga, espera 400, liga, espera 100, desliga, espera 400todas as verificações passaramOs dois mocks compartilham um único log por meio de Rc<RefCell<...>> (veja Ponteiros inteligentes), então você consegue verificar a ordem exata dos eventos. Em um projeto de verdade, as linhas com assert_eq! vão para uma função #[test] e rodam com cargo test no seu PC, sem precisar de placa (veja Testes em Rust e Testes avançados).
Na placa, você escreveria o mesmo driver com as traits de verdade do embedded-hal 1.0. As únicas mudanças: os métodos do pino devolvem um Result (um pino em um chip expansor de E/S pode falhar), então você repassa os erros com ?, e o código não precisa da std em nada:
#![no_std]
use embedded_hal::delay::DelayNs;use embedded_hal::digital::OutputPin;
pub struct StatusLed<P, D> { pin: P, delay: D,}
impl<P: OutputPin, D: DelayNs> StatusLed<P, D> { pub fn new(pin: P, delay: D) -> Self { StatusLed { pin, delay } }
pub fn blink(&mut self, times: u32, on_ms: u32, off_ms: u32) -> Result<(), P::Error> { for _ in 0..times { self.pin.set_high()?; self.delay.delay_ms(on_ms); self.pin.set_low()?; self.delay.delay_ms(off_ms); } Ok(()) }}O Output do Embassy implementa OutputPin, e embassy_time::Delay implementa DelayNs, então StatusLed::new(led, embassy_time::Delay) funciona no Pico. Para testes no PC com essas traits de verdade, o crate embedded-hal-mock (em inglês) oferece pinos e barramentos I2C e SPI falsos já prontos.
Matemática de sensores sem floats
Seção intitulada “Matemática de sensores sem floats”Muitos chips pequenos, como o Cortex-M0+ do RP2040, não têm unidade de ponto flutuante. Floats continuam funcionando, mas em software: mais lentos, e deixam o seu programa maior. Por isso, o código embarcado costuma usar números de ponto fixo: inteiros em uma unidade menor. Em vez de 25.25 graus, você guarda 25250 milésimos de grau.
Um sensor de temperatura do tipo TMP102 manda dois bytes. Os 12 bits de cima são um número com sinal, em passos de 1/16 °C:
// Os 12 bits de cima são a temperatura em passos de 1/16 °C (0,0625 °C),// como um número com sinal (complemento de dois).fn to_millicelsius(bytes: [u8; 2]) -> i32 { // i16 mantém o sinal. `>> 4` em um número com sinal também mantém. let raw = i16::from_be_bytes(bytes) >> 4; // 1/16 °C = 62,5 m°C, então multiplica por 625 e divide por 10. raw as i32 * 625 / 10}
// Imprime 25250 como "25.250" sem nunca encostar em um float.fn format_milli(m: i32) -> String { let sign = if m < 0 { "-" } else { "" }; let m = m.abs(); format!("{sign}{}.{:03}", m / 1000, m % 1000)}
fn main() { let samples = [ [0x19, 0x40], // uma sala quentinha [0x00, 0x10], // o menor passo acima de zero [0xFF, 0x80], // um pouco abaixo de zero [0xE7, 0x00], // um freezer ];
for bytes in samples { let m = to_millicelsius(bytes); println!("{bytes:02X?} -> {m:>6} m°C = {} °C", format_milli(m)); }}Saída:
[19, 40] -> 25250 m°C = 25.250 °C[00, 10] -> 62 m°C = 0.062 °C[FF, 80] -> -500 m°C = -0.500 °C[E7, 00] -> -25000 m°C = -25.000 °CTrês detalhes fazem isto funcionar. i16::from_be_bytes lê os dois bytes como um único número com sinal, com o byte mais significativo primeiro (“big-endian”), que é como a maioria dos sensores manda os dados. >> 4 em um inteiro com sinal mantém o sinal, então as temperaturas negativas saem certas. E format_milli trata o sinal separadamente: -500 / 1000 dá 0 na matemática de inteiros, então, sem esse passo, -0.5 °C seria impresso como 0.500. (O programa usa ponto como separador decimal, como é o padrão em código.)
A divisão de inteiros também trunca: 62,5 m°C virou 62. Meio milésimo de grau não faz diferença para um sensor de ambiente, mas sempre se pergunte quanta precisão uma unidade oferece. E fique de olho na faixa de valores: multiplicar uma leitura grande por um fator grande pode estourar um i32 (veja Overflow de inteiros). No PC, String funciona bem; no chip, você imprimiria com o defmt ou escreveria em uma heapless::String.
Erros comuns
Seção intitulada “Erros comuns”Usar coisas da std em um programa no_std. println!, Vec e String vêm da std, então simplesmente não estão lá:
error: cannot find macro `println` in this scope --> src/main.rs:4:5 |4 | println!("hello"); | ^^^^^^^O compilador diz que não encontrou a macro println neste escopo. Use defmt::info! para os logs e heapless (ou o crate alloc com um alocador) para as coleções.
Esquecer o tratador de panic. Um binário no_std precisa dizer o que um panic faz. Sem um crate como o panic-probe, você recebe:
error: `#[panic_handler]` function required, but not foundOu seja, falta uma função #[panic_handler]. Acrescente panic-probe (ou panic-halt) ao Cargo.toml e use panic_probe as _; ao main.rs.
Sobrescrever um registrador inteiro. reg = 1 << 3 liga o pino 3 e desliga todos os outros. Para mudar um bit só, use reg |= 1 << 3. Para campos, faça o read-modify-write mostrado acima.
Seguir um tutorial antigo. Os crates embarcados mudam rápido, e posts de blog de alguns anos atrás muitas vezes usam o embedded-hal 0.2 ou APIs antigas do Embassy. Comece pela pasta de exemplos da versão do crate de que você realmente depende.
Prática
Seção intitulada “Prática”1. Quais pinos estão ligados?
Seção intitulada “1. Quais pinos estão ligados?”Escreva toggle(reg: u32, pin: u32) -> u32, que inverte um bit, e pins_on(reg: u32) -> Vec<u32>, que lista todos os pinos cujo bit é 1. Comece com os pinos 0, 5 e 25 ligados, imprima a lista e quantos pinos estão ligados, depois inverta os pinos 5 e 7 e imprima de novo. Dica: teste cada pino de 0 a 31 com uma máscara; o u32 tem um método count_ones().
Ver solução
fn toggle(reg: u32, pin: u32) -> u32 { reg ^ (1 << pin)}
fn pins_on(reg: u32) -> Vec<u32> { (0..32).filter(|pin| reg & (1 << pin) != 0).collect()}
fn main() { let mut reg: u32 = (1 << 0) | (1 << 5) | (1 << 25); println!("ligados: {:?} ({} pinos)", pins_on(reg), reg.count_ones());
reg = toggle(reg, 5); reg = toggle(reg, 7); println!("ligados: {:?} ({} pinos)", pins_on(reg), reg.count_ones());}Saída:
ligados: [0, 5, 25] (3 pinos)ligados: [0, 7, 25] (3 pinos)O XOR inverte um bit, seja qual for o valor antigo, então inverter o pino 5 o desligou e inverter o pino 7 o ligou. count_ones() conta os bits 1 para você, então não é preciso um laço para essa parte.
2. Nível da bateria sem floats
Seção intitulada “2. Nível da bateria sem floats”Um ADC de 12 bits dá leituras de 0 a 4095 para 0 a 3300 mV. A bateria está ligada por um divisor de tensão que corta a tensão pela metade, então a tensão real é o dobro da medida. Escreva battery_mv(raw: u16) -> i32 e battery_percent(mv: i32) -> i32, em que 3000 mV é 0% e 4200 mV é 100%, nunca abaixo de 0 nem acima de 100. Experimente as leituras brutas 2606, 2482, 2234 e 1861. Dica: multiplique antes de dividir, para não perder precisão, e dê uma olhada no clamp.
Ver solução
const EMPTY_MV: i32 = 3000;const FULL_MV: i32 = 4200;
fn battery_mv(raw: u16) -> i32 { // ADC de 12 bits (0..=4095) medindo 0..=3300 mV, atrás de um divisor que corta a tensão pela metade raw as i32 * 3300 * 2 / 4095}
fn battery_percent(mv: i32) -> i32 { let percent = (mv - EMPTY_MV) * 100 / (FULL_MV - EMPTY_MV); percent.clamp(0, 100)}
fn main() { for raw in [2606, 2482, 2234, 1861] { let mv = battery_mv(raw); println!("leitura {raw} -> {mv} mV -> {}%", battery_percent(mv)); }}Saída:
leitura 2606 -> 4200 mV -> 100%leitura 2482 -> 4000 mV -> 83%leitura 2234 -> 3600 mV -> 50%leitura 1861 -> 2999 mV -> 0%Dividir por 4095 por último mantém a precisão: raw / 4095 * 6600 daria 0 para toda leitura abaixo de 4095. O maior produto, 4095 * 6600, fica em torno de 27 milhões, o que cabe tranquilamente em um i32.
- O Rust embarcado roda em microcontroladores sem sistema operacional. Sem garbage collector, com segurança verificada na compilação e abstrações de custo zero, o Rust combina muito bem com isso.
#![no_std]deixa você só com acore;#![no_main]permite que um crate de runtime chame a sua função de entrada; você precisa escolher um tratador de panic. Oheaplessoferece coleções de capacidade fixa.- As camadas: PAC (registradores), HAL (uma família de chips), traits do
embedded-hal(compartilhadas), BSP (uma placa) e frameworks como RTIC e Embassy por cima. - Adicione o target do chip com
rustup target add, depois grave o programa e veja os logs com o probe-rs e um debug probe (ou com oespflashem um ESP32). Odefmtdeixa os logs minúsculos. - Pratique no seu PC: máscaras de bits para registradores, drivers escritos com traits e peças falsas, e inteiros de ponto fixo em vez de floats.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. O que #![no_std] faz?
2. Qual camada permite que um único driver de sensor funcione em um Pico, em um ESP32 e em um STM32 sem mudanças?
3. Como ligar o bit 3 de reg sem mudar nenhum outro bit?
4. Por que o código embarcado muitas vezes guarda 25,25 °C como 25250 milésimos de grau?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- The Embedded Rust Book (em inglês): a introdução oficial a
no_std, periféricos, registradores e ao ecossistema embarcado. - Discovery (micro:bit v2) (em inglês): um curso gratuito para iniciantes que leva você de um LED piscando até sensores, passo a passo, em um BBC micro:bit.
- The Embassy Book (em inglês): como começar com o Embassy, como funcionam o executor e as HALs dele e como rodar os exemplos oficiais.
- The Rust on ESP Book (em inglês): o guia da própria Espressif sobre
esp-hal,esp-generate,espflashe as toolchains do ESP32. - Instalação do probe-rs (em inglês): como instalar o probe-rs em todos os sistemas operacionais e configurar o seu debug probe.