This is a template project to develop Embedded Swift Packages for the ESP-IDF platform.
Everything that requires the Swift stdlib will not work because I have not yet figured out how to get hold of a embedded riscv32-version of it. This includes:
DictionariesActually, Dictionaries work fine, using Strings as key does not- String Comparison, String Hashing
- Actors
- Swift 6.0+ toolchain (currently only available as snapshot)
- Install swiftly
- Run
swiftly install -u main-snapshot
- The project was confirmed working with
main-snapshot-2024-07-15
(check withswiftly use
)
- Recent installation of ESP-IDF
- Make sure you have access to
idf.py
from the command line (Guide) - The project was confirmed working with
ESP-IDF v5.2.2-dirty
(check withidf.py --version
)
- Make sure you have access to
- A microcontroller with RISC-V architecture supported by ESP-IDF
- The project was confirmed working with
esp32c6
- The project was confirmed working with
- The project was confirmed working with Linux
- Download or clone this repository
- Open a console in the repository folder
- Configure the Project
- Run
idf.py set-target <device>
. Valid values for<device>
can be listed withidf.py --list-targets
. Do note that only RISC-V targets are supported by the swift compiler (and therefore this project)
- Run
- Build the Project
- Run
idf.py build
. This will also set up everything needed by sourcekit-lsp
- Run
- Open
main/
in your preferred code editor. This is where your Swift Package lives - Well... Code, i guess?
- Build the project:
idf.py build
- Upload and monitor the project:
idf.py flash monitor
If you use VSCode and the Swift Extension, everything should work out of the box.
If LSP does not find a module, try rebuilding your project via idf.py build
. If you have imported a Package or created a target with component requirements, make sure they are included in the idf_component.yml
and run idf.py reconfigure
, followed by idf.py build
.
If not, some configuration of sourcekit-lsp is necessary to use the correct target and point it to the correct header files. Additional flags you need to provide to sourcekit-lsp:
-Xswiftc
--target=riscv32-none-none-eabi
-Xswiftc
-enable-experimental-feature
-Xswiftc
Embedded
-Xswiftc
-wmo
: Required by the above-Xcc
-D__riscv
: Needed for some header files to resolve correctly-Xcc
--config=./.build/release/compile_flags.txt
: A clang configuration file holding all include directories of ESP-IDF, created duringidf.py build
To use IDF libraries, create a new Target in your Swift Package, containing a header file which includes the IDF headers you want.
Example: Getting Access to FreeRTOS
- Add new Target to
Package.swift
and adding it as a dependency to all Targets that want to use it.let package = Package( [...] targets: [ // 1. Add Target .target(name: "CFreeRTOS"), .target( name: "Main", // 2. Add Dependency dependencies: ["CFreeRTOS",]), ] [...]
- Add target sources. Add a folder to the
Source
-directory named after the new target. Add ainclude
directory inside that. Add a C header file inside that. Your Source Directory should now look like this:Source +- CFreeRTOS | +- include | +- freertos_includes.h +- Main +- esp_main.swift
- Include all headers you need in the
freertos_includes.h
:#include "freertos/FreeRTOS.h" #include "freeRTOS/task.h"
- Import the target in your swift source file.
esp_main.swift
:// 1. Import target import CFreeRTOS @_cdecl("app_main") func app_main() { print("🏎️+📦 Hello from an Embedded Swift Package") // 2. Use some C functions vTaskDelay(200) }
Do note that some constants are built using C macros. These can not be used directly in Swift. Instead, you need to create a C constant inside your header file for Swift to use. Fortunately, if this happens, the Swift Compiler lets you know gently, that it is unable to use C macros in Swift code.
To build upon the example above, vTaskDelay
takes the amount of ticks to delay, but I want to declare the amount of milliseconds to delay.
-
There is a C Macro constant for converting one to the other:
ticks = milliseconds / portTICK_PERIOD_MS; vTaskDelay(ticks)
-
Assigning the macro value to a constant in
freertos_includes.h
:#include "freertos/FreeRTOS.h" #include "freeRTOS/task.h" const TickType_t freeRTOS_Tick_Period = portTICK_PERIOD_MS;
-
Makes it accessible in our Swift Module
esp_main.swift
:import CFreeRTOS @_cdecl("app_main") func app_main() { print("🏎️+📦 Hello from an Embedded Swift Package") vTaskDelay(200 / freeRTOS_Tick_Period) // waits for 200ms }
If you want to use components from the ESP component Registry, all you have to do is declare them in a idf_component.yml
in your Package root (so main/idf_component.yml
). Instructions on how to populate this file can be found here.
Every time you change idf_component.yml
, you need to run idf.py reconfigure
in the project folder.
Using the component inside swift is similar to using built-in components. Please refer to Interfacing ESP-IDF
If you include a Swift Package that makes use of a non-builtin ESP Component, you must include its dependencies in your idf_component.yml
and run idf.py reconfigure
afterwards. A recompilation via idf.py build
might also be necessary to regenerate LSP files.
Example: Including the Swift-Package swift-led-strip that provides a Swift-Wrapper for espressif/led_strip
Package.swift
:
let package = Package(
[...]
dependencies: [
.package(url: "https://github.com/anders0nmat/swift-led-strip.git", branch: "main"),
],
targets: [
.target(name: "CFreeRTOS"),
.target(
name: "Main",
dependencies: [
"CFreeRTOS",
.product(name: "LedStrip", package: "swift-led-strip"),
]),
[...]
idf_component.yml
:
dependencies:
espressif/led_strip: "^2.4.1"
esp_main.swift
:
import CFreeRTOS
import LedStrip
@_cdecl("app_main")
func app_main() {
let strip = LedStrip(pin: 8, ledCount: 1)
var is_on = false
while true {
is_on.toggle()
switch is_on {
case true:
strip.setPixel(at: 0, to: Color(
red: (0..<64).randomElement()!,
green: (0..<64).randomElement()!,
blue: (0..<64).randomElement()!))
case false:
strip.clear()
}
strip.refresh()
vTaskDelay(500 / freeRTOS_Tick_Period)
}
}
It is a multi-step process, involving many "optional" steps to get sourcekit-lsp to work, but basically:
- Do the usual ESP-IDF build setup, finding includes, resolving components, ...
- Build the Package into a static library
lib<product-name>.a
- Find and extract the objectfile from
lib<product-name>.a
that defines the application entrypointapp_main
. Rename it to something predicatble like_swiftcode.o
- Link
_swiftcode.o
andlib<product-name>.a
to the IDF build - (optional) Export all included header-files to a
compile-flags-txt
for sourcekit-lsp to correctly resolve them
If you are interested in more reading and the process of making this entire thing work, see Story.md