Skip to content

Expand docs: Baremetal setup, update pico integration #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ Regular Swift is not a good fit for small constrained environments like microcon
- Using compile-time specialization (monomorphization) for generic code
- Minimizing dependencies on external libraries

It's also a good mental model to think of the Swift compiler in Embedded Swift mode as operating on a way a *traditional C compiler* does — specifically in the sense that the compiler produces an object file that does not call into or depend on symbols that are not explicitly used in the source code. This is achieved even for code that uses generics, protocols, tuples, arrays, and more — all the higher-level language features are "compiled out" (e.g. generics are specialized), and standard library code is pulled into the object file as needed (e.g. array implementation).

This results in properties that are a great fit for embedded software development:

- **Small binaries** that can be as tiny as a few hundred bytes for "Hello World"-like programs (fully self-contained).
- **No hidden runtime costs** – Embedded Swift's runtime library does not manage any data structures behind your back, is itself less than a kilobyte in size, and it eligible to be removed if unused.
- **No hidden allocations** which would cause unpredictable performance cliffs.
- **Full C/C++ interoperability** to directly interact with existing C libraries and hardware-specific code, making it easy to integrate with vendor SDKs.
- **Modern language features** like optionals, generics, and strong type safety are all available in Embedded Swift.
- **Full safety of Swift** is retained in Embedded Swift.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ Note that there are no behavior changes in Embedded Swift compared to full Swift
- **Not available**: Values of protocol types ("existentials"), unless the protocol is restricted to be class-bound (derived from AnyObject). E.g. `let a: Hashable = ...` is not allowed. `Any` is also not allowed. See <doc:Existentials> for details and alternatives of existentials.
- **Not available**: Throwing errors or `any Error` type (in contrast with "typed throws", which *is* supported in Embedded Swift).
- **Not available**: Metatypes, e.g. `let t = SomeClass.Type` or `type(of: value)` are not allowed.
- **Not available**: Standard library types that rely on the above, for example `Codable` and `KeyPath`, are not allowed.
- **Not available**: Printing and stringification of arbitrary types (which is achieved via reflection in desktop Swift).
- **Not available**: Using non-final generic class methods. See <doc:NonFinalGenericMethods> for details on this.
- **Not available**: Weak and unowned references.
- **Not available**: Weak and unowned references are not allowed (unsafe unowned references *are* available).

## Compilation facilities that are not available

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ In this guide we'll be targeting a Raspberry Pi Pico as the embedded device that

## Installing Swift

If you don’t have Swift installed, [install it first](https://www.swift.org/install). Because Embedded Swift is experimental and only available in preview toolchains, make sure to install the "Development Snapshot" toolchain (main) instead of a release toolchain (6.0). If you're using a macOS machine, you will need to make sure the installed toolchain is selected as active e.g. by exporting the `TOOLCHAINS` environment variable:
> Warning: Embedded Swift is experimental. Use the latest downloadable 'Trunk Development' snapshot from swift.org to use Embedded Swift. Public releases of Swift do not yet support Embedded Swift.

```shell
$ export TOOLCHAINS=org.swift.59202405011a
```
To install Swift for embedded development, follow the instructions in <doc:InstallEmbeddedSwift>, which guides you through using `swiftly` to install the latest development snapshot with Embedded Swift support.

To test that you have Swift installed, run `swift --version` from your shell or terminal app. It should say "6.0-dev", meaning you have a "Development Snapshot" toolchain.
To test that you have Swift installed, run `swift --version` from your shell or terminal app. It should say "6.2-dev" or similar, confirming you have a "Development Snapshot" toolchain.

## Installing dependencies for embedded development

Expand All @@ -33,7 +31,7 @@ To test that you have all the necessary parts installed, you can run the followi

```shell
$ swift --version
Apple Swift version 6.0-dev (LLVM b66077aefd3be08, Swift 84d36181a762913)
Apple Swift version 6.2-dev (LLVM 81ab6d9f7e4810f, Swift 9cc1947527bacea)
$ cmake --version
cmake version 3.29.2
$ echo $PICO_BOARD
Expand Down
252 changes: 251 additions & 1 deletion Sources/EmbeddedSwift/Documentation.docc/SDKSupport/Baremetal.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,253 @@
# Baremetal use of Embedded Swift

🚧 Under construction...
Programming without an SDK for maximum control and minimal size

## Overview

Developing in "baremetal mode" means programming directly for the hardware without any operating system or SDK abstractions. This provides maximum control and minimum codesize, but requires deeper understanding of the hardware.

Embedded Swift supports true baremetal development, where you directly program hardware peripherals by manipulating memory-mapped registers. This approach is suitable for:

- Extremely resource-constrained environments
- Safety-critical applications that need deterministic behavior
- Projects requiring full control over every aspect of the hardware
- Educational purposes to understand how hardware and software interact

## Key components of a baremetal project

A complete baremetal project typically includes:

1. **Startup code** - Sets up the initial environment before `main()` runs
2. **Interrupt vector table** - Maps hardware events to handler functions
3. **Linker script** - Defines memory layout and sections
4. **Hardware register definition code** - To interface with peripherals
5. **Runtime support** - E.g. implementations of functions like `memcpy` and `malloc`
6. **Application logic** - Your actual embedded application code

For a full working example of all these components, see <doc:STM32BaremetalGuide>. The rest of this document provides general platform-independent guidance when working in baremetal mode. However, much of the problem space of baremetal development is outside of the scope of this documentation, and requires deeper familiary with your specific setup. This information is typically provided by your board vendor, the spec of the MCU, the ISA spec of the execution core, the C toolchain documentation, ELF file format spec, and other similar sources.

## Hardware access patterns

### 1. Direct memory access using pointers

Note that using UnsafePointers to directly access registers at known addresses is not recommended in almost any situation because doing that correctly is tricky, it's inherently unsafe (and shifts the safety responsibility to the user) and can easily cause very hard to debug runtime problems. However, sometimes it might be neccessary to use this method.

One common issue when directly accessing hardware registers is that the compiler may optimize away repeated reads or writes, thinking they're redundant. This is a problem that's inherent to pointers in most programming languages (including C and C++).

```swift
// Accessing a register at address 0x40010000
let gpioBase = 0x40010000

// ❌ Do not do this - the memory write might be optimized out
let gpioDataRegister = UnsafeMutablePointer<UInt32>(bitPattern: gpioBase)!
gpioDataRegister.pointee |= (1 << 5) // Set bit 5
```

Hardware registers are volatile - their values can change independently of your program's execution (due to hardware events, interrupts, or peripheral operation). To ensure correct behavior, you must inform the compiler that these memory locations are volatile, preventing unwanted optimizations:

```swift
// ✅ Correct approach using volatile operations
@inline(never)
func volatileStore(_ value: UInt32, to address: UInt) {
UnsafeMutablePointer<UInt32>(bitPattern: address)!.pointee = value
}

@inline(never)
func volatileLoad(from address: UInt) -> UInt32 {
return UnsafeMutablePointer<UInt32>(bitPattern: address)!.pointee
}

// Using the volatile operations
let gpioBase = 0x40010000
let currentValue = volatileLoad(from: gpioBase)
volatileStore(currentValue | (1 << 5), to: gpioBase)
```

The `@inline(never)` attribute prevents the compiler from inlining these functions, which helps ensure the memory accesses actually occur.

Consider using Swift MMIO (see below) which uses compiler intrinsics for true volatile semantics and abstracts this problem away from the user.

### 2. Using Swift MMIO for type-safe register access

Swift MMIO provides strongly-typed access to memory-mapped hardware and can automatically generate register definitions from SVD files. It can provide a higher-level type-safe access to hardware registers, for example:

```swift
// Type-safe register access
gpioa.odr.write { $0.odr5 = true } // Set pin 5 high
```

See [Swift MMIO](https://github.com/apple/swift-mmio/) for details and <doc:STM32BaremetalGuide> for a guided example of using it.

## Creating a linker script and data segment relocation

A baremetal project requires a custom linker script to properly place code and data in memory. This is a relatively complex task to get right, and requires understanding of the memory map, flash and RAM setup of your target device, as well as understanding the ELF file format and what code/data sections do you expect your entire program to use.

Besides just defining the position of code at runtime, a linker script needs to also:
- Handle and coordinate the initialization of "zero-fill" global variables (aka the BSS section)
- Handle and coordinate the initialization of non-zero writable global variables (copying initial values from flash to RAM)

Here's an incomplete sketch of an example linker script:

```
MEMORY
{
flash (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
sram (rwx) : ORIGIN = 0x20000000, LENGTH = 320K
}

SECTIONS
{
.text : { *(.vectors*) ; *(.text*) } > flash
.rodata : { *(.rodata*) ; *(.got*) } > flash
.bss : { *(.bss*) } > sram ; needs runtime handling
.data : { *(.data*) } > sram AT>flash ; needs runtime handling

...
}
```

A sketch of an example corresponding startup code (in C):

```c
void ResetISR(void) {
// Initialize bss section
uint8_t *bss = &__bss_start;
while (bss < &__bss_end) *bss = 0;

// Initialize read-write data section
extern uint8_t __data_start_flash, __data_start, __data_end;
uint8_t *src = &__data_start_flash;
uint8_t *dst = &__data_start;
while (dst < &__data_end) *dst++ = *src++;

// Call main
extern int main(void);
main();

// If main returns, loop forever
while(1);
}
```

Both these code snippets are not fully functional, they are only demonstrating the complexity of what the linker script and startup code need to do to initialize handle global variables.

Tip: If this handling is not done correctly, a typical symptom is that global variables "don't work", i.e. reading from them doesn't yield the right value, and writing to them doesn't persist. A good way to double check this is by using a debugging a dumping memory at runtime and checking if it matches the virtual memory layout of the ELF file.

## Vector table and interrupts

The vector table is a critical component that maps hardware interrupts and exceptions to specific handler functions in your code. It's typically placed at the beginning of flash memory and contains function pointers that the processor uses when responding to various events.

The processor automatically jumps to the appropriate handler when an interrupt occurs by indexing into this table. If you don't provide a specific handler, it's common to point to a default handler that can help with debugging.

Example vector table structure:

```c
// Vector table for ARM Cortex-M
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a todo or gh issue to migrate these to InlineArray?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's realistic to migrate to InlineArray today -- I think the blocker is @section being able to handle a not-completely-straightforward array, because the vector table also needs the initial SP (which is not a fptr). I can certainly file a GH issue to track this.

__attribute__((section(".vectors"))) const void *VectorTable[] = {
(void*)0x20008000, // Initial stack pointer
ResetISR, // Reset handler
DefaultHandler, // NMI handler
DefaultHandler, // Hard fault handler
// Additional vectors as needed
};
```

If you want to actually handle an interrupt (e.g. a GPIO or UART interrupt) in your Swift code, you can forward declare the function in C, and define it using `@cdecl` in Swift:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using InlineArray here would ease this learning curve by not requiring forward declarations.


```c
// In startup.c or header file
void UART1_IRQHandler(void);
```

```swift
// In Swift code
@_cdecl("UART1_IRQHandler")
func uartInterruptHandler() {
// Handle UART interrupt in Swift
// Clear interrupt flags, process received data, etc.
}
```

However, note that Swift currently does not provide any form of synchronization or "interrupt safety" for the code that executes the interrupt. Namely, if your interrupt handler modifies global variables that are also accessed by your main program, you need to be careful about data races and ensure proper synchronization (such as using atomic operations or disabling interrupts during critical sections). Additionally, interrupt handlers should be kept short and fast to avoid blocking other important system events.

## Building a minimal project
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty great, but likely should move the to build-system support section


To build an Embedded Swift baremetal project with SwiftPM, you will need a setup like this:

- Your main application target defined in Package.swift.
- A helper C code helper target defined in Package.swift - this will contain your C startup code, vector table and possibly an assembly file.
- Invoke `swift build` with a `--triple` argument that specifies.
- Use a `toolset.json` file that defines the common Swift and C compilation flags, and linking flags. This will e.g. enable the Embedded Swift mode when compiling Swift code, and point the linker at the right linker script.

Example file structure:

```
MyBaremetalProject/
├── Package.swift
├── toolset.json
├── Sources/
│ ├── MyApp/
│ │ └── main.swift
│ └── CStartup/
│ ├── startup.c
│ ├── linker.ld
│ └── include/
│ └── startup.h
└── README.md
```

Example toolset.json file:

```json
{
"schemaVersion": "1.0",
"swiftCompiler": {
"extraCLIOptions": [
"-enable-experimental-feature", "Embedded",
"-Xclang-linker", "-nostdlib",
]
},
"linker": {
"extraCLIOptions": [
"-T", "Sources/CStartup/linker.ld",
"--gc-sections",
]
}
}
```

Example Package.swift file:

```swift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "MyBaremetalProject",
products: [
.executable(
name: "MyBaremetalApp",
targets: ["MyApp"]
)
],
targets: [
.executableTarget(
name: "MyApp",
dependencies: ["CStartup"],
swiftSettings: [
.enableExperimentalFeature("Embedded")
]
),
.target(
name: "CStartup",
publicHeadersPath: "include"
)
]
)
```

Example compilation invocation:

```bash
swift build --triple armv7em-none-eabi --toolset toolset.json
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Development for [Raspberry Pi Pico and Pico W](https://www.raspberrypi.com/produ

Before trying to use Swift with the Pico SDK, make sure your environment works and can build the provided C/C++ sample projects.

### CMake setup with a bridging header
## CMake setup with a bridging header

The Pico SDK is using CMake as its build system, and so the simplest way to integrate with it is to also use CMake to build a Swift firmware application on top of the SDK and the libraries from it. The following describes an example set up of that on a "blinky" example (code that just blinks the built-in LED).

Expand Down Expand Up @@ -50,42 +50,41 @@ Notice that we're using functions and variables defined in C in the Pico SDK. Fo
#include "pico/stdlib.h"
```

Finally, we need to define the application's build rules in CMake that will be using CMake logic from the Pico SDK. The following content of `CMakeLists.txt` shows how to *manually call swiftc, the Swift compiler* instead of using the recently added CMake native support for Swift, so that we can see the full Swift compilation command.
Finally, we need to define the application's build rules in CMake that will be using CMake logic from the Pico SDK. The following content of `CMakeLists.txt` leverages CMake 3.29's native Swift language support:

```cmake
cmake_minimum_required(VERSION 3.13)
cmake_minimum_required(VERSION 3.29)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)

project(swift-blinky)
pico_sdk_init()
execute_process(COMMAND xcrun -f swiftc OUTPUT_VARIABLE SWIFTC OUTPUT_STRIP_TRAILING_WHITESPACE)

add_executable(swift-blinky)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
COMMAND
${SWIFTC}
-target armv6m-none-none-eabi -Xcc -mfloat-abi=soft -Xcc -fshort-enums
-Xfrontend -function-sections -enable-experimental-feature Embedded -wmo -parse-as-library
$$\( echo '$<TARGET_PROPERTY:swift-blinky,INCLUDE_DIRECTORIES>' | tr '\;' '\\n' | sed -e 's/\\\(.*\\\)/-Xcc -I\\1/g' \)
$$\( echo '${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}' | tr ' ' '\\n' | sed -e 's/\\\(.*\\\)/-Xcc -I\\1/g' \)
-import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
${CMAKE_CURRENT_LIST_DIR}/Main.swift
-c -o ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
DEPENDS
${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
${CMAKE_CURRENT_LIST_DIR}/Main.swift
)
add_custom_target(swift-blinky-swiftcode DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o)

# Enable Swift language support
enable_language(Swift)

# Set Swift compilation mode to whole module optimization
set(CMAKE_Swift_COMPILATION_MODE wholemodule)

add_executable(swift-blinky Main.swift)
set_target_properties(swift-blinky PROPERTIES
LINKER_LANGUAGE CXX)

target_compile_options(swift-blinky PUBLIC "$<$<COMPILE_LANGUAGE:Swift>:SHELL:
-enable-experimental-feature Embedded
-target armv6m-none-none-eabi -Xcc -mfloat-abi=soft -Xcc -fshort-enums
-Xfrontend -function-sections
-import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
>")

target_link_libraries(swift-blinky
pico_stdlib hardware_uart hardware_gpio
${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
pico_stdlib hardware_uart hardware_gpio
)
add_dependencies(swift-blinky swift-blinky-swiftcode)

pico_add_extra_outputs(swift-blinky)
```

## Configure and build

With these three files, we can now configure and build a Swift firmware for the Pico:

```bash
Expand All @@ -94,7 +93,7 @@ $ export PICO_SDK_PATH=<path_to_pico_sdk>
$ export PICO_TOOLCHAIN_PATH=<path_to_arm_toolchain>
$ ls -al
-rw-r--r-- 1 kuba staff 39B Feb 2 22:08 BridgingHeader.h
-rw-r--r-- 1 kuba staff 1.3K Feb 2 22:08 CMakeLists.txt
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

-rw-r--r-- 1 kuba staff 650B Feb 2 22:08 CMakeLists.txt
-rw-r--r-- 1 kuba staff 262B Feb 2 22:08 Main.swift
$ mkdir build
$ cd build
Expand Down
Loading