Skip to content

Commit 75a3e14

Browse files
committed
Expand docs: Baremetal setup, update pico integration
1 parent 3cd0f63 commit 75a3e14

File tree

5 files changed

+285
-34
lines changed

5 files changed

+285
-34
lines changed

Sources/EmbeddedSwift/Documentation.docc/GettingStarted/Introduction.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ Regular Swift is not a good fit for small constrained environments like microcon
1717
- Using compile-time specialization (monomorphization) for generic code
1818
- Minimizing dependencies on external libraries
1919

20+
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).
21+
2022
This results in properties that are a great fit for embedded software development:
2123

2224
- **Small binaries** that can be as tiny as a few hundred bytes for "Hello World"-like programs (fully self-contained).
2325
- **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.
26+
- **No hidden allocations** which would cause unpredictable performance cliffs.
2427
- **Full C/C++ interoperability** to directly interact with existing C libraries and hardware-specific code, making it easy to integrate with vendor SDKs.
2528
- **Modern language features** like optionals, generics, and strong type safety are all available in Embedded Swift.
2629
- **Full safety of Swift** is retained in Embedded Swift.

Sources/EmbeddedSwift/Documentation.docc/GettingStarted/LanguageSubset.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ Note that there are no behavior changes in Embedded Swift compared to full Swift
1414
- **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.
1515
- **Not available**: Throwing errors or `any Error` type (in contrast with "typed throws", which *is* supported in Embedded Swift).
1616
- **Not available**: Metatypes, e.g. `let t = SomeClass.Type` or `type(of: value)` are not allowed.
17+
- **Not available**: Standard library types that rely on the above, for example `Codable` and `KeyPath`, are not allowed.
1718
- **Not available**: Printing and stringification of arbitrary types (which is achieved via reflection in desktop Swift).
1819
- **Not available**: Using non-final generic class methods. See <doc:NonFinalGenericMethods> for details on this.
19-
- **Not available**: Weak and unowned references.
20+
- **Not available**: Weak and unowned references are not allowed (unsafe unowned references *are* available).
2021

2122
## Compilation facilities that are not available
2223

Sources/EmbeddedSwift/Documentation.docc/GuidedExamples/PicoGuide.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@ In this guide we'll be targeting a Raspberry Pi Pico as the embedded device that
66

77
## Installing Swift
88

9-
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:
9+
> 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.
1010
11-
```shell
12-
$ export TOOLCHAINS=org.swift.59202405011a
13-
```
11+
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.
1412

15-
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.
13+
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.
1614

1715
## Installing dependencies for embedded development
1816

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

3432
```shell
3533
$ swift --version
36-
Apple Swift version 6.0-dev (LLVM b66077aefd3be08, Swift 84d36181a762913)
34+
Apple Swift version 6.2-dev (LLVM 81ab6d9f7e4810f, Swift 9cc1947527bacea)
3735
$ cmake --version
3836
cmake version 3.29.2
3937
$ echo $PICO_BOARD
Lines changed: 251 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,253 @@
11
# Baremetal use of Embedded Swift
22

3-
🚧 Under construction...
3+
Programming without an SDK for maximum control and minimal size
4+
5+
## Overview
6+
7+
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.
8+
9+
Embedded Swift supports true baremetal development, where you directly program hardware peripherals by manipulating memory-mapped registers. This approach is suitable for:
10+
11+
- Extremely resource-constrained environments
12+
- Safety-critical applications that need deterministic behavior
13+
- Projects requiring full control over every aspect of the hardware
14+
- Educational purposes to understand how hardware and software interact
15+
16+
## Key components of a baremetal project
17+
18+
A complete baremetal project typically includes:
19+
20+
1. **Startup code** - Sets up the initial environment before `main()` runs
21+
2. **Interrupt vector table** - Maps hardware events to handler functions
22+
3. **Linker script** - Defines memory layout and sections
23+
4. **Hardware register definition code** - To interface with peripherals
24+
5. **Runtime support** - E.g. implementations of functions like `memcpy` and `malloc`
25+
6. **Application logic** - Your actual embedded application code
26+
27+
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.
28+
29+
## Hardware access patterns
30+
31+
### 1. Direct memory access using pointers
32+
33+
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.
34+
35+
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++).
36+
37+
```swift
38+
// Accessing a register at address 0x40010000
39+
let gpioBase = 0x40010000
40+
41+
// ❌ Do not do this - the memory write might be optimized out
42+
let gpioDataRegister = UnsafeMutablePointer<UInt32>(bitPattern: gpioBase)!
43+
gpioDataRegister.pointee |= (1 << 5) // Set bit 5
44+
```
45+
46+
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:
47+
48+
```swift
49+
// ✅ Correct approach using volatile operations
50+
@inline(never)
51+
func volatileStore(_ value: UInt32, to address: UInt) {
52+
UnsafeMutablePointer<UInt32>(bitPattern: address)!.pointee = value
53+
}
54+
55+
@inline(never)
56+
func volatileLoad(from address: UInt) -> UInt32 {
57+
return UnsafeMutablePointer<UInt32>(bitPattern: address)!.pointee
58+
}
59+
60+
// Using the volatile operations
61+
let gpioBase = 0x40010000
62+
let currentValue = volatileLoad(from: gpioBase)
63+
volatileStore(currentValue | (1 << 5), to: gpioBase)
64+
```
65+
66+
The `@inline(never)` attribute prevents the compiler from inlining these functions, which helps ensure the memory accesses actually occur.
67+
68+
Consider using Swift MMIO (see below) which uses compiler intrinsics for true volatile semantics and abstracts this problem away from the user.
69+
70+
### 2. Using Swift MMIO for type-safe register access
71+
72+
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:
73+
74+
```swift
75+
// Type-safe register access
76+
gpioa.odr.write { $0.odr5 = true } // Set pin 5 high
77+
```
78+
79+
See [Swift MMIO](https://github.com/apple/swift-mmio/) for details and <doc:STM32BaremetalGuide> for a guided example of using it.
80+
81+
## Creating a linker script and data segment relocation
82+
83+
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.
84+
85+
Besides just defining the position of code at runtime, a linker script needs to also:
86+
- Handle and coordinate the initialization of "zero-fill" global variables (aka the BSS section)
87+
- Handle and coordinate the initialization of non-zero writable global variables (copying initial values from flash to RAM)
88+
89+
Here's an incomplete sketch of an example linker script:
90+
91+
```
92+
MEMORY
93+
{
94+
flash (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
95+
sram (rwx) : ORIGIN = 0x20000000, LENGTH = 320K
96+
}
97+
98+
SECTIONS
99+
{
100+
.text : { *(.vectors*) ; *(.text*) } > flash
101+
.rodata : { *(.rodata*) ; *(.got*) } > flash
102+
.bss : { *(.bss*) } > sram ; needs runtime handling
103+
.data : { *(.data*) } > sram AT>flash ; needs runtime handling
104+
105+
...
106+
}
107+
```
108+
109+
A sketch of an example corresponding startup code (in C):
110+
111+
```c
112+
void ResetISR(void) {
113+
// Initialize bss section
114+
uint8_t *bss = &__bss_start;
115+
while (bss < &__bss_end) *bss = 0;
116+
117+
// Initialize read-write data section
118+
extern uint8_t __data_start_flash, __data_start, __data_end;
119+
uint8_t *src = &__data_start_flash;
120+
uint8_t *dst = &__data_start;
121+
while (dst < &__data_end) *dst++ = *src++;
122+
123+
// Call main
124+
extern int main(void);
125+
main();
126+
127+
// If main returns, loop forever
128+
while(1);
129+
}
130+
```
131+
132+
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.
133+
134+
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.
135+
136+
## Vector table and interrupts
137+
138+
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.
139+
140+
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.
141+
142+
Example vector table structure:
143+
144+
```c
145+
// Vector table for ARM Cortex-M
146+
__attribute__((section(".vectors"))) const void *VectorTable[] = {
147+
(void*)0x20008000, // Initial stack pointer
148+
ResetISR, // Reset handler
149+
DefaultHandler, // NMI handler
150+
DefaultHandler, // Hard fault handler
151+
// Additional vectors as needed
152+
};
153+
```
154+
155+
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:
156+
157+
```c
158+
// In startup.c or header file
159+
void UART1_IRQHandler(void);
160+
```
161+
162+
```swift
163+
// In Swift code
164+
@_cdecl("UART1_IRQHandler")
165+
func uartInterruptHandler() {
166+
// Handle UART interrupt in Swift
167+
// Clear interrupt flags, process received data, etc.
168+
}
169+
```
170+
171+
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.
172+
173+
## Building a minimal project
174+
175+
To build an Embedded Swift baremetal project with SwiftPM, you will need a setup like this:
176+
177+
- Your main application target defined in Package.swift.
178+
- A helper C code helper target defined in Package.swift - this will contain your C startup code, vector table and possibly an assembly file.
179+
- Invoke `swift build` with a `--triple` argument that specifies.
180+
- 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.
181+
182+
Example file structure:
183+
184+
```
185+
MyBaremetalProject/
186+
├── Package.swift
187+
├── toolset.json
188+
├── Sources/
189+
│ ├── MyApp/
190+
│ │ └── main.swift
191+
│ └── CStartup/
192+
│ ├── startup.c
193+
│ ├── linker.ld
194+
│ └── include/
195+
│ └── startup.h
196+
└── README.md
197+
```
198+
199+
Example toolset.json file:
200+
201+
```json
202+
{
203+
"schemaVersion": "1.0",
204+
"swiftCompiler": {
205+
"extraCLIOptions": [
206+
"-enable-experimental-feature", "Embedded",
207+
"-Xclang-linker", "-nostdlib",
208+
]
209+
},
210+
"linker": {
211+
"extraCLIOptions": [
212+
"-T", "Sources/CStartup/linker.ld",
213+
"--gc-sections",
214+
]
215+
}
216+
}
217+
```
218+
219+
Example Package.swift file:
220+
221+
```swift
222+
// swift-tools-version: 5.9
223+
import PackageDescription
224+
225+
let package = Package(
226+
name: "MyBaremetalProject",
227+
products: [
228+
.executable(
229+
name: "MyBaremetalApp",
230+
targets: ["MyApp"]
231+
)
232+
],
233+
targets: [
234+
.executableTarget(
235+
name: "MyApp",
236+
dependencies: ["CStartup"],
237+
swiftSettings: [
238+
.enableExperimentalFeature("Embedded")
239+
]
240+
),
241+
.target(
242+
name: "CStartup",
243+
publicHeadersPath: "include"
244+
)
245+
]
246+
)
247+
```
248+
249+
Example compilation invocation:
250+
251+
```bash
252+
swift build --triple armv7em-none-eabi --toolset toolset.json
253+
```

Sources/EmbeddedSwift/Documentation.docc/SDKSupport/IntegrateWithPico.md

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Development for [Raspberry Pi Pico and Pico W](https://www.raspberrypi.com/produ
1111

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

14-
### CMake setup with a bridging header
14+
## CMake setup with a bridging header
1515

1616
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).
1717

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

53-
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.
53+
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:
5454

5555
```cmake
56-
cmake_minimum_required(VERSION 3.13)
56+
cmake_minimum_required(VERSION 3.29)
5757
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
5858
5959
project(swift-blinky)
6060
pico_sdk_init()
61-
execute_process(COMMAND xcrun -f swiftc OUTPUT_VARIABLE SWIFTC OUTPUT_STRIP_TRAILING_WHITESPACE)
62-
63-
add_executable(swift-blinky)
64-
add_custom_command(
65-
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
66-
COMMAND
67-
${SWIFTC}
68-
-target armv6m-none-none-eabi -Xcc -mfloat-abi=soft -Xcc -fshort-enums
69-
-Xfrontend -function-sections -enable-experimental-feature Embedded -wmo -parse-as-library
70-
$$\( echo '$<TARGET_PROPERTY:swift-blinky,INCLUDE_DIRECTORIES>' | tr '\;' '\\n' | sed -e 's/\\\(.*\\\)/-Xcc -I\\1/g' \)
71-
$$\( echo '${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES}' | tr ' ' '\\n' | sed -e 's/\\\(.*\\\)/-Xcc -I\\1/g' \)
72-
-import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
73-
${CMAKE_CURRENT_LIST_DIR}/Main.swift
74-
-c -o ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
75-
DEPENDS
76-
${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
77-
${CMAKE_CURRENT_LIST_DIR}/Main.swift
78-
)
79-
add_custom_target(swift-blinky-swiftcode DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o)
61+
62+
# Enable Swift language support
63+
enable_language(Swift)
64+
65+
# Set Swift compilation mode to whole module optimization
66+
set(CMAKE_Swift_COMPILATION_MODE wholemodule)
67+
68+
add_executable(swift-blinky Main.swift)
69+
set_target_properties(swift-blinky PROPERTIES
70+
LINKER_LANGUAGE CXX)
71+
72+
target_compile_options(swift-blinky PUBLIC "$<$<COMPILE_LANGUAGE:Swift>:SHELL:
73+
-enable-experimental-feature Embedded
74+
-target armv6m-none-none-eabi -Xcc -mfloat-abi=soft -Xcc -fshort-enums
75+
-Xfrontend -function-sections
76+
-import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/BridgingHeader.h
77+
>")
8078
8179
target_link_libraries(swift-blinky
82-
pico_stdlib hardware_uart hardware_gpio
83-
${CMAKE_CURRENT_BINARY_DIR}/_swiftcode.o
80+
pico_stdlib hardware_uart hardware_gpio
8481
)
85-
add_dependencies(swift-blinky swift-blinky-swiftcode)
82+
8683
pico_add_extra_outputs(swift-blinky)
8784
```
8885

86+
## Configure and build
87+
8988
With these three files, we can now configure and build a Swift firmware for the Pico:
9089

9190
```bash
@@ -94,7 +93,7 @@ $ export PICO_SDK_PATH=<path_to_pico_sdk>
9493
$ export PICO_TOOLCHAIN_PATH=<path_to_arm_toolchain>
9594
$ ls -al
9695
-rw-r--r-- 1 kuba staff 39B Feb 2 22:08 BridgingHeader.h
97-
-rw-r--r-- 1 kuba staff 1.3K Feb 2 22:08 CMakeLists.txt
96+
-rw-r--r-- 1 kuba staff 650B Feb 2 22:08 CMakeLists.txt
9897
-rw-r--r-- 1 kuba staff 262B Feb 2 22:08 Main.swift
9998
$ mkdir build
10099
$ cd build

0 commit comments

Comments
 (0)