Chapter 21: main.rs
September 17, 2026 · View on GitHub
Introduction
main.rs is the only file that touches the RP2350. It initializes the HAL,
configures one GPIO output, and runs an infinite async loop that toggles a
LedController and writes the result to the pin. Every concept from Part III —
executor, Timer, GPIO, peripherals — lands here in 80 lines.
Complete Source Code
Here is src/main.rs in full:
/*
* @file main.rs
* @brief Microcontroller entry point
* @author Kevin Thomas
* @date 2025
*
* MIT License
*
* Copyright (c) 2025 Kevin Thomas
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
//! FILE: main.rs
//!
//! DESCRIPTION:
//! RP2350 Embedded Rust Embassy Blink Application.
//!
//! BRIEF:
//! Main application entry point for RP2350 GPIO blink driver using Embassy.
//! Implements async LED blinking on GPIO 16.
//!
//! AUTHOR: Kevin Thomas
//! CREATION DATE: November 28, 2025
//! UPDATE DATE: December 4, 2025
#![no_std]
#![no_main]
mod config;
mod led;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_time::Timer;
use led::{led_state_to_level, LedController};
use panic_halt as _;
/// Main application entry point.
///
/// # Details
/// Initializes Embassy runtime and runs the main blink loop.
/// Uses BlinkController for state management.
///
/// # Arguments
/// * `_spawner` - Embassy task spawner (reserved for future async tasks).
///
/// # Returns
/// * `()` - Never returns (infinite loop).
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let mut led = Output::new(p.PIN_16, Level::Low);
let mut controller = LedController::new();
loop {
let state = controller.toggle();
if led_state_to_level(state) {
led.set_high();
} else {
led.set_low();
}
Timer::after_millis(controller.delay_ms()).await;
}
}
Every line earns its place. We work top to bottom.
Crate Attributes
#![no_std]
#![no_main]
Chapter 7 in two lines. The binary runs on bare metal with no standard main.
Embedded-only dependencies — embassy-rp, cortex-m-rt, panic-halt — exist
because these attributes demanded them.
Module Declarations and Imports
mod config;
mod led;
mod pulls the two library files into this binary's tree. config is used
indirectly (its constant flows in through LedController::new()); led
supplies LedController, led_state_to_level, and their friends.
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_time::Timer;
use led::{led_state_to_level, LedController};
use panic_halt as _;
One item per dependency, exactly the surface this binary needs:
Spawner— the executor handle passed tomain(unused today).LevelandOutput— GPIO output configuration.Timer— the async wait.- the two
ledexports — the state machine. panic_halt as _— the panic handler side-effect import.
Nothing extra compiles in; the tiny binary mirrors its tiny purpose.
The Async Entry Point
/// Main application entry point.
///
/// # Details
/// Initializes Embassy runtime and runs the main blink loop.
/// Uses BlinkController for state management.
///
/// # Arguments
/// * `_spawner` - Embassy task spawner (reserved for future async tasks).
///
/// # Returns
/// * `()` - Never returns (infinite loop).
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
The docstring follows the strict standard even for main: # Details,
# Arguments, and # Returns (here, "never returns"). The attribute is Chapter
14 made concrete: it starts the executor and spawns this async function. The
_spawner parameter is held for future tasks — the leading underscore tells the
compiler we know it is unused.
Initializing the Peripherals
let p = embassy_rp::init(Default::default());
let mut led = Output::new(p.PIN_16, Level::Low);
let mut controller = LedController::new();
Three lines, three lifetimes of setup:
embassy_rp::init(Default::default())— Configures clocks, releases reset, returns thePeripheralstokens (Chapter 16).Output::new(p.PIN_16, Level::Low)— Claims GPIO16 as an output and drives it low (Chapter 17). The tokenp.PIN_16moves into the pin; GPIO16 stays ours forever.LedController::new()— Starts the state machine Off at 500 ms (Chapter 20).
The Blink Loop
loop {
let state = controller.toggle();
if led_state_to_level(state) {
led.set_high();
} else {
led.set_low();
}
Timer::after_millis(controller.delay_ms()).await;
}
Four steps per iteration:
controller.toggle()— advance the state machine and remember the result.if led_state_to_level(state)— translate LedState into a hardware decision.led.set_high()/set_low()— actually drive the wire.Timer::after_millis(controller.delay_ms()).await— sleep the cooperative way, letting the executor idle or run other tasks.
The loop is the heart of the program: at 500 ms per turn, the LED toggles every half second — the blink.
Summary
main.rsis the only embedded file; it wires executor, GPIO, and controller.#![no_std]+#![no_main]make it bare metal;panic_halt as _covers panics.#[embassy_executor::main]starts the executor on the RP2350.- Initialization happens once: HAL init, output pin, controller.
- The loop alternates state-machine and GPIO writes around a cooperative
Timerwait.
The blink driver is complete and testable. Part V teaches the second driver — the button — starting with the hardware reality that makes it interesting: debouncing.