Thread Safety and Memory Safety
Relevant source files
This document covers the thread safety and memory safety guarantees provided by the arm_pl011 crate, focusing on the Pl011Uart implementation and its safe abstractions over hardware register access. For general API usage patterns, see 3.1. For hardware register specifications, see 5.
Purpose and Scope
The arm_pl011 crate provides memory-safe and thread-safe abstractions for PL011 UART hardware access in no_std embedded environments. This page examines the explicit Send and Sync implementations, memory safety guarantees through NonNull and tock-registers, and safe usage patterns for concurrent access scenarios.
Send and Sync Implementations
The Pl011Uart struct explicitly implements both Send and Sync traits through unsafe implementations, enabling safe transfer and sharing between threads.
Safety Guarantees
flowchart TD Pl011Uart["Pl011Uart struct"] Send["unsafe impl Send"] Sync["unsafe impl Sync"] NonNull["NonNull<Pl011UartRegs> base"] SendSafety["Transfer between threads"] SyncSafety["Shared references across threads"] MemSafety["Non-null pointer guarantee"] TockRegs["tock-registers abstraction"] TypeSafe["Type-safe register access"] VolatileOps["Volatile read/write operations"] HardwareExclusive["Hardware access exclusive to instance"] RegisterAtomic["Register operations are atomic"] NonNull --> MemSafety NonNull --> TockRegs Pl011Uart --> NonNull Pl011Uart --> Send Pl011Uart --> Sync Send --> SendSafety SendSafety --> HardwareExclusive Sync --> SyncSafety SyncSafety --> RegisterAtomic TockRegs --> TypeSafe TockRegs --> VolatileOps
Send Safety Justification: Each Pl011Uart instance exclusively owns access to its memory-mapped register region. The hardware controller exists at a unique physical address, making transfer between threads safe as long as no aliasing occurs.
Sync Safety Justification: PL011 register operations are atomic at the hardware level. The tock-registers abstraction ensures volatile access patterns that are safe for concurrent readers.
Sources: src/pl011.rs(L46 - L47)
Implementation Details
| Trait | Safety Requirement | Implementation Rationale |
|---|---|---|
| Send | Safe to transfer ownership between threads | Hardware register access is location-bound, not thread-bound |
| Sync | Safe to share references across threads | Register operations are atomic; concurrent reads are safe |
The implementations rely on hardware-level atomicity guarantees and the absence of internal mutability that could cause data races.
Sources: src/pl011.rs(L42 - L47)
Memory Safety Guarantees
NonNull Pointer Management
flowchart TD
Construction["Pl011Uart::new(base: *mut u8)"]
NonNullWrap["NonNull::new(base).unwrap()"]
Cast["cast<Pl011UartRegs>()"]
Storage["NonNull<Pl011UartRegs> base"]
RegAccess["regs() -> &Pl011UartRegs"]
UnsafeDeref["unsafe { self.base.as_ref() }"]
SafeWrapper["Safe register interface"]
PanicOnNull["Panic on null pointer"]
ValidityAssumption["Assumes valid memory mapping"]
Cast --> Storage
Construction --> NonNullWrap
NonNullWrap --> Cast
NonNullWrap --> PanicOnNull
RegAccess --> UnsafeDeref
Storage --> RegAccess
UnsafeDeref --> SafeWrapper
UnsafeDeref --> ValidityAssumption
The Pl011Uart constructor uses NonNull::new().unwrap() to ensure non-null pointer storage, panicking immediately on null input rather than deferring undefined behavior.
Memory Safety Properties:
- Non-null guarantee:
NonNull<T>type prevents null pointer dereference - Const construction: Construction in const context validates pointer at compile time when possible
- Type safety: Cast to
Pl011UartRegsprovides typed access to register layout
Sources: src/pl011.rs(L51 - L54) src/pl011.rs(L57 - L59)
Register Access Safety
The crate uses tock-registers to provide memory-safe register access patterns:
flowchart TD
UnsafeDeref["unsafe { self.base.as_ref() }"]
TockInterface["tock-registers interface"]
ReadOnly["ReadOnly<u32> registers"]
ReadWrite["ReadWrite<u32> registers"]
WriteOnly["WriteOnly<u32> registers"]
VolatileRead["Volatile read operations"]
VolatileReadWrite["Volatile read/write operations"]
VolatileWrite["Volatile write operations"]
CompilerBarrier["Prevents compiler reordering"]
HardwareSync["Synchronized with hardware state"]
CompilerBarrier --> HardwareSync
ReadOnly --> VolatileRead
ReadWrite --> VolatileReadWrite
TockInterface --> ReadOnly
TockInterface --> ReadWrite
TockInterface --> WriteOnly
UnsafeDeref --> TockInterface
VolatileRead --> CompilerBarrier
VolatileReadWrite --> CompilerBarrier
VolatileWrite --> CompilerBarrier
WriteOnly --> VolatileWrite
Safety Mechanisms:
- Volatile access: All register operations use volatile semantics to prevent compiler optimizations
- Type safety: Register types prevent incorrect access patterns (e.g., writing to read-only registers)
- Memory barriers: Volatile operations provide necessary compiler barriers for hardware synchronization
Sources: src/pl011.rs(L9 - L32) src/pl011.rs(L57 - L59)
Thread Safety Considerations
Concurrent Access Patterns
flowchart TD
subgraph subGraph2["Method Safety"]
ConstMethods["&self methods (read-only)"]
MutMethods["&mut self methods (exclusive)"]
end
subgraph subGraph1["Unsafe Patterns"]
AliasedMutable["Multiple mutable references"]
UnprotectedShared["Unprotected shared mutable access"]
end
subgraph subGraph0["Safe Patterns"]
ExclusiveOwnership["Exclusive ownership per thread"]
ReadOnlySharing["Shared read-only access"]
MutexWrapped["Mutex<Pl011Uart> for shared writes"]
end
SafeTransfer["Safe: move between threads"]
ReadRegisters["Read: fr, mis, ris registers"]
WriteRegisters["Write: dr, cr, imsc, icr registers"]
DataRace["Data race on register access"]
UndefinedBehavior["Undefined behavior"]
AliasedMutable --> DataRace
ConstMethods --> ReadRegisters
ExclusiveOwnership --> SafeTransfer
MutMethods --> WriteRegisters
MutexWrapped --> MutMethods
ReadOnlySharing --> ConstMethods
UnprotectedShared --> UndefinedBehavior
Thread Safety Model:
- Shared immutable access: Multiple threads can safely call
&selfmethods for reading status - Exclusive mutable access: Only one thread should have
&mutaccess for configuration and I/O operations - Hardware atomicity: Individual register operations are atomic at the hardware level
Sources: src/pl011.rs(L78 - L103)
Method Safety Classification
| Method | Signature | Thread Safety | Hardware Impact |
|---|---|---|---|
| is_receive_interrupt() | &self | Safe for concurrent access | Read-only status check |
| putchar() | &mut self | Requires exclusive access | Modifies transmit state |
| getchar() | &mut self | Requires exclusive access | Modifies receive state |
| init() | &mut self | Requires exclusive access | Modifies controller configuration |
| ack_interrupts() | &mut self | Requires exclusive access | Clears interrupt state |
The distinction between &self and &mut self methods reflects the underlying hardware behavior and safety requirements.
Sources: src/pl011.rs(L64 - L103)
Safe Usage Patterns
Recommended Concurrent Access
flowchart TD SingleOwner["Single-threaded ownership"] DirectAccess["Direct method calls"] MultipleReaders["Multiple reader threads"] SharedRef["Arc<Pl011Uart>"] ReadOnlyOps["Only &self methods"] SharedWriter["Shared writer access"] MutexWrap["Arc<Mutex<Pl011Uart>>"] ExclusiveLock["lock().unwrap()"] FullAccess["All methods available"] InterruptHandler["Interrupt context"] AtomicCheck["Atomic status check"] DeferredWork["Defer work to thread context"] AtomicCheck --> DeferredWork ExclusiveLock --> FullAccess InterruptHandler --> AtomicCheck MultipleReaders --> SharedRef MutexWrap --> ExclusiveLock SharedRef --> ReadOnlyOps SharedWriter --> MutexWrap SingleOwner --> DirectAccess
Best Practices:
- Single ownership: Preferred pattern for most embedded applications
- Arc sharing: Use for status monitoring across multiple threads
- Mutex protection: Required for shared mutable access
- Interrupt safety: Minimize work in interrupt context, use atomic operations only
Sources: src/pl011.rs(L94 - L96)
Initialization Safety
The const fn new() constructor enables compile-time safety verification when used with known memory addresses:
// Safe: Address known at compile time
const UART0: Pl011Uart = Pl011Uart::new(0x9000_0000 as *mut u8);
// Runtime construction - requires valid address
let uart = Pl011Uart::new(base_addr);
Safety Requirements:
- Base address must point to valid PL011 hardware registers
- Memory region must remain mapped for lifetime of
Pl011Uartinstance - No other code should directly access the same register region
Sources: src/pl011.rs(L50 - L55)