Bokeh

Making sense of the mess in my head

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 with k_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.