|
| 1 | +# Integrating with Zephyr |
| 2 | + |
| 3 | +**⚠️ Embedded Swift is experimental. This document might be out of date with latest development.** |
| 4 | + |
| 5 | +For an introduction and motivation into Embedded Swift, please see "[A Vision for Embedded Swift](https://github.com/swiftlang/swift-evolution/blob/main/visions/embedded-swift.md)", a Swift Evolution document highlighting the main goals and approaches. |
| 6 | + |
| 7 | +The following document outlines how to setup a Swift to Zephyr project for an emulated ARM Cortex M0, explaining a few key concepts along the way. For a complete working example on real hardware, however, refer to the [nrfx-blink-sdk](../../../../nrfx-blink-sdk/) project that is compatible with nRF or other boards. |
| 8 | + |
| 9 | +## Zephyr Target Architecture Compatibility |
| 10 | + |
| 11 | +Zephyr [supports quite a few target architectures](https://docs.zephyrproject.org/latest/introduction/index.html), but not all are supported by Embedded Swift. Please refer to the following table for an overview of Zephyr-supported architectures that are supported by Swift, along with the correct target triple to use: |
| 12 | + |
| 13 | +| Architecture | Details | Swift Triple | |
| 14 | +|--------------|---------------------|-------------------------| |
| 15 | +| ARMv6-M | Cortex M0, M1, M3 | armv6m-none-none-eabi | |
| 16 | +| ARMv7-M | Cortex M4, M7 | armv7em-none-none-eabi | |
| 17 | +| ARMv8-M | Cortex M23-85 | aarch64-none-none-elf | |
| 18 | +| Intel | 32-bit (i686) | i686-unknown-none-elf | |
| 19 | +| Intel | 64-bit (x86_64) | x86_64-unknown-none-elf | |
| 20 | +| RISC-V | 32-bit | riscv32-none-none-eabi | |
| 21 | +| RISC-V | 64-bit | riscv64-none-none-eabi | |
| 22 | + |
| 23 | +## Zephyr Setup |
| 24 | + |
| 25 | +Before setting up a Swift project that works with Zephyr, you need to setup dependencies and a Zephyr workspace as per the [Getting Started Guide](https://docs.zephyrproject.org/latest/develop/getting_started/index.html). Regardless of your platform (macOS or Linux), ensure that you can build the blinky example without errors before starting with Swift integration: |
| 26 | + |
| 27 | +```bash |
| 28 | +cd ~/zephyrproject/zephyr |
| 29 | +west build -p always -b reel_board samples/basic/blinky |
| 30 | +``` |
| 31 | + |
| 32 | +By default, the `main` revision of the Zephyr sources are checked out when calling `west init`, which contains pre-release and development changes that may cause instability and changing APIs that are not desirable. To checkout a specific release version of Zephyr, use the following commands: |
| 33 | + |
| 34 | +```bash |
| 35 | +cd ~/zephyrproject/zephyr |
| 36 | +git checkout v4.1.0 |
| 37 | +west update |
| 38 | +west packages pip --install |
| 39 | + |
| 40 | +# For older versions of Zephyr (pre 4.1.0), use: |
| 41 | +pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt |
| 42 | +``` |
| 43 | + |
| 44 | +Refer to the [Zephyr Releases](https://docs.zephyrproject.org/latest/releases/index.html) page for more information on current and LTS releases. |
| 45 | + |
| 46 | +## Project Setup |
| 47 | + |
| 48 | +Once Zephyr is setup, the next step is to setup a project with the following files included: |
| 49 | + |
| 50 | +```plain |
| 51 | +SwiftZephyrProject/src/BridgingHeader.h |
| 52 | +SwiftZephyrProject/src/Main.swift |
| 53 | +SwiftZephyrProject/src/Stubs.c |
| 54 | +SwiftZephyrProject/CMakeLists.txt |
| 55 | +SwiftZephyrProject/prj.conf |
| 56 | +``` |
| 57 | + |
| 58 | +These are the minimum required files in order to build a Zephyr project. By convention, source files should be placed in the src/ subdirectory, but this is not a hard requirement. Also, `prj.conf` is required even if it is empty, or the project will not build. |
| 59 | + |
| 60 | +Inside of `src/BridgingHeader.h`, add the following content: |
| 61 | + |
| 62 | +```c |
| 63 | +#pragma once |
| 64 | + |
| 65 | +#include <autoconf.h> |
| 66 | +#include <zephyr/kernel.h> |
| 67 | +``` |
| 68 | + |
| 69 | +The `src/Main.swift` file must contain a `static func main()` as follows: |
| 70 | + |
| 71 | +```swift |
| 72 | +@main |
| 73 | +struct Main { |
| 74 | + static func main() { |
| 75 | + print("Hello Zephyr from Swift!") |
| 76 | + |
| 77 | + while true { |
| 78 | + k_msleep(1000) |
| 79 | + print("Loop") |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Since Embedded Swift requires `posix_memalign` to be defined, add the following to `src/Stubs.c` to define it: |
| 86 | + |
| 87 | +```c |
| 88 | +#include <stdlib.h> |
| 89 | +#include <errno.h> |
| 90 | + |
| 91 | +void *aligned_alloc(size_t alignment, size_t size); |
| 92 | + |
| 93 | +// Embedded Swift currently requires posix_memalign, but the C libraries in the |
| 94 | +// Zephyr SDK do not provide it. Let's implement it and forward the calls to |
| 95 | +// aligned_alloc(3). |
| 96 | +int |
| 97 | +posix_memalign(void **memptr, size_t alignment, size_t size) |
| 98 | +{ |
| 99 | + void *p = aligned_alloc(alignment, size); |
| 100 | + if (p) { |
| 101 | + *memptr = p; |
| 102 | + return 0; |
| 103 | + } |
| 104 | + |
| 105 | + return errno; |
| 106 | +} |
| 107 | +``` |
| 108 | +
|
| 109 | +Finally, add the following line to `prj.conf` so that the output of `print()` statements is sent to stdout: |
| 110 | +
|
| 111 | +```conf |
| 112 | +CONFIG_STDOUT_CONSOLE=y |
| 113 | +``` |
| 114 | + |
| 115 | +### CMakeLists.txt Setup |
| 116 | + |
| 117 | +The `CMakeLists.txt` setup is more involved and complex since target, compilation flags, and library linking must be specified for Swift. First, some initial setup and flags: |
| 118 | + |
| 119 | +```cmake |
| 120 | +cmake_minimum_required(VERSION 3.29) |
| 121 | +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) |
| 122 | +
|
| 123 | +# Enable "wmo" as needed by Embedded Swift |
| 124 | +set(CMAKE_Swift_COMPILATION_MODE wholemodule) |
| 125 | +
|
| 126 | +# Create a new project called "SwiftZephyrProject" and enable "Swift" as a supported language |
| 127 | +project(SwiftZephyrProject Swift) |
| 128 | +``` |
| 129 | + |
| 130 | +Next, set the compiler target to the arch you are building for. For this example we use `armv6m-none-none-eabi` which is compatible with the [Cortex-M0](https://docs.zephyrproject.org/latest/boards/qemu/cortex_m0/doc/index.html) which we will compile for in a later step. If you are targeting a different architecture, see the [Zephyr Target Architecture Compatibility](#zephyr-target-architecture-compatibility) to see which target triple to use. |
| 131 | + |
| 132 | +```cmake |
| 133 | +# Use the armv6m-none-none-eabi target triple for Swift |
| 134 | +set(CMAKE_Swift_COMPILER_TARGET armv6m-none-none-eabi) |
| 135 | +``` |
| 136 | + |
| 137 | +After setting the target triple, some additional additional Swift compiler flags need to be defined: |
| 138 | + |
| 139 | +```cmake |
| 140 | +# Set global Swift compiler flags |
| 141 | +add_compile_options( |
| 142 | + # Enable Embedded Swift |
| 143 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-enable-experimental-feature Embedded>" |
| 144 | +
|
| 145 | + # Enable function sections to enable dead code stripping on elf |
| 146 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xfrontend -function-sections>" |
| 147 | +
|
| 148 | + # Disable PIC |
| 149 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -fno-pic>" |
| 150 | +
|
| 151 | + # Disable PIE |
| 152 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -fno-pie>" |
| 153 | +) |
| 154 | +``` |
| 155 | + |
| 156 | +There are quite a few other Zephyr flags that must also be imported in order to get Zephyr include paths and flags such `-mcpu`, `-mfloat-abi`, and so on: |
| 157 | + |
| 158 | +```cmake |
| 159 | +# Import TOOLCHAIN_C_FLAGS from Zephyr as -Xcc flags |
| 160 | +foreach(flag ${TOOLCHAIN_C_FLAGS}) |
| 161 | + # Skip flags that are not known to swiftc |
| 162 | + string(FIND "${flag}" "-imacro" is_imacro) |
| 163 | + string(FIND "${flag}" "-mfp16-format" is_mfp16) |
| 164 | + if(NOT is_imacro EQUAL -1 OR NOT is_mfp16 EQUAL -1) |
| 165 | + continue() |
| 166 | + endif() |
| 167 | +
|
| 168 | + add_compile_options("$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc ${flag}>") |
| 169 | +endforeach() |
| 170 | +``` |
| 171 | + |
| 172 | +Next, add the following block to automatically grab Zephyr compilation flags (such as `-D__ZEPHYR__=1` and `-DKERNEL`) and set them as Swift compiler definitions. This is required to successfully build Swift code that works with Zephyr: |
| 173 | + |
| 174 | +```cmake |
| 175 | +# Add definitions from Zephyr to -Xcc flags |
| 176 | +get_target_property(ZEPHYR_DEFINES zephyr_interface INTERFACE_COMPILE_DEFINITIONS) |
| 177 | +if(ZEPHYR_DEFINES) |
| 178 | + foreach(flag ${ZEPHYR_DEFINES}) |
| 179 | + # Ignore expressions like "$<SOMETHING>" |
| 180 | + string(FIND "${flag}" "$<" start_of_expression) |
| 181 | + if(NOT start_of_expression EQUAL -1) |
| 182 | + continue() |
| 183 | + endif() |
| 184 | +
|
| 185 | + add_compile_options("$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -D${flag}>") |
| 186 | + endforeach() |
| 187 | +endif() |
| 188 | +``` |
| 189 | + |
| 190 | +Finally, setup targets, libraries, and additional compile options: |
| 191 | + |
| 192 | +```cmake |
| 193 | +target_sources(app PRIVATE src/Stubs.c) |
| 194 | +
|
| 195 | +# The Swift code providing "main" needs to be in an OBJECT library (instead of STATIC library) to make sure it actually gets linker. |
| 196 | +# A STATIC library would get dropped from linking because Zephyr provides a default weak empty main definition. |
| 197 | +add_library(app_swift OBJECT src/Main.swift) |
| 198 | +
|
| 199 | +add_dependencies(app_swift syscall_list_h_target) |
| 200 | +target_compile_options(app_swift PRIVATE |
| 201 | + -parse-as-library |
| 202 | +
|
| 203 | + -Osize |
| 204 | +
|
| 205 | + -Xfrontend -disable-stack-protector |
| 206 | +
|
| 207 | + # FIXME: add dependency on BridgingHeader.h |
| 208 | + -import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/src/BridgingHeader.h |
| 209 | +) |
| 210 | +
|
| 211 | +# Copy include paths from C target to Swift target |
| 212 | +target_include_directories(app_swift PRIVATE |
| 213 | + "$<TARGET_PROPERTY:app,INCLUDE_DIRECTORIES>" |
| 214 | +) |
| 215 | +
|
| 216 | +# Link the Swift target into the primary target |
| 217 | +target_link_libraries(app PRIVATE app_swift) |
| 218 | +``` |
| 219 | + |
| 220 | +### Building and Running |
| 221 | + |
| 222 | +To build the project, ensure that the Zephyr workspace is sourced first: |
| 223 | + |
| 224 | +```bash |
| 225 | +source ~/zephyrproject/.venv/bin/activate |
| 226 | +``` |
| 227 | + |
| 228 | +Run the following command to configure the project with CMake: |
| 229 | + |
| 230 | +```console |
| 231 | +(.venv)> cmake -B build -G Ninja -DBOARD=qemu_cortex_m0 -DUSE_CCACHE=0 . |
| 232 | +Loading Zephyr default modules (Zephyr base (cached)). |
| 233 | +... |
| 234 | +-- Configuring done (7.6s) |
| 235 | +-- Generating done (0.2s) |
| 236 | +-- Build files have been written to: ~/SwiftZephyrProject/build |
| 237 | +``` |
| 238 | + |
| 239 | +Then, the project can be built using `cmake --build`: |
| 240 | + |
| 241 | +```console |
| 242 | +(.venv)> cmake --build build |
| 243 | +[1/135] Preparing syscall dependency handling |
| 244 | + |
| 245 | +[2/135] Generating include/generated/zephyr/version.h |
| 246 | +-- Zephyr version: 4.1.0 (~/zephyrproject/zephyr), build: v4.1.0 |
| 247 | +[130/135] Linking C executable zephyr/zephyr_pre0.elf |
| 248 | +~/zephyr-sdk-0.17.0/arm-zephyr-eabi/bin/../lib/gcc/arm-zephyr-eabi/12.2.0/../../../../arm-zephyr-eabi/bin/ld.bfd: warning: orphan section `.swift_modhash' from `app/libapp.a(Main.swift.obj)' being placed in section `.swift_modhash' |
| 249 | +[135/135] Linking C executable zephyr/zephyr.elf |
| 250 | +~/zephyr-sdk-0.17.0/arm-zephyr-eabi/bin/../lib/gcc/arm-zephyr-eabi/12.2.0/../../../../arm-zephyr-eabi/bin/ld.bfd: warning: orphan section `.swift_modhash' from `app/libapp.a(Main.swift.obj)' being placed in section `.swift_modhash' |
| 251 | +Memory region Used Size Region Size %age Used |
| 252 | + FLASH: 14674 B 256 KB 5.60% |
| 253 | + RAM: 4032 B 16 KB 24.61% |
| 254 | + IDT_LIST: 0 GB 32 KB 0.00% |
| 255 | +Generating files from ~/SwiftZephyrProject/build/zephyr/zephyr.elf for board: qemu_cortex_m0 |
| 256 | +``` |
| 257 | + |
| 258 | +Finally, to run the example in the qemu emulator, use `ninja run`: |
| 259 | + |
| 260 | +```console |
| 261 | +(.venv)> ninja -C build run |
| 262 | +ninja: Entering directory `build' |
| 263 | +[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m0 |
| 264 | +*** Booting Zephyr OS build v4.1.0 *** |
| 265 | +Hello Zephyr from Swift! |
| 266 | +Loop |
| 267 | +Loop |
| 268 | +Loop |
| 269 | +``` |
| 270 | + |
| 271 | +Congrats, you now have a Swift project running using Zephyr! |
| 272 | + |
| 273 | +## West Integration |
| 274 | + |
| 275 | +Up to now we have setup a project that works perfectly fine when used with just CMake and Ninja. However, projects can also be integrated with West, which is the official CLI tool used for Zephyr projects. To use `west`, start by adding a `west.yml` file to the root of the project: |
| 276 | + |
| 277 | +```yml |
| 278 | +manifest: |
| 279 | + remotes: |
| 280 | + - name: zephyrproject-rtos |
| 281 | + url-base: https://github.com/zephyrproject-rtos |
| 282 | + |
| 283 | + projects: |
| 284 | + - name: zephyr |
| 285 | + remote: zephyrproject-rtos |
| 286 | + revision: v4.1.0 |
| 287 | + import: |
| 288 | + name-allowlist: |
| 289 | + - cmsis # required by the ARM port |
| 290 | +``` |
| 291 | +
|
| 292 | +It is recommended to set the `revision` to a tagged version of Zephyr instead of always getting the main revision, which could have changing APIs. |
| 293 | + |
| 294 | +Next, set the `ZEPHYR_BASE` environment variable to tell `west` where the Zephyr workspace is located: |
| 295 | + |
| 296 | +```bash |
| 297 | +(.venv)> export ZEPHYR_BASE=~/zephyrproject/zephyr |
| 298 | +``` |
| 299 | + |
| 300 | +This could even be set as a global env variable for the user in `~/.bashrc` or `~/.zshrc` if desired. |
| 301 | + |
| 302 | +With this, `west` commands now can be run instead of having to use `cmake` and `ninja` to build and run: |
| 303 | + |
| 304 | +```bash |
| 305 | +(.venv)> west build -b qemu_cortex_m0 . -p always |
| 306 | +... |
| 307 | +(.venv)> west build -t run |
| 308 | +-- west build: running target run |
| 309 | +[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m0 |
| 310 | +*** Booting Zephyr OS build v4.1.0 *** |
| 311 | +Hello Zephyr from Swift! |
| 312 | +Loop |
| 313 | +Loop |
| 314 | +Loop |
| 315 | +Loop |
| 316 | +``` |
| 317 | + |
| 318 | +This setup may also be desirable since `west flash` is also available and can be used instead of invoking the flashing tools manually. |
| 319 | + |
| 320 | +If compiling a firmware for a real/physical board such as the `nrf52840dk/nrf52840`, `west flash` will work if the SEGGER J-Link host tools are installed. As an example, with the [nrfx-blink-sdk](../../../../nrfx-blink-sdk/) project: |
| 321 | + |
| 322 | +```console |
| 323 | +> cd nrfx-blink-sdk |
| 324 | +> source ~/zephyrproject/.venv/bin/activate |
| 325 | +(.venv)> export ZEPHYR_BASE=~/zephyrproject/zephyr |
| 326 | +(.venv)> west build -b nrf52840dk/nrf52840 . -p always |
| 327 | +... |
| 328 | +(.venv)> west flash -r jlink |
| 329 | +-- west flash: rebuilding |
| 330 | +ninja: no work to do. |
| 331 | +-- west flash: using runner jlink |
| 332 | +-- runners.jlink: reset after flashing requested |
| 333 | +-- runners.jlink: JLink version: 8.26 |
| 334 | +-- runners.jlink: Flashing file: ~/swift-embedded-examples/nrfx-blink-sdk/build/zephyr/zephyr.hex |
| 335 | +``` |
| 336 | + |
| 337 | +The `-r jlink` param is needed for this example to use the J-Link tools instead of using `nrfjprog`, which is the default for this board and also [deprecated](https://www.nordicsemi.com/Products/Development-tools/nRF-Command-Line-Tools). |
0 commit comments