Share E-Book

Embedded Software with Rust (MEAP) (David Cabanis)(Z-Library)

Author David Cabanis

rust
Language English

Mod's Note- Epub-converted PDF. Embedded Software with Rust is a practical introduction to building firmware that is fast, efficient, and far safer than traditional embedded software written in C or C++. Rust gives developers the low-level control embedded systems demand but adds modern guarantees around memory safety, data races, and error handling. In a field where a single bug can cause crashes, security flaws, or costly field failures, those guarantees are a major advantage. In this engaging book, Dr. David Cabanis shows readers how Rust can deliver the performance and hardware access expected in embedded work while reducing entire categories of defects that have long been treated as unavoidable. More products than ever depend on specialized, connected, resource-constrained devices: consumer electronics, industrial control systems, vehicles, medical devices, robotics, and edge computing platforms. As those systems become more capable and more connected and integrate local AI features, expectations for reliability, security, and maintainability will keep rising. Engineers who can develop software close to the hardware, while meeting modern standards for robustness, are in growing demand. Rust is uniquely well suited to that future, and this book helps readers build those skills in a practical way. Embedded Software with Rust is designed to support learning step by step by introducing essential embedded concepts alongside the Rust features that make them safer and more expressive. As you read, you’ll understand not just what to do, but why it works. The book builds from foundational ideas toward real hardware-focused development in a way suitable for firmware beginners or experienced embedded developers who want to add Rust to their toolbox. Readers learn by connecting language features directly to practical embedded tasks such as peripheral access, memory-mapped I/O, timing, interrupts, and communication interfaces. …

Format PDF
Size 2.7 MB
13
Views
0
Downloads
0.00
Total Donations
(First 20 pages)

Registered users can read the full content for free

Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.

Page 1
(This page has no text content)
Page 2
(This page has no text content)
Page 3
Embedded Software with Rust
Page 4
Welcome Dear Reader, Thank you for purchasing the MEAP edition of Embedded Software with Rust. To get the most from this book, you should already be comfortable with low- level systems programming. A background in C or C++ is ideal. You do not need to be a Rust expert, but some familiarity with the language will help. If you have written firmware before and understand what a linker script does, what a vector table is, and why memory layout matters, you are the reader this book was written for. My interest in embedded Rust came from a practical frustration. C gives you full control of the hardware, but it also gives you full responsibility for every mistake. Memory-safety errors, invalid peripheral access, and silent aliasing between interrupt and foreground code are all things that show up late, often in the field, and often in ways that are hard to reproduce. Rust does not make embedded development easier in the early going. But it does move a significant class of defects from runtime to compile time, and once you have internalized that, the discipline it imposes starts to feel less like a constraint and more like a tool. This book covers what you need to build real no_std firmware on Cortex-M hardware. That means toolchain setup, linker configuration, startup and initialization, memory layout, hardware access through PACs and HALs, debugging and profiling methods, stack and heap management in constrained systems, async programming, performance and size optimization, and Rust/C interoperability for teams adopting Rust incrementally. The goal throughout is not to teach Rust as a language but to show how it applies to the firmware problems you are already working on. The material is hands-on. Each topic connects to working code and real hardware targets, including a Blue Pill board and QEMU for validation
Page 5
before deployment. The book is still being written, and your feedback during MEAP is genuinely useful. If something is unclear, technically off, or missing entirely, please say so in the liveBook Discussion Forum. That is exactly the kind of input that improves a book in progress. Thanks for being an early reader. — Dr David Cabanis In this book Welcome 1 The Foundations of embedded Rust development 2 Setting up your Rust tool-chain 3 Project structure and configuration 4 Startup and Initialization in Bare-Metal Rust 5 Memory-Mapped I/O, Register Abstractions, and Low-Level Control on Cortex-M
Page 6
1 The Foundations of embedded Rust development This chapter covers Why embedded Rust is timely for dependable firmware. Rust from reset to main(). Using Rust’s core crate with an no_std build (no operating system). Platform preview: Cortex-M focus with selected RISC-V examples. What you need to follow along. Embedded systems provide digital access to the physical analog world. They sense and act on real world signals through sensors, actuators, and memory- mapped peripherals. While traditional computers center on people and human facing applications, embedded systems center on the environment they measure and control (1.1). Embedded systems are found across a wide range of industries, including automotive, aerospace, medical devices, and consumer electronics, and are frequently responsible for controlling safety- critical functions where failure can result in harm to people or property. With the continued expansion of IoT ecosystems and connected hardware devices, the adoption of memory-safe programming languages with strong, enforceable safety guarantees is becoming increasingly important. Rust adoption can help industry address the security and safety gaps identified in European regulatory frameworks, including UNECE requirements, and has been formally recognized in NIS-related cybersecurity guidance as a language that supports secure software development through its emphasis on memory safety and concurrency correctness. This book is written for developers who already know their way around embedded programming and want to apply Rust’s strengths to produce firmware that is safer, more reliable, and easier to maintain over time. In this book, you will learn how to harness Rust’s strengths to build robust, secure, and scalable embedded systems, applying pragmatic techniques and best practices that transfer to real firmware projects. By working through the material, you will gain the practical skills to build and run no_std programs both in an emulator and on
Page 7
real hardware, configure microcontroller memory layouts and startup flows, and interact with peripherals through safe drivers and HAL-based abstractions. You will also learn how to share data safely between the main execution context and interrupt handlers, apply debugging techniques and basic profiling methods, and ultimately produce firmware that is production ready and built for long-term maintenance. The teaching approach is deliberately hands-on, using practical code examples, tooling-focused workflows, and proven development methodologies, supported by a ready-to- use environment for experimentation. Embedded Rust brings Rust compile-time guarantees and modern workflows to microcontrollers and other bare metal targets that have tight memory budgets, no operating system, and direct register level control. This chapter explains why that combination matters for building reliable firmware, outlines how an embedded Rust program runs from power on to the main loop, and clarifies the audience, scope, and toolkit used throughout the book. We focus primarily on Arm Cortex-M, the current industry de-facto microcontroller architecture, with selected RISC-V examples to demonstrate portability. Figure 1.1 Embedded systems
Page 8
1.1 Why embedded Rust? Benefits and adoption Embedded firmware has historically forced a choice between control and safety. Rust challenges that assumption by enforcing memory safety at compile time, without the runtime overhead that makes other safe languages unsuitable for microcontrollers. That makes it particularly well suited to the practical realities of firmware, where tight resource budgets and predictable timing are constant concerns. Rust is examined here alongside the most common alternatives, grounding each comparison in the constraints that embedded developers face daily.
Page 9
1.1.1 Compile-time safety and reliability Rust’s ownership and borrowing rules remove common memory safety defects at compile-time without a garbage collector. For firmware this reduces latent crash modes and makes failure behavior explicit through its type-based error handling primitives, Result and Option. The result is fewer field failures and lower verification costs in safety critical contexts. We’ll look at how these rules map onto peripherals and interrupts shortly. 1.1.2 Constraints, Determinism, and Performance Embedded products operate within strict physical boundaries defined by size, power, weight, and cost. Limited Flash and RAM budgets put pressure on code size and data layout, while power budgets penalize unnecessary wake times, cache misses, and extra copies. Rust addresses these constraints by preserving the predictability expected in C-style systems while enabling higher-level structure. By producing code without an interpreter and enabling fine control over allocation and lifetimes, Rust allows for safe designs that avoid redundant copying and hidden dynamic behavior. In a no_std (i.e. without a supporting operating system) setting, this efficiency goes further. Without an operating system, startup, memory placement, and interrupt behavior become explicit. Zero-cost abstractions compile down to optimized machine code, meaning well-tuned Rust on Cortex-M or RISC-V routinely matches C on throughput and latency. Linking against Rust’s core and a minimal runtime keeps binaries small enough for typical microcontroller constraints; the main differences in footprint usually stem from build flags and back-end configuration rather than the language itself. 1.1.3 How Rust compares to other approaches Both C and C++ offer full control and a small runtime, which is why they dominate in microcontrollers. They place the responsibility for memory safety entirely on the developer, and many field failures stem from pointer
Page 10
aliasing bugs, lifetime mistakes, and undefined behavior. Rust retains the control and size profile while moving safety checks to compile-time. It also brings a modern package manager and a unified build story that scales beyond a single project. Go offers fast iteration, strong tooling, and memory safety through a garbage collector, making it a productive choice for server-side and systems programming. For small microcontrollers, however, the runtime and collector are usually not a fit. Deterministic timing becomes difficult to guarantee when collection can run at any point, and this unpredictability is particularly problematic in interrupt-driven or real-time contexts. Stripped down ports exist but they trade away features and still carry runtime costs that are hard to justify on resource-constrained hardware. In contrast, Rust achieves memory safety without a collector, which keeps timing predictable and memory footprint under control. Zig emphasizes simplicity, control, and a small toolchain. It has manual memory management with compile-time features and a growing embedded traction. However, it does not enforce aliasing and lifetime rules at the language level, so many of the safety gains depend on discipline and review. Rust’s borrow checker and type system prevent entire bug classes before code runs, which is particularly valuable in safety-critical settings. MicroPython enables very rapid prototyping and education but it requires an interpreter and a runtime, which increases memory use and reduces predictability. This is undesirable for production firmware that needs tight control of time and size. Rust, on the other hand produces native code with no interpreter and supports gradual migration from prototypes to shipping builds while keeping safety intact. Languages that prioritize ease of use tend to introduce runtime costs or shift safety responsibility onto the developer through discipline and review rather than enforcing it at compile time. Languages that prioritize control tend to leave memory safety as a manual concern. Rust sits at the intersection of both: it retains the size and control profile of C-class systems while enforcing safety at compile time and providing a modern ecosystem. That combination makes it a stronger fit than the alternatives when the product must remain small and efficient.
Page 11
1.2 Rust execution sequence: From reset to main Understanding the sequence from reset to main provides essential grounding for the chapters ahead, where memory layout, startup configuration, and peripheral initialization are examined in detail. A clear mental model of this sequence makes it easier to reason about why certain Rust and toolchain constraints exist, and what actually happens on the hardware before user code runs. With that context in mind, the following diagram (1.2) offers a top- down view of what happens when a microcontroller powers on and how an embedded Rust program begins to execute, making the sequence concrete without yet diving into tooling or deep mechanics. Figure 1.2 Initialization sequence
Page 12
1.2.1 Power on and reset (steps 1 to 2) When a microcontroller powers on or receives a reset signal, the hardware follows a deterministic sequence that requires no operating system and no
Page 13
external coordination. The core fetches two values from a fixed address near the start of program memory. The first value initializes the main stack pointer. The second value is the reset vector, which points to the reset handler. Control transfers to the reset handler immediately. This simplicity is fundamental to embedded systems: the hardware is entirely self-contained, and the sequence is predictable every time the device starts. 1.2.2 Minimal runtime setup (step 2 to 4) Before user code can run safely, the memory environment must be brought to a consistent and known state. The reset handler is responsible for this preparation. It copies the contents of the .data section from non-volatile memory to RAM so that initialized global variables hold the correct values. It clears the .bss section so that zero-initialized statics are consistent and predictable. It may also perform basic hardware setup such as enabling a clock domain or configuring memory protection, and it ensures the vector table location is known so that exceptions and interrupts can be dispatched correctly. Without this step, user code would be operating on undefined memory, making program behavior unreliable regardless of how well the application logic is written. 1.2.3 Transfer to user code (step 5 and 6) Once memory is consistent and the runtime environment is prepared, the reset handler transfers control to the program entry point. In embedded Rust this is typically a function marked with an attribute that designates it as the entry. From this point, user code owns the main control flow. Most programs either enter a main loop that interacts with peripherals and timers, or yield control to lightweight tasks managed by an executor that waits for events. Because there is no operating system, the program itself defines all behavior with respect to time, peripheral use, and power management. This direct ownership of hardware is the defining characteristic of embedded development. 1.2.4 Exceptions and interrupts at a glance (step 7) Real embedded programs do not run in isolation. Hardware events, timers,
Page 14
and fault conditions compete for the processor’s attention at any moment. These are delivered through the vector table, where each event has a matching handler function. When an event fires, the core automatically saves a small register context onto the current stack and branches to the handler. When the handler returns, the saved context is restored and the preempted code resumes as if nothing had occurred. The challenge this introduces is data sharing: state that is accessed from both the main context and an interrupt handler must be managed carefully to avoid race conditions. Rust’s ownership and borrowing rules, combined with simple synchronization primitives, address this at the language level. Handler declaration, priority configuration, masking, and safe data sharing patterns are introduced conceptually here and examined in depth in later chapters. 1.3 What is non-standard library Rust? In this book, embedded Rust means bare-metal firmware that does not link the standard library (std), which in practice means firmware that runs without an operating system. Instead, the program links the core crate and, when a global allocator is explicitly provided, the optional alloc crate, together with a small startup or runtime crate appropriate to the target. 1.3.1 What no_std excludes and what core provides A no_std build omits std and, with it, all OS-backed facilities: files, sockets, threads, and the default heap. In its place you link core, which contains the language fundamentals: primitive numeric types, slices and arrays, traits such as Iterator and Debug, Option and Result for error handling, atomics appropriate to the target, and the compiler intrinsics they require. If you need heap-allocating collections, add alloc and supply a global allocator; otherwise, deeply embedded designs often prefer fixed-capacity structures and static storage. Coming from hosted Rust, the missing heap is the most visible change. The common, heap-allocating types live in alloc/std and are not available unless you opt in to the alloc crate and provide a global allocator (1.1). Table 1.1 Desktop Rust versus Embedded Rust applications
Page 15
Feature Desktop Rust (std- based) Embedded Rust (no_std) Runtime Environment Rich runtime support provided by an OS (Linux, Windows, macOS). Minimal runtime environment provided by the programmer or specialized crates (e.g., cortex-m-rt, riscv- rt). Standard Library Full access to std for file I/O, networking, threads, and dynamic heap allocation. Restricted to core library; no built-in threads, no file I/O, and no dynamic memory allocation by default. Memory Management Automatic memory layout managed by the OS; heap is globally available. Requires explicit memory layout defined by a custom linker script (memory.x). Heap usage requires a custom allocator. Startup Process OS loads the program and executes the standard main() function. The developer must define a custom entry point (_start or similar) and handle low level initialization (copying data, zeroing BSS) before the application begins. Requires direct access to
Page 16
I/O and Peripherals Relies on OS APIs for I/O (sockets, files). memory-mapped registers (MMIO) and hardware abstraction layers (HALs). The absence of std and the presence of core define the baseline. The memory and startup rows are implemented with a linker script and a minimal runtime, which the next two subsections describe. In the following code example, the String S is a Rust fat pointer with an address, a length and a capacity information all located on the stack. The actual string literal value "Bad string" is located on the heap. fn main() { // ┌─── (HEAP) let S = String::from("Bad string"); // └─── Fat pointer: ptr, len, capacity (STACK) This code cannot compile when targeting a no_std Rust application since it relies on the existence of a heap for dynamic memory allocations. Similarly, the following code would need to be adapted: #![no_std] fn my_function() { // Will not compile in pure `no_std` (no allocator available): // let v: Vec<u8> = Vec::new(); // Use fixed-capacity data instead: let mut buf: [u8; 32] = [0; 32]; let mut len = 0; if len < buf.len() { buf[len] = 0xAB; len += 1; } } By replacing the Vec<u8> object by a statically allocated Rust array (buf) we can now compile this code for an no_std application. These restrictions apply to other well known Rust types such as the Box<> and `HashMap<>`types.
Page 17
If you truly need heap-backed collections, you must add alloc and a global allocator (details later in Chapter 7). 1.3.2 Entry and panic requirements Because there is no operating system, a freestanding program cannot rely on the language’s hosted startup path. In Rust, the familiar fn main() is not a “bare” entry point; it is part of the standard-library runtime that expects a host environment. On hosted targets, main is reached through a chain that includes a C-style startup routine, argument setup, language/runtime initialization, and program exit cleanup. That baggage, useful on desktops, is undesirable (and often unavailable) on a microcontroller. In a no_std setting we therefore disable the default main with a crate-level attribute and provide our own entry via linker commands (ENTRY) or specialized labels such as _start. This places you in full control of the earliest instructions executed after reset and lets you keep startup minimal, deterministic, and free of host assumptions. A panic behavior must also be defined explicitly. Since the hosted unwinder and I/O reporting are absent, you supply a #[panic_handler] that never returns. During development it may log or break into a debugger; in production it can signal a fault or rely on a watchdog to reset. This explicit definition makes failure modes predictable and compatible with the constraints of a bare-metal target. #![no_std] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { loop { // optional: log, blink, or wait for watchdog } } 1.3.3 What minimal runtime crates initialize The earlier sections described the bring-up sequence conceptually. In practice, you rarely write that glue yourself. On Cortex-M and RISC-V, runtime crates such as cortex-m-rt and riscv-rt package the early steps so you can focus on application code: they place a vector table, set the initial
Page 18
stack pointer, copy .data, zero .bss, and then transfer control to your entry function declared via an attribute. They also provide attributes to register interrupt and exception handlers and ship sensible weak defaults that you can override. These crates are generic by design. For common single-image firmware they are fit for purpose out of the box. When a project has non-standard needs, you extend the setup through well-defined extension points rather than abandoning the crate. The concrete mechanics of each of the following customizations: 1. Supply your own linker script or memory.x to define memory regions and control section placement. 2. Relocate the vector table (for example, into RAM) or extend it with board-specific interrupts. 3. Add an early hook (before main) to bring up external RAM, clocks, or other essential platform services. 4. Replace the default panic behavior to match your product’s fault- handling strategy. Linker changes, startup hooks, vector table placement, and image layout, are covered in later chapters of this book. 1.4 Core Rust features in embedded context The guarantees outlined in section 1.2 and the execution model in section 1.3 materialize through a few core language mechanics: ownership and borrowing of peripherals, construction-time singletons, type-state APIs, and disciplined sharing across interrupts and async tasks, all of which keep resource control explicit and safe without relying on an operating system. 1.4.1 Ownership and borrowing for hardware resources In embedded work a peripheral is a finite resource. Only one place in the program should be able to reconfigure a timer or a serial port at any moment. Rust expresses this constraint through ownership. A peripheral handle is an owning value that controls access to a memory-mapped register block.
Page 19
Moving the handle transfers exclusive control to the callee. When code needs temporary access it borrows the handle instead. A shared reference permits read-only access; a mutable reference grants short-lived exclusive access and ends when the borrow ends. The compiler enforces that there is never more than one live mutable reference to the same resource and that shared and mutable access never overlap. In practice you move the handle into the module that configures and drives the device and pass short-lived borrows to helpers when needed. This avoids a common C pitfall: accidentally ending up with two pointers (or a pointer plus a global) that both refer to the same peripheral registers, so two parts of the code can read and write the hardware concurrently without any warning. 1.4.2 The singleton pattern for peripherals Most microcontrollers expose each peripheral exactly once. Rust makes that a construction time invariant. Normally, a device or board crate provides a function such as Peripherals::take() that returns an owning structure for all register blocks only once; subsequent calls report that the peripherals have already been taken. You then split this structure into individual, non-Copy handles (for example a clock controller and a GPIO port) and move those handles into the modules that require them. Because the handles are unique and not clonable, a second owner cannot appear by accident. When two modules must use the same device, the owner exposes explicit methods or lends narrowly scoped borrows rather than creating another handle. The following code snippet demonstrates this principle. The peripherals are captured inside cp. Next, a single timer (the systick timer) is moved from the cp variable to the syst variable. As various methods are applied to the syst variable, the timer is mutually borrowed and returned each time. fn main() -> ! { let cp = cortex_m::Peripherals::take().unwrap(); let mut syst = cp.SYST; syst.set_reload(8_000_000); syst.clear_current(); syst.enable_counter(); syst.enable_interrupt(); loop {} }
Page 20
1.3 illustrates the singleton pattern applied to peripheral ownership in embedded Rust. Only one variable can hold a given hardware resource at any time, which rules out the concurrent access bugs common in C-based firmware. Figure 1.3 Singleton design pattern in embedded Rust 1.4.3 Type-enforced hardware correctness
The above is a preview of the first 20 pages. Register to read the complete e-book.

Support Author

0.00
Total Amount (¥)
0
Donation Count
Please enter an amount Minimum ¥1

You will be redirected to Alipay to complete payment, then return here.

Recommended for You

Loading recommended books...
Failed to load, please try again later
Back to List