Bokeh

Making sense of the mess in my head

Creating a Swift type for button input on nRF52 - Part 1

Introduction

In my previous post, Controlling an LED using Embedded Swift on nRF52, we created a Swift struct to encapsulate the C code required to control an LED. In this post, we’ll do the same for a button.

If you want to follow along, the code is available on GitHub with each step having its own commit.

Starting from working code

As a starting point, we will write the whole code in C. This code is based on the Lesson 2 - Exercise 2 sample code from the nRF Connect SDK Fundamentals course from the Nordic Developer Academy.

In the button.c file, we define a single function that performs the whole setup.

#include <stdio.h>
#include <zephyr/drivers/gpio.h>
#include "button.h"

#define SW0_NODE DT_ALIAS(sw0)
static struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);

static struct gpio_callback pin_cb_data;

void setupButton() {
  int ret;

  if (!gpio_is_ready_dt(&button)) {
    return;
  }

  ret = gpio_pin_configure_dt(&button, GPIO_INPUT);
  if (ret < 0) {
    return;
  }

  gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);

  void pin_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) {
    printk("Button pressed\n");
  }

  gpio_init_callback(&pin_cb_data, pin_isr, BIT(button.pin));

  gpio_add_callback(button.port, &pin_cb_data);
}

The button.h file only contains the prototype of the setup function (and a single include that is required by both the C and Swift code).

Info

In C, a function prototype defines the function name, its parameter(s) and return type.
It’s very similar to a method signature in Swift.

#include <zephyr/kernel.h>

void setupButton();

And the BridgingHeader.h file makes it all visible to our Swift code.

#include <autoconf.h>

#include "button.h"

All the Swift code has to do for now, is call that single function.

@main
struct Main {
  static func main() {
    setupButton()
    while true {
      k_msleep(100)
    }
  }
}

Try building and running the code on your hardware at this point. Do you see the expected message in your console when the button is pressed?

Chi va piano va sano

Let’s follow that Italian saying (that translates to “Slow and steady wins the race” ) and begin transforming each section into Swift, step by step; making sure the code compiles and runs at each step.

We first start by moving the static variable referencing the GPIO used for the button directly to the BridgingHeader.h file.

#include <autoconf.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>

#define SW0_NODE DT_ALIAS(sw0)
static struct gpio_dt_spec button = GPIO_DT_SPEC_GET(SW0_NODE, gpios);

And then we check the GPIO is ready. This is similar to what we did for the LED, so it works as expected.

  static func main() {
    guard gpio_is_ready_dt(&button) else { return }

    while true {
      k_msleep(100)
    }
  }

Next, we configure the pin, similar to the LED configuration, but this time as an input.

  static func main() {
    guard gpio_is_ready_dt(&button) else { return }

    let ret = gpio_pin_configure_dt(&button, GPIO_INPUT)
    guard ret == 0 else { return }

    while true {
      k_msleep(100)
    }
  }

The next step is to configure the pin so it triggers when it transitions to an active state.

  static func main() {
    guard gpio_is_ready_dt(&button) else { return }

    let ret = gpio_pin_configure_dt(&button, GPIO_INPUT)
    guard ret == 0 else { return }

    gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE)

    while true {
      k_msleep(100)
    }
  }

and this causes a build error

…/EmbeddedSwift-nRF52-Examples/Button/Main.swift:18:50: error: cannot find 'GPIO_INT_EDGE_TO_ACTIVE' in scope
16 |       let ret = gpio_pin_configure_dt(&button, GPIO_INPUT)
17 |       if ret == 0 {
18 |         gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE)
   |                                                  `- error: cannot find 'GPIO_INT_EDGE_TO_ACTIVE' in scope
19 |       } else {
20 |         print("Can't configure the GPIO as input")

/opt/nordic/ncs/v2.7.0/zephyr/include/zephyr/drivers/gpio.h:202:9: note: macro 'GPIO_INT_EDGE_TO_ACTIVE' unavailable: structure not supported
 200 |  * level 1 and enables it.
 201 |  */
 202 | #define GPIO_INT_EDGE_TO_ACTIVE        (GPIO_INT_ENABLE | \
     |         `- note: macro 'GPIO_INT_EDGE_TO_ACTIVE' unavailable: structure not supported
 203 |                                  GPIO_INT_LEVELS_LOGICAL | \
 204 |                                  GPIO_INT_EDGE | \
ninja: build stopped: subcommand failed.

The Swift compiler is chocking on the GPIO_INT_EDGE_TO_ACTIVE macro, let’s check its definition.

/** Configures GPIO interrupt to be triggered on pin state change to logical
 * level 1 and enables it.
 */
#define GPIO_INT_EDGE_TO_ACTIVE        (GPIO_INT_ENABLE | \
					GPIO_INT_LEVELS_LOGICAL | \
					GPIO_INT_EDGE | \
					GPIO_INT_HIGH_1)

Looking at the Swift C Interop documentation from Apple, Using Imported C Macros in Swift confirms that only simple macros are supported.

This macro is just defining a constant, something we can easily redo in a more Swifty way.

enum GpioInterrupts {
  static let edgeToActive = GPIO_INT_ENABLE | GPIO_INT_LEVELS_LOGICAL | GPIO_INT_EDGE | GPIO_INT_HIGH_1
}

and use that in our call

    gpio_pin_interrupt_configure_dt(&button, GpioInterrupts.edgeToActive)

Handling the handler

The next step is about setting up the code that runs when the button is pressed.
Let’s take a look back at how this is done in the C code.

  void pin_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) {
    printk("Button pressed\n");
  }

  gpio_init_callback(&pin_cb_data, pin_isr, BIT(button.pin));

This code declares a function to be executed on button press and uses it in the gpio_init_callback()call. Let’s look at the prototype of this function.

/**
 * @brief Helper to initialize a struct gpio_callback properly
 * @param callback A valid Application's callback structure pointer.
 * @param handler A valid handler function pointer.
 * @param pin_mask A bit mask of relevant pins for the handler
 */
static inline void gpio_init_callback(struct gpio_callback *callback,
				      gpio_callback_handler_t handler,
				      gpio_port_pins_t pin_mask)

The most interesting part for us at this stage is the handlerparameter, let’s look at its type.

/**
 * @typedef gpio_callback_handler_t
 * @brief Define the application callback handler function signature
 *
 * @param port Device struct for the GPIO device.
 * @param cb Original struct gpio_callback owning this handler
 * @param pins Mask of pins that triggers the callback handler
 *
 * Note: cb pointer can be used to retrieve private data through
 * CONTAINER_OF() if original struct gpio_callback is stored in
 * another private structure.
 */
typedef void (*gpio_callback_handler_t)(const struct device *port,
					struct gpio_callback *cb,
					gpio_port_pins_t pins);

As, in Swift, we’ll want to define a closure to be passed to this function, let’s define a type alias similar to the C typedef.

typealias GpioCallbackHandler = @convention(c) (
  _ port: UnsafePointer<device>?,
  _ callback: UnsafeMutablePointer<gpio_callback>?,
  _ pins: UInt32
) -> Void

Notice the @convention(c) attribute, indicating C function pointer calling convention.
Also notice that the constkeyword in C results in the usage of an UnsafePointer vs an UnsafeMutablePointer when it’s not present.
Finally, notice the usage of optionals, as nothing in C ensures that those parameters cannot be NULL.

With that in place, we can define our closure and call the init function.

  static func main() {
...
    let btnHandler: GpioCallbackHandler? = { _, _, _ in
      print("Button pressed")
    }

    var pin_cb_data = gpio_callback()

    gpio_init_callback(&pin_cb_data, btnHandler, BIT(button.pin))
...

However this code fails to build, we’re facing another macro issue.

…/EmbeddedSwift-nRF52-Examples/Button/Main.swift:54:50: error: cannot find 'BIT' in scope
52 |     var pin_cb_data = gpio_callback()
53 | 
54 |     gpio_init_callback(&pin_cb_data, btnHandler, BIT(button.pin))
   |                                                  `- error: cannot find 'BIT' in scope
55 | 
56 |     while true {

/opt/nordic/ncs/v2.7.0/zephyr/include/zephyr/sys/util_macro.h:44:9: note: macro 'BIT' unavailable: function like macros not supported
 42 |  * assembly language).
 43 |  */
 44 | #define BIT(n)  (1UL << (n))
    |         `- note: macro 'BIT' unavailable: function like macros not supported
 45 | #endif
 46 | #endif

Similar to above, this type of macro is not supported, but as the name indicates (function-like), it can easily be replaced by a function.

func bit(_ n: UInt8) -> UInt32 {
  UInt32(1) << n
}

Using that in our call fixes the issue.

    gpio_init_callback(&pin_cb_data, btnHandler, bit(button.pin))

And we can add the final call required for our callback configuration.

	gpio_add_callback(button.port, &pin_cb_data)

Building and flashing this code should give you the same behaviour as before, a message printed to the console on button press.

Warning

The call back is in fact an ISR (Interrupt Service Routine) and there are several constraints and best practices on the code that should be executed in an ISR. First and foremost to keep it fast.
Printing from an ISR is not advised and is used here for demonstration purposes.

Tidying up

During prototyping, we wrote all our code within the main function.
Now that we have working code, let’s encapsulate that in a dedicated struct.
The initializer allows to configure the pin the button is connected to and the closure to execute on button press.

struct Button {
  let gpio: UnsafePointer<gpio_dt_spec>
  let btnHandler: GpioCallbackHandler
  var pin_cb_data = gpio_callback()

  init(gpio: UnsafePointer<gpio_dt_spec>, btnHandler: GpioCallbackHandler) {
    self.gpio = gpio
    self.btnHandler = btnHandler

    guard gpio_is_ready_dt(gpio) else { return }

    let ret = gpio_pin_configure_dt(gpio, GPIO_INPUT)
    guard ret == 0 else { return }

    gpio_pin_interrupt_configure_dt(gpio, GpioInterrupts.edgeToActive)

    gpio_init_callback(&pin_cb_data, self.btnHandler, bit(gpio.pointee.pin))

    gpio_add_callback(gpio.pointee.port, &pin_cb_data)
  }
}

The closure being the last parameter, we can use a trailing closure at the calling site.

@main
struct Main {
  static func main() {
    let myButton = Button(gpio: &button) { _, _, _ in
      print("Button pressed")
    }

    while true {
      k_msleep(100)
    }
  }
}

This builds and can be flashed to the board, but nothing happens anymore when pressing the button!

Conclusion

In this post, we have seen how to work with a button (or more generally a digital input) in Swift code.

However, we’re left on a bitter note, as our try to encapsulate our logic in a reusable Swift type results in non-working code. In the next post, we’ll look at why this is and ways to fix it.

If that cliffhanger is too much for you, ping me on social media and I’ll give you a hint as to how to solve this.