Skip to content
YMK Must be Kidding
Go back

Pico 2 W 的 Wi-Fi 收不到封包,Claude 猜了六次錯了五次,最後是 bisect 救的

這篇是叫 Claude 從我的筆記整理出來,事情真的發生過,字是他打的,連語氣也是叫他學我的,我只負責瞄一下的喔,好吧,兩下,三下,四下。

一個理論能解釋所有的觀察,不代表它是對的,只代表你還沒去戳它的痛點

先講這是什麼東西

我在做一台硬體 terminal,叫 pico2w-tang-term。就是一個接上 HDMI 跟鍵盤,開機就可以 ssh 到遠端機器的小盒子。兩塊板子:Tang Nano 20K 這塊 FPGA 負責把字畫到螢幕上,Raspberry Pi Pico 2 W 負責字以上的事情:終端機、網路、SSH。Pico 2 W 裡面跑的是 RISC-V 雙核心加 FreeRTOS,Wi-Fi 是板子上內建的。

這篇講的是網路那一段,第一次要讓它連上家裡的 Wi-Fi。想說這有什麼難的,連個 Wi-Fi 而已耶。

症狀:什麼都好,就是收不到

這隻 bug 最討厭的地方是它演得很像別的東西。板子的狀況是:

然後它收不到任何東西。對 router 問「你的 MAC 是多少啊」問了 66 次,沒人理它。可是抓封包一看,router 其實反過來問它 49 次,另一台電腦也回了它 250 次,板子通通當作沒看到。

對,它會拿到 IP 只是因為 DHCP 的回覆是 broadcast,大家都收得到,不是它真的聽得到。

可以說話,卻聽不到。這個組合跟 router 的 client isolation 長得一模一樣,也跟距離太遠、電源不夠一模一樣。它哪個都不是,但是我們還不知道。

第一天:什麼都怪過了

照順序怪過的東西:router 的 client isolation、router 不給上網、WPA 的加密方式、Wi-Fi 晶片的省電模式、板子上某顆供電零件、距離太遠、TCP 跟 UDP 的差別、router 不回 ARP……每一個都認真測過,每一個都沒有關係。

叫 Claude 查,他更認真,排了十八個嫌疑犯一個一個審。先 scan 再連、綁 BSSID、關省電、probe 速度拉五倍、換 WPA3、密碼故意打錯看看……十八個,全都沒有關係。

更好笑的是,審問用的工具有六個本身是壞的。比如說印 IP 的函式回的是同一個 buffer,一行印兩個 IP,印出來兩個一樣,他就根據這個推理了半天。還有一個「它失敗得很快,所以是路由問題」的結論,從頭到尾沒有人真的去計時,後來量了是 18.5 秒,剛好是相反的意思。

儀器壞掉的時候,讀出來的數字會非常支持任何你想相信的理論喔。

猜六次,錯五次

然後開始有「規則」出現了。第一個規則是:sleep_ms 有問題。

結果
初始化之前 sleep_ms(2000)0/9,全掛
改成 vTaskDelay(2000)111/111,全通
改回 sleep_ms(2000)0/7,又全掛
sleep_ms(2000) 挪到初始化之後58/58,全通

很漂亮對不對,一致到讓人想直接收工。可是我看了總覺得怪怪的:sleep_ms 這麼基本的東西,全世界的 Pico 天天在用,怎麼可能是它寫錯?要是它真的有問題,早就被人罵翻了啊。怪它有點說不過去,底下應該還有更深的原因,它只是剛好踩到了什麼。

Claude 一共宣布了六次「找到 root cause 了」。sleep_ms 有問題、一定要在 core 1、不能在 tick core、core 編號對不上、async task 跑錯核心,最後才是真的那個。前五次都錯,而且每一次都能完美解釋當時手上的所有資料。每次都講得很有信心,每次我都差點相信了。

中間我回嘴了兩次,兩次都是憑感覺,兩次都對(這段是他自己寫在筆記裡的,不是我逼他的喔)。第一次就是上面那個「怪 sleep_ms 說不過去」,第二次是這句:

「我們明明有 reproducer,怎麼會找不到?」

他聽完就不猜了,改用切的。

bisect 四刀

有一個一定壞的版本,有一個一定好的版本,那就不要再動腦了,一半一半切:

  1. 不要開 server task,還是壞,沒關係
  2. 整個網路設定換成最小的連線,還是壞,沒關係
  3. 拿掉 probe 裡多做的一件事,還是壞,沒關係
  4. diff build 設定檔,一模一樣

切完剩下一個字面上的差異,就是它。

十八個嫌疑犯全都沒有關係,這件事本身就是線索:代表兇手不在名單上。一個一個審,只審得到你已經想到的人。

真正的原因

Wi-Fi 晶片有一條線叫 WL_HOST_WAKE,用來跟 CPU 說「我有封包了,快來拿」。初始化的時候,這條線的中斷會接在呼叫初始化的那顆 core 上。可是 driver 是在另一顆 core 上等這個中斷的。

所以晶片一直在喊「我有封包了」,喊在一顆沒人在聽的核心上。送封包不需要這條線,所以送得好好的;收封包全靠它,所以收不到。TX 正常、RX 死掉,就是這樣來的。

而且 SDK 自己其實知道這件事,初始化函式的第一行就是一個 assert,檢查你有沒有在對的 core 上。只是 release build 下 NDEBUG 把 assert 關掉了。

uint32_t cyw43_irq_init(void *param) {
    assert(get_core_num() == async_context_core_num(cyw43_async_context));  // release build 看不到這行
    ...
    irq_set_enabled(IO_IRQ_BANK0, true);    // 這個在 RP2 上是每顆 core 各自一份的
}

用 debug build 編一次,一分鐘就結束了。 我們用 release build 猜了一天。

前面那些「規則」為什麼看起來都對?因為 sleep_ms、core 1、tick core 這些東西,都在間接影響初始化最後落在哪顆 core。sleep_ms 什麼都沒做錯,它只是讓 task 睡醒的時候剛好落在另一顆 core 上而已,所以才說怪它說不過去。它們都是真兇的影子,所以每個理論都解釋得通,但沒有一個是真的。

修法

自己建 async_context,pin 在一顆 core 上,然後呼叫初始化的 task 也 pin 到同一顆。這樣兩邊一定一樣,不靠運氣:

async_context_freertos_config_t cfg = async_context_freertos_default_config();
cfg.task_core_id = 1;
async_context_freertos_init(&ctx, &cfg);
cyw43_arch_set_async_context(&ctx.core);

vTaskCoreAffinitySet(NULL, 1u << 1);   // 自己也去 core 1
cyw43_arch_init();

修好之後把原本「有問題」的 sleep_ms 放回去,125/125 全通。delay 用哪個函式根本無關,core 才是全部。

順便挖到另一個不相干的 bug:在這個 port 上把任何 task pin 到 tick core 都會直接當掉,有沒有 Wi-Fi 都一樣。記下來是為了下次不要又跟這個混在一起。

下次怎麼看

  1. 板子可以說話、卻聽不到,先懷疑中斷接錯 core,不要先怪 router。
  2. 卡住的時候先用 debug build 編一次。SDK 的 assert 是寫給你看的,不是裝飾。
  3. 嫌疑犯審到第十幾個都沒有關係,就該停下來改用 bisect 了。
  4. 讀數字之前先看一眼儀器是不是好的。壞掉的 probe 可以支撐任何理論。
  5. AI 說「找到 root cause 了」的時候,先問他:那你去戳一下,看它會不會破功。

Debug build 一分鐘的事,用 release build 猜了一天喔


分享這篇:

Previous Post
自己做的 terminal 在 LG 螢幕上沒有 login 提示、字又糊糊的,兩次都是螢幕的問題
Next Post
家裡 NAS 的 WireGuard 每隔幾週就斷線,原來是 router 的 NAT table 記著舊的 public IP