|
| 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 Setup |
| 10 | + |
| 11 | +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: |
| 12 | + |
| 13 | +```bash |
| 14 | +cd ~/zephyrproject/zephyr |
| 15 | +west build -p always -b reel_board samples/basic/blinky |
| 16 | +``` |
| 17 | + |
| 18 | +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: |
| 19 | + |
| 20 | +```bash |
| 21 | +cd ~/zephyrproject/zephyr |
| 22 | +git checkout v4.1.0 |
| 23 | +west update |
| 24 | +west packages pip --install |
| 25 | + |
| 26 | +# For older versions of Zephyr (pre 4.1.0), use: |
| 27 | +pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt |
| 28 | +``` |
| 29 | + |
| 30 | +Refer to the [Zephyr Releases](https://docs.zephyrproject.org/latest/releases/index.html) page for more information on current and LTS releases. |
| 31 | + |
| 32 | +## Project Setup |
| 33 | + |
| 34 | +Once Zephyr is setup, the next step is to setup a project with the following files included: |
| 35 | + |
| 36 | +```plain |
| 37 | +SwiftZephyrProject/src/BridgingHeader.h |
| 38 | +SwiftZephyrProject/src/Main.swift |
| 39 | +SwiftZephyrProject/src/Stubs.c |
| 40 | +SwiftZephyrProject/CMakeLists.txt |
| 41 | +SwiftZephyrProject/prj.conf |
| 42 | +``` |
| 43 | + |
| 44 | +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. |
| 45 | + |
| 46 | +Inside of `src/BridgingHeader.h`, add the following content: |
| 47 | + |
| 48 | +```c |
| 49 | +#pragma once |
| 50 | + |
| 51 | +#include <autoconf.h> |
| 52 | +#include <zephyr/kernel.h> |
| 53 | +``` |
| 54 | + |
| 55 | +The `src/Main.swift` file must contain a `static func main()` as follows: |
| 56 | + |
| 57 | +```swift |
| 58 | +@main |
| 59 | +struct Main { |
| 60 | + static func main() { |
| 61 | + print("Hello Zephyr from Swift!") |
| 62 | + |
| 63 | + while true { |
| 64 | + k_msleep(1000) |
| 65 | + print("Loop") |
| 66 | + } |
| 67 | + } |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +Since Embedded Swift requires `posix_memalign` to be defined, add the following to `src/Stubs.c` to define it: |
| 72 | + |
| 73 | +```c |
| 74 | +#include <stdlib.h> |
| 75 | +#include <errno.h> |
| 76 | + |
| 77 | +void *aligned_alloc(size_t alignment, size_t size); |
| 78 | + |
| 79 | +// Embedded Swift currently requires posix_memalign, but the C libraries in the |
| 80 | +// Zephyr SDK do not provide it. Let's implement it and forward the calls to |
| 81 | +// aligned_alloc(3). |
| 82 | +int |
| 83 | +posix_memalign(void **memptr, size_t alignment, size_t size) |
| 84 | +{ |
| 85 | + void *p = aligned_alloc(alignment, size); |
| 86 | + if (p) { |
| 87 | + *memptr = p; |
| 88 | + return 0; |
| 89 | + } |
| 90 | + |
| 91 | + return errno; |
| 92 | +} |
| 93 | +``` |
| 94 | +
|
| 95 | +Finally, add the following line to `prj.conf` so that the output of `print()` statements is sent to stdout: |
| 96 | +
|
| 97 | +```conf |
| 98 | +CONFIG_STDOUT_CONSOLE=y |
| 99 | +``` |
| 100 | + |
| 101 | +### CMakeLists.txt Setup |
| 102 | + |
| 103 | +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: |
| 104 | + |
| 105 | +```cmake |
| 106 | +cmake_minimum_required(VERSION 3.29) |
| 107 | +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) |
| 108 | +
|
| 109 | +# Enable "wmo" as needed by Embedded Swift |
| 110 | +set(CMAKE_Swift_COMPILATION_MODE wholemodule) |
| 111 | +
|
| 112 | +# Create a new project called "SwiftZephyrProject" and enable "Swift" as a supported language |
| 113 | +project(SwiftZephyrProject Swift) |
| 114 | +``` |
| 115 | + |
| 116 | +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. The `mfloat-abi=soft`, `-fshort-enums`, and `-fno-pic` flags are specifically for 32-bit arm architectures, so for other architectures they can be removed. However, the other flags and required for building Swift for Embedded and against Zephyr: |
| 117 | + |
| 118 | +```cmake |
| 119 | +# Use the armv6m-none-none-eabi target triple for Swift |
| 120 | +set(CMAKE_Swift_COMPILER_TARGET armv6m-none-none-eabi) |
| 121 | +
|
| 122 | +# Set global Swift compiler flags |
| 123 | +add_compile_options( |
| 124 | + # Enable Embedded Swift |
| 125 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-enable-experimental-feature Embedded>" |
| 126 | +
|
| 127 | + # Enable function sections to enable dead code stripping on elf |
| 128 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xfrontend -function-sections>" |
| 129 | +
|
| 130 | + # Use software floating point operations matching GCC |
| 131 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -mfloat-abi=soft>" |
| 132 | +
|
| 133 | + # Use compacted C enums matching GCC |
| 134 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -fshort-enums>" |
| 135 | +
|
| 136 | + # Disable PIC |
| 137 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -fno-pic>" |
| 138 | +
|
| 139 | + # Add Libc include paths |
| 140 | + "$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -I -Xcc ${ZEPHYR_SDK_INSTALL_DIR}/arm-zephyr-eabi/picolibc/include>" |
| 141 | +) |
| 142 | +``` |
| 143 | + |
| 144 | +The following block will 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: |
| 145 | + |
| 146 | +```cmake |
| 147 | +# Add definitions from Zephyr to -Xcc flags |
| 148 | +get_target_property(ZEPHYR_DEFINES zephyr_interface INTERFACE_COMPILE_DEFINITIONS) |
| 149 | +if(ZEPHYR_DEFINES) |
| 150 | + foreach(flag ${ZEPHYR_DEFINES}) |
| 151 | + # Ignore expressions like "$<SOMETHING>" |
| 152 | + string(FIND "${flag}" "$<" start_of_expression) |
| 153 | + if(NOT start_of_expression EQUAL -1) |
| 154 | + continue() |
| 155 | + endif() |
| 156 | +
|
| 157 | + add_compile_options("$<$<COMPILE_LANGUAGE:Swift>:SHELL:-Xcc -D${flag}>") |
| 158 | + endforeach() |
| 159 | +endif() |
| 160 | +``` |
| 161 | + |
| 162 | +Finally, setup targets, libraries, and additional compile options: |
| 163 | + |
| 164 | +```cmake |
| 165 | +target_sources(app PRIVATE src/Stubs.c) |
| 166 | +
|
| 167 | +# The Swift code providing "main" needs to be in an OBJECT library (instead of STATIC library) to make sure it actually gets linker. |
| 168 | +# A STATIC library would get dropped from linking because Zephyr provides a default weak empty main definition. |
| 169 | +add_library(app_swift OBJECT src/Main.swift) |
| 170 | +
|
| 171 | +add_dependencies(app_swift syscall_list_h_target) |
| 172 | +target_compile_options(app_swift PRIVATE |
| 173 | + -parse-as-library |
| 174 | +
|
| 175 | + -Osize |
| 176 | +
|
| 177 | + -Xfrontend -disable-stack-protector |
| 178 | +
|
| 179 | + # FIXME: add dependency on BridgingHeader.h |
| 180 | + -import-bridging-header ${CMAKE_CURRENT_LIST_DIR}/src/BridgingHeader.h |
| 181 | +) |
| 182 | +
|
| 183 | +# Copy include paths from C target to Swift target |
| 184 | +target_include_directories(app_swift PRIVATE |
| 185 | + "$<TARGET_PROPERTY:app,INCLUDE_DIRECTORIES>" |
| 186 | +) |
| 187 | +
|
| 188 | +# Link the Swift target into the primary target |
| 189 | +target_link_libraries(app PRIVATE app_swift) |
| 190 | +``` |
| 191 | + |
| 192 | +### Building and Running |
| 193 | + |
| 194 | +To build the project, ensure that the Zephyr workspace is sourced first: |
| 195 | + |
| 196 | +```bash |
| 197 | +source ~/zephyrproject/.venv/bin/activate |
| 198 | +``` |
| 199 | + |
| 200 | +Run the following command to configure the project with CMake: |
| 201 | + |
| 202 | +```console |
| 203 | +(.venv)> cmake -B build -G Ninja -DBOARD=qemu_cortex_m0 -DUSE_CCACHE=0 . |
| 204 | +Loading Zephyr default modules (Zephyr base (cached)). |
| 205 | +... |
| 206 | +-- Configuring done (7.6s) |
| 207 | +-- Generating done (0.2s) |
| 208 | +-- Build files have been written to: ~/SwiftZephyrProject/build |
| 209 | +``` |
| 210 | + |
| 211 | +Then, the project can be built using `cmake --build`: |
| 212 | + |
| 213 | +```console |
| 214 | +(.venv)> cmake --build build |
| 215 | +[1/135] Preparing syscall dependency handling |
| 216 | + |
| 217 | +[2/135] Generating include/generated/zephyr/version.h |
| 218 | +-- Zephyr version: 4.1.0 (~/zephyrproject/zephyr), build: v4.1.0 |
| 219 | +[130/135] Linking C executable zephyr/zephyr_pre0.elf |
| 220 | +~/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' |
| 221 | +[135/135] Linking C executable zephyr/zephyr.elf |
| 222 | +~/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' |
| 223 | +Memory region Used Size Region Size %age Used |
| 224 | + FLASH: 14674 B 256 KB 5.60% |
| 225 | + RAM: 4032 B 16 KB 24.61% |
| 226 | + IDT_LIST: 0 GB 32 KB 0.00% |
| 227 | +Generating files from ~/SwiftZephyrProject/build/zephyr/zephyr.elf for board: qemu_cortex_m0 |
| 228 | +``` |
| 229 | + |
| 230 | +Finally, to run the example in the qemu emulator, use `ninja run`: |
| 231 | + |
| 232 | +```console |
| 233 | +(.venv)> ninja -C build run |
| 234 | +ninja: Entering directory `build' |
| 235 | +[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m0 |
| 236 | +*** Booting Zephyr OS build v4.1.0 *** |
| 237 | +Hello Zephyr from Swift! |
| 238 | +Loop |
| 239 | +Loop |
| 240 | +Loop |
| 241 | +``` |
| 242 | + |
| 243 | +Congrats, you now have a Swift project running using Zephyr! |
| 244 | + |
| 245 | +## West Integration |
| 246 | + |
| 247 | +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: |
| 248 | + |
| 249 | +```yml |
| 250 | +manifest: |
| 251 | + remotes: |
| 252 | + - name: zephyrproject-rtos |
| 253 | + url-base: https://github.com/zephyrproject-rtos |
| 254 | + |
| 255 | + projects: |
| 256 | + - name: zephyr |
| 257 | + remote: zephyrproject-rtos |
| 258 | + revision: v4.1.0 |
| 259 | + import: |
| 260 | + name-allowlist: |
| 261 | + - cmsis # required by the ARM port |
| 262 | +``` |
| 263 | +
|
| 264 | +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. |
| 265 | + |
| 266 | +Next, set the `ZEPHYR_BASE` environment variable to tell `west` where the Zephyr workspace is located: |
| 267 | + |
| 268 | +```bash |
| 269 | +(.venv)> export ZEPHYR_BASE=~/zephyrproject/zephyr |
| 270 | +``` |
| 271 | + |
| 272 | +This could even be set as a global env variable for the user in `~/.bashrc` or `~/.zshrc` if desired. |
| 273 | + |
| 274 | +With this, `west` commands now can be run instead of having to use `cmake` and `ninja` to build and run: |
| 275 | + |
| 276 | +```bash |
| 277 | +(.venv)> west build -b qemu_cortex_m0 . -p always |
| 278 | +... |
| 279 | +(.venv)> west build -t run |
| 280 | +-- west build: running target run |
| 281 | +[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m0 |
| 282 | +*** Booting Zephyr OS build v4.1.0 *** |
| 283 | +Hello Zephyr from Swift! |
| 284 | +Loop |
| 285 | +Loop |
| 286 | +Loop |
| 287 | +Loop |
| 288 | +``` |
| 289 | + |
| 290 | +This setup may also desirable since `west flash` is also available and can be used instead of invoking the flashing tools manually. |
| 291 | + |
| 292 | +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: |
| 293 | + |
| 294 | +```console |
| 295 | +> cd nrfx-blink-sdk |
| 296 | +> source ~/zephyrproject/.venv/bin/activate |
| 297 | +(.venv)> export ZEPHYR_BASE=~/zephyrproject/zephyr |
| 298 | +(.venv)> west build -b nrf52840dk/nrf52840 . -p always |
| 299 | +... |
| 300 | +(.venv)> west flash -r jlink |
| 301 | +-- west flash: rebuilding |
| 302 | +ninja: no work to do. |
| 303 | +-- west flash: using runner jlink |
| 304 | +-- runners.jlink: reset after flashing requested |
| 305 | +-- runners.jlink: JLink version: 8.26 |
| 306 | +-- runners.jlink: Flashing file: ~/swift-embedded-examples/nrfx-blink-sdk/build/zephyr/zephyr.hex |
| 307 | +``` |
| 308 | + |
| 309 | +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