Introduction
In the world of embedded Linux and hardware programming, direct memory access is both a necessity and a minefield. Whether you’re working with custom FPGA designs, interfacing with peripheral devices, or building user-space drivers, you need to read and write to memory-mapped registers. But doing so safely, efficiently, and correctly across multiple processes is surprisingly challenging.
This is the story of mmreg – a Rust library and CLI tool that brings safety, concurrency, and simplicity to memory-mapped I/O (MMIO) register access from user-space on Linux systems. Note: mmreg is designed for user-space applications on embedded Linux (via /dev/mem), not for bare-metal or kernel-space development.
The Problem: Memory-Mapped I/O is Hard
What is Memory-Mapped I/O?
In modern computing systems, hardware devices don’t just communicate through dedicated I/O instructions. Instead, specific regions of the physical address space are mapped to hardware registers. When you write to address 0x40000000, you might actually be configuring a GPIO pin, starting a DMA transfer, or enabling an interrupt controller.
This approach, called Memory-Mapped I/O (MMIO), is elegant but fraught with dangers:
- Raw Pointer Arithmetic: You’re dealing with raw memory addresses, often requiring unsafe Rust code
- Concurrent Access: Multiple processes might try to access the same register simultaneously, leading to race conditions
- Page Alignment: Operating systems map memory in pages, requiring careful address alignment calculations
- Permission Issues: Accessing
/dev/memrequires root privileges and proper error handling - Platform Dependencies: User-space access to hardware registers varies across operating systems
- Kernel vs User-space: This approach works on Linux systems with
/dev/memaccess, not bare-metal environments
The Traditional Approach
Historically, developers have written custom, unsafe code for each project:
// Traditional C approach
int fd = open("/dev/mem", O_RDWR | O_SYNC);
void *mapped = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, base_address);
volatile uint32_t *reg = (uint32_t *)(mapped + offset);
*reg = value; // Hope nothing goes wrong!This works, but it’s:
- Unsafe: No compile-time guarantees
- Error-prone: Easy to miscalculate offsets or sizes
- Non-portable: Platform-specific code scattered everywhere
- Race-condition prone: No built-in concurrency safety
Enter mmreg: Safety Meets Hardware
mmreg was born from a simple question: Can we bring Rust’s safety guarantees to the inherently unsafe world of hardware register access?
Core Design Principles
1. Safety First, Performance Second
While direct memory access is inherently unsafe at the FFI boundary, mmreg encapsulates all unsafe code behind safe abstractions:
use mmreg::{read_register_at, write_register_at};
// Safe, ergonomic API
let value = read_register_at(0x4000_0000)?;
write_register_at(0x4000_0000, 0xDEADBEEF)?;No raw pointers in user code. No manual mmap calculations. Just clean, idiomatic Rust.
2. Concurrency Through File Locking
One of mmreg’s killer features is safe concurrent access. Using the fs2 crate, mmreg automatically acquires exclusive locks before accessing memory:
// Process A
interface.map()?; // Acquires lock
interface.write_register("control", 0x1)?;
interface.unmap(); // Releases lock
// Process B blocks here until Process A releases
interface.map()?;
let val = interface.read_register("status")?;This prevents race conditions and data corruption when multiple programs access the same hardware.
3. Register Abstraction and Organization
Real hardware comes with dozens or hundreds of registers. mmreg provides structures to organize them logically:
use mmreg::{Interface, Register, SubRegister};
let mut axi_interface = Interface::new(
"AXI_GPIO",
0x4000_0000,
0x1000,
vec![
Register::new("DATA", 0x00, vec![
SubRegister::new("LED", 0, 8),
SubRegister::new("SWITCH", 8, 8),
]),
Register::new("CONTROL", 0x04, vec![]),
]
);
// Access by name, not magic numbers
axi_interface.write_subregister("DATA", "LED", 0xFF)?;No more hunting through datasheets to remember that bit 3 of offset 0x08 controls the interrupt enable!
4. Bitfield Manipulation Made Easy
Hardware registers are often packed with multiple fields. SubRegisters provide clean bitfield access:
// Instead of: raw = (raw & !mask) | ((value << shift) & mask)
interface.write_subregister("STATUS", "ERROR_CODE", error_code)?;
// Instead of: value = (raw & mask) >> shift
let code = interface.read_subregister("STATUS", "ERROR_CODE")?;The library handles all the masking, shifting, and read-modify-write operations.
Architecture: Inside mmreg
Module Structure
mmreg is organized into focused modules:
memregs.rs: Low-level mmap/munmap wrappers (Linux-specific, unsafe)register.rs: Register and SubRegister abstractionsinterface.rs: High-level interface for grouped registers with lockinglib.rs: Public API and convenience functionsmain.rs: Command-line interface
The Memory Mapping Flow
- Address Alignment: Calculate page-aligned base address
- File Locking: Acquire exclusive lock on
/tmp/mmreg.lock - mmap System Call: Map physical memory to process virtual space
- Volatile Access: Use
std::ptr::{read_volatile, write_volatile} - Cleanup: Munmap and release lock on drop
Cross-Compilation Strategy
Embedded developers need to build for multiple architectures. mmreg’s build system is designed for this:
# Single source of truth: targets.txt
x86_64-unknown-linux-gnu
armv7-unknown-linux-gnueabihf
aarch64-unknown-linux-gnu
riscv64gc-unknown-linux-gnu
# ... and more
# Makefile reads targets.txt
make all # Builds for all targets using 'cross'The GitHub Actions CI automatically builds and tests against all targets in targets.txt, ensuring compatibility across platforms.
Real-World Use Cases
Embedded Linux Systems Development
Working with FPGA soft-core processors or custom IP blocks on embedded Linux (e.g., Zynq, Raspberry Pi, or custom SoCs)? mmreg provides a clean interface:
// Configure AXI GPIO
let mut gpio = Interface::new("GPIO", 0x40000000, 0x1000, registers);
gpio.write_register("DIRECTION", 0xFF)?; // All outputs
gpio.write_register("DATA", 0xAA)?; // Alternating patternUser-Space Driver Development
Building Linux device drivers in user-space (using UIO, /dev/mem, or similar mechanisms):
// Safe DMA configuration
let mut dma = Interface::new("DMA", DMA_BASE, 0x10000, dma_regs);
dma.write_subregister("CTRL", "ENABLE", 1)?;
dma.write_register("SRC_ADDR", source as u32)?;
dma.write_register("DST_ADDR", dest as u32)?;
dma.write_subregister("CTRL", "START", 1)?;
// Poll status
while dma.read_subregister("STATUS", "BUSY")? == 1 {
std::thread::sleep(Duration::from_millis(1));
}Hardware Testing and Validation
The CLI tool is perfect for quick hardware tests:
# Read device ID register
mmreg read 0x40000000
# Write test pattern
mmreg write 0x40000004 0xDEADBEEF
# Script automated tests
for addr in $(seq 0x40000000 4 0x400000FF); do
mmreg write $addr 0x12345678
value=$(mmreg read $addr)
echo "0x$(printf %X $addr): $value"
doneQuality Assurance: CI/CD Pipeline
mmreg takes quality seriously with a comprehensive GitHub Actions workflow:
Three-Stage Pipeline
- Lint Stage: Code formatting (
cargo fmt) and Clippy warnings - Check Stage: Compilation verification and tests
- Build Stage: Cross-compilation for all supported targets (only runs if lint and check pass)
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- run: make lint
check:
runs-on: ubuntu-24.04
steps:
- run: make check
build:
needs: [lint, check]
steps:
- run: make all # Builds all targets from targets.txtAutomated Publishing
Publishing to crates.io is as simple as creating a git tag:
git tag v0.1.2
git push origin v0.1.2
# GitHub Actions automatically publishes to crates.ioPerformance Considerations
Volatile Access
All register reads and writes use volatile operations, ensuring the compiler never optimizes away hardware accesses:
pub(crate) fn read_u32_mapped(ptr: *mut u8, offset: isize, reg_offset: u32) -> u32 {
let reg_ptr = unsafe { ptr.offset(offset + reg_offset as isize) } as *const u32;
unsafe { std::ptr::read_volatile(reg_ptr) }
}Zero-Copy Design
mmreg maps memory directly into the process address space – no intermediate buffers or copies. Reads and writes are as fast as direct hardware access.
Lock Overhead
File locking adds minimal overhead (microseconds) compared to the actual hardware access time (typically nanoseconds to milliseconds depending on the device).
Future Roadmap
mmreg is actively developed with exciting features planned:
Near-Term
- Multi-width Registers: Support for 8, 16, 64-bit registers (currently 32-bit only)
- Batch Operations: Read/write multiple registers atomically
- Better Error Types: Replace
Stringerrors with proper enum types - Integration Tests: Automated testing with QEMU or hardware simulators
Long-Term
- Async API: Non-blocking register access for high-performance scenarios
- Windows Support: Memory-mapped I/O on Windows platforms
- Register Discovery: Auto-generate register definitions from device tree or IP-XACT
Getting Started
Installation
Add mmreg to your Cargo.toml:
[dependencies]
mmreg = "0.1"Or install the CLI tool:
cargo install mmregBasic Example
use mmreg::{Interface, Register};
fn main() -> Result<(), String> {
// Define your registers
let registers = vec![
Register::new("ID", 0x00, vec![]),
Register::new("CONTROL", 0x04, vec![]),
];
// Create interface
let mut device = Interface::new(
"MyDevice",
0x40000000,
0x1000,
registers
);
// Read device ID
let id = device.read_register("ID")?;
println!("Device ID: 0x{:08X}", id);
// Enable device
device.write_register("CONTROL", 0x1)?;
Ok(())
}CLI Usage
# Read register
sudo mmreg read 0x40000000
# Write register
sudo mmreg write 0x40000000 0xDEADBEEFCommunity and Contributing
mmreg is open source under the MIT license. Contributions are welcome!
- GitHub: https://github.com/PurnenduK90/mmreg
- Crates.io: https://crates.io/crates/mmreg
- Documentation: https://docs.rs/mmreg
Contributing Guidelines
- Add tests: Especially for bitfield operations
- Update targets.txt: If you add a new cross-compilation target
- Follow Rust conventions: Code passes
cargo fmtandcargo clippy - Document public APIs: All public functions need doc comments
Conclusion: Safe Hardware Access for the Modern Era
mmreg demonstrates that systems programming doesn’t have to sacrifice safety for performance. By leveraging Rust’s type system and careful API design, we can build abstractions that are:
- Safe: No undefined behavior in user code
- Fast: Zero-overhead abstractions over raw hardware access
- Ergonomic: Clean, intuitive APIs that reduce cognitive load
- Concurrent: Multi-process safe by design
- Portable: Cross-compiles to major embedded platforms
Whether you’re building embedded Linux applications, writing user-space drivers, or testing hardware on Linux systems, mmreg provides the foundation for reliable, maintainable memory-mapped I/O code.
Important: mmreg is designed for user-space applications on Linux systems with /dev/mem access (embedded Linux, standard Linux with appropriate permissions). For bare-metal or RTOS development, consider embedded-hal or platform-specific PACs (Peripheral Access Crates).
The future of user-space hardware programming is safe, concurrent, and written in Rust. Welcome to mmreg.
Ready to try mmreg? Install it today:
cargo install mmregOr add it to your project:
[dependencies]
mmreg = "0.1"Star the project on GitHub and join the community of safe systems programmers!