Homepage: https://borschtsch.github.io/mmiocpp/
MMIO++ is a C++ type-safe memory-mapped I/O framework for developers who already know the feel of C register macros and want to keep that direct style without keeping the usual hazards.
The goal is not to turn register access into a heavyweight abstraction. The goal is to let register code still look familiar:
- define registers and fields close to the hardware layout
- compose named field values the way embedded developers already think about them
- reject cross-register mistakes and mask/value mixups at compile time
- keep the generated code suitable for real MMIO work
In practice, MMIO++ is meant to feel like the better-behaved C++ version of the macro-based register code many embedded teams already write by hand.
Classic C register macros are concise, but they also make it easy to:
- combine values from unrelated registers
- confuse field masks with encoded field values
- write raw integers where only valid field encodings should be allowed
- lose intent in long chains of shifts and bitwise operators
MMIO++ keeps the familiar register-programming shape while using C++ types to stop those mistakes earlier.
Register definitions stay close to the way register maps are documented:
struct SPI_CR : mmio::Register<SPI_CR> {
struct SPIEN : mmio::BitField<SPI_CR, 0, 1> {
static constexpr auto DISABLE = value(0);
static constexpr auto ENABLE = value(1);
};
struct SWRST : mmio::BitField<SPI_CR, 7, 1> {
static constexpr auto IDLE = value(0);
static constexpr auto RESET = value(1);
};
struct DLY : mmio::ValueField<SPI_CR, 8, 2, std::uint8_t> {};
};The public API separates the two concepts that C macro code often blurs together:
FIELD::VALUE_NAMEis an encoded field value used for writes, predicates, and value composition.FIELD::MASKis the automatically derived bit mask used for clear and toggle operations.- Numeric value fields use
FIELD::value(x).
That makes the call sites read like register code, but with stricter rules:
SPI_CR::Instance<0xFFFE0000u> spiCr;
SPI_MR::Instance<0xFFFE0004u> spiMr;
spiCr = SPI_CR::SPIEN::ENABLE | SPI_CR::SWRST::RESET;
spiCr |= SPI_CR::SPIEN::ENABLE;
spiCr &= ~SPI_CR::SWRST::MASK;
spiMr.set<SPI_MR::MSTR::MASTER>();
spiMr.set(SPI_MR::MSTR::MASTER | SPI_MR::DLY::value(7));
spiMr.set<SPI_MR::DLY>(7);
const bool isMaster = spiMr & SPI_MR::MSTR::MASTER;
const bool releasesChipSelect = spiMr & SPI_MR::CSAAT::RELEASE;Zero-valued states still work correctly in predicates. SPI_MR::CSAAT::RELEASE
encodes as zero, but operator& masks the field first and then compares the
masked bits with the encoded value, so the check stays exact instead of
degenerating into an always-true raw & 0 == 0.
The same register definition type also works as a shadow register value:
SPI_MR modeShadow = SPI_MR::MSTR::MASTER | SPI_MR::DLY::value(7);
modeShadow.set<SPI_MR::PCS>(2);
SPI_MR::Instance<0xFFFE0004u> spiMr;
spiMr = modeShadow;That pattern is useful when a register image belongs to an external device, when you want to stage several field updates locally, or when you want to snapshot one live register and commit a modified copy back later.
MMIO++ is designed to stay zero-overhead in the usual embedded sense:
- MMIO-bound register objects still compile down to direct volatile loads and stores.
- Shadow registers are plain non-volatile local values, so the compiler can fold several updates together before one final commit.
- The framework uses compile-time types to reject misuse instead of runtime checks.
include/mmio.hpp: core public header only.examples/: example register maps and driver code built on top of the core header.examples/mmio_demo.cpp: small usage example built through the normal host workflow.tests/mmio_tests.cpp: host-side positive API and behavior checks.tests/compile_fail/*.cpp: compile-fail coverage for misuse cases.targets/qemu-cortex-m/: Cortex-M3 QEMU target harness and runtime tests.targets/qemu-cortex-r5/: Cortex-R5 QEMU target harness and runtime tests.CMakePresets.json: canonical Windows entrypoints for build and test workflows.scripts/bootstrap.ps1: Windows tooling bootstrap.
Install the expected Windows toolchain layout with:
powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1The bootstrap downloads and installs WinLibs, CMake, Arm GNU Toolchain, GNU make, and xPack QEMU Arm into .local. The committed scripts, presets, and toolchain files resolve those repo-local copies directly and do not depend on machine-installed tools.
Use the repo-local CMake executable with the committed workflow presets:
.\.local\cmake\bin\cmake.exe --workflow --preset host
.\.local\cmake\bin\cmake.exe --workflow --preset host-test
.\.local\cmake\bin\cmake.exe --workflow --preset qemu-m3-build
.\.local\cmake\bin\cmake.exe --workflow --preset qemu-m3-run
.\.local\cmake\bin\cmake.exe --workflow --preset qemu-m3-test
.\.local\cmake\bin\cmake.exe --workflow --preset qemu-r5-build
.\.local\cmake\bin\cmake.exe --workflow --preset qemu-r5-run
.\.local\cmake\bin\cmake.exe --workflow --preset qemu-r5-testWhat they do:
host: configure and build the normal Windows host targetshost-test: configure, build, and run the host test suiteqemu-m3-build: build the Cortex-M3 QEMU targetqemu-m3-run: build and run the Cortex-M3 QEMU runtime test targetqemu-m3-test: build and run the Cortex-M3 CTest flowqemu-r5-build: build the Cortex-R5 QEMU targetqemu-r5-run: build and run the Cortex-R5 QEMU runtime test targetqemu-r5-test: build and run the Cortex-R5 CTest flow
The Windows host workflow checks the public API shape, positive usage paths, and compile-fail misuse cases.
The QEMU workflows exercise the code against target CPU address spaces rather than relying only on a desktop shim, so register behavior is validated in a more realistic environment.