What is the Embedonomicon
masterno_std targets in Rust. It provides technical knowledge and practical examples for embedded development. The hosted version of the book can be found at https://docs.rust-embedded.org/embedonomicon/repository·master·Indexed 42 days ago
https://github.com/rust-embedded/embedonomiconA technical guide for bootstrapping support for `no_std` targets in the Rust programming language, specifically for embedded systems. It covers compiler support, creating custom target specification JSON files, using `build-std` for cross-compilation, and integrating assembly via the `cc` crate or pre-assembled `.a` archives.
no_std targets in Rust. It provides technical knowledge and practical examples for embedded development. The hosted version of the book can be found at https://docs.rust-embedded.org/embedonomicon/Pinning alone is not sufficient to safely use stack-allocated arrays for DMA. If a buffer is pinned but resides on a stack frame that is destroyed (e.g., a function returns), the DMA will attempt to write to a corrupted stack frame, leading to undefined behavior. To ensure safety, the DMA API should require a 'static bound on the buffer type. This ensures that the buffer lives for the entire duration of the program and is not tied to a temporary stack frame.
// The API should require a 'static bound to prevent stack corruption
// Example pattern from examples/seven.rsWhen designing a trait for a global singleton, ensure the following:
&self (shared references) to support static storage.Sync to ensure the singleton can be shared across threads/interrupts.Error if you intend to use trait objects, as all consumers would otherwise need to agree on the specific error type.If your target architecture is not directly supported by the standard Rust toolchain, you have several options depending on the level of support available:
rustc has the backend disabled: You must modify the Rust source code to enable the backend (e.g., for AVR).mrustc, an unofficial Rust compiler that translates Rust code into C code, which can then be compiled using GCC.While compiler_fence prevents compiler reordering, it does not prevent hardware reordering. You must use core::sync::atomic::fence (which generates hardware instructions like DMB or DSB on ARM) in the following scenarios:
You can support log levels (e.g., ERROR vs WARN) without increasing the runtime footprint by partitioning the symbol address space using linker sections.
Implementation Strategy:
.log_error and .log_warn).__log_warning_start__) at the start of the warning section.address < __log_warning_start__, it is an ERROR; otherwise, it is a WARN.This allows the logger to remain extremely lightweight while providing severity context.
/* log.x */
.log_error 0 (INFO) :
{
*(.log_error)
}
.log_warn 0 (INFO) : ALIGN(4) {
__log_warning_start__
*(.log_warn)
}While earlier versions of the Embedonomicon used Rust code to initialize the .data and .bss sections, this approach is considered unsound. Modern embedded Rust crates (such as cortex-m-rt and riscv-rt) have migrated to using assembly for this purpose to avoid Undefined Behavior (UB).
Key reasons for avoiding Rust-based initialization include:
.bss section using a pointer derived from a single u8 variable (like _sbss) violates provenance rules, as the write extends far beyond the single byte allocated to that variable._sbss or _ebss often overlap with the addresses of actual global variables in the .data or .bss sections. Using these symbols to write to memory effectively creates mutable aliasing of all global data, which is prohibited in Rust.offset_from: Even if using offset_from to calculate section sizes, the symbols _sbss and _ebss belong to different allocations and do not share provenance, leading to UB.Recommendation: Always use assembly for the initialization of .data and .bss sections to ensure long-term stability and correctness against future compiler or optimizer changes.
When using DMA, you must ensure that the buffer used for the transfer remains valid for the entire duration of the operation.
If you use a stack-allocated buffer and call mem::forget on the Transfer object (the handle representing the ongoing DMA operation), the function containing the buffer may return, causing the stack frame to be reclaimed. The DMA hardware will then continue writing to or reading from memory that has been repurposed for other variables, leading to undefined behavior.
Solution: To make the API safe, require that buffers passed to DMA functions have a 'static lifetime. This ensures the buffer is not on the stack and remains valid regardless of when the Transfer object is dropped or forgotten.
// UNSAFE: Buffer is on the stack and will be invalidated when 'start' returns
fn start(buf: &mut [u8]) -> Transfer {
// ... DMA starts here ...
Transfer { ... }
}
// SAFE: Buffer must be 'static (e.g., a static mut or a leaked allocation)
fn start(buf: &'static mut [u8]) -> Transfer {
// ... DMA starts here ...
Transfer { ... }
}When designing or using a DMA API, passing arrays by value (e.g., [u8; 16]) can lead to pointer invalidation if the buffer is moved or if the local stack frame is destroyed while the DMA transfer is still in progress. To prevent this, buffers must be 'pinned' to ensure they retain their memory location even when moved. Using the Pin newtype is a standard way to provide this guarantee, ensuring the memory address remains valid for the duration of the transfer.
// Example of using Pin to ensure buffer stability
// (Referencing the pattern in examples/six.rs)Linker scripts control the final memory layout by arranging sections (collections of symbols). Key directives include:
MEMORY: Defines the physical memory blocks available (e.g., FLASH, RAM) with their ORIGIN and LENGTH.ENTRY: Specifies the program's entry point (e.g., ENTRY(Reset)). This prevents the linker from discarding the entry point and its call tree.EXTERN: Forces the linker to include a specific symbol in the output even if it isn't explicitly referenced from the entry point. Often used with KEEP.SECTIONS: Defines how input sections are mapped to output sections.> FLASH: Directs a section to a specific memory region.KEEP: Prevents the linker from discarding a section during optimization./DISCARD/: Used to explicitly drop unwanted input sections (e.g., .ARM.exidx.*).A common issue with DMA is that the CPU might attempt to access a peripheral (like a UART/Serial port) while a DMA transfer is already in progress, leading to data loss or hardware errors.
One robust way to prevent this is to use move semantics: the DMA Transfer object should take ownership of the peripheral and only return it to the user once the transfer is complete (e.g., via a .wait() method).
Alternatively, you can use a runtime check (like a Cell flag) inside the peripheral to return an Error::InUse if a transfer is currently active.
// Using move semantics to prevent overlapping use
impl Serial1 {
fn write_all<'a>(&'a mut self, buf: &'a mut [u8]) -> Transfer<'a, Self> {
// Take ownership of 'self' and return it in the Transfer object
Transfer { serial: Some(core::mem::replace(self, ...)), ... }
}
}
impl<'a> Transfer<'a, Serial1> {
fn wait(self) -> &'a mut Serial1 {
// Wait for DMA to finish, then return the peripheral
self.serial.unwrap()
}
}A raw main symbol interface is prone to errors; for example, a user might define main as a non-divergent function (e.g., fn()), which leads to undefined behavior.
To prevent this, expose a macro in your rt crate instead of a raw symbol. This macro can enforce that the user's main function is divergent (e.g., returns ! or is marked appropriately), providing a compile-time error if the signature is incorrect.