Disclaimer: I was quite tired while writing this, so apologies if it's hard to follow!
While property wrappers could work for some of the simple cases, I don't think they work for the more complicated registers where the bits read don't have the same semantic meaning as the bits written.
Additionally, I think there's some confusion about the functionality exposed by MMIO, which is understandable due to the lack of documentation. Here's a deeper dive into what @Register does and the functionality of the current API:
The @Register macro expands annotated structs to include three nested types: Read, Write, and Raw. The properties within these @Register structs must be annotated with bit field macros, falling into two categories: symmetric (e.g., @Reserved and @ReadWrite) and asymmetric (e.g., @ReadOnly, @WriteOnly, @Reserved, @ ReadWrite1Clear).
The Read and Write types expose readable and writable projected bit fields respectively, while the Raw type provides access to all bit fields as unsigned integers matching the full width of the register. This distinction between Read and Write is crucial since registers are in device space and may have vastly different semantics. However, when all bit fields are symmetric, Read and Write types become aliases to a ReadWrite type, and the entire register is considered symmetric.
As demonstrated in the original post, registers provide a modify method to make read-modify-write (RMW) routines easy and "swifty." For asymmetric registers, the modify method takes a closure of (Foo.Read, inout Foo.Write) -> (), presenting the difference between these views to the programmer. Symmetric registers, where there is no distinction between Read and Write, have an overloaded modify method taking a simpler closure of (inout Foo.ReadWrite) -> ().
As an example, let's consider an "Interrupt and Status Register" (e.g., the STM32F746xx's HDMI-CEC CEC_ISR register). These registers typically contain bits with "Write-1-Clear" semantics, meaning reading a 1 indicates the interrupt is active and writing a 1 clears/acknowledges the status. This behavior can lead to some gnarly bugs in RMW routines unless proper care is taken to only clear the IRQ bits that were actually handled.
MMIO helps prevent mistakes by setting the W1C bits of the inout Write value in the modify closure to 0, so the programmer cannot acknowledge an interrupt they didn't explicitly intend to. This can be seen in the example modify below, where the programmer wants to check and handle txacke but not modify the state of any of the other status bits.
@Register(bitWidth: 32)
struct CEC_ISR {
/// Other statues...
// var etc...
/// Tx-Error
@ReadWrite1Clear(bits: 11..<12, as: Bool.self)
var txerr: TXERR
/// Tx-MissingAcknowledgeError
@ReadWrite1Clear(bits: 12..<13, as: Bool.self)
var txacke: TXACKE
/// Reserved, must be kept at reset value.
@Reserved(bits: 13...)
var reserved: Reserved
}
let isr = CEC_ISR(unsafeAddress: 0x1000)
isr.modify { r, w in
if r.txacke {
handleAckError()
w.txacke = true // clear ack error after modify finishes
}
}
Note: @ReadWrite1Clear support isn't yet on main.
Back to the subject of property wrappers... I think they might be able to handle symmetric registers, but as far as I can tell, are unable to express asymmetric registers; hopefully the above background helps demonstrate why differing Read and Write types are valuable.
These static properties are provided to allow interop with raw values which don't have an instance of the typed register to use. One could create a temporary instance but this feels like a kludge rather than a great solution, the programmer now has a value which should not be used as the value's bit pattern may not valid for the current program state.
Example:
var foo = UInt32(0)
let cr1Tmp = CR1.ReadWrite(storage: 0x0)
// ^ never use this value: inconsistent with actual program
foo |= (1 & cr1.$rst.bitMask) << cr1.$rst.bitOffset
So there's a couple things going on here...
-
I'm not convinced RawRepresentable conformance is correct because init(rawValue:) returns Optional<Self> but would always return a non-nil value.
-
Using rawValue instead of storage is definitely an option and fits more with prior art, but omitting the conformance to RawRepresentable made this feel more confusing than just using a different name.
Unfortunately no; the @RegisterBank macro does not have access to the value of bit width properties of its members at expansion time and as a result cannot pre-compute offsets. Additionally @RegisterBanks can have nested @RegisterBanks within them which currently have no notion of a total bit width.
I have some ideas about how this could be made significantly better for both the author and macro implementation, but they require additional language features particularly around struct layout. Specifically, I would like to change RegisterBank to be generic over a struct which actually matches the bank's memory layout and use the struct member's offsets.
I assume by language feature you mean a stdlib macro. @nnnnnnnn and I have preivously briefly discussed this in the context of another package. The issue I see is that bit packing macros would be used for structs in normal program memory, e.g. a small network packet header, whereas the MMIO bit field macros are for structs in device memory which attempt to highlight the differences in reads and writes.