Rust sin std: desarrollo embebido y no_std para microcontroladores

Cuando programas para un microcontrolador, no tienes sistema operativo, no tienes gestor de memoria dinámica y no tienes las utilidades del sistema en las que se basa std. Rust lo resuelve con #![no_std]: un atributo que desactiva la biblioteca estándar y permite usar solo core (tipos primitivos, traits y operaciones básicas sin dependencias del SO).

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}  // En bare metal, detén el programa
}

#[no_mangle]
pub extern "C" fn main() -> ! {
    loop {}
}

#![no_std] elimina la biblioteca estándar. #![no_main] indica que no hay función main convencional (el punto de entrada lo define el runtime del microcontrolador). El atributo #[panic_handler] es obligatorio: define qué hacer cuando el código entra en pánico.

El ecosistema embedded de Rust

Rust para embedded tiene capas bien definidas:

  • core: el subconjunto de std sin heap ni OS. Siempre disponible.
  • alloc: tipos que necesitan heap (Vec, String, Box). Disponible si defines un allocator.
  • embedded-hal: traits para periféricos (GPIO, SPI, I2C, UART). La capa de abstracción de hardware.
  • HAL de chip: implementaciones de embedded-hal para chips específicos (rp2040-hal, stm32f4xx-hal, nrf-hal...).

embedded-hal: la capa de abstracción

El crate embedded-hal define traits para interactuar con periféricos de hardware. Si escribes código contra estos traits, funciona en cualquier microcontrolador que los implemente:

use embedded_hal::digital::OutputPin;
use embedded_hal::delay::DelayNs;

fn parpadear<P: OutputPin, D: DelayNs>(pin: &mut P, delay: &mut D) -> ! {
    loop {
        pin.set_high().unwrap();
        delay.delay_ms(500);
        pin.set_low().unwrap();
        delay.delay_ms(500);
    }
}

// Esta función funciona en RP2040, STM32, nRF, ESP32...
// Solo cambia qué implementación de OutputPin y DelayNs le pasas

Raspberry Pi Pico con rp2040-hal

El RP2040 es el chip del Raspberry Pi Pico. El crate rp2040-hal implementa embedded-hal para este chip:

[dependencies]
rp2040-hal = { version = "0.9", features = ["rt"] }
rp2040-boot2 = "0.3"
cortex-m-rt = "0.7"
cortex-m = "0.7"
embedded-hal = "1"
panic-halt = "0.2"

[profile.release]
opt-level = 's'   # Optimizar para tamaño
#![no_std]
#![no_main]

use rp2040_hal::{
    self as hal,
    pac,
    gpio::Pins,
    Clock,
};
use embedded_hal::digital::OutputPin;
use cortex_m_rt::entry;
use panic_halt as _;

#[link_section = ".boot2"]
#[used]
static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

#[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 = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let mut delay = cortex_m::delay::Delay::new(
        core.SYST,
        clocks.system_clock.freq().to_Hz()
    );

    let sio = hal::Sio::new(pac.SIO);
    let pins = Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);

    let mut led = pins.gpio25.into_push_pull_output();

    loop {
        led.set_high().unwrap();
        delay.delay_ms(500);
        led.set_low().unwrap();
        delay.delay_ms(500);
    }
}

I2C con embedded-hal

use rp2040_hal::{I2C, gpio::FunctionI2C};
use embedded_hal::i2c::I2c;

// Leer datos de un sensor de temperatura I2C (dirección 0x48)
fn leer_temperatura<I: I2c>(i2c: &mut I) -> f32 {
    let mut buf = [0u8; 2];
    i2c.read(0x48, &mut buf).unwrap();

    let raw = ((buf[0] as i16) << 8) | buf[1] as i16;
    raw as f32 * 0.0625  // Factor de conversión del sensor TMP102
}

// En el main, configurar I2C:
let i2c = hal::I2C::i2c0(
    pac.I2C0,
    pins.gpio4.reconfigure(),  // SDA
    pins.gpio5.reconfigure(),  // SCL
    400.kHz(),
    &mut pac.RESETS,
    &clocks.system_clock,
);

let temperatura = leer_temperatura(&mut i2c_instance);
// temperatura ? 23.5

Heap con embedded-alloc

Si necesitas Vec, String o Box en no_std, puedes activar un allocator:

[dependencies]
embedded-alloc = "0.5"
#![no_std]
extern crate alloc;

use embedded_alloc::LlffHeap as Heap;
use alloc::vec::Vec;

#[global_allocator]
static HEAP: Heap = Heap::empty();

#[entry]
fn main() -> ! {
    // Inicializar el heap con 4KB
    {
        use core::mem::MaybeUninit;
        const HEAP_SIZE: usize = 4096;
        static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
        unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
    }

    // Ahora puedes usar Vec, String, Box...
    let mut datos: Vec<u32> = Vec::new();
    datos.push(42);

    loop {}
}

RTIC: concurrencia sin race conditions

RTIC (Real-Time Interrupt-driven Concurrency) es un framework para sistemas embebidos que garantiza en tiempo de compilación que no hay race conditions en los recursos compartidos entre interrupciones:

[dependencies]
rtic = { version = "2", features = ["thumbv6-backend"] }
#[rtic::app(device = rp2040_hal::pac, peripherals = true)]
mod app {
    use rp2040_hal::gpio::Pin;

    #[shared]
    struct Shared {
        contador: u32,
    }

    #[local]
    struct Local {
        led: Pin</* ... */>,
    }

    #[init]
    fn init(ctx: init::Context) -> (Shared, Local) {
        // Inicialización del hardware
        (
            Shared { contador: 0 },
            Local { led: /* configurar led */ },
        )
    }

    #[idle]
    fn idle(_ctx: idle::Context) -> ! {
        loop {
            cortex_m::asm::wfi();  // Wait for interrupt
        }
    }

    #[task(binds = TIMER_IRQ_0, shared = [contador])]
    fn timer(mut ctx: timer::Context) {
        ctx.shared.contador.lock(|c| {
            *c += 1;
        });
    }
}

RTIC usa el sistema de tipos de Rust para garantizar que el acceso a recursos compartidos entre interrupciones siempre está protegido. Si intentas acceder a un recurso compartido sin el lock, el compilador lo rechaza.

Rust embedded está en un punto de madurez donde puedes escribir firmware real para producción. Los HAL más usados (RP2040, STM32, nRF52) tienen documentación extensa y comunidades activas en la organización rust-embedded de GitHub.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP