2026年10月22日 星期四

ESP32 雙核分工:OTA 服務跑 Core 0、應用跑 Core 1——實作紀錄與踩坑筆記

📺 影片版(約 6 分鐘):想先看實際操作的話可以直接看這支影片,完整步驟、設定與程式碼在下方文字版。
YouTube:https://youtu.be/rqsM5M58Kz8(winson-DIY 頻道)

緣起

這片 ESP32 D0WD(WROOM-32,4MB flash)之前的 OTA 韌體(v1.1)已經跑通:ArduinoOTA(UDP 3333 協定)、雙 AP 自動切換(家裡 AP1 / 公司 AP2)、狀態網頁。

但下一個問題很實際:以後要寫別的應用(感測、馬達、MCU 通訊)到這片板子,如果新應用沒把 OTA 帶進去,就不能免 USB 更新了。

所以定了一個架構原則:OTA 是「服務」,不是某一顆韌體的功能 — 讓它常駐在一個核心、永遠在線;應用放在另一個核心,隨便換。

架構

ESP32 是雙核 Xtensa,FreeRTOS 可以把任務釘(pin)在指定核心:

核心 角色 內容
Core 0 常駐服務 WiFi 雙 AP 連線 + 30 秒重試、ArduinoOTA(UDP 3333)、狀態網頁(:80)
Core 1 應用 你的業務邏輯(範例:藍燈 1Hz + 計數)

檔案구조(share-folder 與 Pi5 各一份,保持同步):

ESP32/
├── common/wifi_config.h       # 共用 WiFi/OTA 設定(AP1 家 / AP2 公司 / OTA 密碼)
├── esp32_ota/                 # 純 OTA 韌體 v1.1(首次 USB 刷機可用)
└── esp32_app_template/        # 雙核應用模板 v2.0
    ├── esp32_app_template.ino # 主程式:啟服務 + 建應用任務
    ├── ota_service.h          # 對外只暴露 startOtaService()
    ├── ota_service.cpp        # Core-0 常駐:WiFi/OTA/狀態頁
    └── README.txt

新應用做法:複製整個 esp32_app_template/ 為專案名,只改 .ino 裡標了 ★★★ 的區塊與 LED 腳位,服務檔不動。

核心程式

// esp32_app_template.ino — 先啟服務,再起應用
void setup() {
  Serial.begin(115200);
  startOtaService();   /* Core 0:WiFi + OTA + 狀態頁 */
  /* Core 1:你的應用 */
  xTaskCreatePinnedToCore(appTaskFunc, "app", 8192, nullptr, 2, nullptr, 1);
}

// ota_service.cpp — tryConnect 關鍵段
for (size_t i = 0; i < 2; i++) {
  WiFi.disconnect(true, aps[i].ssid);
  WiFi.setMinSecurity(WIFI_AUTH_WPA2_PSK);   /* WPA2 地板,可向上協商 WPA3 */
  WiFi.begin(aps[i].ssid, aps[i].pass);      /* ★ 這行漏掉會災難(見踩坑 #1) */
  unsigned long t0 = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - t0 < 12000) delay(250);
  if (WiFi.status() == WL_CONNECTED) return (uint8_t)(i + 1);
}

更新流程:

  1. 首次:USB 或 IDE 刷(板子還沒 WiFi 時)。
  2. 之後:python3 ota_flash.py <bin> --ip <板子IP>(或 --ip esp32.local)。
  3. 或用 Arduino IDE:Tools → Port → IP Network,直接 Upload。

驗證結果(2026-10-22)

  • 公司 AP2 1.3 秒關聯成功,IP 192.168.1.170,RSSI -38
  • 狀態頁 :80 全綠:韌體 2.0.0、連線 AP、核配置「Core 0=OTA/WiFi/網頁 | Core 1=應用」
  • OTA 自我刷機(模板 963KB)成功,自動重啟、自動重連
  • 應用任務(core 1)1Hz 藍燈 + tick 計數正常

踩坑筆記(重點)

1. tryConnect 漏了 WiFi.begin(浪費一整天)

v1.1 的試連函式有 WiFi.begin(ssid, pass),移植到任務裡的版本漏了這一行 — 變成「每 12 秒乾等一次狀態」,任何 AP 都「失敗」。症狀非常騙人:

  • 掃描看得到 SSID
  • 關聯每次都 12 秒超時
  • 另一台裝置(筆電)在同一個 AP 上正常連線
  • 斷電重開、換 2.4G 熱點、換密碼都沒用

最後逐行 diff v1.1 vs v2.0 才發現就缺這一行。教訓:看到「掃得到、關聯超時、別台裝置正常」,第一步先看自己的韌體有沒有真的下 WiFi.begin,別急著往 AP 端 / WPA3 / 5GHz / 供電 / 射頻硬體推。

2. esp32 core 3.3.12 的 API 差異

慣用寫法 3.3.12 實際
xTaskCreatePinnedToTask(...) 是 xTaskCreatePinnedToCore
LED_BUILTIN 不存在,自定義(本板藍燈 = GPIO2)
WiFi.encryptionType() 不存在
ArduinoOTA.getType()/getErrorString() 不存在(error 碼 0=AUTH…4=END)
Client c = server.available() Client 是抽象(要用 WiFiClient);available() 已被 accept() 取代(警告)
WiFi.disconnect(true, ssid) 簽名 (bool, unsigned long),第二參不是 SSID
WiFi.disconnectReason() 只有 disconnectReasonName(code),code 自己帶
mDNS 掛 arduino 服務 MDNS.enableArduino(port, auth)(與 ArduinoOTA 自註冊重複會打「Service already exists」ERROR,無害)

3. ArduinoOTA 協定(3.3.x)不是常見的 TCP 推流

流程:傳控制資料包到板子 UDP 3333 → 板子回 OK(或 AUTH nonce 走 PBKDF2 認證)→ 板子主動 TCP 連回你的聽取埠 → 你送出 .bin 原始位元組 → 板子定期回十進位 ack → 驗完 MD5 回 OK 並重啟。ota_flash.py(純標準函式庫,任何有 Python 3 的機器都能跑)就是照這個實作。

4. OTA 密碼

WIFI_OTA_PASS 留空 = 無認證,同一網路任何人可刷你的板。公司/對外網路建議填密碼(腳本 --password 參數配合)。

5. 板子 IP 與網路綁定

IP 是 DHCP 給的,換網路(家↔公司)會變。找不到就:--ip esp32.local(mDNS)、開狀態頁、或 ping 掃。

6. 腳位紅線(這片 D0WD)

  • GPIO 6–11:內部 flash,禁用
  • 34/35/36/39:只讀
  • 32/33 是 ADC1,WiFi 開著仍可量;25/26/27 是 ADC2,WiFi 一開就讀不到
  • 0/2/15 是 strapping,2/15 當輸出要注意上電狀態
  • 藍燈 = GPIO2

7. 小工具坑

cp 來源檔進目錄時,目標若只寫到目錄沒帶檔名,檔案會以來源檔名掉進目錄 — 我們因此製造出一個 os.cpp,連結時報「multiple definition」,花了一輪才找到。(同步檔案時目標永遠寫全路徑。)

結論

一個可以直接照抄的配方:

OTA 服務 = Core 0 常駐任務(WiFi + ArduinoOTA + 狀態頁);應用 = Core 1 可替換任務;兩方共享一份 wifi_config.h — 以後每個新應用都是「生來就有 OTA」,USB 只在首次亮相。

整組原始碼在 Arduino/Source/FW/ESP32/(share-folder 與 Pi5 各一份),模板說明在 esp32_app_template/README.txt。



延伸閱讀

沒有留言:

張貼留言