Chapter 7: nostd and nomain
September 17, 2026 · View on GitHub
Introduction
Every firmware crate in this course starts with the same two lines:
#![no_std]
#![no_main]
These two crate attributes are the moment a program leaves the host and becomes bare metal. This chapter explains what each one really does, what we give up, and what we get in return.
What Does no_main Mean?
A normal Rust program's entry point is a main function named and executed
before anything else. On a microcontroller there is no operating system to
call it, and the CPU does not boot into Rust at all.
The boot sequence starts in assembly and a startup routine (provided by
cortex-m-rt) that:
+----------------+ +------------------+ +------------------+
| Hardware | --> | Vector Table | --> | Reset Handler |
| reset | | (first word SP, | | (sets up stack, |
| | | second PC) | | SVC, sections) |
+----------------+ +------------------+ +------------------+
|
v
+------------------+
| Rust init code |
| (embassy_rp) |
+------------------+
#![no_main] tells the Rust compiler: "do not expect a main function from
the standard library, we are defining our own entry point." Combined with
cortex-m-rt's entry machinery, our Rust code gets called after the CPU is in
a known state.
In Embassy the precise entry point is invented by the executor. The
#[embassy_executor::main] attribute on an async fn replaces main (Chapter
14). That function never returns — the executor runs forever.
What Does no_std Mean?
The Rust standard library std assumes an operating system: it brings threads,
a heap allocator, println!, files, networking — all provided by a kernel. A
microcontroller has no kernel, so we opt out with #![no_std], and suddenly we
have access to only the core library.
core is the part of the standard library that needs no OS:
+----------------------+ +-----------------------------+
| std (needs OS) | | core (no OS required) |
+----------------------+ +-----------------------------+
| Vec, String, Box | | integer types, slices, |
| HashMap, threads | | Option, Result, iterators, |
| println!, files | | traits (Copy, Debug, ...) |
| heap allocator | | panic machinery (hook) |
+----------------------+ +-----------------------------+
Everything we used in Part I — ownership, borrowing, structs, enums, match,
traits, generics — lives in core and works unchanged in no_std. What we
lose, we do not need: Vec and String require an allocator and dynamic
memory; firmware that avoids them has deterministic, bounded memory usage.
Note the escape hatch. Our lib.rs writes:
#![cfg_attr(not(test), no_std)]
Under test — when we compile for the host to run tests — the crate is std.
That is the entire trick behind the host test suite in Chapter 25.
Panic Handling
What happens when a program must give up? On the host, panic! unwinds the
stack. In no_std there may be no stack unwinding, and in any case there is no
console to print to.
Every driver imports a panic handler with a trick import:
use panic_halt as _;
The as _ means "bring the crate in solely for its side effects." panic-halt
installs a panic handler: when a panic occurs, it simply halts the CPU —
typically waiting forever (wfi/b). On our boards, the LED simply stops
blinking and the chip rests. That is the correct default for a bare-metal
library: halt rather than corrupt.
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
Only one panic handler may exist per binary, which is why the imported crate
ships the #[panic_handler] and we never write our own.
Channels of Output
Without println!, how do we see anything? Firmware has its own channels:
+----------------+ +----------------+ +----------------+
| GPIO + LED | | UART (serial) | | SWD debugger |
| blink patterns| | printf-style | | probe-rs |
+----------------+ +----------------+ +----------------+
The blink driver's rhythm of on/off is its output channel. The UART driver
writes real bytes to a terminal over TX/RX — the same channel a desktop print
uses, wired through a physical serial cable.
Summary
#![no_main]surrenders the standard entry point; the CPU boots through a vector table and reset handler, then into Embassy's executor.#![no_std]drops the OS-dependent standard library but keepscore, which contains everything from Part I.- Panics halt the CPU by default (
panic-halt), the safe bare-metal choice. - Output happens through hardware — LEDs, UART, and debug probes — not stdout.
Next we install the toolchain and build our first firmware target.