One of the many things that got me excited in zig is the ability to use packed structs to do MMIO.
The bit shift dance that is usual in C always seems stupidly annoying and error prone to me.

At the moment, I finally have a concrete use case that gives me a good excuse to play with that stuff. So I’m generating my own register map, but now I realize I somehow missed this part of the zig docs :

pub const GpioRegister = packed struct(u8) {
    GPIO0: bool,
    GPIO1: bool,
    GPIO2: bool,
    GPIO3: bool,
    reserved: u4 = 0,
};

const gpio: *volatile GpioRegister = @ptrFromInt(0x0123);

pub fn writeToGpio(new_states: GpioRegister) void {
    // Example of what not to do:
    // BAD! gpio.GPIO0 = true; BAD!

    // Instead, do this:
    gpio.* = new_states;
}

I didn’t realize that. This seems annoying.

I found out this useful thread : MMIO access restriction using meta programming - #7 by lufe but this leaves me a bit … disappointed maybe ?

I like the microzig approach that seems generic and robust, but (except if I’m missing something huge) this does kill fields auto completion. I deal with a huge register map and straightforward autocompletion / using doc comments for registers and bitfields is an absolute killer feature.

Does anybody know more about why this is the current state of things ? I fail to see the reason for atomic read/write not being the default behavior with a volatile pointer in the example above.

floooh July 22, 2026, 8:16am 2

Hmm… I would think that gpio.GPI0 = true; is identical with gpio |= GPI0 (assuming that GPI0 is a bit mask), or gpio |= (1<< GPI0) (assuming that GPI0 is the bit position).

If the memory-mapped register allows read-write that should be fine? But sometimes memory-mapped registers are write-only, or they trigger some action when read (like clearing the register), and in that case that pattern would break of course because these are read-modify-write operations. My only experience with MMIO is old-school home computers and emulators though, not modern embedded systems.

FWIW, for similar things in my emulator code I also tinkered with packed structs, but then went back to regular integer bit twiddling, just because the set of operators if ‘richer’ than what packed-structs allow (e.g. &, |, ^ on multiple bits, or <<, >> don’t work on packed structs without casting to the backing integer and back).