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.
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:
/dev/mem requires root privileges and proper error handling/dev/mem access, not bare-metal environmentsHistorically, 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:
mmreg was born from a simple question: Can we bring Rust’s safety guarantees to the inherently unsafe world of hardware register access?
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.
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.
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!
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.
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/tmp/mmreg.lockstd::ptr::{read_volatile, write_volatile}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.
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 patternBuilding 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));
}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"
donemmreg takes quality seriously with a comprehensive GitHub Actions workflow:
cargo fmt) and Clippy warningsjobs:
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.txtPublishing 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.ioAll 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) }
}mmreg maps memory directly into the process address space – no intermediate buffers or copies. Reads and writes are as fast as direct hardware access.
File locking adds minimal overhead (microseconds) compared to the actual hardware access time (typically nanoseconds to milliseconds depending on the device).
mmreg is actively developed with exciting features planned:
String errors with proper enum typesAdd mmreg to your Cargo.toml:
[dependencies]
mmreg = "0.1"Or install the CLI tool:
cargo install mmreguse 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(())
}# Read register
sudo mmreg read 0x40000000
# Write register
sudo mmreg write 0x40000000 0xDEADBEEFmmreg is open source under the MIT license. Contributions are welcome!
cargo fmt and cargo clippymmreg 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:
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!