MMIO access restriction using meta programming

Hi,

having an embedded background and trying to learn meta programming in zig, I came up with an idea on how to restrict access to individual bit fields in memory mapped IO registers. On the surface it looks like this:

// simple nonsense example for the structure of a memory mapped io register
const TimerCtrl = packed struct(u32) {
    read_only: Field(u8, .read),
    threshold: Field(u16, .readwrite),
    enabled: Field(u1, .readwrite),
    _reserved: Field(u2, .reserved),
    timer_mode: Field(
        enum(u2) {
            one_pulse = 0,
            pwm = 1,
        },
        .readwrite,
    ),
    _reserved2: Field(u3, .reserved),
};


export fn main(timer_ctrl: * volatile Register(TimerCtrl) ) void {

    // the following line causes a compile error, ensuring the
    // read_only bit field is not modified
    timer_ctrl.modify(.{.read_only = 0});

    // configuring some or all of the timer bit fields in a single call
    timer_ctrl.modify(.{.timer_mode=.pwm, .threshold=127});
    
    // Enable the timer in a second modification.
    timer_ctrl.modify(.{.enabled = 1});

    _ = timer_ctrl.read();

}

The full code is here: Compiler Explorer

On one hand I am amazed, how readable the code is -at least on the surface- and the safety guards you can provide using meta programming.

On the other hand I am worried that creating all these types slowes down complilation massively, once you have a device with hundreds or thousends of MMIO registers. Also, I am not sure if this is the intended usage for meta programming… it feels a bit messy.

I have several questions:

  1. A side question: When I did not inline .modify(...) the generated binary becomes quite large, especially when optimized for ReleaseSmall. Why is inlining necessary/important here?
  2. Does anyone have experience on how much creating huge amounts of types slowes down compilation?
  3. Is this a “valid” or intended use case for zig’s meta prrogramming? Is it too messy?
  4. Is there a better way of achieving all of this?

Many thanks for your help!

If you are interested in another take on this area, this is the code I use for MMIO. It is based on ideas taken from here. It works well for my use case. I have not noticed any significant compiler slowness. For comptime known field names, the generated code is as good as I could write by hand. The meta programming does what you might expect – disappears after compilation.

Thank you, I will have a look!

Very interesting to see the similarities and differences!

What I like about my approach, is that all information is combined in a single struct.
Another advantage may be, that it is more easy to add new restrictions. While this probably makes no sense with MMIO, it might be usefull when using this scheme to apply some rule set to something else.
Also, in theory a language server would be able to suggest possible arguments for reg.modify(...) etc, because it does not use anytype for those. However, in practice ZLS is not able to do so anyway.

So the only real plus, that I can see for my approach at the moment, is “all information in a single struct”. For everything else, the approach in the article achieves similar results with more simple code. So I am not sure, if it is worth all the meta magic.

Also interesting is, that the author mentions issues with unnecessary bit shifts and loads from constant sections. I had the same issues before inlining the modify function. Probably the same happens in your case. So maybe it is worth checking, if inline saves you a few instructions for each update/write call.

Last but not least, I probably should have had a look at std.meta when learning meta programming :smiley: Somehow I managed to completely miss that.

Thank you again!

zig comptime execution is rather complex, as it emulates the target your compiling to, zls can reason about simple comptime code

its not going to get much better becuase its currently planned for compiler to implement a protocal for communicating with tools, meaning zls could just ask the compiler about things that would otherwise require reimplementing the compiler.

The method described in the article can actually dispense with anytype there as well. All that’s necessary is introducing a Partial type constructor that takes a struct and produces a new version with all its fields turned into optionals (T → ?T). modify can then take Partial(Write) as argument and only set those bits which aren’t null.

With that, I’m not sure how much of a difference there is in terms of the amount of metaprogramming magic between these two solutions.

1 Like

True! If I understand you correctly, that would basically be the same approach that I used. So the meta programming would get very similar.

You should check out the way we do it in the MicroZig framework:

A lot of very similar concepts as discussed in this thread. All methods are marked inline to ensure that the generated code boils down to a simple address write, read, or read + modify + write. We use another tool in that repo regz to generate a chip.zig file containing the actual definitions for all the chip’s registers at compile time.

2 Likes

Actually I had a look at MicroZig some time ago. But not knowing zig too well, I was a bit overwhelmed by the size of the project and never dared looking at it again since then. So, many thanks you for pointing me to the right direction. I will definitely check it out, now that I understand a bit more.