在 BeagleBone Black 上把自己編的 Linux 7.3 開起來
English version: Booting My Own Linux 7.3 Build on a BeagleBone Black
手上有一塊 BeagleBone Black(TI AM335x,Cortex-A8,512 MiB),SD 卡裡跑著官方的 Debian 13 和 6.12 kernel。這次的目標很單純:在桌機上交叉編譯 mainline Linux 7.3-rc7,透過 TFTP 丟給板子,讓它用自己編的 kernel 開到 login,而且網路要是通的。
聽起來是幾行指令的事,實際上一路踩了五個坑,其中只有一個跟 kernel 本身有關。這篇照實際發生的順序寫下來。
環境
| 角色 | 內容 |
|---|---|
| 開發機 | x86 桌機(28 執行緒),Ubuntu,IP 192.168.10.154 |
| 目標板 | BeagleBone Black,U-Boot 2019.04,SD 上是 Debian 13 + 6.12.45-bone34 |
| Console | J1 debug UART 接 CP2102(3.3V),115200 8N1,/dev/ttyUSB0 |
| 網路 | 板子 eth0 和開發機在同一個 192.168.10.0/24 |
整條路是這樣:
開發機 BeagleBone Black
────── ────────────────
make zImage + dtb ROM → SPL → U-Boot
│ │ (攔下 autoboot)
└─► TFTP root ──── tftp ──────────────► 載到 0x82000000 / 0x88000000
│
serial_console.py ◄──── UART ───────────── bootz → Linux 7.3
│
└─ 掛 SD 上原本的 rootfs
第一步:編 kernel
開發機原本沒有 ARM Linux 的交叉工具鏈(只有 bare-metal 的 arm-none-eabi),先裝:
sudo apt install gcc-arm-linux-gnueabihf # GCC 13.3
kernel source 我已經有一棵 mainline tree,但上面掛著別的開發分支,不想動它,所以開一個 detached worktree 切在 tag 上,build 走 out-of-tree:
git -C ~/workspace/linux-dts worktree add --detach ~/workspace/bbb-bringup/linux v7.3-rc7
cd ~/workspace/bbb-bringup/linux
M="make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- O=../build-arm"
$M omap2plus_defconfig
scripts/config --file ../build-arm/.config --set-str LOCALVERSION "-bringup"
$M olddefconfig
$M -j28 zImage ti/omap/am335x-boneblack.dtb
omap2plus_defconfig是 TI OMAP/AM335x 家族共用的 defconfig。LOCALVERSION=-bringup讓版本字串變成7.3.0-rc7-bringup,開機後一眼就能分辨跑的是不是自己的 kernel。- 新版 kernel 把 TI 的 dts 搬到
arch/arm/boot/dts/ti/omap/,所以 dtb 的 make target 要寫ti/omap/am335x-boneblack.dtb。
28 執行緒編 1 分 51 秒,0 個 warning。產出的 zImage 5.7 MB,比 Debian 的 8.8 MB 小不少,因為 Debian 的 config 開了大量 module 和功能。
要不要 initrd?
SD 卡上的 initrd 和 /lib/modules 都是 6.12 的,配 7.3 kernel 本來就對不起來。所以先確認開機必要的驅動是不是都編成 built-in:
CONFIG_MMC_OMAP_HS=y # SD/eMMC 控制器
CONFIG_EXT4_FS=y # rootfs 檔案系統
CONFIG_SERIAL_8250_OMAP=y # console
CONFIG_SERIAL_8250_CONSOLE=y
CONFIG_TI_CPSW_SWITCHDEV=y # 網卡
CONFIG_DEVTMPFS_MOUNT=y # 不靠 initramfs 也有 /dev
全部是 =y,所以 kernel 可以直接掛 SD 上的 rootfs,不需要 initramfs。
第二步:TFTP server
開發機上沒有現成的 TFTP server。系統有 dnsmasq,只開它的 TFTP 功能就好:--port=0 關掉 DNS、不開 DHCP,--interface 加 --bind-interfaces 只綁接板子的那張網卡。
dnsmasq -k --port=0 --enable-tftp --tftp-root=$HOME/workspace/bbb-bringup/tftp \
--tftp-secure --interface=enp5s0 --bind-interfaces
TFTP root 裡放兩組檔案:從板子 scp 回來的原廠 kernel 當對照組,以及自己編的 zImage-bringup、am335x-boneblack-bringup.dtb。出問題時隨時能切回原廠那組,確認是 kernel 的問題還是環境的問題。
坑 1:bootdelay=0,autoboot 攔不下來
這塊板子的 U-Boot 環境裡 bootdelay=0。開機只印一行 Press SPACE to abort autoboot in 0 seconds,人手不可能按得到。
U-Boot 在 bootdelay=0 時仍會檢查一次 UART 有沒有輸入,所以解法是在它檢查之前,空白鍵就已經在 UART 的接收 FIFO 裡。我寫了一個小程式 serial_console.py,一邊把 UART 輸出加上時間戳寫進 log,一邊在旗標檔存在時每 20 ms 送一個空白,看到 U-Boot 的 => 提示符就停下來並刪掉旗標。
第一版是從開機一路狂送空白,結果灌進了 Linux 的 login prompt。後來改成只在看到 U-Boot 的標記(U-Boot 20xx、Board:、DRAM:)之後、Starting kernel 之前送,Linux 就完全不會收到。
這個工具本身還有一個 bug:判斷「現在是不是在 U-Boot 階段」時,我只在最後 64 bytes 裡找 banner,但 CP2102 一次就丟進好幾百 bytes,banner 還沒被檢查到就被擠出去了,連續兩次上電都沒攔到。後來改成每收到一塊資料就整塊掃過一遍,並且先用 pty 當假 serial port 模擬各種情況,模擬通過了才再上電。上一次電就要人去插拔一次,先在軟體裡模擬比較省事。
坑 2:USB hub 重新列舉,吃掉開頭的 log
CP2102 和 BBB 插在同一顆 USB hub 上。BBB 上電的瞬間,整顆 hub 會斷線重新列舉,/dev/ttyUSB0 消失 2 到 4 秒才回來:
cp210x ttyUSB0: cp210x converter now disconnected from ttyUSB0
usb 1-4: new high-speed USB device number 18 using xhci_hcd
...
usb 1-4.1: cp210x converter now attached to ttyUSB0
這造成兩個問題:
- SPL 的輸出剛好就在這幾秒裡印完,所以完全錄不到。
- 有一次 hub 花了 4 秒才回來,logger 重新打開 port 時 U-Boot 已經印到
Board: BeagleBone Black,banner 早就過了,空白一個都沒送出去。
軟體上的補救:port 重新打開時,只要旗標還在,就不等任何標記,先盲送 4 秒空白。真正的解法是硬體:UART 轉接器改插主機板上直接的 USB 埠,不要跟板子共用 hub,或者 BBB 改用 DC 供電。
坑 3:封包有到,回覆卻從 Tailscale 送出去
攔下 U-Boot 之後,ping 和 tftp 都 timeout:
=> ping 192.168.10.154
ping failed; host 192.168.10.154 is not alive
=> tftp ${loadaddr} zImage
Loading: *
T T T T
在開發機上抓封包,板子送來的 ARP、ICMP echo request、TFTP RRQ 全都有收到,可是開發機一個回覆都沒送:
ARP, Request who-has 192.168.10.154 tell 192.168.10.242
ARP, Reply 192.168.10.154 is-at aa:bb:cc:dd:ee:ff
IP 192.168.10.242 > 192.168.10.154: ICMP echo request, id 0, seq 3, length 8
IP 192.168.10.242.3040 > 192.168.10.154.69: TFTP, length 38, RRQ "zImage" octet
防火牆 INPUT 是 accept,不是被擋。再查回程路由:
$ ip route get 192.168.10.242
192.168.10.242 dev tailscale0 table 52 src 100.x.y.z
原來 tailnet 裡有某台機器宣告了 192.168.10.0/24 的 subnet route。Tailscale 的 policy rule(priority 5270,lookup 52)排在 main table(32766)前面,所以回給板子的封包全被送進了 tailscale0。加一條優先權更高的 rule,讓本地 LAN 走 main table:
sudo ip rule add to 192.168.10.0/24 lookup main priority 2500
之後 tftp 跑到 5.8 MiB/s。「request 有到、reply 不見」的時候,先用 ip route get 查回程實際走哪張網卡,比懷疑防火牆或板子快得多。這條 rule 重開機就會消失,所以 TFTP 開機腳本開跑前會先檢查它還在不在。
第三步:U-Boot 裡手動開機
攔下 autoboot、網路通了之後,在 U-Boot 裡依序:
=> setenv ipaddr 192.168.10.242; setenv serverip 192.168.10.154
=> tftp ${loadaddr} zImage-bringup # 0x82000000, 5694896 bytes
=> tftp ${fdtaddr} am335x-boneblack-bringup.dtb # 0x88000000, 98486 bytes
=> md.l ${loadaddr} 0xc # offset 0x24 要是 016f2818
=> md.b ${fdtaddr} 4 # 要是 d0 0d fe ed
=> setenv bootargs console=ttyS0,115200n8 root=/dev/mmcblk0p3 ro rootfstype=ext4 rootwait earlycon bringup.boot=tftp-own
=> bootz ${loadaddr} - ${fdtaddr}
bootz的中間參數是 initrd,寫-代表不用。bringup.boot=tftp-own是我自己塞的標記,kernel 不認得這個參數,會原封不動放在/proc/cmdline裡,拿來證明這次用的是我設的 bootargs。- 先用
md看一眼 zImage magic 和 FDT magic,可以避免「檔案根本沒載對」這種浪費一次開機的錯誤。
開機 log:
Starting kernel ...
[ 0.000000] Linux version 7.3.0-rc7-bringup (alanhc@alanhc-14700) (arm-linux-gnueabihf-gcc ... 13.3.0 ...)
[ 0.000000] Kernel command line: console=ttyS0,115200n8 root=/dev/mmcblk0p3 ro ... bringup.boot=tftp-own
[ 2.601037] mmcblk0: mmc0:59b4 USDU1 14.9 GiB
[ 4.498186] VFS: Mounted root (ext4 filesystem) readonly on device 179:3.
[ 4.530783] Run /sbin/init as init process
...
BeagleBone login:
開到 login 了。不過 Web console 那行沒有印出 IP。
坑 4:開到 login,網路卻是壞的
ping 不通。翻 log 找到這行:
cpsw-switch 4a100000.switch end0: renamed from eth0
原廠的 bootargs 裡有 net.ifnames=0,我手動精簡 bootargs 時順手拿掉了。沒有這個參數,systemd 會套用 predictable network interface names,把 eth0 改名成 end0,而 rootfs 的網路設定寫的是 eth0,於是拿不到 IP。
回頭翻更早的 log 才發現,用原廠 6.12 走 TFTP 開機的那幾次也有同一行 end0: renamed from eth0,網路一樣是壞的,只是當時只看「有沒有開到 login」,沒去 ping。
補回 net.ifnames=0 再開一次:
cpsw-switch 4a100000.switch eth0: Link is Up - 100Mbps/Full - flow control off
BeagleBone login:
開到 login 後 1 秒內就 ping 得到。
教訓有兩個:刪 bootargs 參數前先弄清楚它在管什麼;「開得到 login」不等於「開機成功」,驗收條件要事先定好。
坑 5:軟重開會卡死
為了不要每次都請人去拔電,我試過從 SSH 下 systemctl reboot。原廠 6.12 的系統兩次都卡在同一個地方:
[ OK ] Stopped ssh.service - OpenBSD Secure Shell server.
[ OK ] Stopped serial-getty@ttyGS0.service - Serial Getty on ttyGS0.
之後就沒有任何輸出,但板子還 ping 得到。第一次卡住時我懷疑是自己狂送空白造成的,第二次已經確定 Linux 階段一個空白都沒送,照樣卡在同一行,所以這是系統本身的問題。這題還沒追,留著當下一個練習:從 journalctl -b -1 看上一次開機記錄了什麼,或是加 systemd.log_level=debug 再重現。
驗收
最後一次上電,從攔下 U-Boot 到驗收全自動跑完,再 SSH 進板子確認:
$ uname -r
7.3.0-rc7-bringup
$ cat /proc/cmdline
console=ttyS0,115200n8 root=/dev/mmcblk0p3 ro rootfstype=ext4 rootwait earlycon net.ifnames=0 bringup.boot=tftp-own
$ ip -4 -br addr show eth0
eth0 UP 192.168.10.242/24
$ systemctl --failed
● bb-usb-gadgets.service loaded failed failed BeagleBoard.org USB gadgets
● iwd.service loaded failed failed Wireless service
失敗的只剩 USB gadget 和 wifi 兩個服務,都是 rootfs 裡沒有 7.3 modules 造成的,在預期之內。
和原廠開機的對照:
| 原廠 6.12 + initrd | 自編 7.3,不帶 initrd | |
|---|---|---|
| kernel 大小 | 8.8 MB | 5.7 MB |
| rootfs 掛上 | 12.6 s | 3.7 s |
Starting kernel 到 login | 約 58 s | 約 42 s |
| 網路 | 正常 | 正常(要帶 net.ifnames=0) |
| failed units | 無 | bb-usb-gadgets、iwd(缺 modules) |
rootfs 快了將近 9 秒,大部分是省掉 initramfs 和裡面的 module 載入。
回頭看
五個坑裡,真正跟 kernel 有關的只有 net.ifnames=0 一個,而且嚴格來說那也是 userspace 的命名規則,不是 kernel 的 bug。其他四個分別是:
- bootloader 設定:
bootdelay=0。 - USB 拓撲:UART 轉接器和板子共用 hub。
- 開發機網路:Tailscale 的 subnet route 搶走了回程路由。
- 原廠 rootfs:軟重開會卡住。
我覺得這就是 bring-up 的常態:kernel 編得過只是起點,大部分時間花在讓「整條鏈路」可觀測、可重現。這次值回票價的投資,是那個加時間戳的 serial logger、開機前先驗 magic number 的習慣,以及每一步都先定好驗收條件(uname -r、/proc/cmdline、ping)。
下一步
- 把 7.3 的 modules 裝進 rootfs(
make modules_install INSTALL_MOD_PATH=...),讓 USB gadget 和 wifi 回來。 - rootfs 也改走 NFS,徹底不碰 SD 卡。
- 追原廠 6.12 軟重開卡死的原因。
- 在 dts 裡刻意關掉某個 node,看 kernel log 怎麼反應。