Memory-mapped IO registers in zig (2021)

(scattered-thoughts.net)

69 points | by fanf2 10 days ago

2 comments

  • tialaramex 10 days ago
    Blergh. Volatile keywords again. This is a mistake in C, a mistake inherited (and of course doubled down on) by C++ but not one Zig needs to repeat.

    What you need is volatile read/write or load/store intrinsics. When you have those, you can express what's actually possible on a hardware platform, and not inadvertently enable people to write nonsense in your language - no need to even have a footgun here, let alone leave it loaded on the table for a newbie to pick it up and blow their whole leg off.

    Although it was eventually un-deprecated in C++ 23, one of the smaller useful accomplishments in C++ 20 was to deprecate the inappropriate use of composite operators on volatile, which is the sort of nonsense entertained by having a keyword instead of intrinsics. When the newbie in your team writes irq_flags |= new they think they're performing a single operation, after all |= isn't two operations, it's just one. You, hopefully, know that's untrue - the irq_flags were marked volatile, so that's decomposed into a separate load and store -- but are you sure you'll notice they screwed this up? Or will you only realise when customers report sometimes IRQ flags "go missing" under load?

    • tiehuis 10 days ago
      There is an issue proposing this approach: https://github.com/ziglang/zig/issues/4284
    • jstanley 10 days ago
      > when customers report sometimes IRQ flags "go missing" under load?

      I wish!

      More likely customers would report that it's "not working properly".

      (I mean, not that it's their responsibility to know why it's not working properly).

    • audunw 10 days ago
      If you remove the volatile keyword on variables/attributes how do you ensure that a variable that needs to be accessed with volatile read/write always uses such intrinsics?

      Seems to me that you want to keep the keyword but give a warning or suppressible error if it’s not accessed through volatileRead/Write intrinsics. (Hobby/hacking level development of embedded software if every other line was a volatileRead/Write call, but good to enforce in professional software development)

      • tialaramex 9 days ago
        Several techniques are possible as a standard library type, you needn't provide a mechanism to alter such a variable at all except via these intrinsics. I am not sufficiently experienced in Zig to offer a strong opinion about how I'd prefer it to be done there, but in general a wrapper type which prevents the programmer from just improperly using the MMIO address (via any other method) needn't add any overhead to the compiled software compared to the raw pointer used today with the keyword.

        We're not trying here to prevent malevolent actors from causing trouble or trying to entirely prevent foot shooting here, only to require that weapons capable of targeting your own feet are kept separate from the ammunition in a locked cabinet, so that the newbie will ask, "Hey, I need to get in the locked cabinet so I can write a composite assignment on an MMIO register" and that's your chance to explain why this doesn't do what they expected and what they should write instead.

    • auxym 10 days ago
      Nim's maintainer agrees with you I believe, and the API is as you suggest (volatileLoad and volatileStore): https://nim-lang.org/docs/volatile.html

      However, under the hood, Nim compiles to C. So these are macros that typecast to volatile, does the read (or write), then casts back to non-volatile.

      (Small plug for my nim project that is somewhat related to OP: https://github.com/EmbeddedNim/svd2nim)

    • 10000truths 10 days ago
      Unfortunately, Zig doesn't currently have intrinsics for non-temporal loads/stores. Not a problem for embedded chips since they don't have cache hierarchies, but high performance bus messaging in x86 and ARM processors (e.g. PCIe TLPs) absolutely requires it.
    • Falell 10 days ago
      > What you need is volatile read/write or load/store intrinsics. When you have those, you can express what's actually possible on a hardware platform, and not inadvertently enable people to write nonsense in your language...

      Linus recently said something similar in a memory model discussion on LKML (long thread, only Linus's posts are really relevant here) https://lore.kernel.org/lkml/CAHk-=whmmeU_r_o+sPMcr7tPr-EU+H...

    • bsaul 10 days ago
      I may not properly understand what you're saying because i haven't had to deal with volative keywords. However, i'm surprised by what (i think i understood from what) you're saying: aren't all register restored on process switching ?
      • foldr 10 days ago
        If two pieces of code are accessing irq_flags concurrently then the load and store operations from each may be interleaved, giving incorrect results.
        • bsaul 9 days ago
          Ok but what makes it specific to volatile ? This pb happens with all memory.

          Are volatile variables used for atomic memory access semantic ?

          • foldr 9 days ago
            I guess the point was that in the case of a non-volatile variable, the variable would typically be in a register and the compiler would typically compile foo |= CONSTANT into a single instruction (though there is no guarantee of any of this). But if the variable is volatile then the compiler would have to load it from memory into a register, perform the bitwise operation, and then store it again. If irq_flags represents a memory mapped register then you'd need to force the second option.
    • hpeter 10 days ago
      Zig is a really nice language tho there are some things I also don't agree with. It's still far away from a stable 1.0 so maybe they work this out.
    • Joker_vD 10 days ago
      Wait, is it even possible to do an atomic volatile modify of a MMIO register? Sure, x86 has LOCK prefix but would it work with memory-mapped stuff?
      • auxym 10 days ago
        From my understanding, the only thing volatile does (IIRC this is implementation specified as the standard is vague) is prevent the compiler from optimizing out reads or writes when it thinks they are "useless". That is, is tells the compiler "this memory location may change independently of that the code is doing, so you always need to read or write it when I say so".

        Atomicity is sort of a different thing. You might need things like critical sections (disable interrupts) or memory fencing for the read-modify-write cycle.

      • matt-noonan 10 days ago
        There are some interesting solutions out there, such as bit-banding used in some ARM Cortex CPUs. This maps entire bytes in the high part of the address space to single bits in the low part of the address space, so that you can make an atomic set or clear of a single physical bit by writing to a byte of memory. https://spin.atomicobject.com/bit-banding/