Timers in Swift on nRF52
Introduction
In this post, we’ll continue exploring Embedded Swift on an nRF 52840 dk, using the nRF Connect SDK. Our goal this time is to execute code after a certain delay, with optional periodic repetition.
In Swift, one would typically use a Timer
class to implement such a feature. Can we use that with Embedded Swift?
Foundation
Embedded Swift is really about the Swift language, and the language only includes the Swift runtime and the standard library, both having some limitations in Embedded Swift.
Timer
is not part of those, it is included in Foundation. In the Apple ecosystem, we take Foundation for granted, but really it is a separate library, that’s not part of the language.
Foundation is a complex system, originating over 30 years ago, it evolved from NeXT’s early days. And it’s still going through quite a bit of change, on its way to being fully open sourced and unified across all platforms where Swift is available.
The code is available on GitHub, mainly in 2 repositories:
- swift-corelibs-foundation: The Foundation Project, providing core utilities, internationalization, and OS independence. It is providing the implementation of these functionalities on non Darwin platforms.
- swift-foundation: The Foundation project, a cross platform implementation of several fundamental types and functions.
Info
If you’re interested in knowing more about this split and the structure of Foundation, check out the posts The Future of Foundation and Foundation Package Preview Now Available on the Swift.org Blog as well as the Swift & Interoperability video from this year’s ServerSide.swift Conference.
Let’s look at the code for Timer.swift in swift-corelibs-foundation.
A quick glance already shows that it’s a class inheriting from NSObject
and that it integrates with the CFRunLoop
mechanism.
Taking that code as is and building it with Embedded Swift is clearly not gonna fly, but maybe we can keep the same API and implement it for our platform?
Timers in nRF Connect SDK
Before we do that, we need to understand how timers work in nRF Connect SDK and what API is available.
So let’s check the nRF Connect SDK documentation for the Timers services.
Here are a few key takeaways from reading the documentation:
- Timers trigger after a certain delay (timeout) and optionally repeat with a given period
- A timer is defined using a variable of type
k_timer
- Timers can be defined at compile-time using macros or at run-time
- In the later case, the timer is initialized with the
k_timer_init
function and started withk_timer_start
- Timers can be stopped and restarted, potentially changing their delay and period
- The expiry function code is executed in an interrupt context, its execution time should be kept to a minimum and it should not block; submit a work item to the system work queue if required
Equipped with that knowledge, we can now create our own implementation of the Foundation Timer API.
Re-implementing the Timer API
In addition to the Timer source code, we can also use the Timer Documentation as a guide to know how to implement the API.
Let’s go over the functions and properties defined by the API:
class func scheduledTimer(withTimeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void) -> Timer
Creates a timer and schedules it on the current run loop in the default mode.
✅ Can implement: We can implement that, dynamically creating a timer with k_timer_init
and starting it with k_timer_start
.
class func scheduledTimer(timeInterval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool) -> Timer
Creates a timer and schedules it on the current run loop in the default mode.
❌ Skip: Uses target and selector, an heritage from Objective-C, requiring dynamic runtime lookup of the implementation to call.
class func scheduledTimer(timeInterval: TimeInterval, invocation: NSInvocation, repeats: Bool) -> Timer
Creates a new timer and schedules it on the current run loop in the default mode.
❌ Skip: Uses an NSInvocation
, similar issue to the previous function.
init(timeInterval: TimeInterval, repeats: Bool, block: (Timer) -> Void)
Initializes a timer object with the specified time interval and block.
❌ Skip: Only creates the timers but requires the timer to be added to a CFRunLoop
, which is an API we don’t have / want to implement at this stage.
For the same reasons, we’ll skip the other initializers.
init(timeInterval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool)
init(timeInterval: TimeInterval, invocation: NSInvocation, repeats: Bool)
init(fire: Date, interval: TimeInterval, repeats: Bool, block: (Timer) -> Void)
init(fireAt: Date, interval: TimeInterval, target: Any, selector: Selector, userInfo: Any?, repeats: Bool)
func fire()
Causes the timer’s message to be sent to its target.
✅ Can implement: We can store a closure and call it anytime.
func invalidate()
Stops the timer from ever firing again and requests its removal from its run loop.
✅ Can implement: The k_timer_stop
function will prevent the timer from firing again.
var isValid: Bool
A Boolean value that indicates whether the timer is currently valid.
✅ Can implement: We can manage a state, setting it to invalid when invalidate
is called.
var fireDate: Date
The date at which the timer will fire.
❌ Skip: Date
is also part of Foundation, so we don’t have it by default in Embedded Swift and would need to implement something ourself. That’s something we’ll look at in a later post but let’s skip for now.
var timeInterval: TimeInterval
The timer’s time interval, in seconds.
✅ Can implement: That’s an information we have.
var userInfo: Any?
The receiver’s userInfo
object.
❌ Skip: Embedded Swift does not support existential types, so Any
is a no go.
var tolerance: TimeInterval
The amount of time after the scheduled fire date that the timer may fire.
❌ Skip: Although a Zephyr timer could fire later than expected if the system can’t honour the exact requested timing, we have no information or control over the potential shift.
static func publish(every: TimeInterval, tolerance: TimeInterval?, on: RunLoop, in: RunLoop.Mode, options: RunLoop.SchedulerOptions?) -> Timer.TimerPublisher
Returns a publisher that repeatedly emits the current date on the given interval.
class Timer.TimerPublisher
A publisher that repeatedly emits the current date on a given interval.
❌ Skip: Those 2 are about Combine, which we don’t have under Embedded Swift.
As we can see, we need to skip over a large chunk of the API, not implementing it or providing some stub implementation that does not really behave as its original counterpart.
In those conditions, implementing the same API doesn’t make a lot of sense and could actually prove dangerous as code using our implementation would behave differently than expected.
Our own API
So, instead, let’s implement a Timer
type that provides similar functionalities, but with its own API (but still inspired by the existing Timer API).
We’ll keep the idea of having a static function create and start a timer and the initializer create the timer without starting it. We’ll thus need a method to start the timer.
When creating a timer, we want to pass a delay, an optional period and the code to execute on trigger.
In Zephyr, the expiry function type is defined as
typedef void (*k_timer_expiry_t)(struct k_timer *timer)
so we’ll create the equivalent in Swift
typealias TimerExpiry = @convention(c) (
_ timer: UnsafeMutablePointer<k_timer>?
) -> Void
We can now define our code skeleton
struct Timer {
static func scheduledTimer(delay: UInt32, period: UInt32? = nil, handler: TimerExpiry?) -> Timer {
}
init(delay: UInt32, period: UInt32? = nil, handler: TimerExpiry?) {
}
func start() {
}
}
The initializer is straightforward. We just add properties to remember the delay and period, and a property to store the k_timer
structure; we initialize it with a reference to the code to execute.
struct Timer {
var timer = UnsafeMutablePointer<k_timer>.allocate(capacity: 1)
var delay: UInt32
var period: UInt32?
init(delay: UInt32, period: UInt32? = nil, handler: TimerExpiry?) {
self.delay = delay
self.period = period
k_timer_init(timer, handler, nil)
}
}
Let’s now look at starting the timer, the Zephyr call to do that is
void k_timer_start(struct k_timer *timer, k_timeout_t duration, k_timeout_t period)
The function takes the duration and period parameters as k_timeout_t
structures. Those are typically constructed in C using the macros K_MSEC()
or K_NO_WAIT
and K_FOREVER
.
Trying to use that in our code
func start() {
k_timer_start(timer, K_MSEC(self.delay), K_MSEC(self.period))
}
leads to compilation errors
macro 'K_MSEC' unavailable: function like macros not supported
We’ve already seen this error in previous posts, as the Swift C Interop only deals with simply macros.
Let’s look at the definition of the C macro
#define K_MSEC(ms) Z_TIMEOUT_MS(ms)
which further expands to
# define Z_TIMEOUT_MS(t) Z_TIMEOUT_TICKS((k_ticks_t)k_ms_to_ticks_ceil64(MAX(t, 0)))
and this goes on several levels.
Going through the different definitions, we find out that the macro converts milliseconds to a k_timeout_t
struct with its ticks element initialized. The conversion is based on the CONFIG_SYS_CLOCK_TICKS_PER_SEC
constant and the results is a 64bit Int, rounded up.
With this understanding, we can write the following helper functions in Swift.
private func msToTimeout(_ delay: UInt32) -> k_timeout_t {
k_timeout_t(ticks: msToTick(delay))
}
private func msToTick(_ delay: UInt32) -> Int64 {
return Int64((Double(UInt64(CONFIG_SYS_CLOCK_TICKS_PER_SEC) * UInt64(delay)) / 1000.0).rounded(.up))
}
and implement our start()
function
func start() {
k_timer_start(timer, msToTimeout(self.delay), msToTimeout(self.period == nil ? 0 : self.period!))
}
And finally, we can implement the static function
static func scheduledTimer(delay: UInt32, period: UInt32? = nil, handler: TimerExpiry?) -> Timer {
let t = Timer(delay: delay, period: period, handler: handler)
t.start()
return t
}
Testing
Let’s write a small test program using what we have so far and observe its behaviour.
@main
struct Main {
static func main() {
let _ = Timer.scheduledTimer(delay: 10000) { timer in
print("10 seconds have elapsed")
}
let _ = Timer.scheduledTimer(delay: 5000, period: 2000) { timer in
print("Ticking every 2 seconds")
}
while true {
k_msleep(5000)
}
}
}
Building and flashing this to our board shows the following output
*** Booting nRF Connect SDK v2.7.0-5cb85570ca43 ***
*** Using Zephyr OS v3.6.99-100befc70c74 ***
Ticking every 2 seconds
Ticking every 2 seconds
Ticking every 2 seconds
10 seconds have elapsed
Ticking every 2 seconds
Ticking every 2 seconds
...
Clean-up
The current implementation works but there’s one issue. We initialize the timer property by allocating a k_timer
structure, which is never deallocated.
var timer = UnsafeMutablePointer<k_timer>.allocate(capacity: 1)
That’s a memory leak. To prevent it, we must ensure we deallocate the structure when it’s no longer required.
A good place to do it is in deinit
. For this, we need to make our struct non-copyable (or switch to using a class).
Let’s do that and implement clean-up in the deinit
struct Timer: ~Copyable {
...
deinit {
k_timer_stop(timer)
timer.deallocate()
}
}
in which we stop the underlying Zephyr timer and release the previously allocated memory.
With this change in place, if we run our above example, no message will be printed anymore.
This is because we don’t keep a reference to the created timers and so they get de-initialized.
To make it work again, let’s change the code to
let timer1 = Timer.scheduledTimer(delay: 10000) { timer in
print("10 seconds have elapsed")
}
let timer2 = Timer.scheduledTimer(delay: 5000, period: 2000) { timer in
print("Ticking every 2 seconds")
}
User data
As with our button example from a previous post (Creating a Swift type for button input on nRF52 - Part 2), the closure can not capture its context and we need some work around to access data from it.
And as with the button, it’s possible to associate some context data with the timer.
The k_timer
structure contains a user_data
member; a void pointer to some data we want to pass around with the timer.
We’ll make the Timer
generic for the user data, which can be any reference type
struct Timer<T: AnyObject>: ~Copyable {
add a property to store the user data
var userData: T
and update the initializer to take the user data as parameter and store it in the timer
structure
init(userData: T, delay: UInt32, period: UInt32? = nil, handler: TimerExpiry?) {
self.delay = delay
self.period = period
k_timer_init(timer, handler, nil)
self.userData = userData
timer.pointee.user_data = Unmanaged.passUnretained(userData).toOpaque()
}
As we keep a reference to userData
in a property, we use passUnretained
when storing the pointer in the timer
structure’s user_data
.
We’ll also provide a static function to extract the user data from a timer
static func getUserData(_ timer: UnsafeMutablePointer<k_timer>?) -> T {
return Unmanaged.fromOpaque(timer!.pointee.user_data).takeUnretainedValue()
}
And update the scheduledTimer
convenience method to pass the user data to the initializer
static func scheduledTimer(userData: T, delay: UInt32, period: UInt32? = nil, handler: TimerExpiry?) -> Timer {
let t = Timer(userData: userData, delay: delay, period: period, handler: handler)
t.start()
return t
}
With that in place, we can for instance toggle an LED using a timer, such as in the sample code below (Led is a class in this case).
let led = Led(gpio: &led0)
let timer = Timer<Led>.scheduledTimer(userData: led, delay: 5000, period: 2000) { timer in
Timer<Led>.getUserData(timer).toggle()
}
Work queue
As was mentioned in the beginning of this post and in the original post on button (see Creating a Swift type for button input on nRF52 - Part 1), the closure is executed in an ISR context. The code of that closure should execute fast and never block.
If this is not the case, the typical way to address this is to use work queues and schedule the code as a work item so it gets executed in another thread.
This is a topic we’ll look at in a future post.
Conclusion
In this post, we’ve built a Timer
struct, with an API inspired by (but not compatible with) the native Timer
class from Foundation. It allows us to easily schedule a closure for later execution and optional repetition.
Foundation offers many more classes revolving around times and dates, an area to explore in a future post.
As always, the code from this post is available on GitHub.
If you have any questions, or specific topic you’d like me to cover, feel free to contact me on Mastodon, X or LinkedIn.