緣起
這片 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);
}
更新流程:
- 首次:USB 或 IDE 刷(板子還沒 WiFi 時)。
- 之後:
python3 ota_flash.py <bin> --ip <板子IP>(或--ip esp32.local)。 - 或用 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。






