@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 thearmv6m-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).