Introducing Swift MMIO

Hi Swift Community!

We're excited to introduce swift-mmio, our first library for the Embedded Swift ecosystem. swift-mmio is designed to provide safe and secure APIs for fundamental low-level operations required in embedded firmware development, such as reading and writing memory-mapped registers. Drawing inspiration from mmio libraries in languages like C++ and Rust, swift-mmio focuses on improving the correctness of code interacting with MMIO.

The swift-mmio package offers a set of macros to define register maps directly within Swift source code:

import MMIO

/// An example 32-bit wide register, encompassing three individual bit fields:
/// - "en" (Enable)
/// - "clken" (Clock Enable)
/// - "rst" (Reset)
@Register(bitWidth: 32)
struct CR1 {
  @ReadWrite(bits: 0..<1, as: Bool.self)
  var en: EN
  @ReadWrite(bits: 1..<2, as: Bool.self)
  var clken: CLKEN
  @ReadWrite(bits: 2..<3, as: Bool.self)
  var rst: RST
}

@RegisterBank
struct Control {
  @RegisterBank(offset: 0x0)
  var cr1: Register<CR1>
  @RegisterBank(offset: 0x4)
  var cr2: Register<CR2>
}

And, leveraging Swift's expressiveness, swift-mmio provides programmers with type safe API for register and bank operations:

var control = Control(unsafeAddress: 0x1000)

// Get a reference to the cr1 register in the control bank.
var cr1 = control.cr1

// Perform a read-modify-write of the cr1 register.
cr1.modify { cr1 in
  // Mutate `en` as a Bool.
  cr1.en = true

  // Mutate `clken` as a raw integer.
  cr1.raw.clken = 0

  // Mutate `rst` using C-style manual bit operations.
  cr1.raw.storage |= (1 & CR1.RST.bitMask) << CR1.RST.bitOffset
}

The swift-mmio API is still evolving, and we're eager to hear from the community. Your feedback and contributions will help shape the future of this library. Feedback is welcome here on the Swift Forums and as GitHub Issues (and PRs) on the repo linked above. Additional features and tasks will be added to the Issues list soon for early contributors to dig in immediately.

76 Likes

Congratulations on beginning this journey with the first library! :wink:

4 Likes

Well done, that's really super cool!!

3 Likes

I love to see swift improve for embedded. My last attempt dates 2 years now where I could not get over one big roadblock. Swift itself demands at least 2mb storage. This is to much for small embedded systems. Has this improved?

1 Like

This is amazing! I can't wait to adopt this in my (future) projects

2 Likes

Embedded builds can get down to under a few hundred bytes of code that is runnable on systems as flashed images. It is to the extent that the reserved stack or second stage boot loaders normally associated with those targets are considerably larger portions of the flashed image than the compiled code. Obviously that means there are a number of things missing that desktop swift has and we need to strike a balance depending on what level of systems are being targeted.

The MMIO package gives a method to build the libraries for interacting with the hardware that leverages one of the super-powers in this scenario: Swift's optimization around inlining that quite honestly is hard to do w/ languages like C/C++. From my experience using this library it optimizes even abstracted register access (to some sort of HAL living on-top of the mmio register interfaces) down to just a singular read/write instruction to a register.

8 Likes

Indeed, this is a core requirement of MMIO. All calls to read() and write() compile down to specific bit-width load/store instructions. The FileCheck based tests are used to verify this requirement.

Tangentially related, we may explore doublewide reads and writes for platforms which support them. On aarch64 this would leverage stp/ldp instructions.

4 Likes

See Embedded Swift for more info on the size reduction effort

3 Likes

I'm curious about the Debug build story, though - generally Swift doesn't inline things and doesn't do any meaningful code-size-reducing optimisations for debug builds. It is apparently possible to construct Swift code in a way that does permit debug builds to still boil down to simple machine instructions - I believe the Atomics library takes some pains to ensure this - but, at the very least that's hard and a lot of extra work.

Maybe this is a bit tangential to this library, since it applies to Embedded Swift generally, but I'm curious if you can elaborate on how debug builds work and if it is indeed an issue for MMIO?

This looks awesome. Looking forward to seeing much more use of Swift in this space.

I have a (possibly really dumb) question. What are the EN, CLKEN, and RST types?

How come it’s not written like so:

@Register(bitWidth: 32)
struct CR1 {
  @ReadWrite(bits: 0..<1)
  var en: Bool
  @ReadWrite(bits: 1..<2)
  var clken: Bool
  @ReadWrite(bits: 2..<3)
  var rst: Bool
}

Hi!

I have a (possibly really dumb) question. What are the EN , CLKEN , and RST types?

This is a great question; these types are generated by @Register macro and contain static information about the bit field which is used for bit select/insert operations.

From the example above, EN is be generated under CR1 with the following content:

enum EN: ContiguousBitField {
  typealias Storage = UInt32
  static let bitRange = 0 ..< 1
}

The bit field EN type contains a number of static members like CR1.EN.bitRange as seen here, but also CR1.EN.bitOffset (0), CR1.EN.bitMask (0b1), and CR1.EN.bitWidth (1).


How come it’s not written like so:

This is also a great question but a bit harder to answer. I'll try to put together an explanation later in the week if not tomorrow! The short answer is: I'm not certain we've settled on the best way to write these fields and format you've described is something I've been thinking about!

2 Likes

I see! I think something similar to what I wrote, where the type of the property is specified using the familiar Swift syntax, would be the most natural.

Regarding how to implement that, I wonder if we could use property wrappers. Developers could access the bit field information via the projected value of the property wrapper. The property wrapper could even have a raw property for more natural namespacing.

The only caveat would be that developers would only be able to access the bit field information on a given instance rather than statically. But maybe that’s OK?

Also, should @Register add RawRepresentable conformance? Then, we could access the raw value similar to other RawRepresentable types like cr1.rawValue instead of cr1.raw.storage. It probably doesn’t make a functional difference but it might reduce the ramp-up time for developers who are new to the package.

Putting it all together:

@Register(bitWidth: 32)
struct CR1 {
  @ReadWrite(bits: 0..<1)
  var en: Bool
  @ReadWrite(bits: 1..<2)
  var clken: Bool
  @ReadWrite(bits: 2..<3)
  var rst: Bool
}

cr1.en = true
cr1.$clken.rawValue = 0
cr1.rawValue |= (1 & cr1.$rst.bitMask) << cr1.$rst.bitOffset

Looking forward to it! Thanks for driving Swift forward in this exciting new direction. :blush:

Would it be technically feasible for the @RegisterBank macro at the top level to infer the offsets based on the bitWidth parameter of the @Register macro?

I wonder if these bit field macros could be more general purpose than MMIO. Perhaps they should be a Swift language feature?

@mgriebling previously brought up a use case unrelated to MMIO and the Swift evolution pitch by @Douglas_Gregor and @Ben_Cohen for an @OptionSet macro feels like a subset of a general bit field macro.

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...

  1. I'm not convinced RawRepresentable conformance is correct because init(rawValue:) returns Optional<Self> but would always return a non-nil value.

  2. 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.

4 Likes

I don't think this is a disqualifier for RawRepresentable conformance, on the contrary. A non-failable initializer can satisfy a failable init protocol requirement. For example, this is a perfectly valid RawRepresentable conformance:

struct UserID: RawRepresentable {
    var rawValue: Int

    init(rawValue: Int) {
        self.rawValue = rawValue
    }
}

In non-generic contexts users will have access to the non-failable init, while in generic contexts only the failable init? will be available (as it should be).

5 Likes

I think the point of @rauhul was that an optional initializer should not be available in first place because it is misleading for the user.
In my opinion, Register is out of scope of RawRepresentable. RawRepresentable is like a bridge between high level types and low (raw) level types. Register instead is a (high level) representation of a device register. The fact that those registers are represented by a raw type doesn't mean that RawRepresentable should be used. Register can be seen more like an UnsafePointer from this point of view (and it doesn't have this conformance indeed)

Also, in your implementation, you must make the type explicit every time in order for the compiler to choose the right initializer (nor can you use the discouraged @_disfavoredOverload, since it should be applied to the optional init).

@lorenzofiamingo I don't understand what you mean by this. Can you give an example?

Sure :slight_smile:

func foo() -> Int { return 0 }
func foo() -> Int? { return 1 }

let f = foo() // Error: Ambiguous use of 'foo'
let f: Int = foo() // 0
let f: Int? = foo() // 1

with @_disfavoredOverload:

func foo() -> Int { return 0 }
@_disfavoredOverload func foo() -> Int? { return 1 }

let f = foo() // 0
let f: Int = foo() // 0
let f: Int? = foo() // 1