Typed Registers (MMIO without Unsafe)
Problem
Every Rust OS kernel uses unsafe for hardware register access. A typical kernel has hundreds to thousands of unsafe { write_volatile(...) } blocks scattered across driver code. Each one is a potential source of memory corruption if the address, width, or access pattern is wrong.
Solution
Rs introduces the #[register] attribute, which declares a memory-mapped I/O register as a typed, compiler-verified construct.
Syntax
Bit Ranges
Field bit ranges use Rust's exclusive-end convention: bits = start..end covers bits [start, end). Width = end − start. Example: bits = 1..5 is 4 bits wide (bits 1, 2, 3, 4).
Register Width
The width parameter on #[register] sets the default register width in bits. Supported values: 8, 16, 32, 64. Default: 32. Individual registers can override with #[reg(... width = 64)]. The width determines the read_volatile/write_volatile pointer type (u8, u16, u32, u64).
Generated Code
The compiler generates:
// Auto-generated by rsc — user never writes this
// Status is read-only: no write() or modify() generated
// Clear is write-only: no read() or modify() generated
Compile-Time Guarantees
| Check | Example Error |
|---|---|
| Read from write-only register | error[RS001]: register aic::Clear is write-only |
| Write to read-only register | error[RS002]: register aic::Status is read-only |
| Field exceeds register width | error[RS003]: field target_cpu (bits 1..5) exceeds u32 width |
| Field value exceeds bit range | error[RS004]: value 20 does not fit in 4-bit field target_cpu |
| Overlapping field bits | error[RS005]: fields enabled and target_cpu overlap at bit 1 |
| Enum variant exceeds field width | error[RS006]: IrqMode has 5 variants but field mode is 2 bits (max 4) |
| Address outside declared bank | error[RS007]: offset 0x2000 exceeds bank_size 0x1000 |
| Enum does not cover all bit patterns | error[RS008]: IrqMode has 3 variants but field mode is 2 bits (4 patterns) — add a variant for pattern 3 |
RS004 fires only for compile-time constants and literals that provably exceed the field width. For runtime values, the generated code applies bit masking (e.g. val.target_cpu as u32 & 0xF), silently truncating to the field width. This is consistent with the no-panic restriction in edition = "rs" — a runtime range check would require a panic or error path.
Usage
Unsafe Accounting
The unsafe blocks exist only inside compiler-generated code. They are:
- Up to 2 per register (one
read_volatilefor readable registers, onewrite_volatilefor writable registers) - Generated from verified attribute metadata
- Not visible to or writable by the user
- Auditable in compiler source (~200 lines of codegen)
User-facing code contains zero unsafe.
Reserved Bits
Bits not covered by any #[field] are reserved. The write() function builds from Self::default(), which zeroes reserved bits. The modify() function performs read-modify-write, preserving reserved bits. Use modify() when hardware requires reserved bits to retain their read-back values. Use write() only when zeroing reserved bits is safe (consult the hardware datasheet).
Concurrent Access
Register access is inherently non-atomic at the bus level. The generated read() and write() functions perform a single read_volatile/write_volatile — they do not provide synchronization. If multiple contexts (interrupt handlers, cores) access the same register, the caller must provide synchronization (spinlock, critical section, per-core banking). The modify() function performs a read-modify-write sequence and is subject to TOCTOU if not externally synchronized.
Fallback Compatibility
In standard Rust mode (non-Rs edition), #[register] can be implemented as a proc-macro crate that generates the same code. This means Rs register declarations are valid Rust with the right dependency — they just lack the compiler-level verification.
Error Reference
See errors/registers.md for detailed descriptions of RS001–RS008.