§1開機:這條路怎麼建立起來
這一節是階段 0 的前置知識。你要多裝一顆 kernel、要能在兩顆之間切換、 之後還要讓 m1n1 換個模式跑 —— 這些都建立在「開機時到底發生什麼」之上。
五棒接力
| 角色 | 做什麼 |
|---|---|
| iBoot | Apple 的韌體。驗證簽章後交棒,你完全碰不到 |
| m1n1 | 初始化硬體、產生 device tree。Asahi 安裝時就放進 stub 分割區了 |
| U-Boot | 把 Apple 的特殊開機環境轉成標準的 Linux 開機介面 |
| GRUB | 讀開機項目清單、顯示選單、把你選的那顆 kernel 載進記憶體 |
| Linux kernel | 掛載 rootfs、載入模組,最後交給 systemd → Hyprland |
kernel 是一組帶版本後綴的檔案
所以多顆可以並存 —— 裝新的不會蓋掉舊的:
/boot/vmlinuz-<版本> # kernel 本體
/boot/initramfs-<版本>.img # 開機用的暫時根目錄
/boot/dtb-<版本>/ # device tree(Asahi 必要,別忘了裝)
/lib/modules/<版本>/ # 驅動模組
那份「有哪些 kernel 可以選」的清單
Fedora 用 BLS(Boot Loader Specification):一顆 kernel 一個設定檔。
ls /boot/loader/entries/ # 每個 .conf 就是選單上的一項
sudo grubby --info=ALL # 人話版:列出每一項的 kernel / initrd / 參數
裝完 fairydust 之後這裡會多一項 —— 那是「有沒有裝成功」最直接的驗證,比重開機試快得多。
/etc/default/grub,
把 GRUB_TIMEOUT 設成 5,同時確認 GRUB_TIMEOUT_STYLE=menu
—— 只改 timeout 而 style 還是 hidden 的話選單照樣不會出現。
然後 sudo grub2-mkconfig -o /boot/grub2/grub.cfg。不先做這件事,新 kernel 開不起來時你會找不到地方選回舊的。
m1n1 不是要新裝的東西
它現在就在你的開機鏈裡,每次開機都在跑 —— 沒有它 Asahi 根本開不起來。 之後要做的不是安裝新程式,而是讓同一支程式換個模式:
| 模式 | 做什麼 |
|---|---|
| 平常(你現在) | 初始化硬體 → 交棒給 U-Boot → 一路到 Linux |
| hypervisor(之後) | 進 EL2、攔截 MMIO、把 macOS 當 guest 開起來 |
實務上會有兩份 m1n1 並存:stub 裡那份繼續負責開你的 Asahi; hypervisor 用的是你自己 build 的新版(官方文件強調「每次都先 chainload working tree 的 m1n1」)。 兩份互不干擾,正常開機流程不受影響。
§2插線與拔線:從中斷到 mux
這一節只講左前 USB-C 孔(0-003f)的 Type-C/DP 狀態路徑。
重點不是「插線就切 DP」,而是:硬體先改 register、用 IRQ 提醒 kernel;kernel 讀回狀態後,才決定要不要設定 ATC PHY。
外接螢幕
MSI/Studio Display
1=可用,0=暫時不可用
拔線、關螢幕電源、螢幕重新初始化,都可能令 HPD 改變。
MacBook Pro
硬體與 Linux kernel
自己的 event、status、data status register 已先更新
SAFE/USB/DP
處理 DP connect / disconnect
Linux kernel
cd321x_interrupt()
讀 event register,ack;再依 event 讀 status / power / data status。
tps + cd321x_connect()
driver 暫存剛讀到的資料,排入 500 ms delayed work。
cd321x_update_work()
拿 snapshot,更新 Type-C partner/role/orientation 等 kernel 狀態。
PLUG_PRESENT = 0
記錄 SAFE → typec_set_mode(..., SAFE) → typec_mux_set() → ATC PHY。接著 return,不會進 cd321x_typec_update_mode()。
DP_CONNECTION = 1
cd321x_typec_update_mode() 計算 DP mode → typec_mux_set() → atcphy_mux_set() → 寫 ATC PHY register。
drm_connector_oob_hotplug_event()
→ DCP 的 dcp_dptx_connect()/dcp_dptx_disconnect()
狀態到底在何時變?
| 時間 | 哪一份狀態改變 | 誰改的 |
|---|---|---|
| 螢幕狀態改變時 | HPD 電位;CD321x 內部的 register | 螢幕與 CD321x 硬體 |
| IRQ handler 讀完後 | tps->status、tps->data_status 等 driver cache | cd321x_interrupt() |
| 500 ms 後 work 執行時 | Linux Type-C framework:partner、角色、方向;cd321x->state | cd321x_update_work() |
| 需要切換時 | ATC PHY 的硬體 register,實際高速 lane 連法 | typec_mux_set() → atcphy_mux_set() |
用 log 對照圖
# 這兩句代表圖中的③ DP branch 已經走到 ATC PHY,且回傳成功
tps6598x 0-003f: DP mux request: mode 4, result 0
phy-apple-atc b03000000.phy: Type-C mux configure mode 6: 0
# 這句代表③仍判斷為 DP,但 cd321x->state 已經是 DP,故不重設 mux
tps6598x 0-003f: DP mux already in mode 4
§3送一張畫面:page flip 與冷插/熱插的差別
先分清楚這顆 kernel 上疊了哪些東西
三層,容易搞混,先分清楚 —— 只有第三層是這次為了熱插拔加的:
| 是什麼 | 誰做的 | 跟外接螢幕的關係 | |
|---|---|---|---|
| 底層 | fairydust 分支本身 |
Asahi 上游的實驗分支 | USB-C 出圖的程式碼全部在這裡。正式 kernel 沒有,所以非用不可 |
| patch 0001 | 0001-drm-apple-reconnect-DP2HDMI-output-on-resume.patch |
build 腳本作者自己加的(原作者 rgvxsthi) | 無關。它修的是內建 HDMI 孔睡醒之後不亮,走的是另一條路徑。
跟著腳本一起來的附贈品,SKIP_PATCHES=1 可以不要 |
| patch 0002 | 0002-pr622-fix-dp-altmode-hotplug.patch(= PR #622) |
這次自己加的 | 這才是這一節在講的東西,修 USB-C DP 的熱插拔 |
換句話說「fairydust 是分支,不是 patch」;而 0001 雖然跟著 fairydust 的 build 腳本一起來, 但它不是 fairydust 的一部分,也跟 USB-C 沒關係。下面所有的 before/after 都只在講 0002。
它只改兩個檔、42 行,但是兩個完全不同的問題。
第一塊 · iomfb.c —— 為什麼會卡住幾十秒
先講「page flip」是什麼:合成器每畫好一張畫面,就交給 kernel 說「換成這張」, 然後停下來等 kernel 回報「換好了」才畫下一張。這是正常運作方式。
問題在於,螢幕剛接上、DCP 還沒真正把顯示管線開起來的那段時間
(driver 裡那個旗標叫 valid_mode,這時候是 0),
DCP 會收下那張畫面然後默默丟掉,不回報。firmware 自己的 log 寫得很直白:
swap_submit_dcp: swallowed swap ID 452 as fControllerPowerState is 0
swap_submit_dcp: swallowed swap ID 452 as timings are not enabled
合成器不知道畫面被丟了,就一直等,直到內建的 10 秒逾時。 它想畫幾張就卡幾個 10 秒 —— 這就是「整台電腦卡住幾十秒」的來源。
patch 做的事:送出去之前先看一眼 valid_mode。
還是 0 就別送了,直接自己標記「這一幀完成」讓合成器繼續跑下去。三行:
if (!dcp->valid_mode) {
schedule_work(&dcp->vblank_wq);
return;
}
注意這不是「修好顯示」,是「不要因為顯示還沒好就把整台機器拖住」。 差別很重要 —— 它解釋了為什麼套完之後不卡了,但畫面依然沒出來。
第二塊 · atc.c —— 為什麼熱插不會亮、冷插卻會
USB-C 那條線裡有幾組高速通道,同一組實體線路可以拿來跑 USB3,也可以拿來跑 DisplayPort, 兩者互斥。要從 USB3 切成 DP,得先把負責 USB3 的那個電路(叫 PIPE handler)停下來, 否則就是在一條還在通電的線路底下重新接線 —— 硬體會保護性地退回只剩 USB2,DP 根本起不來。
這就是冷插會動、熱插不會動的原因。 開機時就插著的話,那時 USB3 還沒起來,沒有東西要停; 熱插的時候 USB3 已經在跑了,才會撞到。
原本的程式碼發現「PIPE handler 還開著」時,只是印一個警告然後硬切 —— 那正是你在 dmesg 看到的:
WARNING: drivers/phy/apple/atc.c:2328 at atcphy_mux_set+0x138/0x230 [phy_apple_atc]
patch 做的事:把那個「只抱怨不處理」換成真的去處理 —— 切換前先把 PIPE handler 停到一個空的 dummy PHY 上,讓線路安靜下來,再切模式。 USB3 會暫時斷掉(等 dwc3 重新設定回來),USB2 不受影響。
驗證方式:strings phy-apple-atc.ko | grep 'park PIPE handler'。
有這個字串才代表模組是打過 patch 的 —— 這行已經在 實作 §2.2 的迴圈裡。
§4目前卡住的那一步
套完 #622 之後,熱插拔的失敗鏈條變成這樣:
- 拔線那一刻,DCP 吞掉一張 page flip,永遠不回報完成
- aquamarine(Hyprland 的 DRM 後端)那張 flip 就一直掛在「等待中」
- 之後每一次 commit 都被它自己的保護擋掉 ——
hyprland.log裡Cannot commit when a page-flip is awaiting出現了 14 次 - 連手動用
wlr-randr強制 modeset 都救不回來: kernel 那邊確實跑完了(set_digital_out_mode finished:8162), 但畫面 commit 進不去
關鍵是 Hyprland 並不是「沒反應」。它的 log 顯示它有偵測到、有列出 16 個模式、也有嘗試 modeset:
drm: Connector DP-1 connected
drm: Modesetting DP-1 with 1920x1080@60.00Hz
ERR: drm: Cannot commit when a page-flip is awaiting
| 層 | 可以說是它的問題的地方 |
|---|---|
| kernel / driver | DCP 收下 page flip 卻從不送完成事件。#622 讓「等待」不再卡 10 秒, 但遺失的 flip 在合成器那邊的帳沒有被清掉 |
| Hyprland / aquamarine | 一旦有 flip 卡住,就再也不接受任何 commit,沒有逾時、沒有復原路徑。 硬體丟事件是常態,合成器不該因此永久鎖死一個輸出 |
兩邊都有道理,所以先別急著替誰定罪 —— 這正是值得寫進 issue 讓真正懂的人判斷的東西。