How to Add Custom Board Support in Zephyr

Most Zephyr tutorials on custom board support will lead you straight into a build error. Not because they were wrong when written, but because Zephyr 3.7 quietly deprecated the board format those tutorials describe, and Zephyr 4.x removed it entirely. If you’ve been staring at a Board not found error after following a blog post from 2023, that’s why. The directory structure changed, the identification mechanism changed, and the YAML schema is new. Your custom PCB deserves a board definition that actually matches the build system it’s targeting.
This guide walks you through creating a zephyr custom board definition using the HWMv2 (Hardware Model v2) format, the standard in Zephyr 4.x. By the end, you’ll have a buildable board that boots to a shell on UART, using exactly 6 files in a clean directory tree.
Prerequisites: A working Zephyr SDK, a west workspace, and comfort with the build system, Kconfig, and devicetree basics. We’ll use an nRF5340 application core as the concrete example, but the pattern applies to any supported SoC.
Warning: If you see tutorials referencing a flat
boards/arm/my_board/structure without aboard.ymlfile, you’re looking at the deprecated HWMv1 format. It will not work with Zephyr 4.x.
What Changed in the HWMv2 Board Model
The HWMv2 zephyr board definition model made three structural shifts. First, board identity is now driven by a board.yml YAML file, not by directory naming conventions or board.cmake alone. Second, boards live under a vendor subdirectory. Third, multi-core and multi-variant SoCs (like the nRF5340 with its app and network cores) are first-class citizens through a qualifiers system.
Here’s what changed at the directory level:
# Old HWMv1 (Zephyr ≤ 3.6) # New HWMv2 (Zephyr 4.x)
boards/ boards/
└── arm/ └── <vendor>/
└── my_custom_board/ └── my_custom_board/
├── board.cmake ├── board.yml ← NEW entry point
├── my_custom_board.dts ├── my_custom_board.dts
├── my_custom_board_defconfig ├── my_custom_board_defconfig
└── Kconfig.board ├── Kconfig.my_custom_board
└── board.cmakeThe board.yml is how the build system discovers your board. Without it, your board doesn’t exist to Zephyr.
┌──────────────┐
│ board.yml │ ← Build system entry point
└──────┬───────┘
│ declares SoC
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌───────────┐ ┌────────────────────┐
│ .dts file │ │ _defconfig│ │ Kconfig.board_name │
│ (hardware) │ │ (kernel) │ │ (symbol def) │
└──────────────┘ └───────────┘ └────────────────────┘
│
│ #include
▼
┌──────────────────┐
│ SoC .dtsi │
│ (from Zephyr) │
└──────────────────┘Step-by-Step: Create Your Zephyr Custom Board Files
Our example: a custom carrier board called my_custom_board, built around the nRF5340 application core. It has a UART console on pins P0.20 (TX) and P0.22 (RX), and one LED on P0.28.
Pick Your Location: In-Tree vs. Out-of-Tree
You can place your board inside the Zephyr tree at zephyr/boards/<vendor>/, but for product work, out-of-tree is the right call. It keeps your definition independent of Zephyr version updates and lives in your own repo.
Create this structure in your project workspace:
mkdir -p my_boards/boards/mycompany/my_custom_boardThen tell Zephyr where to find it by setting BOARD_ROOT at build time:
west build -b my_custom_board -- -DBOARD_ROOT=$(pwd)/my_boardsOr, for a persistent setup, add it to your west workspace CMakeLists.txt:
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/my_boards)Write board.yml — The Discovery File
This is the single most important file. Without valid YAML here, the build system won’t find your board.
board:
name: my_custom_board
vendor: mycompany
socs:
- name: nrf5340
variants:
- name: nrf5340-cpuappField-by-field:
name: Must match your directory name and file prefixes exactly.vendor: Must match the vendor directory name. If you’re not an official Zephyr vendor, pick a consistent slug for your company.socs: A list of SoCs this board uses. For the nRF5340, you declarenrf5340and specify thenrf5340-cpuappvariant since this SoC has two cores. The build system uses this to resolve Kconfig and devicetree at the SoC level.
Running west build -b my_custom_board/nrf5340-cpuapp causes the build system to match this YAML and resolve the full board + SoC combination.
Write the Devicetree Source (.dts)
This file describes your actual hardware: which UART pins you routed, which GPIO drives an LED, where RAM and flash live. Create my_custom_board_nrf5340_cpuapp.dts:
/dts-v1/;
#include <nordic/nrf5340_cpuapp_qkaa.dtsi>
#include "my_custom_board-pinctrl.dtsi"
/ {
model = "My Custom Board nRF5340 Application Core";
compatible = "mycompany,my-custom-board";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
zephyr,sram = &sram0;
zephyr,flash = &flash0;
};
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 28 GPIO_ACTIVE_LOW>;
label = "Green LED 0";
};
};
aliases {
led0 = &led0;
};
};
&uart0 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_default>;
pinctrl-1 = <&uart0_sleep>;
pinctrl-names = "default", "sleep";
};
&gpio0 {
status = "okay";
};You’ll also need a pin control file. Create my_custom_board-pinctrl.dtsi:
&pinctrl {
uart0_default: uart0_default {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 20)>;
};
group2 {
psels = <NRF_PSEL(UART_RX, 0, 22)>;
bias-pull-up;
};
};
uart0_sleep: uart0_sleep {
group1 {
psels = <NRF_PSEL(UART_TX, 0, 20)>,
<NRF_PSEL(UART_RX, 0, 22)>;
low-power-enable;
};
};
};Key points: The #include of the SoC .dtsi gives you all the base peripherals, memory regions, and interrupt controllers. Your .dts only adds what’s specific to your PCB: pin assignments, enabled peripherals, and chosen nodes that the Zephyr kernel needs to locate console, RAM, and flash.
Write the Kconfig Defconfig (_defconfig)
Create my_custom_board_nrf5340_cpuapp_defconfig. This sets kernel and driver defaults for your board:
# Console
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_SERIAL=y
# GPIO (for LED)
CONFIG_GPIO=y
# Shell (optional, enables interactive UART shell)
CONFIG_SHELL=y
# Logging
CONFIG_LOG=y
# ARM-specific
CONFIG_ARM_MPU=yThink of this as the board-level default configuration. Application developers can still override any of these symbols in their own prj.conf. Your defconfig should enable exactly what the board’s hardware needs to function at a baseline: console output, GPIO for LEDs, and core MCU features. For a deeper dive into layered configuration, see our Kconfig deep-dive.
Write Kconfig.my_custom_board
This small file defines the board-level Kconfig symbol and wires it to the correct SoC:
config BOARD_MY_CUSTOM_BOARD
select SOC_NRF5340_CPUAPPThat’s the whole file. Its role is narrow but critical: when the build system selects your board, this ensures the correct SoC Kconfig tree is activated. The filename must be Kconfig.<board_name>, because the build system globs for it.
Write board.cmake
This file tells the build system how to flash your board. If you’re using a J-Link debugger:
board_runner_args(jlink "--device=nRF5340_xxAA_APP" "--speed=4000")
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)If you use nrfjprog instead:
board_runner_args(nrfjprog "--nrf-family=NRF53" "--softreset")
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)Pick the runner that matches your debug probe. You can include multiple runners; the first include becomes the default for west flash.
Build, Flash, and Verify
With all six files in place, build the hello_world sample:
west build -b my_custom_board/nrf5340-cpuapp \
zephyr/samples/hello_world \
-- -DBOARD_ROOT=$(pwd)/my_boardsA successful build ends with:
[244/244] Linking C executable zephyr/zephyr.elf
Memory region Used Size Region Size %age Used
FLASH: 32784 B 1 MB 3.13%
SRAM: 8640 B 448 KB 1.88%Flash and connect a serial terminal at 115200 baud:
west flashYou should see:
*** Booting Zephyr OS build v4.1.0 ***
Hello World! my_custom_board/nrf5340-cpuappTroubleshooting
| Error | Likely Cause |
|---|---|
Board "my_custom_board" not found | BOARD_ROOT not set, or board.yml is missing/malformed. Verify the path resolves to the directory containing boards/. |
Devicetree error: undefined node | Missing #include of the SoC .dtsi, or referencing a node label that doesn’t exist at the SoC level. |
CONFIG_SOC_... unmet dependency | Kconfig.my_custom_board isn’t selecting the correct SOC symbol. Cross-check against an in-tree board using the same SoC. |
pinctrl: undefined NRF_PSEL | Missing #include "...-pinctrl.dtsi" in your .dts, or the pin control .dtsi file has a naming mismatch. |
runner: device not found | board.cmake specifies the wrong device string or runner for your debug probe. |
Where to Go After Your First Build
Once your board builds and boots, here’s what typically comes next:
Board revisions. HWMv2 supports revision-specific configurations. If you’re already planning a rev B of your PCB, you can create my_custom_board_B.conf and my_custom_board_B.overlay files, then build with west build -b my_custom_board/nrf5340-cpuapp@B. This keeps one board definition supporting multiple hardware revisions cleanly.
Shield support. If your product uses modular hardware (sensor boards, display add-ons), Zephyr’s shields system lets you layer devicetree overlays on top of your board definition. See our shields guide for the mechanics.
CI integration. Add your board to twister runs with --board my_custom_board/nrf5340-cpuapp. Point twister at your out-of-tree root with --board-root. This catches regressions against Zephyr upstream updates before they hit production.
Upstream contribution. If your board is based on open hardware, consider contributing it to the Zephyr tree. The nrf5340dk board files under zephyr/boards/nordic/nrf5340dk/ are an excellent reference implementation for the zephyr porting process. Study them to see how Nordic handles multi-core qualifiers and multiple runner configurations.
The whole definition is 6 files: board.yml, .dts, pinctrl .dtsi, _defconfig, Kconfig.<name>, and board.cmake. Once you’ve built one, the second board takes twenty minutes. Keep your definitions out-of-tree, pin them to your product repo, and they’ll survive every Zephyr version bump cleanly.
Hubble Network connects your custom hardware to satellite networks straight from a Bluetooth chip — no extra radios, no gateway infrastructure. See how it works →