Hands-On: Build a Custom Zephyr BSP in 90 Minutes with AI
Building a custom BSP shouldn’t take days.
But for most embedded teams, it still does.
Developers dig through documentation, reverse-engineer examples, and fight with Devicetree, Kconfig, and build systems just to get hardware running. It’s slow, error-prone, and hard to scale.
AI changes that.
In this fast-paced, hands-on workshop, you’ll build a custom Zephyr BSP from scratch in just 90 minutes by using AI as your development partner. Instead of piecing together fragmented knowledge, you’ll learn how to guide AI to generate board configurations, validate your setup, and accelerate hardware bring-up without losing control of the system.
We’ll start with a quick overview of how Zephyr abstracts hardware and what a BSP actually consists of. Then you’ll immediately apply that knowledge by building your own board support package step-by-step. Along the way, you’ll use AI to assist with Devicetree configuration, board definitions, peripheral setup, and build integration, while learning how to critically evaluate and refine the results.
This is not about replacing the engineer. It’s about amplifying what you can do.
By the end of the workshop, you won’t just understand how Zephyr BSPs work. You’ll have built one yourself and developed a repeatable, AI-assisted workflow that can cut your hardware bring-up time dramatically.
What You’ll Learn:
- What a Zephyr BSP includes and how the pieces fit together
- How to create and configure a custom board using Devicetree and Kconfig
- A practical, repeatable process for bringing up new hardware in Zephyr
- How to use AI to accelerate BSP development while staying in control
- Best practices for building portable, maintainable hardware abstractions
If you’re still bringing up hardware the same way you did five years ago, this workshop will show you a faster, more scalable approach.
Hardware:
Participants will use the NXP FRDM-MCXA156 development board, provided as part of the workshop by NXP.
@Jacob, the kconfig-tuner skill required input target_file does not default to ask if it is not specified. I just tried lab3 without the prj.conf specified.
I think this is because later in the skill the Validation section says "target_file exists: (dry-run, no file requested)". At least Gemini 3.8 interpreted that to mean treat missing as dry-run. But without a target how does it know the project? In addition, Gemini 3.8 interpreted the AGENTS.md/CLAUDE.md rule 11 as "forbid skills from asking follow-up questions."
diff --git a/.claude/skills/kconfig-tuner/SKILL.md b/.claude/skills/kconfig-tuner/SKILL.md
index 02cd76f..5b5df17 100644
--- a/.claude/skills/kconfig-tuner/SKILL.md
+++ b/.claude/skills/kconfig-tuner/SKILL.md
@@ -23,12 +23,18 @@ Translates plain-English Zephyr feature requests into a minimal, verified set of
| Input | Type | Default | |
|---|---|---|---|
| - | goal |
English description of what to enable | (none -- ask) |
| - | target_file |
path to the prj.conf or _defconfig to edit |
(none -- ask) |
| + | goal |
English description of what to enable | (required) |
| + | target_file |
path to the prj.conf or _defconfig to edit |
(required unless explicit dry-run) |
dry_run |
if true, propose the diff but do not write the file | false |
+Mandatory Gate: If target_file is omitted and the user did NOT explicitly request "dry-run" or "dry run", the agent MUST STOP immediately and ask the user for target_file. The repository contains multiple lab projects; the agent must never guess or silently default to dry-run mode.
+
Procedure
+0. Check required inputs (Input Gate):
-
- If
dry_runis true (the user explicitly requested dry-run mode in their prompt, e.g. "dry run" or "dry-run"): proceed to Step 1.
- If
-
- If
target_filewas NOT specified: STOP IMMEDIATELY. Do not run grep checks, do not generate diffs or the output report. Ask the user: "Which target file (for example,Labs/Labs/Lab3/Starter/app/prj.conf) should be updated? This repository contains multiple projects."
- If
-
- If
target_filewas specified: verifytest -f <target_file>before editing.- Parse the goal into 1 or more candidate Kconfig symbols.
- "shell over UART" ->
CONFIG_SHELL,CONFIG_SHELL_BACKEND_SERIAL,CONFIG_SERIAL,CONFIG_UART_CONSOLE - "logging" ->
CONFIG_LOG
@@ -41,16 +47,18 @@ Translates plain-English Zephyr feature requests into a minimal, verified set ofIf grep returns no result, the symbol does not exist. **Refuse to include unverified symbols. Never fabricate.** 3. **Construct the minimal diff.** Drop any symbol whose default is already `y` for the target board (use the reference `_defconfig` to check). Keep only what the user must add. -4. **Optionally write to `target_file`.** If `dry_run` is true, just report. +4. **Apply diff or report.**
- If
-
- If
dry_runis true: report the verified diff without modifying files.
- If
-
- If
target_fileis provided: write/append the verified symbols totarget_file.
Self-Validation Protocol
# Check How to verify + --- --- --- 1 Goal was parsed into >=1 candidate the diagnosis lists at least one CONFIG_ symbol 2 Every proposed CONFIG_ symbol exists in the Zephyr tree for each symbol, grep -rl "^config <SYMBOL>$" /workdir/zephyr /workdir/modulesreturns at least one path; the path is reported3 No fabricated symbols the count of (proposed) == count of (verified) - 4 Target file exists if write was requested test -f <target_file>before edit+ 4 Target file handling if write requested, test -f <target_file>; if omitted without dry-run, stop and prompt user for path5 Diff is minimal (no symbols already implied by board defconfig) manual cross-check; report which were dropped If check 2 or 3 fails: the offending symbol must NOT be reported. Report only the verified subset, plus a note about the unverified ones with the suggestion to ask the user for guidance.
@@ -61,7 +69,7 @@ No retries on symbol verification -- a missing symbol means the candidate was wrOutput format
- If
-The agent MUST wrap the output in a fenced code block (text ...) so that monospace alignment and line breaks are preserved in Markdown:
+When target_file is provided or dry-run is explicitly requested, the agent MUST wrap the output in a fenced code block (text ...) so that monospace alignment and line breaks are preserved in Markdown:
kconfig-tuner result: PASS (or FAIL)
@@ -80,21 +88,28 @@ Validation:
[x] candidates parsed: 4 symbols
[x] every symbol verified in Zephyr tree
[x] no fabricated symbols
- [-] target_file exists: (dry-run, no file requested)
+ [x] target_file exists: Labs/Labs/Lab2/Starter/app/prj.conf (or: [-] dry-run requested)
[x] minimal diff: no symbols dropped as already-default
Examples
-### Example 1: enable shell over UART
+### Example 1: target_file omitted without dry-run
+
+User: "/kconfig-tuner enable logging with UART backend in immediate mode"
+
+Output: Agent stops immediately and asks:
+"Which target file (e.g. Labs/Labs/Lab2/Starter/app/prj.conf or boards/nxp/frdm_mcxa156_workshop/frdm_mcxa156_workshop_defconfig) should be updated? This repository contains multiple projects."
+
+### Example 2: explicit dry-run mode
-User: "what do I need in prj.conf to get a shell prompt over the console?"
+User: "Use the kconfig-tuner skill (dry-run mode) to determine the minimum verified CONFIG_* set for this goal: enable shell over UART"
-Output: 4 symbols (SHELL, SHELL_BACKEND_SERIAL, SERIAL, UART_CONSOLE), each verified.
+Output: 4 symbols (SHELL, SHELL_BACKEND_SERIAL, SERIAL, UART_CONSOLE), each verified, marked with [-] target_file exists: (dry-run mode requested).
-### Example 2: refuse unverified
+### Example 3: refuse unverified
-User: "enable CONFIG_FANCY_FEATURE"
+User: "enable CONFIG_FANCY_FEATURE in Labs/Labs/Lab2/Starter/app/prj.conf"
Output: kconfig-tuner refuses. Symbol not found in Zephyr tree (verified via grep). Suggest the user check the spelling or find the closest matching real symbol.
diff --git a/CLAUDE.md b/CLAUDE.md
index c869456..e2234ae 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -96,7 +96,7 @@ beningo_eoc26_zephyrAI/
- Prefer mirroring
frdm_mcxa156_reference/over generating from scratch. When a structural decision exists in the reference BSP, mirror it. - Never claim a build succeeded without showing the exit code.
west builderrors must be captured by thebuild-doctorskill, not paraphrased. -
Never assume
west flashorwest debugworks in-container on macOS or Windows. USB passthrough is not supported. Flash from the host with VS Code F5 (Cortex Debug + LinkServer) orLinkServer flash MCXA156:FRDM-MCXA156 load build/zephyr/zephyr.elf.
-11. Skills do not offer pedagogical choices. When invoked as a skill (build-doctor,devicetree-author,kconfig-tuner,zephyr-bsp-scaffold,renode-runner), produce the diagnosis or artifact, run the Self-Validation Protocol, and stop. Do not ask the attendee how they want to teach the topic, whether to apply the fix, or whether this is a teaching moment. The attendee decides next steps in their own prompt. Workshop framing belongs inCLAUDE.md, not in skill output.
+11. Skills do not offer pedagogical choices. When invoked as a skill (build-doctor,devicetree-author,kconfig-tuner,zephyr-bsp-scaffold,renode-runner), produce the diagnosis or artifact, run the Self-Validation Protocol, and stop. Do not ask the attendee how they want to teach the topic, whether to apply the fix, or whether this is a teaching moment. The attendee decides next steps in their own prompt. Workshop framing belongs inCLAUDE.md, not in skill output. Exception: if a required input (such astarget_file) is missing and no dry-run flag was set, stop immediately and ask the user for the required input.Hardware Target
@@ -109,7 +109,7 @@ beningo_eoc26_zephyrAI/
The Practical Zephyr BSP Model
-A Zephyr board is a small system of files, each responsible for one layer of configuration. There is no canonical "N-file model" — boards vary, and structures shift across versions and vendors. What matters is the layering. Understanding the split is what makes Zephyr portable.
+A Zephyr board is a small system of files, each responsible for one layer of configuration. There is no canonical "N-file model" - boards vary, and structures shift across versions and vendors. What matters is the layering. Understanding the split is what makes Zephyr portable.
At a minimum, most boards rely on:
Feedback: the video recording file manager and console window text is very small and partially cut off.
Ran into two issues in lab1.
/renode-runnerfailed loading platform: missing NXP_MCXA_GPIO.cs and NXP_OSTimer.cs (Error E04: Could not resolve type).
There was an error executing command 'machine LoadPlatformDescription @/workdir/app/renode/platforms/boards/frdm_mcxa156.repl'
Error E04: Could not resolve type: 'GPIOPort.NXP_MCXA_GPIO'.
At /workdir/app/renode/platforms/boards/frdm_mcxa156.repl:22:1:
gpio0: GPIOPort.NXP_MCXA_GPIO @ sysbus 0x40102000
- Then after resolving that, bus conflict: silence range <0x400A0000 0x1C000> overlapped ostimer0 at 0x400AD000.
There was an error executing command 'sysbus SilenceRange <0x400A0000 0x1C000>'
Could not register Antmicro.Renode.Peripherals.Bus.Silencer at the given address <0x400A0000, 0x400BBFFF> because it conflicts with peripheral Antmicro.Renode.Peripherals.Timers.NXP_OSTimer registered at <0x400AD000, 0x400ADFFF>
Fix:
--- a/renode/scripts/single-node/frdm_mcxa156_hello_world.resc
+++ b/renode/scripts/single-node/frdm_mcxa156_hello_world.resc
@@ -8,2 +8,4 @@ mach create $name
+include @renode/peripherals/NXP_MCXA_GPIO.cs
+include @renode/peripherals/NXP_OSTimer.cs
machine LoadPlatformDescription @renode/platforms/boards/frdm_mcxa156.repl
@@ -65,2 +67,4 @@ sysbus SilenceRange <0x4009B000 0x4000>
-sysbus Tag <0x400A0000, 0x400BBFFF> "periph_gap_lpuart0_porta" 0xFFFFFFFF
-sysbus SilenceRange <0x400A0000 0x1C000>
+sysbus Tag <0x400A0000, 0x400ACFFF> "periph_gap_lpuart0_ostimer" 0xFFFFFFFF
+sysbus SilenceRange <0x400A0000 0xD000>
+sysbus Tag <0x400AE000, 0x400BBFFF> "periph_gap_ostimer_porta" 0xFFFFFFFF
+sysbus SilenceRange <0x400AE000 0xE000>
Am using Mac and getting below error while following lab 0 command [docker compose -f docker/docker-compose.yml build]. Can you help?
failed to solve: process "/bin/sh -c set -eux; case \"${TARGETARCH}\" in amd64) curl -fL -o /tmp/renode.deb \"https://github.com/renode/renode/releases/download/v${RENODE_VERSION}/renode_${RENODE_VERSION}_amd64.deb\"; apt-get update; apt-get install -y --no-install-recommends /tmp/renode.deb; rm /tmp/renode.deb; rm -rf /var/lib/apt/lists/; ;; arm64) curl -fL -o /tmp/renode.tar.gz \"https://github.com/renode/renode/releases/download/v${RENODE_VERSION}/renode-${RENODE_VERSION}.linux-arm64-portable-dotnet.tar.gz\"; mkdir -p /opt/renode; tar -xzf /tmp/renode.tar.gz -C /opt/renode --strip-components=1; ln -s /opt/renode/renode /usr/local/bin/renode; rm /tmp/renode.tar.gz; ;; ) echo \"Unsupported TARGETARCH: ${TARGETARCH}\" >&2; exit 1 ;; esac; renode --version" did not complete successfully: exit code: 1
Interesting. If you haven't gotten it to work yet, give this command a try:
docker compose -f docker/docker-compose.yml build --build-arg TARGETARCH=amd64
I did the first lab and it worked the first time. I was really impressed that the claude agent found an issue with the skill fill that didn't work for me using Windows 11, made a correction during that session, and then suggested how to fix it after the fact. Your notes have an excellent amount of detail. Thank you for putting this together!
Thank you.
The labs are excellent. Thanks for creating it! However, do you know if claude code can be paired up with an opensource model and run local llm? ( to save claude api costs! :p)
I don't see why not. You can use the Ollama API's to have claude call into it. I've not tried it myself though.
No, I haven't verified it. I use MacOS. You should be able to take the generation process though and get it running on your machine.
We had several Linux folks during the live session who had it running.
hint: Use AI to help you through the set up
Great workshop! Congrats! I couldn't attend live, but I thoroughly enjoyed the recorded version and all the information it contained.
Thanks!
I did the first lab and it worked the first time. I was really impressed that the claude agent found an issue with the skill fill that didn't work for me using Windows 11, made a correction during that session, and then suggested the prompt to fix it after the fact. Your notes have an excellent amount of detail. Thank you for putting this together! (prompt suggested below)
Update the skill's check 6 to use 2>&1 instead of 2>/dev/null
Great demonstration of very powerful, yet practical ways to use AI to accelerate development. It was an eye-opener!
Thank you.
Is this Q&A panel the "Slack" described in the README.md or is there an actual slack channel for the conference? Sorry I am late to the party. :-)
THere is a conference attendee forum you can also use, but yes you can ask questions here.
Page 4, Section Software Tools of the Labs\Manual\beningo_eoc26_zephyrAI.pdf the hyperlink for LinkServer is broken (404 page not found). The end of the embedded URL in the PDF is missing "ER" between "LINK" and "SERVER", which seems strange since the utility is literally called LinkServer, but :LINKERSERVER will resolve to a page where you can find the device drivers for your host os.
Thanks!
Using Windows 11 host in Lab 0 Step 6 and using git bash I had to add // (instead of /) to keep docker from allowing MSYS to translate the container specific path to my Host Windows path.
docker compose -f docker/docker-compose.yml run --rm zephyr //workdir/app/verify.sh
Thanks for letting me know!








In lab4
devicetree-authoron Gemini 3.8 (medium) added thebme280label on the first use of the skill. I did not run into the compiler issue for theundefined-node-labelerror.What I did run into was having the explicitly disable the p3t1755 node in order to compile. Disabling only the parent controller did not remove the child node/peripheral from the build.