The robot's computer · Chapter 07 · Time: 3 hours · Level: Intermediate · Status: Partly test-built
The LiDAR gets a serial port with a fixed name (/dev/lidar), the Wi-Fi card works and stays awake, Bluetooth loads its firmware, your user can open the depth camera, and you know why the controller board needs nothing.
The stock NVIDIA kernel (5.15.148-tegra, L4T R36.4.3) knows only part of this robot. The controller board and
the Ethernet port work out of the box. The LiDAR has no serial port, the Wi-Fi card has no driver, and the
Bluetooth radio starts without its firmware and finds nothing. This chapter builds the two missing kernel modules
from source for this exact kernel, hands the Bluetooth radio to the driver NVIDIA ships for it, and writes the udev
rules that give the LiDAR a stable name and let your user open the depth camera. The factory image solved the same
problems with vendor modules; this build uses mainline Linux driver code and nothing from Hiwonder.
Everything here was done on this robot between 2026-09-26 and 2026-10-07. Two scripts in this chapter were written
down after the fact from commands run one by one; the boxes say which.
New idea: a kernel module
A kernel module is a piece of kernel code in a.kofile that the kernel loads while it runs. Most device
drivers are modules. A module is built against one exact kernel build: its vermagic string (for example
5.15.148-tegra SMP preempt mod_unload modversions aarch64) must match the running kernel, or the kernel
refuses it. Modules live under/lib/modules/<kernel release>/. On this robot/etc/depmod.dsays
search updates ubuntu built-in, so a module in theupdates/folder wins over any other copy.depmod
rebuilds the index of modules and the device IDs each one handles;modprobeloads a module by name or by
device ID. That index is why a driver loads by itself at boot: the kernel announces "PCI device 10ec:c852" and
modprobefinds the module that claims it. A module you build yourself is not signed by NVIDIA; the kernel
loads it and sets a "tainted" flag, which only marks the kernel as running outside code.
New idea: a udev rule
When the kernel finds a device it tellsudev, a system service that creates the files under/dev. A udev
rule is one line in/etc/udev/rules.d/*.rules: a set of conditions (SUBSYSTEM=="tty",
KERNELS=="1-2.2.4:1.0",ATTRS{idVendor}=="3251") and actions (SYMLINK+="lidar",MODE="0660",
GROUP="video"). Files are read in name order, so the number in front (60-) sets the order. Rules matter on a
robot because the kernel numbers serial ports in the order devices appear: the LiDAR can bettyUSB0today and
ttyUSB1after a cable is jiggled. A rule that matches the USB port path gives it one name that never moves.
New idea: a firmware file
Wi-Fi and Bluetooth chips have their own small processor. Its program, the firmware, is not stored on the chip;
the driver sends it at start-up from a file under/lib/firmware. The driver asks for a file by name (for
examplertw89/rtw8852c_fw.bin). If the file is missing, or the driver does not know the chip and asks for
nothing, the chip runs without its program and does little or nothing. That is exactly what happened to this
robot's Bluetooth.
New idea: serial ports on a robot
Microcontrollers and many sensors talk over a serial line: a stream of bytes at a fixed speed (the baud rate).
On a computer that stream appears as a tty device file. A USB-to-serial chip such as the CH340 appears as
/dev/ttyUSBn; a microcontroller that speaks the USB "CDC ACM" standard appears as/dev/ttyACMn. This robot
has one of each: the controller board (1,000,000 baud) and the LiDAR (230400 baud). Both start streaming data
as soon as the port is open.
| Part | ID | Where | Driver | Device | On the stock system |
|---|---|---|---|---|---|
| Controller board (STM32) | USB 1a86:55d4 |
USB port 1-2.1 | cdc_acm (in the stock kernel) |
/dev/ttyACM0 |
works |
| COIN-D6 LiDAR (CH340 adapter) | USB 1a86:7523 |
USB port 1-2.2.4 | ch341 (missing) |
/dev/ttyUSB0, /dev/lidar |
no tty |
| Aurora 930 depth camera | USB 3251:1930 |
USB port 1-2.2.1 | none in the kernel; the vendor program opens the raw USB device | /dev/bus/usb/001/NNN |
needs a permission rule |
| Wi-Fi RTL8852CE | PCI 10ec:c852 |
0001:01:00.0 |
rtw89 (missing; stock only has rtl8822ce) |
wlP1p1s0 |
no Wi-Fi |
| Bluetooth (same Realtek chip) | USB 0bda:0852 |
USB port 1-3 | btusb binds but loads no firmware; NVIDIA's rtk_btusb works |
hci0 |
finds 0 devices |
| Ethernet | Realtek | PCI | r8168 (in the stock rootfs) |
enP8p1s0 |
works |
Look before you change anything. Run on the robot over the wired link (ssh rosorin; Wi-Fi does not exist yet):
On the robot:
lsusb
for d in /sys/bus/usb/devices/*; do [ -f $d/idVendor ] && [ "$(cat $d/idVendor)" = 1a86 ] && echo "$(basename $d) $(cat $d/idProduct) $(ls $d | grep -E ":1\.0$" | while read i; do readlink $d/$i/driver 2>/dev/null | xargs -r basename; done)"; done
modinfo -n ch341; zcat /proc/config.gz | grep -E "CONFIG_USB_SERIAL(_CH341)?="
Check
On 2026-09-26 the loop andmodinfoprinted:1-2.1 55d4 cdc_acm 1-2.2 8091 hub 1-2.2.4 7523 modinfo: ERROR: Module ch341 not found. CONFIG_USB_SERIAL=mThe controller board (
55d4) has its driver. The LiDAR's CH340 (7523) has none: the stock kernel was built
with# CONFIG_USB_SERIAL_CH341 is not set.lsusblists1a86:7523 QinHeng Electronics CH340 serial converter,3251:1930 Linux Foundation Aurora 930and0bda:0852 Realtek Semiconductor Corp. Bluetooth Radio.
Both modules are compiled on the robot itself, against the kernel headers NVIDIA installs with L4T.
On the robot:
ls -la /lib/modules/$(uname -r)/build; dpkg-query -W -f='${Package} ${Version} ${db:Status-Abbrev}\n' nvidia-l4t-kernel-headers; gcc --version | head -1; make --version | head -1
Check
On 2026-09-26:lrwxrwxrwx. 1 root root 102 Jan 8 2025 /lib/modules/5.15.148-tegra/build -> /usr/src/linux-headers-5.15.148-tegra-ubuntu22.04_aarch64/3rdparty/canonical/linux-jammy/kernel-source nvidia-l4t-kernel-headers 5.15.148-tegra-36.4.3-20250107174145 hi gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0 GNU Make 4.3Nothing was installed for this: the headers came with the flashed rootfs.
himeans installed and held (the
hold comes from chapter 9 on this robot; on your build it may still readii).
Do not upgrade the kernel
A plainapt upgradebefore the L4T packages are held would pull NVIDIA's 36.4.7 packages, including a new
kernel and a bootloader package that updates the QSPI flash (docs/decisions.md 2026-09-26). Every module you
build in this chapter would then stop loading. Chapter 9 holds allnvidia-l4t-*packages before its upgrade.
Until then, install nothing with apt except what a chapter tells you to.
All install scripts in this guide live in ~/setup on the robot. Write each file there yourself as the chapter
explains it, or copy it from your clone of the repo on the Mac. The scripts were copied this way on this robot:
On your laptop:
ssh rosorin 'mkdir -p ~/setup ~/build'
cd ~/CCode/rosorin-pro/scripts
scp -q build_ch341.sh install_ch341.sh rollback_ch341.sh build_rtw89.sh install_rtw89.sh rollback_rtw89.sh rosorin:setup/
scp -q setup/wifi_powersave_off.sh setup/robot_bt_rtk.sh rosorin:setup/
The STM32 board enumerates as a standard CDC ACM device, and the stock kernel's cdc_acm driver binds it.
Your user can open it because /dev/ttyACM0 belongs to group dialout and burgerbarn is in that group
(chapter 4 created the user with it).
On the robot:
ls -l /dev/ttyACM0
readlink -f /sys/class/tty/ttyACM0/device/driver
id
Check
Read on the robot on 2026-10-07:crw-rw---- 1 root dialout 166, 0 Oct 7 20:26 /dev/ttyACM0 /sys/bus/usb/drivers/cdc_acmand
idlists20(dialout)among the groups. The boot log says
cdc_acm 1-2.1:1.0: ttyACM0: USB ACM device.
The factory image gave the board the name /dev/rrc with its own udev rule. This rebuild does not: the base
driver opens /dev/ttyACM0 directly (ros2/rosorin_base/launch/base.launch.py line 26). The board is the only
ACM device on the robot (ls /dev/ttyACM* lists only ttyACM0). udev also makes
/dev/serial/by-id/usb-1a86_USB_Single_Serial_5C67040084-if00 for it; nothing uses that name.
If it fails
- Two programs reading one serial port silently split the bytes between them (the IMU looked like 60 Hz
instead of 110). The driver in chapter 10 opens the port exclusively (TIOCEXCL), so a second opener gets
"busy" instead. Do not read the port whilerosorin-baseruns.- When the board's USB drops, it comes back as
ttyACM0after about 3 s (tested 2026-09-28, noted in
stop_motors.py). How the running driver behaves through such a drop is untested (docs/lessons.md).
The COIN-D6 LiDAR sits behind a CH340 USB-serial chip. Linux has a driver for it, ch341, but NVIDIA's kernel
was built without it, and the headers package does not contain its source (ch341.c not in headers, checked
2026-09-26). The source comes from NVIDIA's public kernel sources for exactly this release. The factory image used
WCH's vendor module instead, which names the port ttyCH341USB*; the mainline driver names it ttyUSB*.
scripts/build_ch341.sh is 18 lines. Piece by piece:
On the robot:
#!/bin/bash
# Build mainline ch341.ko (USB CH340/CH341 serial) for the running L4T kernel, from NVIDIA's
# R36.4.3 public sources. Runs as a normal user on the robot (needs nvidia-l4t-kernel-headers, gcc).
# Stock R36.4.3 kernel has CONFIG_USB_SERIAL_CH341 unset. Rebuild after any L4T kernel update.
set -euo pipefail
B=~/build; mkdir -p "$B/ch341"; cd "$B"
set -euo pipefail stops the script at the first failing command, unset variable or failing pipe stage. Everything
happens under ~/build.
On the robot:
URL=https://developer.download.nvidia.com/embedded/L4T/r36_Release_v4.3/sources/public_sources.tbz2
[ -f public_sources.tbz2 ] || curl -fL -o public_sources.tbz2 "$URL"
echo "2c177804679e3ed650dabec6fa958388579896f170570c6171a1b6c386669216 public_sources.tbz2" | sha256sum -c -
Download NVIDIA's source bundle once (226,034,384 bytes) and check it against the SHA-256 recorded on 2026-09-26.
If the file is damaged or different, sha256sum -c fails and the script stops.
On the robot:
tar xjf public_sources.tbz2 Linux_for_Tegra/source/kernel_src.tbz2 Linux_for_Tegra/source/kernel_src.tbz2.sha1sum
cd Linux_for_Tegra/source
# NVIDIA's .sha1sum references their build path; compare the hash only
[ "$(sha1sum kernel_src.tbz2 | cut -d' ' -f1)" = "$(cut -d' ' -f1 kernel_src.tbz2.sha1sum)" ]
Take only the kernel tarball and NVIDIA's checksum file out of the bundle, and compare the hashes. The bracket test
fails (and set -e stops the script) if they differ.
On the robot:
P=kernel/kernel-jammy-src/drivers/usb/serial/ch341.c
tar xjf kernel_src.tbz2 "$P"; cp "$P" "$B/ch341/ch341.c"
cd "$B/ch341"; printf 'obj-m := ch341.o\n' > Makefile
make -C /lib/modules/$(uname -r)/build M="$PWD" modules
modinfo -F vermagic ch341.ko; sha256sum ch341.ko
Extract the one source file (889 lines), write a one-line Makefile that says "build ch341.o as a module", and
let the kernel's own build system compile it against the running kernel's headers (-C .../build M=<this folder>
is the standard way to build an out-of-tree module). The last line prints the vermagic and a hash so you can
compare.
The complete file, ~/setup/build_ch341.sh:
On the robot:
#!/bin/bash
# Build mainline ch341.ko (USB CH340/CH341 serial) for the running L4T kernel, from NVIDIA's
# R36.4.3 public sources. Runs as a normal user on the robot (needs nvidia-l4t-kernel-headers, gcc).
# Stock R36.4.3 kernel has CONFIG_USB_SERIAL_CH341 unset. Rebuild after any L4T kernel update.
set -euo pipefail
B=~/build; mkdir -p "$B/ch341"; cd "$B"
URL=https://developer.download.nvidia.com/embedded/L4T/r36_Release_v4.3/sources/public_sources.tbz2
[ -f public_sources.tbz2 ] || curl -fL -o public_sources.tbz2 "$URL"
echo "2c177804679e3ed650dabec6fa958388579896f170570c6171a1b6c386669216 public_sources.tbz2" | sha256sum -c -
tar xjf public_sources.tbz2 Linux_for_Tegra/source/kernel_src.tbz2 Linux_for_Tegra/source/kernel_src.tbz2.sha1sum
cd Linux_for_Tegra/source
# NVIDIA's .sha1sum references their build path; compare the hash only
[ "$(sha1sum kernel_src.tbz2 | cut -d' ' -f1)" = "$(cut -d' ' -f1 kernel_src.tbz2.sha1sum)" ]
P=kernel/kernel-jammy-src/drivers/usb/serial/ch341.c
tar xjf kernel_src.tbz2 "$P"; cp "$P" "$B/ch341/ch341.c"
cd "$B/ch341"; printf 'obj-m := ch341.o\n' > Makefile
make -C /lib/modules/$(uname -r)/build M="$PWD" modules
modinfo -F vermagic ch341.ko; sha256sum ch341.ko
Run it as your normal user (not root):
On the robot:
bash ~/setup/build_ch341.sh
Check
The compile ends with these lines (2026-09-26 22:56 UTC):CC [M] /home/burgerbarn/build/ch341/ch341.o MODPOST /home/burgerbarn/build/ch341/Module.symvers CC [M] /home/burgerbarn/build/ch341/ch341.mod.o LD [M] /home/burgerbarn/build/ch341/ch341.koand on this robot the last two lines were:
5.15.148-tegra SMP preempt mod_unload modversions aarch64 e35a5fd29267e7f36f89243567a0ffc4d979d0d69e9388654d3997a0a367ce36 /home/burgerbarn/build/ch341/ch341.koThe first word of the vermagic must equal
uname -r. Whether a rebuild produces the same SHA-256 byte for
byte is not known; a different hash with the right vermagic is not by itself a fault.
Not test-built as a script
build_ch341.shwas written into the repo on 2026-09-26 23:27 UTC from commands that had been run one at a
time between 22:53 and 22:56. It was never run as a whole, and it is not on the robot (~/setuphas no copy).
Two of its lines differ from what ran: the source path was found withtar tjf kernel_src.tbz2 | grep
(result:kernel/kernel-jammy-src/drivers/usb/serial/ch341.c, now hard-coded), and the SHA-1 comparison
replaced asha1sum -cthat failed (see below).
If it fails
sha1sum -c kernel_src.tbz2.sha1sumreports/dvs/git/dirty/git-master_linux/out/out_src/kernel_src.tbz2: FAILED open or read. NVIDIA's checksum file names the path on their build machine. The hash itself
(a87adbe400f6e08742733cdb815dbf0b2df5596a) matched; that is why the script compares only the hash.ch341.cis not in/lib/modules/$(uname -r)/build: the headers package has no driver sources. Use the
public sources as above.- After any L4T kernel update the module no longer matches (wrong vermagic) and the LiDAR has no tty. Rebuild
and reinstall (docs/lessons.md 2026-09-26).
scripts/install_ch341.sh copies the module into the kernel's updates/ folder and writes the LiDAR's udev rule.
On the robot:
#!/bin/bash
# Install mainline ch341 (built from NVIDIA R36.4.3 kernel source) + /dev/lidar udev rule.
# Rollback: sudo bash ~/setup/rollback_ch341.sh
set -euo pipefail
[ "$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; }
KREL=$(uname -r); SRC=/home/burgerbarn/build/ch341/ch341.ko
DST=/lib/modules/$KREL/updates/ch341/ch341.ko; RULE=/etc/udev/rules.d/60-rosorin-lidar.rules
It must run as root, and it names the source (your build), the destination inside the running kernel's module tree,
and the rule file.
On the robot:
[ "$(modinfo -F vermagic "$SRC" | cut -d" " -f1)" = "$KREL" ] || { echo "vermagic mismatch"; exit 1; }
[ -e "$DST" ] && { echo "already installed at $DST"; exit 1; }
install -D -m 0644 "$SRC" "$DST"
depmod -a "$KREL"
Refuse a module built for another kernel, refuse to overwrite an existing install, copy the file, and rebuild the
module index so modprobe and autoloading find it.
On the robot:
echo 'SUBSYSTEM=="tty", KERNELS=="1-2.2.4:1.0", SYMLINK+="lidar"' > "$RULE"
udevadm control --reload
udevadm trigger --subsystem-match=tty --action=add
The rule: for a tty device whose USB parent is port 1-2.2.4, interface 0, add the symlink /dev/lidar. Port
1-2.2.4 is the LiDAR's place in the USB tree (bus 1, root port 2, hub port 2, hub port 4). Then udev reloads its
rules and replays the "add" event for existing ttys, so the link appears without a reboot.
On the robot:
sleep 1
echo "== verify"
modinfo -n ch341; sha256sum "$DST"
ls -l /dev/lidar /dev/ttyUSB* 2>&1
echo "CH341_INSTALLED"
The complete file, ~/setup/install_ch341.sh:
On the robot:
#!/bin/bash
# Install mainline ch341 (built from NVIDIA R36.4.3 kernel source) + /dev/lidar udev rule.
# Rollback: sudo bash ~/setup/rollback_ch341.sh
set -euo pipefail
[ "$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; }
KREL=$(uname -r); SRC=/home/burgerbarn/build/ch341/ch341.ko
DST=/lib/modules/$KREL/updates/ch341/ch341.ko; RULE=/etc/udev/rules.d/60-rosorin-lidar.rules
[ "$(modinfo -F vermagic "$SRC" | cut -d" " -f1)" = "$KREL" ] || { echo "vermagic mismatch"; exit 1; }
[ -e "$DST" ] && { echo "already installed at $DST"; exit 1; }
install -D -m 0644 "$SRC" "$DST"
depmod -a "$KREL"
echo 'SUBSYSTEM=="tty", KERNELS=="1-2.2.4:1.0", SYMLINK+="lidar"' > "$RULE"
udevadm control --reload
udevadm trigger --subsystem-match=tty --action=add
sleep 1
echo "== verify"
modinfo -n ch341; sha256sum "$DST"
ls -l /dev/lidar /dev/ttyUSB* 2>&1
echo "CH341_INSTALLED"
Run it, then reboot to prove the module loads by itself:
On the robot:
sudo bash ~/setup/install_ch341.sh
sudo systemctl reboot
After the robot is back (about 30 s on 2026-09-26):
On the robot:
lsmod | grep -E "^(ch341|usbserial) "
modinfo -n ch341
ls -l /dev/lidar /dev/ttyUSB*
echo "failed units: $(systemctl --failed --no-legend | wc -l)"
stty -F /dev/lidar 230400 raw -echo && timeout 2 cat /dev/lidar | wc -c
Check
The install script ends withCH341_INSTALLED. After the reboot on 2026-09-26 23:31 UTC:ch341 20480 0 usbserial 40960 1 ch341 /lib/modules/5.15.148-tegra/updates/ch341/ch341.ko lrwxrwxrwx 1 root root 7 Sep 26 23:31 /dev/lidar -> ttyUSB0 crw-rw---- 1 root dialout 188, 0 Sep 26 23:31 /dev/ttyUSB0 crw-rw---- 1 root dialout 188, 1 Sep 26 23:31 /dev/ttyUSB1 failed units: 0 2759927,599 bytes in 2 s: the LiDAR streams without being asked. The boot log shows
ch341 1-2.2.4:1.0: ch341-uart converter detectedandusb 1-2.2.4: ch341-uart converter now attached to ttyUSB0.ttyUSB1was the factory voice box's own CH340, which nothing uses; with the voice box unplugged (as
on the robot today) onlyttyUSB0exists.
If it fails
vermagic mismatch: the module was built for another kernel. Rebuild withbuild_ch341.shon the running
kernel.already installed at ...: runsudo bash ~/setup/rollback_ch341.shfirst./dev/ttyUSB0exists but/dev/lidardoes not: the LiDAR is not on USB port1-2.2.4. The rule follows the
port, not the device; keep the LiDAR's cable where the kit put it. Check the path with
udevadm info -q property -n /dev/ttyUSB0 | grep ID_PATH(on this robot:
ID_PATH=platform-3610000.usb-usb-0:2.2.4:1.0).- On 2026-09-28 the LiDAR disconnected and came back while the robot was handled, moving from
ttyUSB1to
ttyUSB0. The symlink follows it; the LiDAR reader in chapter 14 also reopens/dev/lidarafter an error.- The journal fills with
brltty: Ignored Byte: the Braille daemon is grabbing serial devices. Chapter 6
purges it. It was not the reason the LiDAR had no tty.- The boot log says
module verification failed ... tainting kernelfor an out-of-tree module: expected.
The module is unsigned; the kernel still loads it.
Undo, if you need to: sudo bash ~/setup/rollback_ch341.sh removes the module and the rule and prints
CH341_ROLLED_BACK.
The RTL8852CE card needs the rtw89 driver, which entered mainline Linux after 5.15. The stock rootfs only has
rtl8822ce, for a different chip, so there is no Wi-Fi until you build one. The source used is
github.com/a5a5aa555oo/rtw89, branch 6.6-lts: a backport of the mainline driver whose README says it supports
kernels 5.15 to 6.5. The better-known morrownr/rtw89 needs 6.6 or newer. The factory image used the same backport
(DKMS rtw89 6.6.95). The owner approved this source on 2026-09-27.
The firmware the driver needs is already in the stock rootfs, and the kernel's Wi-Fi stack is built as modules:
On the robot:
ls /lib/firmware/rtw89/; zcat /proc/config.gz | grep -E "^CONFIG_(MAC80211|CFG80211)="
Check
On 2026-09-27:rtw8851b_fw.bin rtw8852a_fw.bin rtw8852b_fw-1.bin rtw8852b_fw.bin rtw8852c_fw.bin CONFIG_CFG80211=m CONFIG_MAC80211=m
scripts/build_rtw89.sh, 16 lines:
On the robot:
#!/bin/bash
# Build rtw89 (RTL8852CE Wi-Fi, PCI 10ec:c852) for the running L4T kernel.
# Source: github.com/a5a5aa555oo/rtw89 branch 6.6-lts = backport of mainline Linux rtw89,
# README: kernels 5.15-6.5 only. Stock R36.4.3 kernel (5.15.148-tegra) has no rtw89.
# Runs as a normal user on the robot. Rebuild after any L4T kernel update.
# Usage: bash build_rtw89.sh [commit] (no arg = branch head; commit is printed, record it)
set -euo pipefail
B=~/build/rtw89
[ -d "$B/.git" ] || git clone -b 6.6-lts https://github.com/a5a5aa555oo/rtw89.git "$B"
cd "$B"
[ -n "${1:-}" ] && git checkout -q "$1"
git log -1 --format='commit %H %cI %s'
Clone the branch once into ~/build/rtw89. If you pass a commit, check it out; either way print which commit you
are building, so you can write it down.
On the robot:
make KVER="$(uname -r)" clean >/dev/null
make KVER="$(uname -r)" -j"$(nproc)" modules
for m in *.ko; do printf '%s %s\n' "$(modinfo -F vermagic "$m" | cut -d' ' -f1)" "$(sha256sum "$m")"; done
echo RTW89_BUILT
The project's own Makefile takes the kernel release in KVER. It builds all the chips the driver supports (ten
.ko files); only the 8852C ones are used here. The loop prints, for each module, the kernel it was built for and
its hash.
Run it with the commit this robot runs, 2310939e2002117cd2b223c6d4f997e537142b78:
On the robot:
bash ~/setup/build_rtw89.sh 2310939e2002117cd2b223c6d4f997e537142b78
Check
The script prints the commit first. On the robot today:commit 2310939e2002117cd2b223c6d4f997e537142b78 2025-07-21T01:42:00+08:00 Revert "rtw89: 8852b: update fw to v0.29.128.0"Then ten lines that each start with
5.15.148-tegra(one per module:rtw89_core_git.ko,
rtw89_pci_git.ko,rtw89_8852c_git.ko,rtw89_8852ce_git.koand six for other chips), andRTW89_BUILT.
Not test-built with the commit argument
On 2026-09-27 the owner ran the clone and the build himself, without a commit argument; the branch head at
that moment was2310939e, the commit recorded in docs/hardware.md and still checked out in~/build/rtw89.
Building with the commit passed as an argument has not been run. If the branch has moved since, a build
without the argument gives you different code from this robot's.
scripts/install_rtw89.sh:
On the robot:
#!/bin/bash
# Install rtw89 modules built by build_rtw89.sh. Firmware rtw89/rtw8852c_fw.bin ships in stock rootfs.
# Rollback: sudo bash ~/setup/rollback_rtw89.sh
set -euo pipefail
[ "$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; }
KREL=$(uname -r); SRC=/home/burgerbarn/build/rtw89; DST=/lib/modules/$KREL/updates/rtw89
[ -e "$DST" ] && { echo "already installed at $DST"; exit 1; }
ls "$SRC"/*.ko >/dev/null
for m in "$SRC"/*.ko; do
[ "$(modinfo -F vermagic "$m" | cut -d' ' -f1)" = "$KREL" ] || { echo "vermagic mismatch $m"; exit 1; }
done
install -d "$DST"; install -m 0644 "$SRC"/*.ko "$DST"/
depmod -a "$KREL"
modprobe rtw89_8852ce_git
sleep 3
echo "== verify"
lsmod | grep rtw89; dmesg | grep -i rtw89 | tail -15
ip -br link | grep -v -E '^(lo|can0|l4tbr0|usb)'
nmcli -f DEVICE,TYPE,STATE dev
echo RTW89_INSTALLED
Same pattern as ch341: root only, refuse if already installed, check every module's vermagic, copy into
updates/rtw89/, rebuild the index. Then it loads rtw89_8852ce_git (the PCI glue for the 8852CE; it pulls in
the core, PCI and chip modules it depends on) and shows what happened.
On the robot:
sudo bash ~/setup/install_rtw89.sh
Check
On 2026-09-27 22:47 UTC the verify part printed:== verify rtw89_8852ce_git 16384 0 rtw89_8852c_git 806912 1 rtw89_8852ce_git rtw89_pci_git 61440 1 rtw89_8852ce_git rtw89_core_git 348160 2 rtw89_8852c_git,rtw89_pci_git mac80211 778240 2 rtw89_pci_git,rtw89_core_git cfg80211 765952 3 rtw89_8852c_git,rtw89_core_git,mac80211 [25535.479154] rtw89_8852ce_git 0001:01:00.0: Adding to iommu group 3 [25535.480038] rtw89_8852ce_git 0001:01:00.0: loaded firmware rtw89/rtw8852c_fw.binAt every boot since, the kernel log also shows
Firmware version 0.27.56.14 (1942d927),
chip rfe_type is 1andwlP1p1s0: renamed from wlan0.
Prove it will load by itself at boot (the PCI ID is in the module index) and that the radio sees networks:
On the robot:
modinfo -F alias rtw89_8852ce_git | grep -i c852; modprobe -R "pci:v000010ECd0000C852sv*sd*bc*sc*i*"
sudo nmcli dev wifi rescan; sleep 5; nmcli -t -f SSID,CHAN,SIGNAL dev wifi list | wc -l; nmcli -t -f CHAN dev wifi list | sort -n | uniq | tr "\n" " "
Check
On 2026-09-27:pci:v000010ECd0000C852sv*sd*bc*sc*i* rtw89_8852ce_git 33 1 6 11 52 60 100 11633 networks on 2.4 GHz and 5 GHz channels.
If it fails
vermagic mismatch .../rtw89_xxx.ko: built for another kernel. Rebuild after any L4T kernel update, like
ch341.already installed at /lib/modules/.../updates/rtw89: runsudo bash ~/setup/rollback_rtw89.shfirst
(it unloads everyrtw89*module, removes the folder and printsRTW89_ROLLED_BACK).- The kernel log shows
rtw89_core_git: module verification failed: signature and/or required key missing - tainting kernel: expected for an unsigned module.
Replace <your-wifi-name> with your network's name. --ask makes nmcli prompt for the password instead of taking it
on the command line, so it never lands in your shell history. It needs a terminal, so run it in an interactive ssh
session:
On the robot:
sudo nmcli --ask dev wifi connect <your-wifi-name>
Use your own network's name. NetworkManager saves the connection with that name and connects to it at every boot.
On the robot:
nmcli -f DEVICE,STATE,CONNECTION dev | grep wl; ip -br -4 addr show wlP1p1s0; iw dev wlP1p1s0 link | grep -E "freq|signal|tx bitrate|rx bitrate"; nmcli -g connection.autoconnect con show <your-wifi-name>
Check
On 2026-09-27 23:12 UTC:wlP1p1s0 connected <your-wifi-name> p2p-dev-wlP1p1s0 disconnected -- wlP1p1s0 UP 192.168.1.108/24 freq: 5300 signal: -44 dBm rx bitrate: 258.0 MBit/s HE-MCS 10 HE-NSS 2 HE-GI 0 HE-DCM 0 tx bitrate: 286.7 MBit/s HE-MCS 11 HE-NSS 2 HE-GI 0 HE-DCM 0 yesThen from the Mac,
ssh rosorin-wifi hostnameprintsrosorin. The router hands out 192.168.1.108 to the
Wi-Fi MAC<robot-wifi-MAC>(a reservation the owner made on 2026-09-29, chapter 3). With both links up,
the default route is Ethernet.
If it fails
- From the Mac,
ssh burgerbarn@192.168.1.108stops withWARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!.
The factory install used the same address, and~/.ssh/known_hostsstill holds its keys. The ssh aliases
from chapter 6 carryHostKeyAlias 192.168.1.22, so bothrosorinandrosorin-wificheck the rebuilt
robot's key. Use the alias.
On 2026-10-07 at 17:44 UTC the Wi-Fi firmware hung while leaving power-save mode. The kernel logged
SER catches error: 0x1000 and firmware failed to ack for leaving ps mode, dumped the firmware state, and the
link did not come back: the robot was off the network, with everything else still running, until the owner
rebooted it. The same errors had appeared about twice an hour before (23 times on 2026-10-05/06), and the link had
always recovered. Power save was on because Ubuntu ships default-wifi-powersave-on.conf
(wifi.powersave = 3).
NetworkManager reads the files in /etc/NetworkManager/conf.d/ in name order and a later file wins. The fix is a
file whose name sorts after default-.... scripts/setup/wifi_powersave_off.sh, 12 lines:
On the robot:
#!/bin/bash
# Robot: WiFi power save off (2026-10-07: rtw89_8852ce firmware hung leaving power save, robot fell off the network).
# Run on the robot. --undo restores Ubuntu's default (power save on).
F=/etc/NetworkManager/conf.d/zz-wifi-powersave-off.conf # must sort after default-wifi-powersave-on.conf to win
IF=$(iw dev | awk '/Interface/{print $2; exit}')
if [ "$1" = --undo ]; then
sudo rm -f "$F"; sudo nmcli general reload conf; sudo iw dev "$IF" set power_save on
else
printf '# 2026-10-07: rtw89_8852ce firmware hung leaving power save; 2 = disable\n[connection]\nwifi.powersave = 2\n' | sudo tee "$F" >/dev/null
sudo nmcli general reload conf; sudo iw dev "$IF" set power_save off
fi
echo "config: $(NetworkManager --print-config 2>/dev/null | grep wifi.powersave) | live: $(iw dev "$IF" get power_save)"
IF is the first wireless interface iw reports (wlP1p1s0).wifi.powersave = 2, which NetworkManager defines as "disable". nmcli general reload conf makes NetworkManager read it again; iw ... set power_save off turns power save off on the running cardOn the robot:
bash ~/setup/wifi_powersave_off.sh
Check
Expected last line:config: wifi.powersave=2 | live: Power save: offRead on the robot on 2026-10-07 at 20:29 UTC:
Power save: off,wifi.powersave=2, and zero
SER catches error/failed to ack for leaving ps modelines in the kernel log since power save was turned
off at 17:52.
If it fails
The first attempt on 2026-10-07 named the file99-wifi-powersave-off.conf. Digits sort before letters, so
Ubuntu'sdefault-wifi-powersave-on.confwas read after it and won:NetworkManager --print-configstill said
wifi.powersave=3. Renamed tozz-..., it sayswifi.powersave=2. If the config line shows 3, list the folder
and check the order.
Not test-built as a script; not yet proven
The commands inside the script were run by hand on 2026-10-07 17:52 UTC (same file content, same reload, same
iwcommand); the script itself has not been run. Whether power save off stops the firmware hangs is not
proven: one afternoon without an error is not the 2-per-hour baseline measured over days. Undo with
bash ~/setup/wifi_powersave_off.sh --undo.
The Wi-Fi card's Bluetooth half is a USB device, 0bda:0852. The stock kernel binds it to the generic btusb
driver, whose Realtek helper on 5.15 does not know the chip: the kernel logs unknown IC info, lmp subver 8852 ... assuming no firmware upload needed, loads no firmware, and a 20 s discovery on 2026-10-07 found zero devices in a
house full of them.
NVIDIA ships a second driver for Realtek Bluetooth, rtk_btusb, in the package nvidia-l4t-kernel-oot-modules
(/lib/modules/5.15.148-tegra/updates/drivers/bluetooth/realtek/rtk_btusb.ko). It asks for firmware files named
rtl8852cu_fw and rtl8852cu_config at the top of /lib/firmware, the same layout NVIDIA uses for its own devkit
chip (/lib/firmware/rtl8822cu_fw, rtl8822cu_config). Ubuntu's linux-firmware package already contains the right
files under different names: rtl_bt/rtl8852cu_fw.bin and rtl_bt/rtl8852cu_config.bin. So the fix is two
symlinks, one line that stops btusb from taking the device at boot, and moving the device from one driver to the
other now.
scripts/setup/robot_bt_rtk.sh, 31 lines. The header and the device search:
On the robot:
#!/bin/bash
# Robot Bluetooth (RTL8852CE BT, USB 0bda:0852) on NVIDIA's rtk_btusb (nvidia-l4t-kernel-oot-modules) instead of the
# in-kernel btusb/btrtl, which on 5.15 does not know the chip ("unknown IC info, lmp subver 8852") and loads no
# firmware (2026-10-07: discovery found 0 devices). Firmware: the linux-firmware files, named the way NVIDIA ships its
# devkit chip's (/lib/firmware/rtl8822cu_fw + rtl8822cu_config at the top level). Run on the robot.
# robot_bt_rtk.sh firmware links + blacklist btusb (so rtk_btusb gets the chip at boot) + re-bind now
# robot_bt_rtk.sh --undo remove the links and the blacklist, give the device back to btusb
set -u
DEV=$(for d in /sys/bus/usb/devices/*; do [ "$(cat $d/idVendor 2>/dev/null):$(cat $d/idProduct 2>/dev/null)" = 0bda:0852 ] && basename $d; done | head -1)
[ -z "$DEV" ] && { echo "no 0bda:0852 on USB"; exit 1; }
DEV becomes the USB path of the Bluetooth device (1-3 on this robot), found by its vendor and product ID.
On the robot:
rebind() { # $1 = from driver, $2 = to driver
for i in /sys/bus/usb/devices/$DEV/$DEV:1.*; do
n=$(basename $i); [ -e /sys/bus/usb/drivers/$1/$n ] && echo $n | sudo tee /sys/bus/usb/drivers/$1/unbind >/dev/null
done
sleep 1
echo $DEV:1.0 | sudo tee /sys/bus/usb/drivers/$2/bind >/dev/null 2>&1
sleep 3
}
Every USB driver has bind and unbind files in /sys/bus/usb/drivers/<driver>/. Writing an interface name
(1-3:1.0) to unbind detaches it from that driver; writing it to another driver's bind attaches it there.
The function detaches every interface of the device from driver $1, then binds interface 0 to driver $2, which
claims the device's other interfaces itself.
On the robot:
if [ "${1:-}" = --undo ]; then
rebind rtk_btusb btusb
sudo rm -f /lib/firmware/rtl8852cu_fw /lib/firmware/rtl8852cu_config /etc/modprobe.d/rosorin-bt-rtk.conf
else
sudo ln -sfn rtl_bt/rtl8852cu_fw.bin /lib/firmware/rtl8852cu_fw
sudo ln -sfn rtl_bt/rtl8852cu_config.bin /lib/firmware/rtl8852cu_config
printf '# 2026-10-07: RTL8852CE BT (0bda:0852) belongs to rtk_btusb (NVIDIA oot); btusb/btrtl 5.15 loads no firmware for it\nblacklist btusb\n' | sudo tee /etc/modprobe.d/rosorin-bt-rtk.conf >/dev/null
sudo modprobe rtk_btusb
rebind btusb rtk_btusb
fi
The normal path: the two firmware links, blacklist btusb in /etc/modprobe.d/ (so at boot the kernel does not
load btusb for the device and rtk_btusb gets it), load rtk_btusb, and move the device over. --undo reverses
all of it.
On the robot:
for i in /sys/bus/usb/devices/$DEV/$DEV:1.*; do echo "$(basename $i) -> $(basename "$(readlink -f $i/driver 2>/dev/null)")"; done
journalctl -k --since "-20 s" --no-pager -o cat | grep -iE "rtk_btusb|bluetooth|hci" | grep -iE "fw|firmware|patch|version|error|fail|lmp" | tail -8
hciconfig -a 2>/dev/null | grep -E "hci|UP|Manufacturer|LMP" | head -4
The read-back: which driver each interface has now, what the driver logged about firmware in the last 20 s, and the
controller's state.
The complete file, ~/setup/robot_bt_rtk.sh:
On the robot:
#!/bin/bash
# Robot Bluetooth (RTL8852CE BT, USB 0bda:0852) on NVIDIA's rtk_btusb (nvidia-l4t-kernel-oot-modules) instead of the
# in-kernel btusb/btrtl, which on 5.15 does not know the chip ("unknown IC info, lmp subver 8852") and loads no
# firmware (2026-10-07: discovery found 0 devices). Firmware: the linux-firmware files, named the way NVIDIA ships its
# devkit chip's (/lib/firmware/rtl8822cu_fw + rtl8822cu_config at the top level). Run on the robot.
# robot_bt_rtk.sh firmware links + blacklist btusb (so rtk_btusb gets the chip at boot) + re-bind now
# robot_bt_rtk.sh --undo remove the links and the blacklist, give the device back to btusb
set -u
DEV=$(for d in /sys/bus/usb/devices/*; do [ "$(cat $d/idVendor 2>/dev/null):$(cat $d/idProduct 2>/dev/null)" = 0bda:0852 ] && basename $d; done | head -1)
[ -z "$DEV" ] && { echo "no 0bda:0852 on USB"; exit 1; }
rebind() { # $1 = from driver, $2 = to driver
for i in /sys/bus/usb/devices/$DEV/$DEV:1.*; do
n=$(basename $i); [ -e /sys/bus/usb/drivers/$1/$n ] && echo $n | sudo tee /sys/bus/usb/drivers/$1/unbind >/dev/null
done
sleep 1
echo $DEV:1.0 | sudo tee /sys/bus/usb/drivers/$2/bind >/dev/null 2>&1
sleep 3
}
if [ "${1:-}" = --undo ]; then
rebind rtk_btusb btusb
sudo rm -f /lib/firmware/rtl8852cu_fw /lib/firmware/rtl8852cu_config /etc/modprobe.d/rosorin-bt-rtk.conf
else
sudo ln -sfn rtl_bt/rtl8852cu_fw.bin /lib/firmware/rtl8852cu_fw
sudo ln -sfn rtl_bt/rtl8852cu_config.bin /lib/firmware/rtl8852cu_config
printf '# 2026-10-07: RTL8852CE BT (0bda:0852) belongs to rtk_btusb (NVIDIA oot); btusb/btrtl 5.15 loads no firmware for it\nblacklist btusb\n' | sudo tee /etc/modprobe.d/rosorin-bt-rtk.conf >/dev/null
sudo modprobe rtk_btusb
rebind btusb rtk_btusb
fi
for i in /sys/bus/usb/devices/$DEV/$DEV:1.*; do echo "$(basename $i) -> $(basename "$(readlink -f $i/driver 2>/dev/null)")"; done
journalctl -k --since "-20 s" --no-pager -o cat | grep -iE "rtk_btusb|bluetooth|hci" | grep -iE "fw|firmware|patch|version|error|fail|lmp" | tail -8
hciconfig -a 2>/dev/null | grep -E "hci|UP|Manufacturer|LMP" | head -4
On the robot:
bash ~/setup/robot_bt_rtk.sh
Check
On 2026-10-07 17:58 UTC:1-3:1.0 -> rtk_btusb 1-3:1.1 -> rtk_btusb rtk_btusb: number_of_total_patch = 2 rtk_btusb: patch_length 0xd5c9 rtk_btusb: Svn version: -1849729604 rtk_btusb: fw: exists, config file: exists rtk_btusb: load_firmware done rtk_btusb: read_ver_rsp->lmp_subver = 0xa40a rtk_btusb: patch_entry->lmp_sub = 0x8852 rtk_btusb: Rtk patch end 0 hci0: Type: Primary Bus: USB UP RUNNING LMP Version: (0xc) Subversion: 0xa40a Manufacturer: Realtek Semiconductor Corporation (93)The chip's version number changed from
0x8852(the bare ROM) to0xa40a(patched): the firmware is
running.
Now let the radio listen for 12 s and count what it hears:
On the robot:
{ echo "scan on"; sleep 12; echo "scan off"; sleep 1; echo "devices"; sleep 1; echo quit; } | bluetoothctl 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -E "^Device|NEW\] Device" | sed 's/.*Device //' | sort -u > /tmp/bt_scan.txt
echo "devices seen: $(wc -l < /tmp/bt_scan.txt)"
echo "wifi: $(cat /sys/class/net/wlP1p1s0/operstate) | rtw89 errors last 2 min: $(journalctl -k --since '-2 min' --no-pager -o cat | grep -c 'SER catches')"
Check
On 2026-10-07 17:59 UTC:devices seen: 16 wifi: up | rtw89 errors last 2 min: 0Before the fix, the same house gave 0 devices.
If it fails
no 0bda:0852 on USB: the radio is not enumerated; checklsusbforRealtek Semiconductor Corp. Bluetooth Radio.- After a reboot
hci0is bound tobtusbagain: the blacklist file is missing./boot/initrdcontains no
Bluetooth modules (checked 2026-10-07), so the file in/etc/modprobe.dis enough.- The Wi-Fi hang of 2026-10-07 came about 30 s after a Bluetooth discovery. The same Wi-Fi errors had been
logged before any Bluetooth use that day, so a link between the two is not proven.
Not yet seen across a real reboot
The robot last booted at 17:51 UTC on 2026-10-07, seven minutes before this fix. A boot was simulated
(btusbremoved, the device unbound and re-bound on the USB bus): it came back onrtk_btusb, patched. A real
power-on has not been observed with the fix in place. Undo withbash ~/setup/robot_bt_rtk.sh --undo.
The Aurora 930 is a USB device with one vendor-specific interface (class 0xff): no UVC, no /dev/video. Its
closed vendor library talks to it directly over USB, which needs read and write access to the camera's raw USB
device file under /dev/bus/usb. By default only root has that. The vendor's own rule gave every 3251:* device
mode 0666 (anyone may write). This rebuild narrows it to the one product ID, group video, mode 0660.
burgerbarn is in group video.
These three lines are taken from scripts/install_aurora930.sh (lines 15-17), which chapter 15 runs to install the
camera driver. You can apply the rule now:
On the robot:
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="3251", ATTRS{idProduct}=="1930", MODE="0660", GROUP="video"' \
| sudo tee /etc/udev/rules.d/60-rosorin-aurora930.rules >/dev/null
sudo udevadm control --reload && sudo udevadm trigger --subsystem-match=usb --attr-match=idVendor=3251
On the robot:
ls -l /dev/bus/usb/001/$(printf %03d $(cat /sys/bus/usb/devices/1-2.2.1/devnum))
Check
On 2026-09-28 15:53 UTC:crw-rw---- 1 root video 189, 5 Sep 28 15:52 /dev/bus/usb/001/006The device number (
006) changes when the camera re-enumerates; the path1-2.2.1and the group do not.
Not run on its own
On this robot the rule was written byinstall_aurora930.shtogether with the driver install on 2026-09-28.
Running the three lines alone first is a split of that script; the commands are identical, and the script
writes the same file again in chapter 15.scripts/rollback_aurora930.shremoves the rule together with the
driver.
ls -l /dev/lidar shows /dev/lidar -> ttyUSB0 after a reboot, andstty -F /dev/lidar 230400 raw -echo && timeout 2 cat /dev/lidar | wc -c prints a number in the tens ofmodinfo -n ch341 and modinfo -n rtw89_8852ce_git both point into /lib/modules/5.15.148-tegra/updates/.ssh rosorin-wifi hostname prints rosorin, and on the robot iw dev wlP1p1s0 get power_savePower save: off.readlink -f /sys/class/bluetooth/hci0/device/driver prints /sys/bus/usb/drivers/rtk_btusb andhciconfig shows UP RUNNING.ls -l /dev/ttyACM0 shows group dialout; the camera's /dev/bus/usb node shows group video, modecrw-rw----.Where this comes from
Scripts:scripts/build_ch341.sh,scripts/install_ch341.sh,scripts/rollback_ch341.sh(commit c1cc0f1,
2026-09-26);scripts/build_rtw89.sh,scripts/install_rtw89.sh,scripts/rollback_rtw89.sh(fa7c278,
2026-09-27);scripts/setup/wifi_powersave_off.sh(e4b3e87) andscripts/setup/robot_bt_rtk.sh(e6d5716),
2026-10-07;scripts/install_aurora930.shlines 15-17 (d0d97b0, 2026-09-28).
Docs:docs/hardware.md(Sensors/I/O, Network, Depth camera, WiFi 2026-10-07 17:44, Bluetooth 2026-10-07),
docs/lessons.md2026-09-26 (CH341 config, rebuild after kernel update, brltty, serial TIOCEXCL) and
2026-09-28 (LiDAR re-enumeration),docs/decisions.md2026-09-26/27 (keep COIN-D6, rtw89 source, L4T hold).
Command log (UTC): ch341 build 2026-09-26 22:49-22:56, reboot test 23:31; rtw89 install 2026-09-27 22:47,
Wi-Fi join check 23:12; Aurora rule 2026-09-28 15:52; power save 2026-10-07 17:52; Bluetooth 2026-10-07
17:58-18:00. Thenmcli --ask dev wifi connect <your-wifi-name>command is from the robot's/var/log/auth.log
(survey_live_robot.md). Live read-back on the robot 2026-10-07 20:2x UTC: udev rules, module paths and
vermagic, rtw89 commit2310939e, boot kernel messages, power-save state, Bluetooth driver.
← First boot and base settings · Contents · Make a backup you can restore →