A SwiftPM package that enables seamless Swift development for the Raspberry Pi Pico SDK, targeting the RP2xxx device family with a streamlined developer experience.
This project is licensed under the MIT License.
The easiest way to get started is with the Example project located in the Example/ directory:
-
Clone the repository and navigate to the Example project:
cd Example/ -
Install swiftly and other dependencies if using linux: Linux: First, install required system dependencies:
sudo apt install build-essential libhidapi-hidraw0 libhidapi-libusb0
Linux and macOS: Install Swiftly from swift.org and ensure all dependencies mentioned at the end of the build script are installed.
-
Run the build script for the first time:
bash build.sh
β±οΈ Note: The first run will take a couple of minutes as it downloads all dependencies (Pico SDK, toolchains, and Swift packages).
-
Open in VSCode (optional but recommended): Install the recommended extensions when prompted. This enables full IDE integration with debugging and flashing capabilities.
Once your project is built, you have two options for programming your RP2xxx device:
-
Using cortex-debug (with debug probe): Connect an SWD debug probe (like a Raspberry Pi Debug Probe or Picoprobe) to your device. This enables full debugging with breakpoints, variable inspection, and step-through execution.
-
Using picotool (USB flashing): Hold down the BOOTSEL button while connecting your device via USB (or press it while pressing the reset button). The device will appear as a USB mass storage device, and picotool can directly write the firmware without needing a debug probe.
The Example project includes a small PIO demo based on hello_pio from pico-examples. It ships a hello.pio file and uses the PIOASM SwiftPM plugin to assemble it and expose a Swift helper (hello.program_init). In Example.swift, the PIO program is loaded and a state machine drives GPIO 20 by pushing values into the TX FIFO.
Swift-specific blocks can be embedded directly in a .pio file using a % swift { ... } section. In hello.pio, that block defines a Swift extension on the generated hello namespace with program_init(...), which wires up pin mapping, GPIO init, pin direction, state machine config, and enable. This lets you keep PIO program + Swift setup side by side in a single source file.
The % swift { ... } section is placed after the .program body in hello.pio, so the assembly remains up top and the Swift helper lives at the bottom of the same file.
CPicoSDK follows a versioning scheme that tracks the underlying Pico SDK version:
- Major.Minor versions match the Pico SDK version (e.g.,
2.2) - Patch version is calculated as:
(Pico SDK patch Γ 100) + CPicoSDK revision- This gives us 100 possible revisions for each Pico SDK patch version
- Allows for CPicoSDK-specific updates without changing the base SDK version
Example: When Pico SDK 2.2.1 is released, CPicoSDK will release:
2.2.100- Initial release tracking Pico SDK 2.2.12.2.101-2.2.199- Up to 99 additional CPicoSDK updates for the same base SDK
β οΈ Note: This versioning scheme prioritizes clarity about which Pico SDK version is bundled, but may introduce breaking changes between revisions. Always check release notes when updating.
CPicoSDK brings the power of Swift to embedded development on the RP2xxx microcontroller family. This project enables you to:
- π Use Pico SDK in Swift with full SwiftPM integration
- π§ Develop in VSCode with complete debugging support, SourceKit-LSP autocompletion, and one-click flashing
- π― Target RP2xxx devices with a modern, type-safe language
- π» Cross-platform development on macOS and linux
- β‘ Streamlined workflow with automated environment setup and dependency management
This project is built with the intention of streamlining the developer experience when building for embedded systems, making it as simple as developing any other Swift package.
- β Fully supported: Pico 2 (RP2350A), Pico 2 W (RP2350A + CYW43439), Pimoroni Pico Plus 2 (RP2350B), Pimoroni Pico Plus 2 W (RP2350B + CYW43439)
- π§ In progress: More RP2xxx boards and configuration combinations are actively being developed
- π Debugging: Currently using
cortex-debugin VSCode, following the proven pico-vscode approach. LLDB support is being worked on but requires additional development time.
This matrix is temporary while we work on a more generic approach that ties traits to non-specific board configurations.
| Board | Combination | Variant Trait | Radio Trait |
|---|---|---|---|
| Pico 2 | pico2 |
Variant_RP2350A |
Radio_None |
| Pico 2 W | pico2_w |
Variant_RP2350A |
Radio_CYW43439 |
| Pimoroni Pico Plus 2 | pimoroni_pico_plus2_rp2350 |
Variant_RP2350B |
Radio_None |
| Pimoroni Pico Plus 2 W | pimoroni_pico_plus2_w_rp2350 |
Variant_RP2350B |
Radio_CYW43439 |
- Full Pico SDK Access: Interact with hardware peripherals (GPIO, ADC, I2C, SPI, UART, PWM, DMA, etc.) directly from Swift
- WiFi & Networking: Built-in support for CYW43 wireless chip with lwIP and HTTP client capabilities
- Automated Environment Setup: One-command setup that downloads and configures all dependencies
- Integrated Debugging: Set breakpoints, inspect variables, and step through Swift code on real hardware
- UF2 Generation: Automatic binary generation ready for drag-and-drop flashing
- Build Configurations: Support for Debug, Release, RelWithDebInfo, and MinSizeRel builds
Install Swiftly and you're ready to go! Get it from swift.org
There is no Windows support at the moment. This is mostly due to lack of ways to test the platform. If interested please consider contributing support or opening/+1 an issue requesting it to track and quantify interest.
First, install required system dependencies:
sudo apt install build-essential libhidapi-hidraw0 libhidapi-libusb0Then install Swiftly from swift.org and ensure all dependencies mentioned at the end of the build script are installed.
Note: This project currently uses a very specific Swift version (
main-snapshot-2025-11-03) due to bugs that need resolution in newer versions. This requirement will be relaxed as upstream issues are addressed.
- Create your Swift package that depends on CPicoSDK:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MyPicoProject",
products: [
.library(name: "MyPicoProject", type: .static, targets: ["MyPicoProject"]),
],
dependencies: [
.package(
url: "https://github.com/gonzalolarralde/CPicoSDK", exact: "2.2.2",
traits: [.init(name: "Variant_RP2350A"), .init(name: "Radio_None")] // Pico 2
traits: [.init(name: "Variant_RP2350A"), .init(name: "Radio_CYW43439")] // Pico 2W
),
],
targets: [
.target(
name: "MyPicoProject",
dependencies: ["CPicoSDK"],
plugins: [.plugin(name: "PIOASM", package: "CPicoSDK")]
),
]
)- Write your Swift code:
import CPicoSDK
@main
struct App {
static func main() {
stdio_init_all()
gpio_init(25)
gpio_set_dir(25, true)
while true {
gpio_put(25, true)
sleep_ms(250)
gpio_put(25, false)
sleep_ms(250)
}
}
}- Build and flash (see the Example directory for a complete build. sh script):
./build.sh --flashThe build script will:
- Download and configure the Pico SDK, ARM toolchain, and all dependencies
- Set up VSCode with debugging configurations
- Build your Swift code
- Link it with the Pico SDK
- Generate UF2 and ELF binaries
- Optionally flash to your device
When you run the environment preparation step, CPicoSDK automatically generates:
.vscode/settings.json: VSCode workspace configuration.vscode/launch.json: Debug configurations for cortex-debugbuildServerConfig.json: SourceKit-LSP configuration for code completiontoolset.json: SwiftPM toolchain configuration.swift-version: Swiftly version pinning
This gives you a complete IDE experience with:
- Syntax highlighting and code completion
- Inline error checking
- Hardware debugging with breakpoints
- Variable inspection
- One-click build and flash
This section explains how CPicoSDK works internally and how the various pieces fit together.
CPicoSDK uses a hybrid approach combining SwiftPM plugins for Swift-native tooling with bash scripts for complex build orchestration. This approach balances maintainability with the flexibility needed for embedded cross-compilation workflows.
The env.json file is the central configuration that drives the entire build environment. It specifies:
- Toolchain versions: Swift snapshot, ARM GCC, CMake, Ninja
- SDK versions: Pico SDK, picotool, OpenOCD versions
- Paths: Where to find tools and dependencies (with variable substitution support like
${PICO_SDK_PATH}) - Base configuration: Default board, build type, SwiftPM triple, and base library selection
- Combinations: Per-board overrides and trait mappings (Variant/Radio) used to generate targets
Example:
{
"vars": {
"SWIFT_VERSION": "main-snapshot-2025-11-03",
"SDK_VERSION": "2.2.0",
"TOOLCHAIN_VERSION": "14_2_Rel1",
"BOARD": "pico2",
"IMPORTED_LIBS": "pico_stdlib,hardware_gpio",
"SWIFTPM_TRIPLE": "armv7em-none-none-eabi",
"BUILD_TYPE": "RelWithDebInfo"
},
"combinations": {
"pico2": {
"vars": {
"BOARD": "pico2",
"IMPORTED_LIBS_MORE": ""
},
"traits": ["Variant_RP2350A", "Radio_None"]
},
"pico2_w": {
"vars": {
"BOARD": "pico2_w",
"IMPORTED_LIBS_MORE": "pico_cyw43_arch,pico_cyw43_arch_lwip_poll,pico_lwip,pico_lwip_arch,pico_lwip_http"
},
"traits": ["Variant_RP2350A", "Radio_CYW43439"]
}
}
}This configuration is:
- Read by the PrepareEnvironmentPlugin to set up the environment (base vars + combinations)
- Resolved (variables expanded) and exported as bash environment variables
- Used by GenerateCPicoSDKPlugin to generate per-combination headers and
Package.swift - Consumed by FinalizeBinaryPlugin during the linking stage
The root build.sh script is specifically for CPicoSDK maintainers to regenerate the generated headers and Package.swift. Users consuming CPicoSDK as a dependency don't need to run it. Package.swift is generated from Package.swift.template, so edit the template instead.
The script:
- Cleans generated build artifacts and copies
Package.swift.templatetoPackage.swift - Locates the
swiftlyexecutable - Sets the
PICO_SDK_BUNDLE_PATH(defaults to~/.pico-sdk) - Invokes the
PrepareEnvironmentPluginwith flags to disable most side effects - Sources the generated preparation script to export environment variables
- Calls the
GenerateCPicoSDKPluginto generate per-combination headers and targets
This separation allows the header generation to be version-controlled while keeping the environment setup flexible.
Swift cannot directly consume hundreds of individual C header files efficiently in an embedded context. To solve this, CPicoSDK generates a compound header file per combination (e.g. CPicoSDK_pico2.h) that:
- Aggregates all relevant Pico SDK headers into a single file based on
IMPORTED_LIBS - Processes through the C preprocessor with the correct target architecture flags
- Resolves all macros, includes, and definitions into one cohesive header
- Wraps in a Swift module via a modulemap
Generation process (in Plugins/GenerateCPicoSDKPlugin/GenerateCPicoSDKPlugin.swift + Plugins/GenerateCPicoSDKPluginTool/CMakeHarness):
-
Create a source header that includes all requested Pico SDK headers:
#define __ARM_ARCH_8M_MAIN__ 1 #include <pico/stdlib.h> #include <hardware/gpio.h> // ... all other requested libraries
-
Use CMake to invoke the C preprocessor with the correct target flags
-
Generate
CPicoSDK_<combination>.hwith all macros expanded and definitions resolved -
Wrap it in a modulemap:
module _CPicoSDK_<combination> [system] { umbrella header "include/CPicoSDK_<combination>.h" export * } -
Copy to
Sources/_CPicoSDK_<combination>/where SwiftPM can find it
This approach:
- β Provides fast compilation (single header vs. hundreds)
- β Ensures correct macro expansion for the target architecture
- β Works around SwiftPM limitations with embedded toolchains
- β Requires regeneration when library selection or SDK version changes
Located at Plugins/FinalizeBinaryPluginTool/CMakeHarness/shims.c, this file provides POSIX compatibility shims for functions that the Swift runtime expects but aren't provided by the bare-metal ARM toolchain.
For example:
int posix_memalign(void **memptr, size_t alignment, size_t size) {
// Implementation using memalign from newlib
}These shims are compiled into the final binary during the finalization stage, ensuring Swift's memory management works correctly on bare metal.
CPicoSDK depends on PicoSDKDownloader, a separate package that replicates the pico-vscode model for downloading dependencies.
When you run the PrepareEnvironmentPlugin, it:
- Invokes the
pico-bootstrapcommand from PicoSDKDownloader - Downloads the Pico SDK, ARM toolchain, CMake, Ninja, picotool, and OpenOCD
- Extracts them to
~/.pico-sdk(orPICO_SDK_BUNDLE_PATH) - Verifies checksums and versions
This ensures:
- π Reproducible builds: Everyone gets the same tool versions
- π¦ Zero-configuration: No manual installation required
- π Offline-friendly: Downloaded tools are cached locally
CPicoSDK uses three SwiftPM command plugins to orchestrate the build:
Purpose: Set up the complete build environment
What it does:
- Merges base vars and combination overrides from
env.jsonwith user overrides - Resolves variable substitutions (e.g.,
${HOME},${PICO_SDK_PATH}) - Downloads dependencies via PicoSDKDownloader
- Generates
toolset.jsonfor the ARM cross-compiler - Creates
.swift-versionfor Swiftly - Writes VSCode settings and launch configurations
- Generates
buildServerConfig.jsonfor SourceKit-LSP - Outputs a bash script with exported environment variables and helper functions
Key files:
PrepareEnvironmentPlugin.swift: Main plugin logicEnv.swift: Environment modeling and combination resolutionResolvers.swift: Environment variable resolutionGenerators.swift: Config file generationExtensions.swift: Utilities (JSON parsing, async process execution)
Purpose: Generate per-combination compound headers and Package.swift
What it does:
- Reads
IMPORTED_LIBSplus combination overrides (e.g.IMPORTED_LIBS_MORE) - Creates a source header with the appropriate includes per combination
- Invokes CMake to preprocess the header with target-specific flags
- Wraps the result in a modulemap
- Copies to
Sources/_CPicoSDK_<combination>/ - Generates
Package.swiftfromPackage.swift.templatewith combination targets
Why it exists: SwiftPM doesn't support prebuild commands for header generation, so this must be run manually during development.
Purpose: Link Swift object files with Pico SDK and generate the final binary
What it does:
- Takes the compiled Swift
.ofiles from SwiftPM - Links them with Pico SDK libraries using the CMake harness
- Runs as a native Swift plugin (no standalone shell build script)
- Includes
shims.cfor runtime compatibility - Generates both ELF (for debugging) and UF2 (for flashing) binaries
- Optionally flashes to a connected device
Why it's necessary: SwiftPM can't directly produce firmware binaries - it generates object files that must be linked with the Pico SDK's startup code, linker scripts, and library implementations.
While the plugins are written in Swift for type safety and integration with SwiftPM, many complex operations are delegated to bash scripts:
Why bash?
- β Direct access to CMake, GNU toolchain, and POSIX utilities
- β Easier to debug and iterate during development
- β Familiar to embedded developers
- β Handles complex multi-tool pipelines naturally
Why not all bash?
- β No access to SwiftPM's package graph and dependency information
- β No type safety or structured error handling
- β Harder to generate JSON and structured config files
The hybrid approach lets us:
- Use Swift for orchestration, environment resolution, and file generation
- Use bash for invoking CMake, gcc-arm-none-eabi, picotool, etc.
- Keep
build.shfiles simple and maintainable - Gradually migrate more logic to Swift over time (see TODOs)
CPicoSDK/
βββ Package.swift # Generated package manifest with traits
βββ Package.swift.template # Template used to generate Package.swift
βββ env.json # Central configuration
βββ generator_vars.json # Available hardware options and libraries
βββ toolset.json # Generated by PrepareEnvironmentPlugin
βββ build.sh # Maintainer tool for header + manifest regeneration
βββ Sources/
β βββ _CPicoSDK_pico2/ # Generated compound header (git-tracked)
β β βββ include/CPicoSDK_pico2.h # The compound header
β β βββ module.modulemap # Swift module wrapper
β βββ _CPicoSDK_pico2_w/ # Generated compound header (git-tracked)
β βββ _CPicoSDK_pimoroni_pico_plus2_rp2350/
β βββ _CPicoSDK_pimoroni_pico_plus2_w_rp2350/
β βββ CPicoSDK/ # Swift wrapper target
βββ Plugins/
β βββ PrepareEnvironmentPlugin/
β β βββ PrepareEnvironmentPlugin.swift
β β βββ Env.swift
β β βββ Generators.swift
β β βββ Resolvers.swift
β β βββ Extensions.swift
β β βββ BuildType.swift
β βββ GenerateCPicoSDKPlugin/
β β βββ GenerateCPicoSDKPlugin.swift # Header generation logic
β β βββ Env.swift
β β βββ Extensions.swift
β βββ GenerateCPicoSDKPluginTool/
β β βββ CMakeHarness/
β β βββ CMakeLists.txt # CMake project for preprocessing
β βββ FinalizeBinaryPlugin/
β β βββ FinalizeBinaryPlugin.swift # Linking and UF2 generation
β β βββ Env.swift
β β βββ Extensions.swift
β βββ FinalizeBinaryPluginTool/
β βββ CMakeHarness/
β βββ CMakeLists.txt # CMake project for linking
β βββ shims.c # POSIX compatibility layer
βββ Example/
βββ Package.swift # Example project consuming CPicoSDK
βββ build.sh # User-facing build script
βββ Sources/
βββ Example/
βββ main.swift
For CPicoSDK maintainers:
- Modify
env.json,generator_vars.json, orPackage.swift.template - Run
./build.shto regeneratePackage.swiftand the compound headers - Commit the updated
Package.swiftandSources/_CPicoSDK_*outputs - Tag a new version
For users consuming CPicoSDK:
- Add CPicoSDK as a dependency
- Run
./build.sh(which calls the plugins) - Develop Swift code with full IDE support
- Flash and debug on hardware
See TODO.md for detailed tasks, including:
- Moving more bash logic into Swift plugins
- Supporting multiple boards and configurations via traits
- Migrating from compound header to per-library targets
- LLDB integration for debugging
- Better handling of build types and debug symbols
Contributions are welcome! Key areas of interest:
- π― Adding support for more RP2xxx boards (Pico, Pico W, Pico 2, etc.)
- π Improving LLDB integration for debugging
- β‘ Optimizing the header generation process
- π§ͺ Adding test coverage
This project builds upon the excellent work of:
- The Raspberry Pi Pico SDK team
- The pico-vscode extension developers
- The Swift Embedded community
Happy embedded Swift hacking! π