Adding a board
Bringing up a new device is mostly writing config layers — a build resolves from TOML across the config model’s axes, so a new board is a set of small TOML files plus any vendored blobs and kernel fragments. The one exception is a genuinely new chip family, which also needs a small Rust change; see What needs code.
Which track are you on? This is the bring-up track (resolve → update → verify → build) — for a board or patch that has no lock yet. If you only want to build one of the shipped recipes, take the shorter Getting started track (doctor → build) instead. To add a patch rather than a board, see Adding a patch.
Start with the generator.
boot2deb new-device <name>scaffolds the device (and a matching recipe) for you — it offers the valid SoC/boot-method/kernel/feature choices, fills every derivable value, and leaves the researched ones marked# TODO:. Run it (--soc <soc>non-interactively, or answer the prompts on a terminal; add--overlay <dir>to scaffold into your own tree), then edit the TODO values below. The rest of this page explains what those files mean and which values you must research. Seenew-device.
Where your files go: overlay or in-tree
Two ways to add a board, chosen by intent:
- Out-of-tree overlay — you are bringing up a board for yourself. Put the files in
your own directory and pass
--overlay <dir>; there is nothing to fork, and the board’s lock is written back beside it. This is the third-party path — see Overlays. - In-tree — you are contributing a board back to boot2deb. Add the files to the vendored tree and open a pull request.
The files are identical either way; only their location differs. The rest of this page describes those files.
What needs code
Most of a board is data, but three axes are closed Rust enums — Arch, Soc, and
BootMethod in crates/core/src/model.rs — chosen for type safety and exhaustiveness
checking. A board built on a chip family that already exists (any RK35xx SoC, the
rockchip-rkbin boot method) needs no code: the variant is already there. A genuinely
new family does:
- New SoC (e.g. a non-Rockchip chip) — add a variant to the
Socenum near the top ofmodel.rsand to itskebab_enum!invocation grouped just below the enum definitions, then rebuild. The compiler flags everymatchthat must now handle it. - New architecture or new boot method — the same, on
Arch/BootMethod. A new boot method also needs the engine taught how to write its payloads.
This is a deliberate boundary: closed enums give the compiler a single source of truth and catch a half-added target at compile time, at the cost of a recompile for a new family. Within an existing family it is pure config.
The layers to write
Work from the bottom of the hardware stack up, adding only what is new:
First, check whether the board joins a family that is already here. If its SoC and
boot method are both supported, a new board can be a device file and nothing else — no
overlay, no kernel, no engine change. That is not an aspiration: the ASUS
C100P and Chromebit CS10
each ship as a single TOML, and the Chromebit is a stick PC with no SD slot, no keyboard,
no EC and no analog audio. The rule that makes it work is that anything true of the whole
family belongs on the SoC layer, not on the board that happened to need it first —
socs/rk3288/ carries the family’s radio blobs, initramfs module list and network stack
for exactly that reason. When you find yourself copying a file from one board to another,
move it up instead.
-
arch (
arches/<arch>.toml) — only for a CPU architecture not already present. Arch-wide kbuild facts: the cross triple, theARCH=values for kbuild and u-boot, the kernel image path. -
soc (
socs/<soc>.toml, plussocs/<soc>/overlay/for files baked into the rootfs) — the SoC’s shared properties: device-tree directory, force-loaded modules, arch, and any SoC-wide firmware packages.- Media-accel sources are optional and ride the feature. Supply the
[userspace.mpp],[userspace.librga],[userspace.libmali],[ffmpeg.base], and[ffmpeg.rockchip]stanzas here only if a board of this SoC will enable amedia-accel-*feature (the feature compiles them into.debs); copy the block fromsocs/rk3588.toml. A headless SoC that never transcodes omits them entirely. Selecting arequires_media_accelfeature on a SoC that lacks them is a resolve-time error, so the coupling is checked, not assumed.
- Media-accel sources are optional and ride the feature. Supply the
-
boot-method (
boot-methods/<method>.toml) — how this family boots. The file’s shape depends on the method, because the methods differ in kind:rockchip-rkbincompiles a bootloader: the u-boot source + ref and the raw-gap offsets (whereidbloaderandu-boot.itbsit outside any partition, and where the rootfs partition starts).depthchargecompiles nothing — the firmware is the board’s own and what it loads is the signed kernel — so the file carries the ChromeOS kernel partition’s geometry, its GPT attribute bits, and the command line to sign into the kernel.
A field from the other method is an unknown field and fails to parse, which is the point: an image cannot half-belong to two boot chains.
boot-methods/<method>/overlay/ships any boot-time files (e.g. the extlinux generator), andoverlay-pre/ships config a package’s own maintainer scripts must see while they run (see Two overlay stages). -
device (
devices/<device>.toml) — the board itself, stating only its deltas: itssoc,boot_method,supported_boot_methods,kernel_dtb,image_size,hostname,supported_kernels/default_kernel,default_suite,default_layout, plus whatever its boot method requires:- under
rockchip-rkbin: auboot_defconfig, and — only if the board departs from the SoC’s defaults — an[rkbin]block. The bootloader blobs are inherited from the soc layer and merged per field, so a board on the SoC’s usual memory omits the block entirely; a board with different DRAM overrides justtpl. - under
depthcharge: a[depthcharge]block naming the board profile and the profiles the unit supports. Nouboot_defconfig, no blobs — this board compiles no bootloader, and resolution does not ask it to. device_config_fragmentsgotcha: naming a fragment here makes its file mandatory.device_config_fragments = ["device/my-board"]requiresfragments/device/my-board.configto exist — a missing file failsresolve. A board with no board-specific kconfig deltas usesdevice_config_fragments = []to add none. Do not name a fragment you have not written.
- under
-
kernel (
kernels/<kernel>.toml) — the orthogonal kernel axis. Ask first whether the board needs a kernel of yours at all.- If Debian’s own kernel already runs the hardware — which it does for any SoC and
board that are fully upstream — write a
flavor = "distro-package"definition naming the package (linux-image-armmp) and you are done. No source ref, no defconfig, no fragments, no patches, and one definition serves every suite. This is the better answer where it applies:aptkeeps the board’s kernel patched, which a kernel you compiled does not. - Otherwise write a
mainlineorvendordefinition with its source refs,.configfragments, and patch profile. Version-coupled, so a new kernel version is a new file. A compiled kernel that applies no series writespatch_profile = "none"and then never reads thepatchesrepo.
Note that a distro kernel and the two compile-only device fields (
device_dts,device_config_fragments) are mutually exclusive, and resolution says so: nothing would ever build the DTB or merge the fragments. - If Debian’s own kernel already runs the hardware — which it does for any SoC and
board that are fully upstream — write a
Two overlay stages
Each layer may ship two trees of files that are copied into the rootfs:
overlay/— laid in after every package. It therefore wins over whatever the packages shipped, which is what nearly all config wants.overlay-pre/— laid in before any package is installed. This is for config a package’s own maintainer scripts must see while they run, where winning afterwards is too late because the package already acted. Two examples, both from the Veyron Chromebooks:- initramfs settings (
MODULES=list) must precede the kernel package, or the first initramfs is built atMODULES=most— far over the signed payload’s size budget — and then thrown away and rebuilt. depthcharge-tools’ kernel hook re-signs and re-flashes a ChromeOS kernel partition. Installed with no config present, it runs at its defaults and goes looking for that partition on the build host’s disks. Its config has to be there first.
- initramfs settings (
Use overlay/ unless the package acts before your file would arrive.
Supporting assets:
- blobs (
blobs/<soc>/) — vendored bootloader binaries the device/boot-method references. - fragments (
fragments/<name>.config) — kernel.configfragments merged onto the base defconfig, referenced by name from a kernel or device. - patch profile — lives in the separate
patchesrepo, referenced by the kernel; see Adding a patch. Omitted entirely by apatch_profile = "none"kernel. - board device tree — only when the board’s
.dtsis not yet upstream; seedevice_dts.
Finally, a recipe (recipes/<recipe>.toml) pins one point across the axes — device,
kernel, suite, features, layout.
Values you must research
Most fields fail loudly at resolve: bad image geometry, a missing fragment, or an
unvendored keyring are all caught up front. Two fields are not validated until the
stage that consumes them compiles, so a typo produces a late, confusing failure:
| Value | Layer | Fails at |
|---|---|---|
kernel_dtb | device | the kernel build — the DTB is not produced (unless the board carries device_dts, below, which makes this a resolve-time check) |
uboot_defconfig | device | the u-boot build — unknown defconfig |
Take both from the board’s upstream support: kernel_dtb is the device tree the mainline
kernel builds for the board (under arch/<arch>/boot/dts/<dt_dir>/), and
uboot_defconfig is the board’s u-boot defconfig. Confirm each exists in the exact
kernel/u-boot versions you pin before you trust a green resolve.
When the board’s device tree is not upstream
A freshly-supported SoC often has every driver in mainline but none of its boards. Fork
the nearest in-tree board .dts, put it in your overlay, and list it in device_dts:
kernel_dtb = "rockchip/rk3576-my-box.dtb"
device_dts = ["devices/my-box/dts/rk3576-my-box.dts"]
The kernel stage copies it into the tree and registers the DTB with kbuild, so it ships
in the linux-image deb like any in-tree board. kernel_dtb is then validated at
resolve — it must be the DTB one of those sources builds — so the table above no longer
applies to it. Iterate with build <recipe> --stage dtb, which rebuilds only the DTB.
Keep device_dts for the new board file. An edit to an existing upstream .dts is a
patch in the kernel’s patch profile; a source that would overwrite an in-tree file is
refused rather than silently shadowing it.
Bring it up
With the layers written, use the CLI’s checks as guardrails, in order:
# 1. Does it resolve to a coherent build point? Also runs the geometry / fragment /
# keyring preflight, so this is a real coherence gate, not just a merge print.
cargo run -p boot2deb-cli -- resolve <recipe>
# 2. Resolve upstream refs + hash blobs into the lock.
cargo run -p boot2deb-cli -- update <recipe> --kernel-ref <ref>
# 3. Is the host equipped to build it?
cargo run -p boot2deb-cli -- doctor <recipe>
# 4. Does the patch series apply cleanly to the pinned kernel? Auto-fetches the locked
# kernel — no hand-cloned tree — or add --kernel-src ../linux if you have a checkout.
cargo run -p boot2deb-cli -- verify-patches <recipe>
# 5. Does the .config generate (and, with --reference-config, match a reference)?
cargo run -p boot2deb-cli -- verify-config <recipe>
# 6. Build.
cargo run -p boot2deb-cli -- build <recipe>
resolve, update, and the two verify-* commands fail with a typed error before any
compile starts, so most config mistakes surface in seconds rather than partway through a
build. The verify-* commands auto-fetch the pinned source trees, so this whole sequence
works on a fresh clone with no hand-cloned kernel — see
Verification.
A worked example: a second RK3588 board
A board on an existing SoC needs only a device file and a recipe — arch, soc, and
boot-method all reuse the shipped layers. Suppose my-board is another RK3588 module.
devices/my-board.toml:
description = "My RK3588 board"
soc = "rk3588" # reuse the shipped SoC layer
boot_method = "rockchip-rkbin"
supported_boot_methods = ["rockchip-rkbin"]
uboot_defconfig = "my-board-rk3588_defconfig" # research: must exist in u-boot
kernel_dtb = "rockchip/rk3588-my-board.dtb" # research: must exist in the kernel
device_config_fragments = [] # no board-specific kconfig deltas
supported_kernels = ["rk3588-mainline-7.1"]
default_kernel = "rk3588-mainline-7.1"
default_suite = "forky"
default_layout = "combined"
hostname = "my-board"
image_size = "2G"
[rkbin] # board-memory-specific DDR init
atf = "rk3588_bl31_v1.51.elf"
tpl = "rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.19.bin"
recipes/my-board-forky.toml:
device = "my-board"
kernel = "rk3588-mainline-7.1"
suite = "forky"
features = ["media-accel-rockchip"] # or [] for a plain image
layout = "combined"
Then run the bring-it-up sequence against my-board-forky. A new SoC
would additionally need socs/<soc>.toml (with the required userspace/ffmpeg stanzas) and
its fragments; a new family would need the code change from
What needs code.
Document your board
Give each board a page under Boards, the way the Turing RK1 page does, since flashing is inherently per-board — a Turing Pi module flashes through the BMC, a standalone SBC takes an SD card or a maskrom loader, a laptop boots UEFI. A useful skeleton:
# <Board name>
The `<recipe>` recipe builds a bootable Debian <suite> image for the <board> (<SoC>).
It pins kernel `<ver>`, u-boot `<ver>`, and <features>.
Build it as in [Getting started](../getting-started.md):
cargo run -p boot2deb-cli -- build <recipe>
## Flash
<how this board takes an image: card reader, BMC, maskrom, UEFI…>
## Serial console
<UART pins / adapter / baud>
## First boot
<credentials, resize-on-first-boot, hostname>