Embedded Swift on the Raspberry Pi Pico/RP2040 without the Pico SDK

This is sort of a follow-up on Embedded Swift running on the Raspberry Pi Pico, but with a different take.

I managed to create a very small Embedded Swift "toolchain" for building pure Swift executables for the Raspberry Pi Pico (it might also work with other RP2040 boards, but I'm only testing on the Pico). The "toolchain" is just a Makefile, so no SwiftPM integration yet, but I feel we're not too far off. The repository is here if you want to try it out:

It should work on macOS and Linux as hosts (tested with Ubuntu 22.04 via Docker). Please report back if you try this out and manage to get something running on a Pico.

Unlike our other approach (in Embedded Swift running on the Raspberry Pi Pico), this doesn't use the Raspberry Pi Pico C/C++ SDK as a basis, which has pros and cons:

  • Much less build system complexity. This makes it much more feasible IMO to get it working with SwiftPM eventually. I found the complexity of the Pico SDK's CMake config very overwhelming. The Makefile we have here isn't pretty, but it's soooo much smaller.

  • We have to build everything from scratch, so almost nothing works yet. I wrote a few simple functions to talk to a GPIO pin (very ugly code currently), but that's it for now — we have no timers, no I²C, no SPI, etc.

  • This clean slate is also exciting, of course. Lots of room for experimentation and learning.

The main application code is all Swift. There's very little C and Assembly involved for the bootstrapping. I copied the necessary files (linker script etc.) from the Pico SDK and patched them slightly. The bootstrapping code still does a few things we don't really need. I hope to make whittle it down to the bare minimum we need eventually, if only to understand it better.

I'm using the C compiler and linker from the ARM Embedded LLVM toolchain. If I'm reading @kubamracek current work in the Swift repo correctly (e.g. [embedded] Start building and including lld even in Darwin toolchains by kubamracek · Pull Request #70715 · apple/swift · GitHub), we may soon be able to use the Swift linker for this?

By the way, the current executable, which blinks an LED and listens to a GPIO input, is less than 1 KB:

/Applications/LLVMEmbeddedToolchainForArm-17.0.1-Darwin/bin/llvm-size SwiftPico.elf
   text	   data	    bss	    dec	    hex	filename
    992	      0	      0	    992	    3e0	SwiftPico.elf

The next step I'd love to take is SwiftPM integration as this would make it so much easier to integrate other packages. I started experimenting with building an SE-0387-style SDK for the RP-2040, but I haven't succeeded yet. I'll probably post questions in a separate thread.

28 Likes

I didn't play with the new 387 stuff yet (FP coprocessor support?), which is probably much better, but maybe my cross compiler sample might be helpful too: Swift Package Manager Cross Compilation Destinations · GitHub

3 Likes

This is awesome work, @ole! :blush:

1 Like

@ole Amazing work! So happy to see Swift running on embedded platforms. I love coding for them but I'm not the biggest fan of Arduino (thought they are doing amazing work!).

1 Like

I'm not entirely convinced Swift SDKs are the right answer for embedded platforms. The primary reason to have a Swift SDK is a need for a special libc and dependencies like swift-corelibs-dispatch and swift-corelibs-xctest that are hard to build as a package. Another potential use case is the ability to define test runners and debuggers, but this latter possibility hasn't been explored yet.

Since you don't immediately need either of those in this case, if you can fit all the required platform things into a separate package it should be much easier for users to depend on it. That also makes it highly customizable and easy to update in one go, as opposed to adding an out-of-package Swift SDK dependency.

If you need to add unsafe compiler/linker flags to such "embedded platform support" package, please let us know. I hope we'll be able to work around or find fixes for those new requirements.

1 Like

Thank you, Helge. I think I'd rather go with the new SDK approach (if it's necessary at all, see @Max_Desiatov's reply above) because why not use the new stuff? I never used cross-compilation with destination.json so I think my time is better spent focusing on the new stuff. Both approaches seem very similar to me.

Oh yes, absolutely use the new stuff!! As mentioned I haven't looked at it yet, I thought my stuff might be potentially helpful to build upon the new things.
My things essentially streamline the work by @johannesweiss into distinct steps (getting a target toolchain and things like that).

1 Like

Is there a good intro on the new things? E.g. how do Linux toolchains work?

Interesting. I think a special libc might be interesting/necessary for some embedded environments. For example, the Pico C SDK does depend on an embedded libc AFAIK, but that's a topic for the other thread Embedded Swift running on the Raspberry Pi Pico. You're right that the from-scratch approach we're discussing in this thread doesn't need it.

That would be nice, although I fear the stuff we need to do for the RP2040 will be difficult to wrap in a package:

  • The second-stage bootloader (boot2) is assembled and linked with a special linker script that puts it at the right location in flash
  • Then we calculate a checksum over the bootloader binary and write that checksum into the binary (the RP2040 won't boot unless this checksum is there). This is currently done by a Python script.
  • The runtime (crt0) for the main executable (setting up IRQ handlers, exception handlers etc.) is assembled and linked with the main executable
  • The main executable also needs to be linked with a special linker script

Maybe the checksum calculation can be done in a SwiftPM plugin? But I'm not sure we can integrate this cleanly.

We can prebuild boot2 and crt0, but then we're also no longer talking about a simple package that you can include as a dependency, right (because of the binary files involved)? And it doesn't solve how to pass the linker script to the final link step.

I think my biggest problem in doing any SwiftPM experiments right now is that I can't use swift as the linker to build a Pico executable – I have to use Clang for linking. I don't think I can tell SwiftPM to link with Clang unless I build an SE-0387-style SDK?

Here are the errors I get when I try to link with Swift (on Linux; I understand linking ELF files on macOS is work in progress: [embedded] Start building and including lld even in Darwin toolchains by kubamracek · Pull Request #70715 · apple/swift · GitHub):

/usr/bin/swift --version
Swift version 5.11-dev (LLVM 13124099c3f0229, Swift d6871edc839adec)
Target: aarch64-unknown-linux-gnu

"/usr/bin/swiftc" \
	-enable-experimental-feature Embedded \
	-target armv6m-none-none-eabi \
	-Xlinker --script=pico-sdk-comps/memmap_default.ld \
	-Xlinker -z -Xlinker max-page-size=4096 \
	-Xlinker --gc-sections \
	-Xlinker --wrap=__aeabi_lmul \
	build/bs2_default_padded_checksummed.S.obj build/crt0.S.obj build/bootrom.c.obj build/pico_int64_ops_aeabi.S.obj build/main.o \
	-o "build/SwiftPico.elf"
error: link command failed with exit code 1 (use -v to see invocation)
clang: error: no such file or directory: '/usr/lib/swift/armv6m/swiftrt.o'
error: fatalError
make: *** [Makefile:141: build/SwiftPico.elf] Error 1

OK, so it's looking for /usr/lib/swift/armv6m/swiftrt.o and doesn't find it. That directory doesn't exist. Should it exist? Will it exist in the future as the embedded toolchain improves?

Linking with -nostartfiles gets rid of that error, but produces more:

"/usr/bin/swiftc" \
	-enable-experimental-feature Embedded \
	-target armv6m-none-none-eabi \
	-nostartfiles \
	-Xlinker -nostdlib \
	-Xlinker --script=pico-sdk-comps/memmap_default.ld \
	-Xlinker -z -Xlinker max-page-size=4096 \
	-Xlinker --gc-sections \
	-Xlinker --wrap=__aeabi_lmul \
	build/bs2_default_padded_checksummed.S.obj build/crt0.S.obj build/bootrom.c.obj build/pico_int64_ops_aeabi.S.obj build/main.o \
	-o "build/SwiftPico.elf"
error: link command failed with exit code 1 (use -v to see invocation)
/usr/bin/ld.gold: error: cannot find -lswiftCore
/usr/bin/ld.gold: error: cannot find -lc
/usr/bin/ld.gold: error: cannot find -lm
/usr/bin/ld.gold: error: cannot find -lclang_rt.builtins-armv6m
/usr/bin/ld.gold: internal error in set_address, at ../../gold/output.h:322
clang: error: ld.lld command failed with exit code 1 (use -v to see invocation)
error: fatalError
make: *** [Makefile:142: build/SwiftPico.elf] Error 1

I'm afraid I don't really understand what I'm doing here, so it's hard to ask better questions.

1 Like

I ran GitHub - apple/swift-sdk-generator: Generate Swift SDKs for cross-compilation with the default settings. It produced a working Mac-to-Linux cross-compilation SDK, i.e. I successfully used it to build a Linux binary on the Mac.

The JSON files in the generated .artifactbundle look a lot like destination.json:

xyx.artifactbundle/info.json:

{
  "artifacts" : {
    "5.9.2-RELEASE_ubuntu_jammy_aarch64" : {
      "type" : "swiftSDK",
      "version" : "0.0.1",
      "variants" : [
        {
          "path" : "5.9.2-RELEASE_ubuntu_jammy_aarch64/aarch64-unknown-linux-gnu",
          "supportedTriples" : [
            "arm64-apple-macosx13.0"
          ]
        }
      ]
    }
  },
  "schemaVersion" : "1.0"
}

xyx.artifactbundle/5.9.2-RELEASE_ubuntu_jammy_aarch64/aarch64-unknown-linux-gnu/swift-sdk.json:

{
  "targetTriples" : {
    "aarch64-unknown-linux-gnu" : {
      "sdkRootPath" : "ubuntu-jammy.sdk",
      "toolsetPaths" : [
        "toolset.json"
      ]
    }
  },
  "schemaVersion" : "4.0"
}

xyx.artifactbundle/5.9.2-RELEASE_ubuntu_jammy_aarch64/aarch64-unknown-linux-gnu/toolset.json:

{
  "librarian" : {
    "path" : "llvm-ar"
  },
  "schemaVersion" : "1.0",
  "swiftCompiler" : {
    "extraCLIOptions" : [
      "-use-ld=lld",
      "-Xlinker",
      "-R/usr/lib/swift/linux/"
    ]
  },
  "cxxCompiler" : {
    "extraCLIOptions" : [
      "-lstdc++"
    ]
  },
  "linker" : {
    "path" : "ld.lld"
  },
  "rootPath" : "swift.xctoolchain/usr/bin"
}
3 Likes

This should be doable with a plugin. Maybe even the Python script can be packaged in an .artifactbundle and then called from the plugin? You can just pretend it's a "binary", by sticking an executable flag on it and adding an appropriate Python shebang header. IMO in SwiftPM we're too prescriptive in saying that package plugins must delegate to binary executables, any executable script should do, assuming an interpreter for it is available.

IIUC this can be done today with something like LinkerSetting.unsafeFlags(["-T", "<your_linker_script>"]) passed to your target that needs it in Package.swift.

IIUC we're basically always delegate to Clang for linking anyway. Pass -v to your swiftc invocation to see that it invokes Clang under the hood.

It should not, this file is irrelevant in the embedded mode as of right now. There are apparently still Swift Driver bugs present, where unrelated non-embedded options are passed in the embedded mode. I've fixed some of them recently in Don't pass `-rpath` and `-lswiftCore` in embedded mode by MaxDesiatov · Pull Request #1516 · apple/swift-driver · GitHub, apparently there are more lurking out there in the codebase.

These also look like driver bugs to me, some of them possibly fixed by the PR I linked above.

2 Likes

OK, I created a repo with a minimal SwiftPM package: just an executable product with an empty main method, not including any of the RP2040-specific stuff. Here's the repo: GitHub - ole/SwiftPM-embedded-minimal

Now I want to build this for Embedded (armv6m-none-none-eabi in my case). Depending on the configuration (Mac vs. Linux and debug vs. release), I'm getting different build errors. I have documented these in two issues:

I don't necessarily expect these builds to succeed as they are because the linker would certainly miss some required symbols. But the errors I'm seeing seem to occur at an earlier stage, and they're the same errors I'm seeing with my more complex RP2040-specific setup (incl. linker scripts etc.), which I pushed here: [WIP] Try to build with SwiftPM by ole · Pull Request #4 · ole/swift-rp-pico-bare · GitHub.

Any help is appreciated.

2 Likes

I have successfully used SwiftPM to build Swift sources to a static library and link it with other object files using lld. This is still a very small example, but I hope it is at least partially helpful for you.

(Note that swiftly cannot currently be used as is due to this issue)

2 Likes

Thank you @kebo! And thanks for the link, I'll take a look.