Embedded Swift running on the Raspberry Pi Pico

@nikolai.ruhe and I managed to get Embedded Swift code running on the Raspberry Pi Pico microcontroller (RP2040 MCU, Cortex-M0+, ARMv6-M), using a current nightly Swift toolchain.

If you have a Pico, you can try this out for yourself, the code is on GitHub:

The program turns the Pico’s onboard LED on and off in a blinking pattern. The Swift code drives the LED’s GPIO pin high or low by writing directly into the memory-mapped register of the MCU.

(We tested this on macOS only and currently our CMake script relies on Xcode’s xcrun to find the Swift toolchain. It should be possible to make it work on Linux too with not too much effort.)

How it works

  • The main program is written in C and built on top of the Raspberry Pi Pico C/C++ SDK.

  • The build system is CMake because that’s what the Pico SDK uses.

  • We define a Swift library named SwiftLib in a subdirectory. We use CMake to build our Swift code into a library, which is then statically linked into the main executable. This is where we need to specify the compiler flags to build in Swift Embedded mode and for the armv6m-none-none-eabi target.

  • SwiftLib includes a C header as its public interface.

  • The main C program calls into SwiftLib via this C header.

For example, this C code in main.c calls a SwiftLib function we defined in the C header:

// SwiftLib/SwiftLib.h
extern void swiftlib_gpioSet(int32_t pin, bool is_high);

// main.c
swiftlib_gpioSet(LED_PIN, true);

The Swift implementation of the function looks like this:

// SIO = the RP2040’s single-cycle I/O block.
// Reference documentation: https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf#tab-registerlist_sio
let SIO_BASE: UInt = 0xd0000000
let SIO_GPIO_OUT_SET_OFFSET: Int = 0x00000014
let SIO_GPIO_OUT_CLR_OFFSET: Int = 0x00000018

/// Drive a GPIO output pin high or low.
@_cdecl("swiftlib_gpioSet")
public func gpioSet(pin: Int32, high: CBool) {
    let mask: UInt32 = 1 << pin
    let sioBasePtr = UnsafeMutableRawPointer(bitPattern: SIO_BASE)!
    if high {
        sioBasePtr.storeBytes(of: mask, toByteOffset: SIO_GPIO_OUT_SET_OFFSET, as: UInt32.self)
    } else {
        sioBasePtr.storeBytes(of: mask, toByteOffset: SIO_GPIO_OUT_CLR_OFFSET, as: UInt32.self)
    }
}

This setup obviously has a lot of things that can be improved, but I find it very exciting that Embedded Swift is already so far along that it’s usable. Thank you @kubamracek and everyone else who’s been working on it!

I’d love to hear if any of you can get this to run on a Pico. Fun!

Issues

Here are some things we’d like to improve:

SwiftPM instead of CMake

Building with SwiftPM would make it much easier to manage the build and to integrate other Swift libraries. The Pico C SDK has a complex configuration and seems quite tied to CMake – getting it to build with SwiftPM will be a challenge if it’s possible at all.

Alternatively, we might be able to integrate a SwiftPM package into the CMake build, but I don't know how. We haven't yet tried to build a plain Embedded Swift executable for armv6m-none-none-eabi that would run directly on the Pico. For this we'll need to figure out how to tell the linker about the Pico's memory map etc. (the Pico C SDK currently does this for us).

Integrate Swift-MMIO

This would also be much easier if we could build with SwiftPM. We briefly tried today to set up a simple SwiftPM library target and build it with Embedded Swift for armv6m-none-none-eabi.

That worked, but as soon as we added Swift-MMIO as a dependency, we got build errors because SwiftPM then tried to build SwiftSyntax (MMIO depends on it for macros) in Embedded Swift mode, which won’t work. I don’t know how to tell SwiftPM which targets to build for the host (for macros) and which for the target platform.

Empty shims for some runtime functions

When we integrated our Embedded Swift library into the CMake build, we initially got some linker errors because the linker expected to find some runtime functions for memory allocation and atomics that we didn't provide.

We managed to silence the linker by providing empty implementations for these functions (see the code here. This works because our Swift code doesn't rely on these runtime abilities (I think), so Swift never calls them. But obviously this isn't ideal. I'm not sure if @kubamracek’s recent work on a -no-allocations mode for Embedded Swift would solve this problem (at least for posix_memalign, probably not for the atomics APIs).

42 Likes

This is really really great. I know how I'll be spending Christman now @ole !

4 Likes

I think it is quite reasonable to build stuff for the Pico with SwiftPM. But this kind brings up an interesting side-part; what about existing libraries and such? Those are going to rely on the libc variants used. Swift on its own can do almost all of the correct stuff with regards to getting GPIO and even I2C, SPI or UART. But when say getting the HID or bluetooth stacks going that would be a TON of work to do (granted doing so could have some huge benefits for re-writing it in Swift). Ostensibly one could use either the libc that the Pico SDK uses, or perhaps even go the route of some other libc (and re-compile the libraries for linking via lld and friends). One could even go to the extent of building up a artifactbundle for swiftpm to use via the --experimental-swift-sdk flag.

On the other hand there is also a route of adding the swift sources in as CMake entities (this would need probably modification of the Pico SDK to support that).

This is a bug in SwiftPM - it is attempting to build the macros for the target architecture and not the host (which it really ought to be). You can work-around that by building MMIO for the host os, grabbing the paths to the binaries and manually adding the macro library paths into the compilation. That is obviously a less than ideal solution.

Here is a trick to get past those: use -Xfrontend -function-sections that will allow the dead stripping to remove the references.

P.S.
Note: This code does not always do what you might hope

It might work in some cases but volatile writes are not currently fully supported in Swift. The only current way to do that is how MMIO does it - via c functions (which can technically be defined in a header only inline static function like this: https://github.com/apple/swift-mmio/blob/main/Sources/MMIOVolatile/include/MMIOVolatile.h)

6 Likes

Thank you for this. I can confirm that this works, so we don't need the empty shims anymore. I pushed this change to the repo.

Thanks, that's good to know. We'd love to use MMIO once we've figured out how to best integrate it into the build process.

1 Like

This is fantastic work, thank you for sharing it on GitHub!

This should be possible with Swift SDKs. The build system used for SDK components has no impact on whether you can build software with those components using SwiftPM or not. You only have to assemble an artifact bundle in a format that SwiftPM can understand.

Yes, I would love this to happen. In fact, I'd encourage everyone interested to contribute to the Swift SDK Generator project so that anyone could generate such bundles in a reproducible way.

Correct, we're well aware of it and I'm working on fixing it right now.

Unfortunately, there's a non-trivial amount of preparatory effort required to make it work. SwiftPM previously used a single BuildParameters value for the whole build graph, which not only meant that the build system was using the same triple for building everything, but also implied sharing all of the build parameters. If you were using debug build configuration, all macros, plugins, and their dependencies (including Swift Syntax) were built in debug build configuration too. If you used DWARF debug information instead of CodeView when cross-compiling on Windows, your macros and plugins were built with DWARF as well. (Note that these caveats only apply to swift build and SourceKit-LSP, the way Xcode does this is completely different).

Fortunately, the foundational work to use separate build parameters for each build plan target has been merged recently, we just haven't completely wired it up yet. This also unblocks a lot of future fixes and improvements, potentially even allowing us to bring SwiftPM plugins into the build graph, as opposed to building and calling into plugins manually, which currently complicates or limits a lot of things in undesirable ways. But that's a long-term project if it's possible at all, fixing cross-compilation with macros is the immediate goal in the meantime.

7 Likes

Thank you @Max_Desiatov. Great to see that cross-compilation for macros is coming.

Turning the Pico C SDK into a Swift SDK is an interesting option. I think we’ll explore this, though I still expect this to be quite challenging. But maybe I don't yet understand the concept of an SDK (in the sense of SE-0387 well enough, or how it maps to the Pico SDK.

As I understand it, an SE-0387-style SDK is a set of precompiled libraries (plus headers etc.), correct? The Pico C SDK is designed to be modular and configured via CMake when you build your Pico executable. There seems to be a mismatch here.

For example, if your program wants to access the analog-digital converter (ADC), you'd tell CMake to link with the hardware_adc library (target_link_libraries(<YOURPROJECT> pico_stdlib hardware_adc)), and that would make the corresponding hardware/adc.h header available and include the library in the build.

How the SDK is built (e.g. which parts and for which RP2040 board) can also be configured by setting #defines (again via CMake). This seems to go against the idea of having a precompiled SDK, unless you build an SDK for your exact configuration, which may be feasible if we can generate the appropriate artifact bundle from a script. Is this what you had in mind?

Another data point: this discussion: Can I precompile the Pico SDK?:

You are much better off building the other thing into a static library, and performing the link step with the SDK if you can - The cmake build hides a huge amount of complexity of building a runnable binary, and doing things like making / % operators use the hardware divider etc.

Another potential issue may be (or maybe not) that the Pico SDK generally expects to be built with GCC and not Clang. I assume the C/C++ compiler specified in an SDK's toolset.json has to be Clang, correct?

(The current Pico SDK release 1.15.1 has experimental Clang support, but only for LLVM Embedded Toolchain for Arm v14.0.0, source: release notes.)

Note: All of this is based on a very limited understanding of everything. I’d never used the Pico SDK at all until a few days ago, so I'm hardly an expert.

I'm sure I'll be back with more questions.

2 Likes

By the way, the size of our executable is 11 KB in a release build:

$ arm-none-eabi-size SwiftPico.elf
   text    data     bss     dec     hex filename
   9804       0    1180   10984    2ae8 SwiftPico.elf

This includes the blinking LED as well as some logging via the Pico's UART interface. I'm not sure if we could make it even smaller by being smarter about how we configure the Pico SDK. Like I said, I don't really know what I'm doing.

5 Likes

it likely could get smaller if you adjusted the linker script:
llvm-readelf or arm-none-eabi-readelf passing -a will show you the sizes of each section.

The Pico SDK sets aside heap and stack space in the binary as well as 256 bytes of .boot2 which is the second stage boot loader. The real cost of the program is roughly the sum of the .init .text and .data sections. However im not sure that an artifact bundle should really generalize stripping the potentially unused sections out entirely.

Technically, unless you are allocating classes and such you don't need the heap section, and also technically (if you are willing to drop to assembly for a few bits of the start routines) you can get rid of the .boot2 sections. However doing that becomes a bit cagey when you try to enable the CYW43 drivers (used for the PicoW) and attempt to get wifi/bluetooth working. That system is rather contorted and was too much for me to follow. Same goes for the tinyusb library. Im sure with work those types of things could be done 100% in Swift.

Other things of interest are interfaces like I2C or SPI; those with proper register manipulation are relatively easy to expose directly to swift similarly to how you were able to manipulate the GPIO registers directly. Even more interesting - PIO also can be done in a similar manner.

Many of the higher level abstractions like HAL's similar to the PicoSDK and Arduino have abstraction costs due to c functions not benefiting from the whole module optimization swift can offer w/ the embedded mode. Between those and potentially using Swift's LTO options the abstraction cost can boil away into nothingness while still retaining safe interactions for the end developer.

5 Likes

Great work!
I have some Pico's laying around, so will try to replicate your work this week(end).

1 Like

Here's an update on our progress: We're currently trying to make more of the RP2040's features available from Swift. Since the original C based Pico SDK exposes all the features in a portable and configurable way we're looking into making the SDK available from Swift.

This means that the SDK's headers and preprocessor settings must be visible from Swift and the SDK should be imported as a Swift module. We also have to link against some compiled C code.

We came up with three ideas on how to do this:

  1. Copy the Pico SDK into a mixed mode Xcode target. This would make C functions visible from Swift. But since SwiftPM does not currently support mixed mode this approach relies on Xcode. We haven't tried this yet.

  2. Create a package for the Swift code. Add a target that reveals the Pico SDK's C API as a systemLibrary. For this we need a compiled Pico SDK and expose it's headers by describing it in a pkg-config (.pc) file. Preprocessor flags (defines) would probably go into a prefix header.

  3. Create a package for the Swift Code. Add a normal C target that contains the Pico's headers (but no impl. files). An example for this approach could be found in the Swift/WinRT project (in the CWinRT target).

@Philippe_Hausler: [BLE/WiFi support] Same goes for the tinyusb library. Im sure with work those types of things could be done 100% in Swift. Other things of interest are interfaces like I2C or SPI [...]

If I understand you correctly you are proposing to port the Pico SDK to Swift and rebuild all or most of its functionality. This is definitely a viable approach with its own benefits.

2 Likes

It was so simple to get this up & running thanks to you. Within 20 minutes I had it running with modifications. I wanted the Swift code to be the one communicating, and over serial. So that's what I did, and it was super straightforward.

5 Likes

@Joannis_Orlandos Great! Thanks for trying it out.

I think it is a good long-term goal; one of the major catches with C APIs is that there is cpu overhead for each function that is called. For desktop systems this is usually not a huge deal, but for small systems like RP2040 it builds up quickly and can directly impact things like bit-banging GPIOs. Swift on the other hand has the advantage that we can leverage inlining and optimizations that fold the calls down to the register access directly.

Furthermore there are two layers to speak of here: the hardware interface layer and the hardware abstraction layer. With embedded build those will both fold up together. But the hardware abstraction layer can be done such that it is somewhat uniform between devices. That way I2C on RP2040 works the same as STM32 or ESP32 even though those three hardware targets have different register layouts.

5 Likes

Wow, this is great to see. Am currently working on a MQTT application in Swift that I will need to port to Pico, would be great to prune the swift linux version for pico than port to C/C++.

1 Like

OK, I got myself a Pico. And after getting the regular C SDK examples running I wanted to try the Swift example from the repo. However, I get stumped by this message:

error: unknown target 'armv6m-none-none-eabi'

I expect this is because the wrong Swift toolchain is used (I've installed the 5.9 Development chain) and entered the relevant toolchain ID in CMakeList.txt.

Is this what is going on? And how can I fix it?

1 Like

5.9 has no support for these embedded triples, you'll have to use development snapshots off main instead.

2 Likes

I gave the development snapshot a try (this one: https://download.swift.org/development/xcode/swift-DEVELOPMENT-SNAPSHOT-2024-02-15-a/swift-DEVELOPMENT-SNAPSHOT-2024-02-15-a-osx.pkg )

I also set it in CMakeList.txt:
set(Swift_Toolchain "org.swift.59202402151a")

however, I still get the same error.

I'm not sure how exactly your CMake setup looks and what swiftc invocations you're using. For me with org.swift.59202402081a (which is a dev snapshot from last week) this standalone invocation works:

swiftc -Osize -target armv6m-none-none-eabi \
  -enable-experimental-feature Embedded \
  -wmo test.swift -c -o test.o

This command gives me the same error:

# swiftc -Osize -target armv6m-none-none-eabi \
  -enable-experimental-feature Embedded \
  -wmo test.swift -c -o test.o
error: unknown target 'armv6m-none-none-eabi

So I guess I'm not using the correct Swift version yet.

How exactly do you select the installed toolchain? When building on the command line, after installation you have to run this command on macOS (assuming you've installed the toolchain in your user directory):

export TOOLCHAINS=$(plutil -extract CFBundleIdentifier raw \
  ~/Library/Developer/Toolchains/swift-latest.xctoolchain/Info.plist)
4 Likes