Rust embarcado (introdução)
LidoEsta aula apresenta uma introdução ao desenvolvimento embarcado com Rust, abordando o ambiente no_std, a camada de abstração de hardware (HAL), as vantagens de Rust em sistemas embarcados e o ecossistema de ferramentas e crates disponíveis. O aluno aprenderá na prática como escrever código sem sistema operacional e compreenderá a estrutura básica de um projeto embarcado.
Bem-vindo à aula de introdução ao Rust embarcado! Nesta aula, vamos explorar como a linguagem Rust pode ser utilizada para desenvolver software para microcontroladores e sistemas embarcados, onde os recursos são limitados e a confiabilidade é crucial. Você aprenderá sobre o ambiente no_std, que é a base para a programação embarcada em Rust, além de conhecer as camadas de abstração de hardware (HAL) e o ecossistema de ferramentas disponíveis.
Rust tem se destacado como uma linguagem segura e eficiente para sistemas embarcados, oferecendo garantias de memória sem coletor de lixo, o que é essencial em ambientes com restrições de memória e tempo real. Nesta aula, vamos mergulhar nos conceitos fundamentais, com exemplos práticos e referências para que você possa aprofundar seus conhecimentos.
no_std na prática
O atributo #![no_std] é a porta de entrada para o desenvolvimento embarcado em Rust. Ele indica que o código não dependerá da biblioteca padrão (std), que presume a existência de um sistema operacional. Em vez disso, usamos a biblioteca core, que fornece funcionalidades básicas como tipos primitivos, iteradores e traits fundamentais, sem exigir alocação dinâmica ou entrada/saída padrão.
Na prática, isso significa que você não poderá usar String, Vec ou println! diretamente. Em vez disso, você utilizará tipos como &str, arrays e slices, e para comunicação com o mundo externo, fará uso de periféricos do microcontrolador (como UART, GPIO, etc.) através de HALs ou acesso direto a registradores.
Um exemplo mínimo de um programa no_std para um microcontrolador ARM Cortex-M (usando o crate cortex-m-rt) seria:
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _; // define o comportamento de pânico (halt)
#[entry]
fn main() -> ! {
// código do programa embarcado
loop {}
}
Neste código, #![no_main] indica que não usamos a função main tradicional, mas sim um ponto de entrada definido pelo runtime do microcontrolador. O crate panic_halt define o que acontece em caso de pânico (geralmente, parar a execução). O loop infinito garante que o programa nunca termine.
Para compilar e executar esse código, você precisa de um alvo de compilação específico (como thumbv7em-none-eabihf para Cortex-M4F) e usar o probe-run ou um debugger para carregar o binário no hardware.
HAL (visão geral)
HAL (Hardware Abstraction Layer) é uma camada de software que fornece uma API uniforme para acessar os periféricos do microcontrolador, independentemente do fabricante. Em Rust, existem HALs por fabricante (como stm32f4xx-hal para STM32) e também HALs genéricos (como embedded-hal), que definem traits comuns para operações como digital I/O, comunicação serial (UART), SPI, I2C, etc.
O objetivo do HAL é permitir que o código seja portável entre diferentes microcontroladores, desde que eles implementem os mesmos traits. Por exemplo, você pode escrever uma biblioteca que funcione com qualquer placa que implemente o trait Read para um sensor, e depois usar essa biblioteca em diferentes projetos.
Um exemplo de uso de HAL com o crate stm32f4xx-hal para piscar um LED:
use stm32f4xx_hal::prelude::*;
use stm32f4xx_hal::gpio::{Edge, ExtiPin, GpioExt};
use stm32f4xx_hal::pac;
use stm32f4xx_hal::timer::Timer;
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let mut gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
let mut timer = Timer::syst(dp.SYST, &clocks).counter_hz();
timer.start(1.Hz());
loop {
led.set_high();
block!(timer.wait()).unwrap();
led.set_low();
block!(timer.wait()).unwrap();
}
}
Esse código configura o pino PA5 como saída (LED), cria um timer de 1 Hz e alterna o estado do LED. O HAL encapsula os detalhes de registradores e permite uma leitura mais clara.
Existem também HALs para outras famílias, como AVR (Arduino), ESP32, RP2040 (Raspberry Pi Pico), entre outros. A escolha do HAL depende do microcontrolador que você está usando.
Por que Rust em embarcados
Rust traz várias vantagens para o desenvolvimento embarcado em comparação com C/C++:
- Segurança de memória: O compilador impede erros como uso após liberação, dupla liberação e buffer overflow, que são comuns em C e podem causar falhas críticas.
- Sem coletor de lixo: A memória é gerenciada por regras de ownership e borrowing, garantindo que não haja pausas imprevisíveis e que o consumo de memória seja determinístico.
- Zero-cost abstractions: As abstrações de alto nível não têm custo em tempo de execução, permitindo código expressivo sem sacrificar desempenho.
- Concorrência segura: O sistema de tipos previne corridas de dados, essencial em sistemas com interrupções e múltiplas tarefas.
- Ecossistema crescente: A comunidade de Rust embarcado é ativa e há muitos crates prontos para uso.
Essas características tornam Rust ideal para sistemas críticos, como dispositivos médicos, automotivos e IoT, onde erros podem ter consequências graves.
Ecossistema
O ecossistema de Rust embarcado é composto por ferramentas e crates que facilitam o desenvolvimento:
- Embassy: Um framework assíncrono para embarcados que fornece execução de tarefas sem alocação, timers e comunicação assíncrona entre periféricos.
- RTIC (Real-Time Interrupt-driven Concurrency): Um framework para sistemas de tempo real baseado em interrupções, com análise de prioridades em tempo de compilação.
- cortex-m-rt: Crate que fornece o ponto de entrada e o vetor de interrupções para processadores ARM Cortex-M.
- embedded-hal: Traits genéricos para periféricos, usados por HALs de fabricantes.
- probe-run: Ferramenta para carregar e depurar programas em microcontroladores via probe (como ST-Link, J-Link).
- cargo-embed: Utilitário para gravar firmware via bootloader ou probe.
Além disso, existem placas de desenvolvimento com suporte oficial, como a Raspberry Pi Pico (RP2040) e a Nucleo da STMicroelectronics, que possuem exemplos e templates.
Para começar, você pode usar o cargo generate com um template de projeto para sua placa, ou usar o cortex-m-quickstart como base.
Boas práticas e observações finais
Ao desenvolver para embarcados, é importante seguir algumas boas práticas:
- Sempre use
#![no_std]em bibliotecas que serão usadas em embarcados, para garantir portabilidade. - Utilize
embedded-haltraits em vez de APIs específicas de fabricante sempre que possível, para facilitar a reutilização de código. - Evite alocação dinâmica; use buffers estáticos ou pools de memória.
- Teste seu código em hardware real desde o início, pois emuladores podem não capturar todos os problemas.
- Mantenha o firmware pequeno e eficiente, pois o flash e a RAM são limitados.
Esta introdução fornece uma base sólida para você explorar o mundo do Rust embarcado. Nos próximos passos, você pode aprofundar-se em tópicos como interrupções, DMA, comunicação serial e desenvolvimento assíncrono.
Referências
- Documentação da biblioteca core do Rust
- The Embedded Rust Book
- embedded-hal no GitHub
- cortex-m-rt no docs.rs
- Embassy - async embedded framework
- RTIC - Real-Time Interrupt-driven Concurrency
- cortex-m-quickstart template
Exercícios
- Explique a diferença entre
stdeno_stdem Rust e por queno_stdé necessário para embarcados. - O que é um HAL? Dê um exemplo de um crate HAL para um microcontrolador específico.
- Quais são as principais vantagens de usar Rust em sistemas embarcados em relação a C?
- Cite três ferramentas ou crates importantes no ecossistema de Rust embarcado e descreva brevemente o que cada uma faz.
- Escreva um programa mínimo em Rust
no_stdpara um microcontrolador ARM Cortex-M que pisca um LED conectado ao pino PA5, usando o HALstm32f4xx-hal. (Não é necessário compilar, apenas escreva o código.)
std assume a presença de um sistema operacional e fornece funcionalidades como alocação dinâmica de memória, threads, entrada/saída padrão e comunicação com o sistema. Em sistemas embarcados, não há SO, então usamos core, que é um subconjunto de std sem essas dependências. no_std é necessário para que o código funcione em ambientes sem OS, garantindo que o binário seja pequeno e não dependa de recursos indisponíveis.stm32f4xx-hal, que implementa traits do embedded-hal para a família STM32F4. Ele permite configurar pinos GPIO, UART, SPI, I2C, timers, etc., de forma segura e portável.embedded-hal: define traits comuns para periféricos, permitindo portabilidade. 2) cortex-m-rt: fornece o ponto de entrada e o vetor de interrupções para processadores Cortex-M. 3) probe-run: ferramenta de linha de comando para carregar e depurar programas em microcontroladores via probe.#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::prelude::*;
use stm32f4xx_hal::gpio::GpioExt;
use stm32f4xx_hal::pac;
use stm32f4xx_hal::timer::Timer;
#[entry]
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let mut rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let mut gpioa = dp.GPIOA.split();
let mut led = gpioa.pa5.into_push_pull_output();
let mut timer = Timer::syst(dp.SYST, &clocks).counter_hz();
timer.start(1.Hz());
loop {
led.set_high();
block!(timer.wait()).unwrap();
led.set_low();
block!(timer.wait()).unwrap();
}
}