MMIO++

Macro-familiar register code, without macro-era type holes.

MMIO++ keeps the shape embedded developers already know: register names, field names, encoded values, and masks. The difference is that the compiler now enforces which pieces may be combined, assigned, cleared, compared, or rejected.

The Project Story

Embedded register code usually starts from the datasheet, not from an object model. That is why so many teams stay with C macros even when they know the tradeoffs: the code stays direct, the names stay familiar, and the hardware intent remains visible.

MMIO++ is aimed at that exact moment. It does not try to hide registers behind a framework dialect. It keeps the register-programming shape intact and uses C++ types to block the mistakes that waste time during bring-up: cross-register mixes, mask-versus-value confusion, and write forms that look plausible but mean the wrong thing on the bus.

Why This Is Credible

  • The API keeps register call sites close to the way firmware engineers already read datasheets and macro-based headers.
  • Compile-fail coverage locks down the category mistakes that raw macros tend to let through.
  • QEMU runtime tests exercise register reads and writes at real target addresses instead of stopping at host-side simulation.
  • The same register definitions work for live MMIO access and local shadow values, so staged updates do not fork the programming model.

Why The API Looks Familiar

Classic macro style

SPI_CR = SPI_CR_SPIEN_ENABLE | SPI_CR_SWRST_RESET;

SPI_CR |= SPI_CR_SPIEN_ENABLE;
SPI_CR &= ~SPI_CR_SWRST_Msk;

SPI_MR = SPI_MR_MSTR_MASTER | SPI_MR_DLY(7);

MMIO++ style

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 = SPI_MR::MSTR::MASTER | SPI_MR::DLY::value(7);

The visual rhythm stays close to register-map macros. The difference is that the type system now distinguishes valid encoded values from masks and unrelated register fields.

Values vs Masks

MMIO++ treats encoded values and masks as different public tokens because they do different jobs in real register code.

  • FIELD::VALUE_NAME is for whole-register writes, predicates, and OR-safe set operations.
  • FIELD::MASK is for clear or toggle operations.
  • FIELD::value(x) is for numeric value fields.

Register Operations

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);

One Register Definition, Two Jobs

Real firmware often needs both styles of work: direct MMIO access for the live peripheral, and a local register image when a sequence should be staged before one final write. MMIO++ keeps those two paths on the same definition instead of forcing a second API for shadow values.

SPI_MR::Instance<0xFFFE0004u> spiMr;
SPI_MR shadow = spiMr;

shadow.set<SPI_MR::PCS>(1);
shadow.set<SPI_MR::DLY>(7);

spiMr = shadow;