Can I combine an LED and a button on the ESP32C6 with Swift?
This article is part of a series.
- Part 1: Can I program a an ESP32C6 with the esp-idf?
- Part 2: Can I program an ESP32C6 with the Swift example code?
- Part 3: How does the espressif SDK handle inputs?
- Part 4: How does the espressif SDK handle wifi?
- Part 5: This Article
- Part 6: Can I add Wifi to the ESP32C6 project with Swift?
- Part 7: Can I make a Swift-wrapped HTTP GET request from the ESP32C6?
So I wasn’t satisfied with just adding a switch to the existing demo… I had to start making it fancier.
For some reason right about when I switched to writing my own Swift code my esp-idf
install stopped working? It took me awhile to realize to was the tooling, not my code. The idf was handing out error messages about the creation of project_description.json
on ALL projects and idf.py menuconfig
crashed hard in multi-colored glory when called, even on examples that worked previously.
I reinstalled the whole esp-idf again and everything worked fine. I’m not sure if it was a software update overnight on my computer or if my spelunking around in that folder did something. Thank goodness for high speed internet because that was another big download.
After all that I ended up with two more steps of the project done:
- rewriting the base: https://github.com/carlynorama/swift_esp32c6_hello/tree/a_start_with_LED
- adding a switch: https://github.com/carlynorama/swift_esp32c6_hello/tree/b_add_a_switch
Compare to:
- PicoW Version from Last Year: https://github.com/carlynorama/swift-pico-w-hello/tree/main/01-OnboardLED
- PicoW Version from Last Year: https://github.com/carlynorama/swift-pico-w-hello/tree/main/02-Switch
- Base project: https://github.com/swiftlang/swift-embedded-examples/tree/main/esp32-led-blink-sdk
Project Anatomy
I’m going to focus on recapping the project with both the LED and the button working, which has the following files:
|- main
| |- Bool_Int.swift
| |- BridgingHeader.swift
| |- CMakeLists.txt
| |- Delay.swift
| |- DigitalIndicator.swift
| |- MomentaryInput.swift
| |- GPIOPin.swift
| |- Main.swift
| |- idf_component.yml
|
|- CMakeLists.txt
|- .gitignore
All of these files were added by hand to main/CMakeLists.txt
# List of Swift source files to build.
target_sources(${COMPONENT_LIB}
PRIVATE
Main.swift
Bool_Int.swift
GPIOPin.swift
DigitalIndicator.swift
MomentaryInput.swift
Delay.swift
)
Main.swift
Notice that the only call directly backed by the esp32C6 sdk is print
, many SDK’s implement print but it isn’t guaranteed. ESP_LOG
and print
do not behave identically. That’s its own post! What we do have is a DigitalIndicator
type and a MomentaryInput
type. I think it’s possible those would be better as protocols (DigitallyIndicating
and ``DigitalReceiving`?), but for now they’re concrete types.
@_cdecl("app_main")
func main() {
print("Hello from Swift on ESP32-C6!")
guard var led = DigitalIndicator(15) else {
fatalError("Difficulty setting up pin.")
}
guard let button = MomentaryInput(9) else {
fatalError("Difficulty setting up button.")
}
while true {
if button.isActive {
led.blink(millis: 500)
} else {
led.blink(millis: 2000)
}
}
}
DigitalIndicator.swift
Also NO esp32c6 specific code
A DigitalIndicator has some more things defined
OutputPin
: currently concrete implementation of the GPIOPin protocol, GPIOPin.swiftGPIOLevel
a binary enum with cases (.high, .low), GPIOPin.swiftdelay(_:millis:UInt32)
, Delay.swift
struct DigitalIndicator {
let pin:OutputPin
let activeLevel: GPIOLevel
var expectedState: Bool
func set(on:Bool) {
let level = on ? activeLevel.asBool : !activeLevel.asBool
pin.setLevel(levelHigh:level)
}
mutating func turnOn() {
self.set(on: true)
expectedState = true
}
mutating func turnOff() {
self.set(on: false)
expectedState = false
}
mutating func toggle() {
let level: Bool = expectedState ? false : true
self.set(on: level)
expectedState = level
}
mutating func blink(millis:UInt32) {
self.toggle() // Toggle the boolean value
delay(millis)
}
mutating func blink(onMillis:UInt32, offMillis:UInt32) {
self.turnOn()
delay(onMillis)
self.turnOff()
delay(offMillis)
}
}
extension DigitalIndicator {
init?(_ pinNum: UInt32, activeLow:Bool = true) {
self.pin = OutputPin(pinNumber: pinNum, activeLow:activeLow)
self.activeLevel = GPIOLevel(!activeLow)
self.expectedState = false
}
}
Delay.swift
A ESP32C6 specific implementation of a delay function. TODO: why did the demo code use configTICK_RATE_HZ
instead of portTICK_PERIOD_MS
?
func delay(_ millis:UInt32) {
vTaskDelay(millis / (1000 / UInt32(configTICK_RATE_HZ)))
}
GPIOPin.swift:GPIOLevel and Bool_Int.swift
No esp32c6 specific code. But maybe it could. Considering creating a typealias for archInt and archUInt.
These two work to clarify that 1
, true
and .high
all mean the same thing, and to translate C’s 1
/0
returns to true
/false
easily as needed.
//TODO: raw value? Bool or UInt32?
enum GPIOLevel:Equatable {
case high
case low
}
extension GPIOLevel {
init(_ v:Int32) {
if v == 0 {
self = .low
} else if v == 1 {
self = .high
} else {
fatalError("Expected 0 or 1, got \(v)")
}
}
init(_ v:UInt32) {
if v == 0 {
self = .low
} else if v == 1 {
self = .high
} else {
fatalError("Expected 0 or 1, got \(v)")
}
}
init(_ v:Bool) {
if v == false {
self = .low
} else {
self = .high
}
}
var asUInt32:UInt32 {
(self == .high) ? 1 : 0
}
var asInt32:Int32 {
(self == .high) ? 1 : 0
}
var asBool:Bool {
(self == .high) ? true : false
}
}
extension Bool {
init(_ v:UInt32) {
if v == 0 {
self = false
} else if v == 1 {
self = true
} else {
fatalError("Expected 0 or 1, got \(v)")
}
}
init(_ v:Int32) {
if v == 0 {
self = false
} else if v == 1 {
self = true
} else {
fatalError("Expected 0 or 1, got \(v)")
}
}
var asUInt32:UInt32 {
self ? 1 : 0
}
}
GPIOPin.swift:GPIODirection
This has a esp32C6 dynamic var, could be an extension in sub package.
enum GPIODirection {
case input
case output
}
//esp32C6 implementation
extension GPIODirection {
var esp32C6:gpio_mode_t {
switch(self) {
case .input:
return GPIO_MODE_INPUT
case .output:
return GPIO_MODE_OUTPUT
}
}
}
GPIOPin.swift:GPIOPin
GPIOPin is a protocol
protocol GPIOPin {
var pinNumber:UInt32 { get }
var direction: GPIODirection { get }
}
GPIOPin.swift:OutputPin
I decided to make OutputPin a Struct
(esp32c6 specific) rather than a Protocol
for now because I didn’t want to deal with something like DigitalIndicator<ESP32C6OutputPin>(15)
for now. This could also be handled by typaliasing, but I have some questions around how to best use Swift embedded with existentials and generics that I’d like to temporarily side step.
This being a struct is a tad deceiving, It’s backed by C functions that point to specific hardware registers. In a Swift-all-the-way-down implementation I would maybe choose a final Class
or even an Actor
if that becomes a thing one could do?
//MARK: OutputPin
struct OutputPin:GPIOPin {
let pinNumber:UInt32
let direction:GPIODirection = .output
func setLevel(levelHigh:Bool) {
gpio_set_level(gpio_num_t(Int32(pinNumber)), levelHigh.asUInt32)
}
func setLevel(level:GPIOLevel) {
gpio_set_level(gpio_num_t(Int32(pinNumber)), level.asUInt32)
}
}
//esp32C6 init
extension OutputPin {
init(pinNumber:UInt32, activeLow:Bool = true) {
//validate is GPIO output pin.
let ledPin = gpio_num_t(Int32(pinNumber))
self.pinNumber = pinNumber
guard gpio_reset_pin(ledPin) == ESP_OK else {
fatalError("cannot reset output pin \(pinNumber)")
}
guard gpio_set_direction(ledPin, self.direction.esp32C6) == ESP_OK else {
fatalError("cannot reset output pin \(pinNumber)")
}
}
}
GPIOPin.swift:InputPin
ESP32C6 specific implementation, see notes above with OutputPin
struct InputPin:GPIOPin {
let pinNumber:UInt32
let direction:GPIODirection = .input
//If the pad is not configured for input (or input and output) the returned value is always 0.
func readLevel() -> GPIOLevel {
GPIOLevel(gpio_get_level(gpio_num_t(Int32(pinNumber))))
}
}
extension InputPin {
init(pinNumber:UInt32, activeLow:Bool = true, useInternalHardware:Bool = true) {
//validate is GPIO output pin.
let ledPin = gpio_num_t(Int32(pinNumber))
self.pinNumber = pinNumber
guard gpio_reset_pin(ledPin) == ESP_OK else {
fatalError("cannot reset output pin \(pinNumber)")
}
guard gpio_set_direction(ledPin, self.direction.esp32C6) == ESP_OK else {
fatalError("cannot reset output pin \(pinNumber)")
}
//validate pin has hardware
if activeLow && useInternalHardware {
//use gpio_pull_mode_t
gpio_set_pull_mode(ledPin, GPIO_PULLUP_ONLY)
} else if !activeLow && useInternalHardware {
gpio_set_pull_mode(ledPin, GPIO_PULLDOWN_ONLY)
} else {
//could set with GPIO_FLOATING, but for now want to leave it as default
}
}
}
Summary
All of this was pretty easy once I got the the tooling issues sorted. The next batch, getting the WiFi working will require adding a C bridging component. I tried without but the WiFi library uses function like macros, and while there maybe a fancy work around, i’m just going to add a lib.
This article is part of a series.
- Part 1: Can I program a an ESP32C6 with the esp-idf?
- Part 2: Can I program an ESP32C6 with the Swift example code?
- Part 3: How does the espressif SDK handle inputs?
- Part 4: How does the espressif SDK handle wifi?
- Part 5: This Article
- Part 6: Can I add Wifi to the ESP32C6 project with Swift?
- Part 7: Can I make a Swift-wrapped HTTP GET request from the ESP32C6?