The Embedonomicon

repository·master·Indexed 42 days ago

https://github.com/rust-embedded/embedonomicon

A 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.

Tokens
12K
Snippets
34
Records
55
Agent score
53%

What's inside Embedonomicon

  1. Prevent stack corruption with 'static bounds

    master

    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.rs
  2. Define a GlobalLog trait for singleton interfaces

    master

    When designing a trait for a global singleton, ensure the following:

    • The methods take &self (shared references) to support static storage.
    • The trait requires Sync to ensure the singleton can be shared across threads/interrupts.
    • Avoid associated types like Error if you intend to use trait objects, as all consumers would otherwise need to agree on the specific error type.
  3. How to handle unsupported architectures in Rust

    master

    If your target architecture is not directly supported by the standard Rust toolchain, you have several options depending on the level of support available:

    1. LLVM supports the architecture, but rustc has the backend disabled: You must modify the Rust source code to enable the backend (e.g., for AVR).
    2. LLVM does not support the architecture, but a fork does: You can replace the LLVM submodule in the Rust build system with your fork.
    3. Only vendor-provided GCC supports the architecture: Use mrustc, an unofficial Rust compiler that translates Rust code into C code, which can then be compiled using GCC.
  4. When to use `atomic::fence` instead of `compiler_fence`

    master

    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:

    • Multi-core systems: If the DMA and CPU are on different cores/masters.
    • Cortex-M7 with Data Cache (DCache): If you are using a cache, you must use memory barriers or manually invalidate the buffer to ensure the DMA sees the correct data.
    • Non-standard ARM processors: If the architecture does not guarantee that memory transactions are ordered.
  5. Implement multiple log levels using linker sections

    master

    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:

    1. Separate Sections: Place different log levels into different linker sections (e.g., .log_error and .log_warn).
    2. Boundary Symbol: Define a specific symbol in the linker script (e.g., __log_warning_start__) at the start of the warning section.
    3. Decoding Logic: A decoder can then classify any received address: if 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)
    }
  6. Why avoid initializing .data and .bss sections using Rust

    master

    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:

    1. Pointer Provenance Violations: Writing to the .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.
    2. Aliasing Violations: Linker symbols like _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.
    3. Provenance and 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.
    4. Abstract Machine Uncertainty: It is unclear if it is safe to execute any Rust code before the Rust abstract machine is fully initialized.

    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.

  7. Avoid Undefined Behavior with Stack Buffers and `mem::forget`

    master

    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 { ... }
    }
  8. Ensure buffer stability using Pin

    master

    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)
  9. Understand Linker Script directives

    master

    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.*).
  10. Prevent Overlapping Peripheral Use with Move Semantics

    master

    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()
        }
    }
  11. Ensure type safety for the `main` function using macros

    master

    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.