Embedded & IoT No. 467

Zephyr RTOS skill for Claude Code

Devicetree and Kconfig without the guesswork. Claude sets up a pinned west workspace, describes hardware in board overlays instead of C constants, enables the minimal Kconfig set, and uses DT-spec driver APIs with readiness checks. When builds or devices fail it reads the generated zephyr.dts and .config first, then fixes compatibles, status, or dependencies. Logic is tested on native_sim with twister.

  • namezephyr-rtos
  • size243 lines · ≈2,250 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds Zephyr RTOS applications with west — workspace manifests, board targets and qualifiers, devicetree overlays and bindings, Kconfig prj.conf fragments, DT-spec driver APIs (gpio_dt_spec, i2c_dt_spec), kernel primitives (threads, work queues, message queues), logging and shell, sysbuild with MCUboot, native_sim and twister tests. Use when starting or debugging a Zephyr app on nRF, STM32, ESP32, RP2040 or other boards, when devicetree or Kconfig errors appear, a device is not ready, a sample needs porting to custom hardware, or tests need to run in CI.

Try asking

  • Port the Zephyr blinky sample to our custom nRF52840 board with an overlay
  • Build fails with undefined reference to __device_dts_ord — what did I miss?
  • Add a BME280 over I2C to this Zephyr app and test it in CI with twister

Use it when

  • Starting or structuring a Zephyr application and west workspace
  • Devicetree, pinctrl, or Kconfig errors block the build
  • A driver reports device not ready on real hardware
  • Setting up twister tests or MCUboot via sysbuild

Skip it when

  • Plain FreeRTOS task and ISR design — use freertos-tasks
  • OTA rollout strategy, signing keys, and rollback policy — use firmware-ota

How to check the result

A pristine build succeeds for every target board, the generated zephyr.dts and .config contain the intended nodes and symbols, and twister suites pass on native_sim.

The common mistake it prevents

Adding a devicetree node whose compatible matches no binding or whose driver is disabled in Kconfig, so it silently does nothing until a cryptic linker error appears.

~/.claude/skills/zephyr-rtos/SKILL.md

--- name: zephyr-rtos description: Builds Zephyr RTOS applications with west — workspace manifests, board targets and qualifiers, devicetree overlays and bindings, Kconfig… license: MIT ---

Done well means: the app lives in its own west workspace with a pinned Zephyr revision, hardware is described in devicetree overlays (never hardcoded pin numbers), features are enabled in prj.conf with the smallest set of symbols, every device is checked with *_is_ready_dt() before use, and logic is tested on native_sim with twister.

Ground rules

  • Read the generated files before guessing: build/zephyr/zephyr.dts (final devicetree) and build/zephyr/.config (final Kconfig). Most “it is not working” problems are visible there.
  • Configure hardware in devicetree, software in Kconfig. A pin, bus, or address in C code is a bug waiting for the next board revision.
  • Start from the closest upstream sample (zephyr/samples/...) and change one thing at a time.
  • Pin the Zephyr revision in west.yml. Upgrading Zephyr is a migration: read the release notes’ migration guide for each version crossed.
  • Check board voltage levels in the board docs and SoC datasheet before connecting peripherals; most Zephyr targets use 1.8 V or 3.3 V I/O that is not 5 V tolerant.

Workflow

- [ ] 1. Workspace: west init (app manifest), west update, SDK + host tools installed
- [ ] 2. Build an upstream sample (blinky) for the exact board; flash it
- [ ] 3. Create the app: CMakeLists.txt, prj.conf, src/main.c, boards/<board>.overlay
- [ ] 4. Describe hardware in the overlay; enable drivers in prj.conf
- [ ] 5. Use DT-spec APIs; check readiness; log with LOG_MODULE_REGISTER
- [ ] 6. Inspect zephyr.dts and .config when anything is off
- [ ] 7. Unit/integration tests on native_sim via twister; CI build for each board

1. Workspace (application-as-manifest)

# my-app/west.yml
manifest:
  remotes:
    - name: zephyrproject-rtos
      url-base: https://github.com/zephyrproject-rtos
  projects:
    - name: zephyr
      remote: zephyrproject-rtos
      revision: v4.2.0            # pin a release tag; update deliberately
      import:
        name-allowlist: [cmsis, hal_nordic, hal_stm32, mcuboot, mbedtls]   # only what you need
  self:
    path: my-app
python3 -m venv .venv && . .venv/bin/activate && pip install west
west init -l my-app            # use the local manifest
west update
west zephyr-export
pip install -r zephyr/scripts/requirements.txt
west sdk install               # Zephyr SDK toolchains (recent Zephyr versions)

The tag above is an example; use the release the project targets (LTS lines are the safer choice for products).

2–3. Build and flash

west build -b nrf52840dk/nrf52840 my-app -p auto     # -p auto: pristine when needed
west build -b nucleo_f401re my-app -d build-nucleo   # separate build dir per board
west flash                                            # uses the board's runner (J-Link, OpenOCD, pyOCD...)
west debug                                            # GDB session through the runner
west build -t menuconfig                              # interactive Kconfig for this build
west build -t rom_report                              # flash usage by symbol

Board names use hardware model v2: board/soc[/cpucluster][/variant], e.g. nrf5340dk/nrf5340/cpuapp. List them with west boards.

App skeleton

# CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_app)
target_sources(app PRIVATE src/main.c)
# prj.conf
CONFIG_GPIO=y
CONFIG_I2C=y
CONFIG_LOG=y
CONFIG_SENSOR=y
CONFIG_MAIN_STACK_SIZE=2048

Board-specific Kconfig goes in boards/<board>.conf; overlays in boards/<board>.overlay are picked up automatically (use the board name with / replaced by _, e.g. nrf52840dk_nrf52840.overlay).

4. Devicetree overlays

/* boards/nrf52840dk_nrf52840.overlay */
/ {
    aliases {
        status-led = &led1;
    };
};

&i2c0 {
    status = "okay";
    clock-frequency = <I2C_BITRATE_FAST>;
    bme280: bme280@76 {
        compatible = "bosch,bme280";
        reg = <0x76>;
    };
};
  • Node compatible must match a binding in zephyr/dts/bindings (or your own dts/bindings/*.yaml); otherwise the node is silently unused and no driver loads.
  • status = "okay" enables a node; many SoC peripherals are disabled by default.
  • Pin multiplexing on nRF/STM32 uses pinctrl-0/pinctrl-names with pinctrl nodes; copy the pattern from the board’s .dts.
  • Devicetree errors show as long macro failures like __device_dts_ord_XX undefined: the node is disabled, has no matching driver enabled in Kconfig, or the compatible is wrong.

5. Driver code with DT specs

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(app, LOG_LEVEL_INF);

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(status_led), gpios);
static const struct device *const bme = DEVICE_DT_GET(DT_NODELABEL(bme280));

int main(void)
{
    if (!gpio_is_ready_dt(&led) || !device_is_ready(bme)) {
        LOG_ERR("device not ready");
        return -ENODEV;
    }
    gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE);

    while (1) {
        struct sensor_value t;
        if (sensor_sample_fetch(bme) == 0 &&
            sensor_channel_get(bme, SENSOR_CHAN_AMBIENT_TEMP, &t) == 0) {
            LOG_INF("temp %d.%06d C", t.val1, t.val2);
        }
        gpio_pin_toggle_dt(&led);
        k_sleep(K_SECONDS(1));
    }
}
  • Devicetree property names convert to C with - → _ (status-led → status_led).
  • Prefer *_dt_spec structs (gpio_dt_spec, i2c_dt_spec, spi_dt_spec, adc_dt_spec, pwm_dt_spec) over raw device pointers plus pin numbers.
  • Every API returns negative errno on failure. Check it.

Kernel primitives

NeedUse
Long-running activityK_THREAD_DEFINE (static) with explicit stack size and priority
Deferred work from ISR or callbackk_work_submit on the system work queue (keep it short) or a dedicated k_work_q
Periodic workk_work_delayable or k_timer (timer expiry runs in ISR context)
Pass fixed-size messagesk_msgq
Signalk_sem, k_event, k_poll for several sources
Mutual exclusionk_mutex (priority inheritance)

Cooperative priorities are negative (never preempted by other threads); preemptible are ≥ 0. Lower number = higher priority. Blocking inside the system work queue stalls every driver that uses it.

Logging, shell, stacks

CONFIG_LOG_MODE_DEFERRED=y        # default; keeps ISR logging cheap
CONFIG_SHELL=y                    # interactive commands over UART/RTT
CONFIG_THREAD_ANALYZER=y          # stack usage report
CONFIG_THREAD_ANALYZER_AUTO=y
CONFIG_STACK_SENTINEL=y           # or CONFIG_HW_STACK_PROTECTION=y where supported
CONFIG_ASSERT=y                   # development builds

Thread analyzer output shows each thread’s stack use; keep 25% headroom, then turn the analyzer off for release.

MCUboot and OTA

Use sysbuild to build MCUboot and the app together:

west build -b nrf52840dk/nrf52840 --sysbuild my-app -- -DSB_CONFIG_BOOTLOADER_MCUBOOT=y

Sign images with the project’s key (never ship the default dev key in bootloader/mcuboot), and see firmware-ota for slot layout, confirmation, and rollback policy.

Testing

west build -b native_sim my-app && ./build/zephyr/zephyr.exe     # run on the host
west twister -T tests/ -p native_sim                             # ztest suites
west twister -T my-app -p nrf52840dk/nrf52840 --build-only        # CI build matrix

Use the ztest framework (ZTEST_SUITE, ZTEST) for unit tests; emulated buses and sensors exist for many drivers so logic can be tested without hardware.

Debugging table

SymptomCheck
undefined reference to __device_dts_ord_NNode disabled, driver Kconfig off, or wrong compatible
Device not ready at runtimeDriver init failed: wiring, address, power, pinctrl; enable driver log level DBG
Kconfig symbol ignored.config shows its value; dependency unmet (menuconfig shows why)
Overlay has no effectWrong file name for the board; check build log “Found devicetree overlay”
Hard fault, ***** MPU FAULT *****Stack overflow; thread analyzer, raise stack
Flash fullwest build -t rom_report; disable unused subsystems, CONFIG_SIZE_OPTIMIZATIONS=y

Verification

  1. Pristine build (-p always) succeeds for every target board without warnings.
  2. zephyr.dts and .config contain the intended nodes and symbols (grep for them).
  3. Twister suites pass on native_sim in CI; hardware smoke test passes on the board.
  4. Thread analyzer shows ≥ 25% stack headroom per thread after exercising all features.
  5. For MCUboot builds: an image signed with the production key boots, an unsigned one is rejected.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc