├── .gitignore ├── .vscode └── settings.json ├── CMakeLists.txt ├── README.md ├── SwiftLib ├── CMakeLists.txt ├── README.md ├── SwiftLib.h └── SwiftLib.swift ├── main.c └── pico_sdk_import.cmake /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /build 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Building Swift with CMake requires Ninja 3 | "cmake.generator": "Ninja", 4 | } 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | 3 | # User configuration 4 | # Change these values to match your config. 5 | # 6 | # 1) Path to Raspberry Pi Pico C/C++ SDK 7 | # This assumes the SDK is cloned to a sibling directory of this project. 8 | # Alternatively, you can set PICO_SDK_PATH as an environment variable in your shell. 9 | if (NOT (DEFINED ENV{PICO_SDK_PATH}) AND (NOT PICO_SDK_PATH)) 10 | set(PICO_SDK_PATH "${CMAKE_CURRENT_LIST_DIR}/../pico-sdk") 11 | message("PICO_SDK_PATH is not set in environment, using value from CMakeLists.txt ('${PICO_SDK_PATH}')") 12 | endif () 13 | # 2) Swift toolchain 14 | # Change this to the current Swift nightly toolchain you installed. 15 | # The ID is in /Library/Developer/Toolchains/[TOOLCHAIN].xctoolchain/Info.plist 16 | # Alternatively, set the TOOLCHAINS environment variable. 17 | if (NOT (DEFINED ENV{TOOLCHAINS})) 18 | set(Swift_Toolchain "org.swift.59202312071a") 19 | message("Swift toolchain: ('${Swift_Toolchain}') (using value from CMakeLists.txt as TOOLCHAINS is not set in environment)") 20 | else () 21 | set(Swift_Toolchain "$ENV{TOOLCHAINS}") 22 | message("Swift toolchain: ('${Swift_Toolchain}') (using TOOLCHAINS value from environment)") 23 | endif () 24 | 25 | # initialize the SDK based on PICO_SDK_PATH 26 | # note: this must happen before project() 27 | include(pico_sdk_import.cmake) 28 | 29 | # Configure Swift. This must happen before `project()`. I don't know why. 30 | # Use nightly Swift compiler, configured for Embedded Swift. 31 | # Find path to swiftc and store it in swiftc_Path 32 | execute_process( 33 | COMMAND xcrun --toolchain "${Swift_Toolchain}" --find swiftc 34 | OUTPUT_VARIABLE swiftc_Path 35 | OUTPUT_STRIP_TRAILING_WHITESPACE 36 | ) 37 | set(CMAKE_Swift_COMPILER 38 | "${swiftc_Path}" 39 | ) 40 | set(CMAKE_Swift_FLAGS 41 | # -wmo: Whole-module optimization is always required for Embedded Swift. 42 | # -Xfrontend -function-sections: enables dead stripping of unused runtime functions. 43 | "-target armv6m-none-none-eabi \ 44 | -enable-experimental-feature Embedded \ 45 | -wmo \ 46 | -Xfrontend -function-sections" 47 | ) 48 | # Disable CMake’s automatic Swift compiler check. The compiler check always 49 | # fails for Embedded Swift because it tries to compile a Swift program that 50 | # includes `print()`, which isn't available in Embedded Swift. 51 | set(CMAKE_Swift_COMPILER_FORCED TRUE) 52 | 53 | # === Begin project configuration 54 | 55 | project(SwiftPico LANGUAGES C CXX Swift) 56 | 57 | # Initialize the Raspberry Pi Pico SDK 58 | pico_sdk_init() 59 | 60 | set(CMAKE_C_STANDARD 11) 61 | set(CMAKE_CXX_STANDARD 17) 62 | 63 | if (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0") 64 | message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") 65 | endif() 66 | 67 | add_executable(SwiftPico 68 | main.c 69 | ) 70 | 71 | add_subdirectory(SwiftLib) 72 | 73 | target_link_libraries(SwiftPico 74 | $ 75 | pico_stdlib 76 | ) 77 | 78 | # create map/bin/hex file etc. 79 | pico_add_extra_outputs(SwiftPico) 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This repository is not being maintained. Please don't send issues or pull requests. 3 | 4 | # Embedded Swift on the Raspberry Pi Pico 5 | 6 | A proof of concept for executing Embedded Swift code on the [Raspberry Pi Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/). The main program is written in C (built with the official Pico C SDK) and calls into a statically linked Swift library. 7 | 8 | ## Prerequisites and Installation 9 | 10 | ### Hardware 11 | 12 | - A [Raspberry Pi Pico](https://www.raspberrypi.com/products/raspberry-pi-pico/) 13 | - Optional but recommended: a [Raspberry Pi Debug Probe](https://www.raspberrypi.com/products/debug-probe/) for more convenient flashing (see below). This also enables debugging. 14 | 15 | ### Software 16 | 17 | - Host OS: macOS 13.x or 14.x 18 | 19 | Tested on macOS 13.6.2. It’ll probably work on Linux with minimal modifications to tell CMake how to find the Swift toolchain, but I haven’t tested this. 20 | 21 | - A recent nightly Swift toolchain from [swift.org](https://www.swift.org/download/). Tested with the Xcode toolchain from December 7, 2023. 22 | 23 | - A clone of the [Raspberry Pi Pico C/C++ SDK](https://github.com/raspberrypi/pico-sdk/): 24 | 25 | ```sh 26 | cd .. 27 | git clone https://github.com/raspberrypi/pico-sdk.git 28 | cd pico-sdk/ 29 | git submodule update --init 30 | cd .. 31 | ``` 32 | 33 | This project expects to find the SDK in a sibling directory named `pico-sdk`. You can change this below if your SDK is in a different place. 34 | 35 | - The GCC toolchain for ARM embedded platforms (the Pico SDK builds with GCC by default): 36 | 37 | ```sh 38 | brew install --cask gcc-arm-embedded 39 | ``` 40 | 41 | - [CMake](https://cmake.org/) and [Ninja](https://ninja-build.org/): 42 | 43 | ```sh 44 | brew install cmake ninja 45 | ``` 46 | 47 | The Pico SDK uses CMake as its build system and we’re piggybacking on that. And CMake’s Swift support only works with Ninja. The Swift library is also built with CMake. The unfortunate consequence is that we can’t easily use a [SwiftPM](https://www.swift.org/package-manager/) package for the Swift library as we’d have to tell CMake how to build the package. 48 | 49 | ## Configuration 50 | 51 | Open the file `CMakeLists.txt` in the root folder. Edit these two lines to match your setup: 52 | 53 | ```cmake 54 | … 55 | set(PICO_SDK_PATH "${CMAKE_CURRENT_LIST_DIR}/../pico-sdk") 56 | … 57 | set(Swift_Toolchain "org.swift.59202312071a") 58 | … 59 | ``` 60 | 61 | ## Building 62 | 63 | ```sh 64 | cd pico-embedded-swift 65 | mkdir build 66 | cd build 67 | cmake -G Ninja .. 68 | ninja 69 | ``` 70 | 71 | This produces the executable in the `build` directory in several formats, e.g. `SwiftPico.elf`, ``SwiftPico.uf2`, `SwiftPico.bin`. 72 | 73 | ## Running on the Pico 74 | 75 | You have two options to copy the executable to the Pico: 76 | 77 | 1. Via the USB Mass Storage interface: Connect the Pico to your computer while holding down the BOOTSEL button. When you release the button, the Pico will appear as a removable drive. Now copy `SwiftPico.uf2` to the Pico “drive”. The Pico will automatically reboot and run the program (you can ignore macOS’s “disk not ejected properly” message). 78 | 79 | 2. Via the debug probe. The debug probe is connected to your PC and talks to the Pico via its debug port. This allows you to reflash the Pico without having to disconnect it. 80 | 81 | I use [probe.rs](https://probe.rs/) for this, which is a tool from the Rust community, but it works in this context too. Provided you have [Rust](https://www.rust-lang.org/) installed, you can install probe-rs with `cargo install probe-rs-debugger --features cli`. 82 | 83 | ```sh 84 | probe-rs run --chip RP2040 SwiftPico.elf 85 | ``` 86 | -------------------------------------------------------------------------------- /SwiftLib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(SwiftLib 2 | SwiftLib.swift 3 | ) 4 | -------------------------------------------------------------------------------- /SwiftLib/README.md: -------------------------------------------------------------------------------- 1 | Static Swift library that gets linked into the main C program. Built with CMake, so we can’t easily use a SwiftPM package here. -------------------------------------------------------------------------------- /SwiftLib/SwiftLib.h: -------------------------------------------------------------------------------- 1 | // SwiftLib’s public C API. The main C program uses this to call into SwiftLib. 2 | 3 | #include 4 | #include 5 | 6 | extern uint32_t swiftlib_ledOnDuration(); 7 | extern uint32_t swiftlib_ledOffDuration(); 8 | extern void swiftlib_gpioSet(int32_t pin, bool is_high); -------------------------------------------------------------------------------- /SwiftLib/SwiftLib.swift: -------------------------------------------------------------------------------- 1 | // SIO = the RP2040’s single-cycle I/O block. 2 | // Reference documentation: https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf#tab-registerlist_sio 3 | let SIO_BASE: UInt = 0xd0000000 4 | let SIO_GPIO_OUT_SET_OFFSET: Int = 0x00000014 5 | let SIO_GPIO_OUT_CLR_OFFSET: Int = 0x00000018 6 | 7 | @_cdecl("swiftlib_ledOnDuration") 8 | public func ledOnDuration() -> UInt32 { 9 | 200 10 | } 11 | 12 | @_cdecl("swiftlib_ledOffDuration") 13 | public func ledOffDuration() -> UInt32 { 14 | 400 15 | } 16 | 17 | /// Drive a GPIO output pin high or low. 18 | /// 19 | /// - Warning: The goal of this is to demonstrate that Swift can access the 20 | /// MCU's memory-mapped registers directly. But this isn't safe because Swift 21 | /// doesn't support volatile memory access yet. The actual read and write 22 | /// must happen in C (this is also how 23 | /// [Swift-MMIO](https://github.com/apple/swift-mmio) does it). 24 | @_cdecl("swiftlib_gpioSet") 25 | public func gpioSet(pin: Int32, high: CBool) { 26 | let mask: UInt32 = 1 << pin 27 | let sioBasePtr = UnsafeMutableRawPointer(bitPattern: SIO_BASE)! 28 | if high { 29 | // Volatile memory access, not actually safe in Swift 30 | sioBasePtr.storeBytes(of: mask, toByteOffset: SIO_GPIO_OUT_SET_OFFSET, as: UInt32.self) 31 | } else { 32 | // Volatile memory access, not actually safe in Swift 33 | sioBasePtr.storeBytes(of: mask, toByteOffset: SIO_GPIO_OUT_CLR_OFFSET, as: UInt32.self) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include "pico/stdlib.h" 2 | #include "SwiftLib/SwiftLib.h" 3 | #include 4 | #include 5 | 6 | int main() 7 | { 8 | stdio_init_all(); 9 | printf("main() start\n"); 10 | #ifndef PICO_DEFAULT_LED_PIN 11 | #warning Program requires a board with a regular LED (e.g. *not* the Pico W as its LED is wired to the Wi-Fi chip) 12 | #else 13 | const uint LED_PIN = PICO_DEFAULT_LED_PIN; 14 | gpio_init(LED_PIN); 15 | gpio_set_dir(LED_PIN, GPIO_OUT); 16 | printf("main loop start\n"); 17 | while (true) 18 | { 19 | printf("loop tick\n"); 20 | // Call into SwiftLib 21 | swiftlib_gpioSet(LED_PIN, true); 22 | sleep_ms(swiftlib_ledOnDuration()); 23 | swiftlib_gpioSet(LED_PIN, false); 24 | sleep_ms(swiftlib_ledOffDuration()); 25 | } 26 | #endif 27 | } 28 | -------------------------------------------------------------------------------- /pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | # GIT_SUBMODULES_RECURSE was added in 3.17 33 | if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") 34 | FetchContent_Declare( 35 | pico_sdk 36 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 37 | GIT_TAG master 38 | GIT_SUBMODULES_RECURSE FALSE 39 | ) 40 | else () 41 | FetchContent_Declare( 42 | pico_sdk 43 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 44 | GIT_TAG master 45 | ) 46 | endif () 47 | 48 | if (NOT pico_sdk) 49 | message("Downloading Raspberry Pi Pico SDK") 50 | FetchContent_Populate(pico_sdk) 51 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 52 | endif () 53 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 54 | else () 55 | message(FATAL_ERROR 56 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 57 | ) 58 | endif () 59 | endif () 60 | 61 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 62 | if (NOT EXISTS ${PICO_SDK_PATH}) 63 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 64 | endif () 65 | 66 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 67 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 68 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 69 | endif () 70 | 71 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 72 | 73 | include(${PICO_SDK_INIT_CMAKE_FILE}) 74 | --------------------------------------------------------------------------------