Build a Swift executable for the Raspberry Pi Pico without the Pico C SDK. Tested on macOS 14 and Linux (Ubuntu 22.04).
-
Download and install a current nightly Swift toolchain ("Trunk Development (main)"). Tested with 2024-02-15, newer versions will probably work too.
On macOS, the download is a
.pkg
installer that will install the toolchain into/Library/Developer/Toolchains
or~/Library/Developer/Toolchains
. On Linux, you can probably use swiftly or swiftenv, or use one of the official Swift Docker images (I tested withswiftlang/swift:nightly-jammy
). -
If you're on macOS, install LLVM using Homebrew:
brew install llvm
Note: This is necessary even though Xcode already comes with LLVM. We need (a) a linker that can produce
.elf
files, and (b) the toolllvm-objcopy
. Xcode currently doesn’t provide these. Don’t worry, installing LLVM from Homebrew, won’t mess up your Xcode setup; Homebrew won’t place these tools into yourPATH
. -
If you're on macOS, put the
ld.lld
executable (this is the linker from LLVM you just installed) in yourPATH
. SwiftPM will be looking for this program and it needs to be able to find it. I have a~/bin
directory in my home directory, which is in myPATH
, so I put a symlink told.lld
in that folder:ln -s /opt/homebrew/opt/llvm/bin/ld.lld ~/bin/ld.lld
(If you’re on an Intel Mac, the path to your Homebrew may vary.)
Alternatively, you could put this symlink in
/usr/local/bin
or some other directory in yourPATH
. Note: I advise against putting thellvm/bin
directory itself in yourPATH
as Xcode and other build tools might get confused which one to use, but I haven’t tested this. -
Install
elf2uf2-rs
andprobe-rs
(requires a working Rust installation; see https://www.rust-lang.org/tools/install for guidance):cargo install elf2uf2-rs --locked cargo install probe-rs --features cli
We’ll use these tools to create the UF2 file for the Pico and/or flash the ELF file to the Pico. If you have other tools to do these jobs, feel free to use them. These are not required for the actual build process.
The normal SwiftPM commands swift build
and swift run
won’t work. You need to call swift package link
with the correct arguments (see below) to build. This will use our custom SwiftPM command plugin for building and linking.
# Build and link final executable App.elf
swift package --triple armv6m-none-none-eabi \
--toolchain /Library/Developer/Toolchains/swift-latest.xctoolchain/ \
link \
--objcopy=/opt/homebrew/opt/llvm/bin/llvm-objcopy
Adjust the path to the Swift toolchain you downloaded accordingly. It should be either /Library/Developer/…
or ~/Library/Developer/…
. Note that we’re passing in the path to llvm-objcopy
from the Homebrew LLVM install.
Note: as of 2024-02-22, the macOS build produces dozens of warnings of the type "Class xyz is implemented in both [path to toolchain] and [path to Xcode]. One of the two will be used. Which one is undefined." I don't understand why these messages appear, but they don’t seem to be a problem.
# Build and link final executable App.elf
swift package --triple armv6m-none-none-eabi link
The build will produce an RP2040 executable in .build/plugins/Link/outputs/App.elf
.
If you have a Raspberry Pi Debug Probe or a second Pico that you can use as a debug probe, use probe-rs
to flash it to the Pico:
probe-rs run --chip RP2040 .build/plugins/Link/outputs/App.elf
Note: If probe-rs
shows an error of the form "ERROR probe_rs::cmd::run: Failed to attach to RTT continuing..." after it says "Finished", you can ignore it. Use Ctrl+C to exit probe-rs.
Alternatively, use elf2uf2-rs
to create a UF2 file which you can then copy to the Pico in BOOTSEL mode:
# Creates .build/plugins/Link/outputs/App.elf
elf2uf2-rs .build/plugins/Link/outputs/App.elf
You should see the Pico’s onboard LED blinking.
Note: I have observed that after some builds the LED is blinking very fast or appears to be on the entire time. Anecdotally, this seems to happen especially on the first deploy to a Pico, although I can’t explain why. If this happens to you, try passing larger numbers in the calls to delayByCounting
in App.swift
. Our current method of faking a timer by counting is very primitive because we have only implemented the bare minimum of functionality. This will improve as we implement a proper Timer API.