•
7 min read

Link is Up 不代表通:在沒有序列埠的機上盒上做 Android 移植

Table of Contents

基於興趣,我花了一段時間把 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 驅動),失敗分六層,而且前面幾層會互相掩蓋:

層症狀根因
1MODULE.TARGET.ETC.wpa_supplicant.conf already defined兩個 soong 模組裝到同一路徑,其中一個沒有 soong config 閘門
2found duplicate sysprop assignments同一個 sysprop 被賦不同值(相同值的重複是允許的)
3No Broadcom or Synaptics PCI device enumerated!PCIe 的 reset GPIO 和 Wi-Fi 的中斷腳撞號,控制器 probe 直接 EBUSY
4Direct firmware load ... error -2韌體放在 /vendor/etc/wifi/<chip>/,但 request_firmware() 只看 /vendor/lib/firmware/
5Cannot call isSupported because mWifiHal is null整個 vendor Wi-Fi HAL 沒被裝進 image
6dhd_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_idMDIO 那兩支腳位通
Link is Up - 1Gbps/FullPHY 和對端在網線側協商成功,經 MDIO 回報
PHY reg10 的 Local/Remote Receiver OK同上,網線側
MAC 的 TX 計數器在動frame 進了 MAC 的 FIFO,不代表上了線
RX = 0 而且 errors = 0RGMII 收方向完全沒有訊號進來

最後那一行是唯一有診斷價值的。如果是時序問題(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 最安全這題害了三次

接下來要蓋的東西

說了這麼多,真正的產出不只是一台能開機的機器。寫這篇的時候我逐段問「那正確的判準是什麼」,那份清單剛好就是三個工具的規格:

  1. Probe library:包好的量測函式,核心規則是在無法區分「不存在」和「讀不到」時拒絕回答「不存在」;查 adb 之前先驗 server 健康;每個實驗開始前先斷言輸入已經產生。
  2. 刷機後驗證套件:固定一組探針,跑完和前一版 diff。把「我這個改動有幫助嗎」從判斷題變成 diff。
  3. 消費端稽核清單:每一個要設定或照抄的屬性,先 grep 誰消費、怎麼消費。這一條如果早點做,網路那五輪很可能壓成兩輪。

把迴圈自動化刻意排在後面。這次最大的時間消耗不是缺自動化,是我接受了模糊的量測,而把一個會產生錯誤結論的迴圈加速,只會讓同樣的錯誤發生得更快。護欄要先於油門。

如果你要開始一個類似的移植,最省時間的兩件事是:把除錯通道當成第一個 milestone 而不是副產品,然後為每一輪 build 留一行筆記寫「改了什麼、預期看到什麼」。我沒做第二件,二十三輪之後回頭重建哪一輪對應哪個改動,成本遠高於當時寫那一行。