Building the Linux System for a Matter Gateway: RDK X5 SDK and Buildroot in WSL

Compiling the D-Robotics RDK X5 SDK in WSL and baking a Rust gateway into a Buildroot image: cross-compiling, overlays, disabled services, autostart, and the IPv6-off-by-default kernel.

The previous post covered writing the gateway program in Rust with rs-matter. Right now it runs on a PC. To turn it into a gateway you can actually sell, one layer is still missing: the gateway’s own Linux system.

Dev boards usually ship with an Ubuntu image that works the moment you plug them in. Most people’s first instinct: just install the gateway program on Ubuntu and call it done?

Fine for development. Not for production. This post documents how we built the D-Robotics RDK X5 SDK inside WSL and used Buildroot to produce the gateway system image, plus a few pitfalls you only find by actually doing it.

Why not just use Ubuntu

The D-Robotics X5 SDK itself offers two root filesystems: an Ubuntu 22.04 image for development and Buildroot for production. For a gateway, the difference is obvious:

Ubuntu image Custom Buildroot system
Size Several GB A few hundred MB; the system partition is only 250MB
What’s inside A full desktop-grade set of packages Only what you choose to include
Boot services Lots, and you’re not sure which are running Every one is visible and removable
Updates apt, package by package Whole system image replaced; with A/B partitions you can roll back
Security review You have to justify a pile of software you don’t use Small attack surface, easy to explain

A gateway sits in someone’s home and nobody logs into it for years. What it needs: start the gateway program at boot, a read-only system that can’t be corrupted by writes, and automatic fallback to the previous version if an update fails. All of that is standard practice in Buildroot; on Ubuntu you’d have to fight to strip things down.

Why start on the RDK X5

In the main processor selection post, we put the RDK X5 under “works, but not a good fit for a gateway”: 10 TOPS of AI compute that a gateway will never use, and a relatively high cost.

But it has one very practical advantage: the SDK is publicly downloadable, with official documentation for building, system trimming, secure boot, A/B OTA and OP-TEE secure storage. Rockchip’s SDK is also Buildroot-based and works much the same way. So for the system customization step, we used the best-documented SDK, the X5’s, to get the whole workflow running end to end first.

A caveat up front: this post covers an image that has been built but not yet flashed and tested on a board. Flashing and real-world runs come in a later post once the board arrives.

Environment: a dedicated WSL distro

X5 SDK LNX6.1.176_PL5.1_V2.0.0 requires Ubuntu 22.04 and takes about 9.5GB once extracted. My daily driver is Ubuntu 24.04 and I didn’t want the SDK’s dependencies mixed in, so I installed a separate distro with its virtual disk on the D drive:

wsl --install -d Ubuntu-22.04 --name x5build --location 'D:\wsl\x5build' --no-launch

When running wsl commands from Git Bash, prefix them with MSYS_NO_PATHCONV=1, otherwise Git Bash rewrites paths starting with / into Windows paths.

Main components in the SDK:

Component Version
Linux kernel 6.1.176
U-Boot 2022.10
ATF 2.8
Buildroot 2025.02
Root filesystem glibc, SysV init
Cross toolchain ARM GNU Toolchain 11.3 (aarch64), bundled with the SDK

Fetching the code, installing dependencies and the first full build:

B=https://archive.d-robotics.cc/downloads/sdk/LNX6.1.176_PL5.1_V2.0.0/board_support_package
curl -O $B/platform_source_code.tar.gz.md5sum
curl -C - --retry 5 -O $B/platform_source_code.tar.gz   # 约 4.1GB,断了能续传
md5sum -c platform_source_code.tar.gz.md5sum
tar -I pigz -xf platform_source_code.tar.gz && cd platform_source_code

sudo build/install_host_deps.sh
sudo tar -xf toolchain/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu.tar.xz -C /opt/

./bd.sh lunch 8   # board_x5_soc_debug_config.mk
./bd.sh           # 第一次完整编译,约 12 分钟

lunch 8 is the debug configuration, with adb and debugging tools; for production you switch to the release configuration, lunch 9.

Watch the memory too: my global WSL limit is 8GB, while the SDK recommends 16GB. For now I’m only building the gateway and repackaging, and 8GB is enough. If you later need to build all of Buildroot from source and run short on memory, change memory= in C:\Users\<用户名>\.wslconfig and then run wsl --shutdown. Note that this shuts down every WSL distro.

Building the gateway program into the system

I wrapped the whole flow in a script, so after a code change it’s a single line on Windows:

wsl -d x5build -- bash /mnt/d/<项目目录>/x5/build.sh

The script does a few things: syncs the source, cross-compiles the gateway program, drops it into the root filesystem and repackages the image. A few details along the way are worth mentioning.

Sync the source into the Linux filesystem before building

The project code lives on the Windows D drive, which is /mnt/d inside WSL. Building Rust directly under /mnt/d is much slower, because every file access crosses into the Windows filesystem. So the script first uses rsync to copy the code into a directory on WSL’s own filesystem (excluding target and .git), and builds there.

The glibc target, not musl

Back during main processor selection, we planned to build a statically linked binary with aarch64-unknown-linux-musl that would work on both Rockchip and D-Robotics.

On the X5 we switched to aarch64-unknown-linux-gnu, with the linker pointed at the SDK’s bundled GCC 11.3:

CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/opt/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gcc \
  cargo build --release --target aarch64-unknown-linux-gnu

The reason is that the X5’s root filesystem is glibc to begin with. Linking with the SDK toolchain, whose glibc is older than the one in the firmware, guarantees the binary runs on the board, with no musl compatibility issues to deal with.

Overlay directory: restore first, then layer on top

The SDK provides a root filesystem overlay directory, system/buildroot/prebuilt/boot-utils-runtime; anything placed there goes into the image as-is. Our own files live in the project’s x5/overlay/.

On its first run, the script backs up the SDK’s original overlay directory as a snapshot. On every build after that, it restores the snapshot first, then layers our files on top.

The benefit is reversibility: delete a file from x5/overlay/ and the image goes back to the SDK original, with no leftovers from the previous build.

Disabling boot services we don’t need

Disabling services also works through the overlay: drop in a script with the same name containing only exit 0 to replace the SDK’s original init script. So far we’ve disabled:

Service Why it’s disabled
S50telnet Plaintext remote login, which doesn’t meet cybersecurity regulations like the EU’s EN 18031 or the UK’s PSTI
S40bluetoothd The module’s built-in Bluetooth isn’t used; Bluetooth Mesh goes through a separate Silicon Labs module
S65ptp4l, S66phc2sys PTP precision time sync, which a gateway doesn’t need
S90cam-service Camera service
S99iiod IIO sensor service

sshd and adb stay for now during development and get disabled for production.

Autostart: the pitfall behind a single &

The gateway program has to start automatically at boot and restart if it crashes. The X5 uses SysV init, so an /etc/init.d/S95gateway script does the job.

First thing to watch out for: there’s no bash on the board. Starting with V2.0.0, the X5 SDK no longer ships bash, so system scripts must be POSIX sh.

The second pitfall is subtler. The most obvious approach is to put the supervisor loop in the background in the init script:

while true; do /usr/bin/gateway; sleep 2; done &

Looks fine. But at boot, when rcS finishes running all the init scripts and exits, it sends a hangup signal to the background processes it started, and the loop gets killed along with it. The gateway program starts for a split second at boot, then vanishes.

The right way is start-stop-daemon -b, which detaches the daemon from rcS’s session:

LOOP="cd /userdata/gateway && while true; do /usr/bin/gateway </dev/null 2>&1 | logger -t gateway; sleep 2; done"
start-stop-daemon -S -b -m -p /var/run/gateway.pid -x /bin/sh -- -c "$LOOP"

If the program exits abnormally, it restarts after 2 seconds, and its output goes to the system log via logger. There’s no logread on the X5; logs are in /userdata/log/kernel/message, so use grep gateway to find them. Gateway data goes in /userdata/gateway/data/: that partition is writable, while the system partition is read-only.

Two SDK defaults you must change

IPv6 is off in the kernel by default

This was the nastiest pitfall of the whole process.

The X5 SDK’s default kernel config says # CONFIG_IPV6 is not set, in both hobot_x5_soc_defconfig and hobot_x5_soc_perf_defconfig.

And Matter is built on IPv6: mDNS for device discovery and the communication between Apple Home and the gateway all go over IPv6. With the SDK’s default kernel, the gateway program starts, but Apple Home will never find it.

The script checks both configs, flips them to CONFIG_IPV6=y if needed, and rebuilds the kernel with ./bd.sh boot. This needs re-checking whenever you move to a new SDK version.

Partition table: dropping the 700MB app partition

The SDK’s default partition table includes a 700MB app partition that holds nothing but D-Robotics sample programs, which the gateway has no use for. The script uses a small bit of Python to remove it from x5-soc-debug-gpt.json and x5-soc-release-gpt.json.

Once the partition table changes, you need a full rebuild to generate a new GPT. The script checks whether the change has already been made and skips it if so, so it doesn’t do a full build every time.

With it gone, the system partition is 250MB and userdata is only 50MB. 50MB is enough for the gateway’s device table and Matter data for now, but it must be re-planned around real requirements before production.

What comes out

File Purpose
out/product/emmc_disk.img Full eMMC image, flashable via fastboot, DFU or TFTP
out/product/emmc_disk.simg Sparse image, smaller, but doesn’t work with DFU
out/product/uart_usb/ Bootloader firmware for flashing blank chips
out/product/*.img Individual per-partition images

The xburn flashing tool is in software_tools/download_tools/ in the SDK download directory.

Still to do

Building the image is only the first step. Before production, there’s still:

  1. Flash and test on a board: autostart, IPv6, Matter commissioning and logging all need a run-through on real hardware.
  2. A/B partitions and OTA: the SDK comes with seamless A/B updates. ./bd.sh otapackage generates full and delta update packages, and the device side provides a C interface in libupdate.so; our program handles downloading and calling it. But the default is a single partition, so the partition table has to be changed to enable it. That way there’s no need to integrate RAUC or SWUpdate separately.
  3. Switch to the release configuration: remove adb and the debugging tools, disable sshd.
  4. Secure boot: the verification at each stage after U-Boot has to switch to our own signing keys, burned into eFuse. Burning eFuse is irreversible, so validate on a handful of samples first.

Wrapping up

Looking back, what took time in this step wasn’t the build commands themselves; the SDK’s bd.sh wraps them up nicely. What really took time were the defaults: IPv6 off in the kernel, no bash on the board, background processes killed by rcS, a 700MB sample partition stuffed into the partition table. None of them throws an error. They just make the gateway quietly stop working at some step.

Next up: once the board arrives, flash the image and hook up the Silicon Labs Bluetooth module, so the gateway controls a Bluetooth Mesh light on real hardware for the first time.

If you’re also building gateways or IoT devices on the D-Robotics X5, Rockchip or another Buildroot SDK, I’d love to compare notes: linktr.ee/weiyii

Read and share with WeChat

QR code for this article

Scan to open, then share from the WeChat menu.