Skip to Content
Course content

70: Embedded Rust Basics (no_std)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you've lived your whole life in a fully furnished, modern apartment. You have running water, electricity, a refrigerator, and a trash collection service. You don't think about how the water gets to the tap or how the power reaches the outlets; you just use the "services" provided by the building. This is exactly what std (the Rust Standard Library) is for you. It provides the "utilities"—heap allocation, file system access, and networking—by leaning on the underlying Operating System (Windows, Linux, macOS).

Now, imagine you're going on a hardcore backpacking trip into the wilderness. You've left the apartment behind. There is no plumbing, no power grid, and no one to take out your trash. You only have what you can fit in your pack. If you want water, you have to find a stream and filter it yourself. If you want fire, you have to strike a flint. This is no_std. You are stripped of the luxuries of an OS, and you're interacting directly with the raw hardware.

Cutting the Cord with #![no_std]

To tell the Rust compiler that we're heading into the wilderness, we use an inner attribute at the very top of our crate root. I want you to notice the exclamation mark; it's an inner attribute, affecting the entire crate.

#![no_std]

The moment you add this, the compiler stops linking the standard library. You'll suddenly find that std::vec::Vec, std::string::String, and std::fs are gone. It's a bit jarring at first, but it's necessary because those types assume there is an allocator (a heap) and an OS to manage memory and files. On a tiny microcontroller with 32KB of RAM, those assumptions are dangerous lies.

Living off the Land with core

You aren't completely stranded, though. Rust provides a crate called core. Think of this as your "survival kit." It contains everything that doesn't require an OS to function: basic types, Option, Result, and most of the iterator logic you're used to. In a no_std environment, the compiler automatically maps things like core::option::Option to just Option, so your logic remains mostly the same.

But here is the catch: no Vec and no String. If you need a collection, you'll either have to use fixed-size arrays [T; N] or bring in a third-party crate like heapless that provides "stack-allocated" versions of these structures. I've spent far too many hours trying to use a String in a bootloader only to realize I didn't have a memory allocator defined.

Handling the Crash

In a normal program, when your code panics, the standard library catches it, prints a nice error message to the console, and exits the process. But remember: we have no console and no OS to "exit" to. If the hardware panics in no_std, the CPU literally doesn't know what to do next.

You are required to define your own panic_handler. This is a function that the compiler calls when something goes horribly wrong. In a real production environment, you might log the error to a flash chip or trigger a system reset. For basics, we usually just loop forever to freeze the system for debugging.

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {
        // Spin forever. In a real chip, you'd maybe blink an LED rapidly.
    }
}

The ! return type is the "never" type. It tells Rust that this function will never return, which is exactly what happens when your embedded device hits a fatal error.

Talking to the Metal

Without std::io, how do you actually do anything? In embedded Rust, we use Peripheral Access Crates (PACs) and Hardware Abstraction Layers (HALs). Instead of calling println!, you write values to specific memory addresses called "Registers."

Essentially, the hardware designers mapped the physical pins of the chip to specific addresses in memory. To turn on an LED, you don't "call a function" in the traditional sense; you flip a specific bit at a specific memory address. The HAL crates wrap these ugly memory writes into nice Rust functions so you don't have to memorize a 1,000-page datasheet just to blink a light.




📋 Practical Task

Building a Bare-Metal Heartbeat LED

You are tasked with setting up the skeleton for a "Heartbeat" indicator on a microcontroller. Since we don't have a physical chip in this browser environment, you will write the no_std boilerplate required to make this work.

Create a main.rs file that satisfies the following requirements:

  • Disable the standard library.
  • Implement a panic_handler that enters an infinite loop.
  • Define a main` function that is marked as #[no_mangle] (to prevent the compiler from changing the name, which the hardware bootloader needs to find the entry point).
  • Inside main, simulate "toggling a register" by creating a static mut variable named LED_REGISTER and flipping its value from 0 to 1 using an unsafe block.

Constraint: You must not use any std imports. Only use core.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.