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 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.
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).
I like having a packed union which is either the raw backing integer, or a packed struct. That way, you can do regular bit twiddling and operations on the struct!
For MMIO I use a integer type (if its large an array of them) to represent the MMIO region itself but use extern/packed structs and enums in the usage code.
This guarantees the correct size of memory load/store for the region and makes it explicit when you are reading and writing preventing things like gpio.GPIO0 = true; gpio.GPIO1 = true; being two reads and two writes.
That makes sense I guess. But this is how I would touch most of theses registers anyways, then.
I guess // BAD! gpio.GPIO0 = true; BAD! might have freaked me out more than needed.
Interesting data point.
But in my case I don’t see much use of << and friends. It’s more just setting fields with values, and taking advantage of that kind of things
Yeah, this is definitely a valid approach. I saw something similar in this blog post
And now that I think about it, avoiding multiple read/write when updating multiple values in the same register is maybe more important than the atomicity of read / write
I got bit using a packed struct over MMIO when I first started with zig, the register was 8 bits representing pending events but on read the bits are zeroed as a side-effect.
The intended usage was to read the register in full then loop over the bits in the value to learn of the events but I was accessing each field of the packed struct pointer meaning the moment I read the first field all other fields were zeroed.
It took me an embarrassingly long time to realise I was only ever detecting the first event as true and never seeing any other events.
The problem with accessing individual members (and the equivalent |=, &= etc. in C) is that it is a non-atomic read-modify-write (RMW) operation (with emphasis on non-atomic, as volatile does not have anything to do with atomicity whatsoever). Even with a single-core MCU, this may cause problems because either the CPU may drive an interrupt right in the middle of the RMW operation, or the peripheral changes the register in some way, causing the value you’ve just read to become outdated, which in turn causes you to corrupt the register.
So actually, microzig’s approach (and also what Linux does with accessor functions , fun fact) may be considered the only correct approach. Yes, missing auto-completion is slightly annoying, but then, Zig is very greppable, so the inconvenience becomes much more minor than in C where you have to chase headers.
EDIT: Also, some MCUs offer special registers for modify-operations, specifically because of this problem (e.g. the Microchip/Atmel SAMs’ have OUT for GPIO output, but then also OUTSET, OUTCLR, OUTTGL to set, clear, and toggle bits in the OUT register, respectively), so prefer those when available.
EDIT 2: Correction after further inspection of Microzig
This code here reads, set, and then writes. Is there something preventing an interrupt to change the value of the register in the middle ? What am I missing ?
Oh, for sure ! Order of magnitude less annoying. But autocompletion makes it out of the annoying category altogether…
Yes, sure, if you have a dedicated interface then you just write blindly in there and trust the digital designer. But to be fair, that was a bit blurry in my mind when I open the thread. It’s “safer” because you don’t read value altogether, not because you make the read/modif/write dance safer.
Hmm, I have to admit I only briefly looked at microzig’s Mmioafter I’ve written my own (a very incomplete codebase), because I was interested in their approach. I missed this. I don’t think it’s correct, because exactly, the value might change in between.
What I do instead is that I use the atomic ldrex and strex ARM instructions. The meat being this:
while (true) {
var r = self.rmw();
r.raw().* &= orig_mask;
r.raw().* |= new_data;
r.commit() catch continue;
break;
}
Where rmw() does ldrex (read) and creates a Rmw context (which gets optimized out in codegen), then I do my modifications, then commit() does strex (write) and lets me know whether the operation has actually completed (i.e. nothing else has done anything to the register in between the ldrex and strex). If it has not, we retry, making this an atomic operation.
I do agree, with you and to me the only thing that i think would make this choice even better is if something like this would work.
const Sensor = u32;
pub fn readValue(sensor: *const Sensor) u32 {
return sensor.*; // not realistic but just for demo
}
pub fn main() !void {
const my_sensor: u32 = 0;
while (true) |_| {
const value = my_sensor.readValue();
try uart_write("sensor : {d}\n", .{value});
try uart_flush();
}
}
it’s a shame we can’t alias/typedef values, without having to wrap it in something in order to be able to do field access. I’m curious why this wouldn’t be desirable, and would love to know why this wasn’t considered. The only argument outside of the usual complexity or burden I can think of, is that it reduce the overall readability for non readers, because you can’t really know for sure if what’s being accessed is an aggregate type or a trivially copy able type.
Either way I agree with you I too tend to rely on just bitshifting, but I would love to try a language that has such ability to alias even simple type and still get method syntax