基於興趣,我花了一段時間把 LineageOS 23.2(Android 16)刷到手上一台 Amlogic S905X4 的 Android TV 機上盒上。同 SoC 有官方支援的機型,所以 kernel 和大部分 HAL 都現成,理論上只要把 device tree 和 vendor blob 換成這台的就好。
實際上跑了二十三輪 build。這篇不是「我做出來了」,而是那二十三輪裡我信錯的東西。
跳進去之前我們先講清楚這篇的主軸。回頭看,耗掉時間最多的不是任何一個技術難點,而是一類特定的訊號:看起來像成功,或看起來像證據,但其實只證明了一小段。 這條線在 Wi-Fi 和乙太網路兩邊各出現一次,形狀幾乎一樣。所以下面不按子系統分,按訊號的失效方式分。
沒有 UART 的盲測環境
這台機器只有一個 USB Type-A 埠,而且那個埠和 OTG 共用。板子上的 UART 沒有引出來。
這兩件事加起來的後果是:kernel panic 的 backtrace 我看不到。 訊息會進 /dev/kmsg,但如果系統沒活到 adb 起來,那些訊息就跟著電一起消失。我試過 ramoops,寫了一個 marker 進 /dev/kmsg 然後熱重開,marker 沒存活,那條路不通。
所以整個移植的第一個目標不是 Wi-Fi 也不是網路,而是 adb 本身。而 adb 剛好也是壞的其中一項,這就有點雞生蛋。
修法最後落在 device tree 的一行上:
&dwc2_a {
controller-type = <1>; /* 看起來最矛盾的那個值 */
};
1 在 Amlogic 的 usbtype.h 裡是 USB_HOST_ONLY。填 host-only 卻換到 device mode,看起來毫無道理,直到你去追 cmdline:bootloader 會傳 otg_device=1,而 get_otg_mode() 把這個組合翻成 USB_DEVICE_ONLY,順便呼叫 force_disable_xhci_port_a()。填字面上合理的 3(OTG)反而讓那個埠留在 host mode,adb 永遠不出現。
老實說,這個值我試錯過一次。第一次我同時改了 controller-type 和 USB3 PHY 的 otg,結果連在上面的 USB 鍵盤也不能用了。拆開來測才知道 otg = 0 會讓 role-switch 的中斷根本不註冊,host 和 device 一起死。
除錯通道是第一個 milestone,不是副產品。 這句話聽起來像廢話,但我是在沒有它的情況下硬撞了幾輪之後才真的接受。
權威來源不在樹裡,在出貨 image 裡
移植的時候你會一直問同一種問題:「出貨的韌體到底是怎麼設的?」
直覺做法是去原始碼樹裡找。這個做法很容易出錯,因為樹裡同一個檔名常常有好幾份,而只有一份會上車。
我碰到最誇張的一次是某個設定檔在樹裡有三個位置:SoC 產品目錄下一份、共用 vendor 目錄下一份、另一個產品線的目錄下一份。SoC 產品目錄那份看起來最像「就是它」,裡面的內容也正好解釋得通我看到的症狀。改它,完全沒反應。抽出貨 image 才發現真正裝上去的是共用 vendor 目錄那份,而 SoC 產品目錄那份的安裝規則只存在於另一條產品線。
後來我固定用這個流程:
# 抽 retail 韌體的 image(EROFS 或 ext4 都可以)
fsck.erofs --extract=/tmp/vt vendor.img
# 看真正裝上去的那份
cat /tmp/vt/etc/<config>
# 用 sha256 對回樹裡的來源,確定該改哪一個
sha256sum /tmp/vt/etc/<config> <tree>/.../<config>
Hash 一對上,就不用再猜了。vendor_dlkm.img 也可以這樣抽,裡面的 lib/modules/modules.load 是「出貨機到底自動載入哪些 kernel module」的權威答案。我在這題上浪費過時間:先前我從 init rc 的一行 chown 反推「出貨機應該沒有自動載入 Wi-Fi driver」,寫進筆記,後來自己標成未驗證。抽 image 花三分鐘,結論是原本的推論剛好是對的,而且還多一項(連 glue module 也沒載)。推論三小時還是推論。
還有一個低級但很傷的坑:我 scratchpad 裡有個檔名帶 _cur 的 DTS,cur 是 current 的意思。它是某一次抽出時的 current。我基於它比對,得出「PCIe reset GPIO 還是錯的」這個結論,而來源 DTS 早就改好了。從那之後我動手前都重抽一次。
工具用同一個回答表達「否」和「我不知道」
這次踩到四次同一個形狀的坑,值得單獨講。
第一次:我想知道裝置上某幾個 .so 在不在,於是在 non-root 的 adb shell 裡跑:
for l in libcrypto.so libnl.so libxml2.so; do
[ -f /vendor/lib/$l ] && echo "HAVE $l" || echo "MISS $l"
done
三個都回 MISS。三個都在裝置上。/vendor/lib 的目錄可以列,但裡面的檔案對 shell user 不能 stat,而 [ -f ] 走的是 stat(2):stat 失敗和檔案不存在,在 shell 裡是同一個回答,而且完全無聲。 我因此判斷要從出貨 image 搬三顆 lib 進來,整條計畫都建在假陰性上。
ls -l 反倒可靠,因為它的錯誤訊息會區分:
ls: /vendor/lib/libcrypto.so: Permission denied ← 存在,只是看不到屬性
ls: /vendor/lib/nosuchfile.so: No such file or directory
附帶一個意外好用的副作用:ls /vendor/lib/*.so 在 non-root 下吐出的那一整串 Permission denied,本身就是完整的檔名清單。glob 由 shell 展開(只需要目錄的 r-x),stat 才失敗。
第二次:刷完機後裝置連續五分鐘在 adb devices 和 fastboot devices 都是空的。我已經準備判定那次改動讓它開不起來了,結果:
adb kill-server && adb start-server && adb devices
# <serial> device ← 一直都好好的
adb server 自己卡住,和裝置真的不在,回報的是同一個空清單。這一條特別容易造成連鎖誤判,因為我當時正在驗證一個「是否修好開機」的改動。
第三次和第四次是我自己加上去的過濾器。一次是用 grep -c "^dhdpci.ko$" 檢查模組有沒有進到 build 出來的載入清單,得到 0,而 depmod 產生的那份清單用的是不帶 .ko 的模組名,pattern 永遠不會 match。另一次更蠢:我在看一大段 kernel log 時習慣性接了 | tail -8,而我要找的那幾行剛好在第 9 行之前。
還有一種變形不是回答「否」,而是回答了另一個問題。查腳位有沒有被 mux 的時候:
adb shell cat /sys/kernel/debug/pinctrl/*/pinmux-pins
# pin 70 (GPIOZ_0): UNCLAIMED
UNCLAIMED 講的是有沒有 driver 宣告所有權,不是暫存器現在是什麼值。bootloader 設好的腳位在這裡一樣顯示 UNCLAIMED。所以它能證明「我的 driver 沒拿到」,不能證明「硬體沒被設定」。
共同形狀是:工具用同一個回答表達「否」和「我不知道」。遇到回答是「否」而且後果很大的時候,先問一句「這個工具有沒有辦法表達『我查不到』?」如果沒有,換一個會區分的。而如果那個過濾器是你自己接上去的,先拿掉它看全貌。
成功訊號屬於哪一段
這是整篇最貴的一課,兩個子系統各上一次。
先看 Wi-Fi。它是 Broadcom 的 PCIe 模組(BCM43752A2,dhdpci 驅動),失敗分六層,而且前面幾層會互相掩蓋:
| 層 | 症狀 | 根因 |
|---|---|---|
| 1 | MODULE.TARGET.ETC.wpa_supplicant.conf already defined | 兩個 soong 模組裝到同一路徑,其中一個沒有 soong config 閘門 |
| 2 | found duplicate sysprop assignments | 同一個 sysprop 被賦不同值(相同值的重複是允許的) |
| 3 | No Broadcom or Synaptics PCI device enumerated! | PCIe 的 reset GPIO 和 Wi-Fi 的中斷腳撞號,控制器 probe 直接 EBUSY |
| 4 | Direct firmware load ... error -2 | 韌體放在 /vendor/etc/wifi/<chip>/,但 request_firmware() 只看 /vendor/lib/firmware/ |
| 5 | Cannot call isSupported because mWifiHal is null | 整個 vendor Wi-Fi HAL 沒被裝進 image |
| 6 | dhd_bus_start failed to sync with dongle | 下面講 |
每修完一層,錯誤訊息就換一個,感覺像在前進。但前五層都只是在擋視線,第六層才是真的。這裡有一組指紋值得記住,因為使用者感受一樣(Wi-Fi 開不起來)而修法完全不同:mWifiHal is null 是 framework 連對象都找不到;Failed or timed out awaiting driver ready 是 HAL 跑起來了、在等 wlan0 然後逾時。後者是「driver 不在」的正確行為,不是新 bug。
第六層卡在這裡:
wlc_ver_major 12, wlc_ver_minor 1
dhd_msgbuf_dump_iovar_name: resumed on timeout rxcnt_timeout 1 ioctl_cmd 262 \
trans_id 65238 state 3 busstate=2 ioctl_received=0
dhd_bus_start failed to sync with dongle
Firmware: ← 版本字串是空的
注意第一行。韌體版本號讀到了,所以晶片活著、bus 通、PCIe 沒問題。我把這個當成「基礎建設都好了,剩下是上層設定問題」,於是去懷疑驅動和韌體版本不配,把驅動降到和出貨機一字不差的版本重測,失敗一模一樣,連 trans_id 都相同。然後懷疑編譯 flag 不同,比對兩顆 .ko 的字串指紋,一致。再懷疑 DTB 的 PCIe 設定不同,逐欄比對,全同。
真正該量的東西其實一行就夠:
adb shell grep dhdpcie /proc/interrupts
# 70: 0 0 0 0 meson-gpio-irqchip 57 Edge dhdpcie_host_wake
# 71: 0 0 0 0 GIC-0 175 Level dhdpcie:0000:01:00.0
四個 CPU 上都是 0 次。而驅動自己的 log 說 intstatus=0x1,晶片有待處理中斷。晶片在拉,CPU 收不到。
那個版本號之所以讀得到,是因為驅動對少數路徑有退路:dhd_msgbuf_wait_ioctl_cmplt: reschedule dhd_dpc, cnt=1, intstatus=0x1 它自己重新排程 DPC 去輪詢,所以撿到了完成事件。一般 IOCTL 沒有那個退路。我讀到的「成功」來自一條有輪詢退路的路徑,而我把它當成整個中斷路徑的健康證明。
為什麼不改用 MSI?因為 MSI 是刻意關掉的,而且兩層都關:
// dhdpcie_chip_support_msi(),在 bcmdhd 裡
if (... si_chipid(bus->sih) == BCM43752_CHIP_ID ...) return FALSE;
// 註解寫:intstatus is not getting cleared from these firmwares
// dhd_config.c,CUSTOMER_HW_AMLOGIC + BCMPCIE
conf->d2h_intr_method = 0; // 對所有 PCIe 強制 INTx
Broadcom 為這顆晶片關掉,Amlogic 又為整個 PCIe 路徑強制 INTx。所以要嘛修 INTx 遞送,要嘛別依賴中斷送達。bcmdhd 本來就有輪詢模式,但要兩個改動一起才有用:
# driver Makefile
DHDCFLAGS += -DDHD_READ_INTSTATUS_IN_DPC
# chip 的 config 檔
dhd_poll=1
dhd_watchdog_ms=10
單設 dhd_poll=1 沒用。它會把 bus->poll 設起來,但真正去排程 DPC 的那段程式碼被包在 #ifdef DHD_READ_INTSTATUS_IN_DPC 裡,而那個巨集預設沒有定義。設了參數卻沒有人理它,這種組合在 BSP 裡很常見。
刷上去之後雙頻掃到十幾個 AP,而 /proc/interrupts 的計數依然是 0。那才是最有說服力的部分:Wi-Fi 能動完全是輪詢撐起來的,中斷確實從未送達,診斷因此沒有爭議空間。代價是每 10 ms 一次 DPC,開機時 load average 會衝到 6 到 8 再回落。
然後乙太網路把同一課又上了一次,而且這次我有前車之鑑還是中了。補上外接 PHY 的節點之後:
meson8b-dwmac eth0: PHY [0.0:00] driver [RTL8211FD-VD-CG] (irq=73)
meson8b-dwmac eth0: configuring for phy/rgmii link mode
meson8b-dwmac eth0: Link is Up - 1Gbps/Full - flow control rx/tx
這看起來已經結束了。Link is Up、千兆、全雙工、流控都談好。實際上一個封包都收不到。
問題在於這些訊號分別屬於哪一段。MDIO 是獨立的兩支管理腳位,RGMII 資料路徑是另外十四支。 整理一下各個訊號的管轄範圍:
| 訊號 | 真正證明的事 |
|---|---|
讀得到 phy_id | MDIO 那兩支腳位通 |
Link is Up - 1Gbps/Full | PHY 和對端在網線側協商成功,經 MDIO 回報 |
| PHY reg10 的 Local/Remote Receiver OK | 同上,網線側 |
| MAC 的 TX 計數器在動 | frame 進了 MAC 的 FIFO,不代表上了線 |
| RX = 0 而且 errors = 0 | RGMII 收方向完全沒有訊號進來 |
最後那一行是唯一有診斷價值的。如果是時序問題(RGMII delay 沒調好),會收到壞封包,errors 會跳。errors 是 0 代表連壞封包都沒有,所以不是時序,是這條路根本沒接上。我花了兩輪在調 delay 才接受這件事。
無聲的覆寫
接上面。RGMII 沒接上,而我已經照出貨 DTB 把 pinctrl 抄過來了。問題是抄的那份等於沒有生效,而且沒有任何警告。
Linux 的 pinctrl core 在 probe 時只會自動套用一個 state,而它是用字面字串 "default" 找的:
/* 照抄出貨 DTB 的寫法 */
pinctrl-0 = <&external_eth_rmii_pins>;
pinctrl-names = "external_eth_rmii_pins"; /* ← 不叫 default,所以沒人套用 */
名字不是 default,driver core 就不碰它,而且不會抱怨。腳位停在 bootloader 留下的狀態,pinmux-pins 顯示 UNCLAIMED,而我們上一節已經知道 UNCLAIMED 不能用來判斷暫存器值。兩個盲點剛好疊在一起。
改名之後十四支腳位全部 mux 上了。RX 還是 0。
第二個覆寫更隱蔽,在 driver 裡面。Amlogic 的 dwmac-meson8b 會依 phy-mode 去設 PRG_ETH0 的 EXT_PHY_MODE 欄位,我把 phy-mode 從 "rmii" 改成 "rgmii",進 DTB 也確認了。但同一個 driver 後面還有一段:
/* aml_cust_setting():如果 DTB 裡有 mc_val,整個暫存器直接覆寫 */
if (!of_property_read_u32(np, "mc_val", &mc_val))
writel(mc_val, priv->ioaddr + PRG_ETH0);
出貨 DTB 的 mc_val 是 0x4BE0C,解出來是一整組 RMII 設定:mode = 4、RGMII TX clock 關閉、RMII 反相開啟。它在 phy-mode 那條路徑之後執行,所以我前一輪的修正被默默作廢。
這個值當初就出現在我的屬性 diff 裡,在「只有出貨那邊有」的欄位。我把它當成無害的額外項掠過,之後多花兩輪才回到它。刪掉它,網路就通了,ping -s 1472 -c 50 五十發全到,零 error。
而 driver 自己其實一直在講話:
[2 T226] aml_cust_setting
這行在的時候就是覆寫發生了,刪掉 mc_val 之後它消失。免費的確認訊號,只要你知道它代表什麼。
兩個覆寫的共同點是都不報錯。要看見它們,唯一的方法是去讀消費端的程式碼:誰讀這個屬性、什麼時候讀、讀了之後做什麼。我後來把「照抄參考 DTB」的原則改成:它是好的起點,但只到「我能讀到消費端程式碼或量到硬體」為止。這一題上有三處照抄反而是錯的,phy-mode、pinctrl 佈局、還有 PHY 的 reset GPIO。判斷標準不是「出貨機怎麼寫」,而是「我們這棵樹的 driver 怎麼讀這個值」。
讓實驗可信
前面每一個誤判,追到底都是同一件事:我接受了一個不乾淨的量測。所以最後講怎麼讓實驗本身站得住。
一個 build 只動一個變數。 有一輪我同時加了 wifi HAL、把驅動模組放回開機自動載入、補了三個 BoardConfig 變數。那輪 kernel panic,裝置停在 fastboot。三個改動都可能是元凶,我沒辦法分辨,那一輪的三小時完全報廢。下一輪只改驅動版本一個變數,立刻得到乾淨的結論。
對照組出現同樣症狀,不是證據。 我曾經指控某個驅動模組造成首次開機失敗,後來發現完全沒有裝那個韌體的對照組也會失敗。同一個症狀在對照組出現,代表它跟你改的東西無關。
跑實驗前先確認實驗本身有產生預期的輸入。 這一條是我這次最難堪的錯。我寫了一輪掃描去試 RGMII delay 的各種組合,結論是「delay 沒有影響」。但那時 ping 因為沒有 route,一個封包都沒發出去,TX 計數器完全沒動,我沒注意。沒有輸入的實驗不會有輸出,那一輪的結論沒有任何證據支撐,而我把它寫進筆記當成已知事實用了好幾輪。
把迴圈縮短。 改 DTB 要重 build,一輪七十五分鐘。但 Amlogic 的 BSP 其實有 sysfs 介面可以直接讀寫 MAC 和 PHY 的暫存器:
# eng build 先開 root
adb shell setprop persist.sys.root_access 3
adb shell 'echo r 0 > /sys/class/ethernet/macreg; cat /sys/class/ethernet/macreg'
adb shell 'echo r 0 > /sys/class/ethernet/phyreg; cat /sys/class/ethernet/phyreg'
七十五分鐘變幾秒。找到這個之後的進度比之前快一個數量級。
開機十幾秒就掉回 fastboot,要先想到 kernel panic,不是刷機失敗。 機制是 panic 造成 watchdog reset,sys.boot_completed 永遠到不了,markBootSuccessful() 不會被呼叫,bootloader 的重試計數用完就把裝置停在 fastboot。表面症狀跟刷壞了幾乎一樣。判定一行就夠:
adb logcat -d -s bootstat | grep "boot reason"
# Canonical boot reason: watchdog ← panic,不是刷機問題
全部被推翻的假設列出來是這樣。這張表是整篇最有用的部分,因為每一行都是我當時覺得很有道理的東西:
| 假設 | 怎麼被推翻 |
|---|---|
| 驅動和韌體版本 skew | 降到和出貨機一字不差的版本,失敗完全相同,連 trans_id 都一樣 |
| 編譯 flag 和出貨機不同 | 兩顆 .ko 的字串指紋一致 |
| DTB 的 PCIe 設定不同 | 逐欄比對全同 |
| DTB 的網路設定和出貨機沒差 | 逐欄比對只剩 phandle 編號和三個額外屬性,mc_val 就在那三個裡面,我掠過了 |
| 某個計數器顯示的 15.6 秒是硬體徵狀 | 我讀錯了,首次只花 94 毫秒;15.6 秒是逾時被算進重試時的累計值 |
| 缺三顆 vendor lib | [ -f ] 假陰性,實際只缺一顆 |
| 某個 GPIO 是 -1 導致修正流程被跳過 | 那段修正本來就沒編進去,兩邊都沒有 |
| 裝置消失五分鐘等於開不起來 | adb server 卡住 |
| 出貨機用另一家的 PHY driver,所以網路卡住 | PHY 是雙源設計,兩個 driver 都出貨、ID 不重疊,這台裝的是 Realtek 那顆 |
| RX 不通是 RGMII delay 沒設 | 綁上正確 driver 後 RX 仍 0;最終零 error 證明時序本來就對 |
| 照抄出貨 DTB 最安全 | 這題害了三次 |
接下來要蓋的東西
說了這麼多,真正的產出不只是一台能開機的機器。寫這篇的時候我逐段問「那正確的判準是什麼」,那份清單剛好就是三個工具的規格:
- Probe library:包好的量測函式,核心規則是在無法區分「不存在」和「讀不到」時拒絕回答「不存在」;查 adb 之前先驗 server 健康;每個實驗開始前先斷言輸入已經產生。
- 刷機後驗證套件:固定一組探針,跑完和前一版 diff。把「我這個改動有幫助嗎」從判斷題變成 diff。
- 消費端稽核清單:每一個要設定或照抄的屬性,先 grep 誰消費、怎麼消費。這一條如果早點做,網路那五輪很可能壓成兩輪。
把迴圈自動化刻意排在後面。這次最大的時間消耗不是缺自動化,是我接受了模糊的量測,而把一個會產生錯誤結論的迴圈加速,只會讓同樣的錯誤發生得更快。護欄要先於油門。
如果你要開始一個類似的移植,最省時間的兩件事是:把除錯通道當成第一個 milestone 而不是副產品,然後為每一輪 build 留一行筆記寫「改了什麼、預期看到什麼」。我沒做第二件,二十三輪之後回頭重建哪一輪對應哪個改動,成本遠高於當時寫那一行。