Blog | About

Zig extension for Zephyr - blinking an LED from userspace

Apr 23, 2026

Last post in the Zig extension for Zephyr RTOS series was about blinking an LED, but we used the extension that runs on kernel space. What about extensions that run on user space?

A simple try

Now that all the API we needed was already translated, we only need two things: update our build.sh for ext1 with the new changes we’d done on k-ext1, and update the extension itself. For the extension, we first get the device:

const led = c.GPIO_DT_SPEC_GET(c.DT_ALIAS("led1"), "gpios");

Using led1 will get us the green LED on the MCXN947. Then, we configure it, just after registering the subscriber:

if (!c.gpio_is_ready_dt(&led)) {
    c.printk("[zig][ext1]LED is not ready!\n");
    return 2;
}

var ret = c.gpio_pin_configure_dt(&led, c.GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
    c.printk("[zig][ext1]gpio_pin_configure_dt failed!\n");
    return 3;
}

And finally we toggle it on inside the loop. To differ a bit from the kernel extension, besides using a different LED, let’s change it only when we’re notified odd values:

c.printk("[zig][ext1]Read val: %ld\n", l);

if (l % 2 == 1) {
    c.printk("[zig][ext1]Toggling light on odd value!\n");
    ret = c.gpio_pin_toggle_dt(&led);
    if (ret < 0) {
        c.printk("[zig][ext1]Failed to toggle light!\n");
    }
}

We can build and run. If we do that, no new light... odd. If we look at the serial output, we see:

[app]Loading extension [ext1].
[app]Thread 0x30003f98 created to run extension [ext1], at userspace.
[app]Thread [0x30003f98] registered event [0x3000a760]
r0/a1:  0x00000000  r1/a2:  0x00000000  r2/a3:  0x00000000
r3/a4:  0x00000000 r12/ip:  0x00000000 r14/lr:  0x00000000
 xpsr:  0x00000000
Faulting instruction address (r15/pc): 0x58f82301
[app]Fatal handler! Thread: 0x30003f98

Why?

Granting access to objects for userspace

The simple answer is that there are limitations to what kernel objects a userspace thread can access. But it is possible to give them access to objects when needed. And that’s what we need here: to give the userspace threads access to the LED devices.

To do that, we can use k_object_access_grant from the application. It grants the access to the threads that will eventually run the extensions. We can change the run_extension_on_thread function on <zephyr-dir>/samples/subsys/llext/edk/app/src/main.c:

// (...)

/* Allow threads to access the LEDs */
k_object_access_grant(DEVICE_DT_GET(DT_GPIO_CTLR(DT_ALIAS(led0), gpios)), thread);
k_object_access_grant(DEVICE_DT_GET(DT_GPIO_CTLR(DT_ALIAS(led1), gpios)), thread);
k_object_access_grant(DEVICE_DT_GET(DT_GPIO_CTLR(DT_ALIAS(led2), gpios)), thread);

// (...)

Note that we need to grant the access to the GPIO controller for each LED, hence the intermediate DT_GPIO_CTLR above. After we rebuild, we can see it working:

And it was easy enough, as the devicetree work has been done previously.

For the curious, the complete code is available on my Zephyr fork.


tags: zig, zephyr, zig-zephyr-series