用平臺:像裝 App 一樣裝腳本,免編譯燒錄)
ESP32 能不能像手機(jī)一樣安裝應(yīng)用你別說還真能只是這個“應(yīng)用”不是 APK也不是 IPA而是一份腳本加配置文件。我最近在 ESP32 上做了一套小型應(yīng)用平臺改功能不再需要反復(fù)拔線重?zé)苯釉诰W(wǎng)頁里上傳腳本就能“裝應(yīng)用”重啟后還能自動運(yùn)行。這篇文章會把整套思路、架構(gòu)、代碼和我在實(shí)操中踩過的坑都攤開講一遍如果你也在糾結(jié)“固件燒寫到什么時候是個頭”那這篇內(nèi)容值得看完。1. 為什么要在 ESP32 上做“應(yīng)用平臺”1.1 傳統(tǒng)開發(fā)方式真正讓人難受的地方這幾年玩 ESP32 的人越來越多環(huán)境從 Arduino 到 ESP-IDF 都有但無論走哪條路只要還保持“改一行代碼就編譯一次、燒錄一次”的節(jié)奏時間一長一定會暴躁。我自己的經(jīng)歷是給家里做一個小型桌面終端顯示溫濕度、控制燈帶、偶爾讀幾個傳感器。剛開始挺開心寫了幾版后開始變味改一個閾值要重編譯調(diào)一個頁面樣式也要重編譯板子裝在亞克力盒子里每次燒錄都要拆殼接線。這些痛點(diǎn)的本質(zhì)是你的“固件”和“你的業(yè)務(wù)邏輯”沒有解耦。設(shè)備燒進(jìn)什么就是什么想變只能整體更換二進(jìn)制。手機(jī)系統(tǒng)之所以舒服是因?yàn)椴僮飨到y(tǒng)只負(fù)責(zé)基礎(chǔ)服務(wù)和運(yùn)行環(huán)境真正的功能來自“應(yīng)用”應(yīng)用可以被單獨(dú)安裝、升級、卸載。ESP32 雖然小但它的 Flash、RAM、網(wǎng)絡(luò)能力完全可以支撐一套簡化版的應(yīng)用管理機(jī)制。1.2 這里說的“安裝應(yīng)用”到底在裝什么不用把“安裝應(yīng)用”這四個字想得太高大上。拆開看手機(jī)安裝一個應(yīng)用其實(shí)做了三件事把代碼和資源放到系統(tǒng)指定的目錄寫入一條注冊記錄告訴系統(tǒng)“這個應(yīng)用存在、入口在哪”然后由系統(tǒng)的啟動器或用戶去觸發(fā)運(yùn)行。ESP32 上做同樣的事就是三步腳本文件放到一個固定目錄比如/apps/。維護(hù)一份清單apps.json記錄應(yīng)用名稱、版本、入口文件、是否開機(jī)自啟。用一段固定的服務(wù)代碼去讀取這份清單并提供 HTTP 接口讓用戶在網(wǎng)頁上執(zhí)行安裝、卸載、啟動。由于 ESP32 并不是一個完整的操作系統(tǒng)跑的不是 Linux所以這份“平臺”是搭在一個運(yùn)行時的上面的。我這里采用的是 MicroPython 方案應(yīng)用就是.py腳本。解釋器本身先作為固件燒進(jìn)去之后換功能只動腳本不再動底層固件。這套思路搬到 ESP-IDF 上同樣成立只是你需要一個腳本引擎或者一個能動態(tài)加載二進(jìn)制鏡像的 loader。2. 整體架構(gòu)與方案選型2.1 三條可行路線對比要做“應(yīng)用安裝”這件事方案不止一種我梳理了三種比較主流的路線各有各的適用場景。方案實(shí)現(xiàn)思路優(yōu)點(diǎn)缺點(diǎn)適合誰解釋器腳本方案固件燒入 MicroPython 或 Lua 運(yùn)行時應(yīng)用為腳本文件開發(fā)效率高改腳本即生效排查方便運(yùn)行效率比原生二進(jìn)制低RAM 占用固定成本較高DIY 愛好者、小批量產(chǎn)品原型、快速迭代場景OTA 雙分區(qū)方案將不同功能固件打包為多個分區(qū)鏡像通過 OTA 下載切換代碼執(zhí)行效率高可復(fù)用原生 SDK 生態(tài)更像“系統(tǒng)升級”不是獨(dú)立應(yīng)用多應(yīng)用共存復(fù)雜對性能敏感、功能相對固定的產(chǎn)品原生二進(jìn)制動態(tài)加載使用 IDA/鏈接腳本將獨(dú)立 app 編譯為位置無關(guān)代碼由 loader 加載性能最好接近手機(jī)上本地應(yīng)用門檻高資料少排錯難度大有時間啃底層、需要極致性能的人多數(shù)人不是在做操作系統(tǒng)內(nèi)核而是想讓開發(fā)過程舒服一點(diǎn)所以我選了第一種。腳本應(yīng)用在當(dāng)前 ESP32 的算力下做控制邏輯、采集邏輯、簡單的 HTML 服務(wù)端頁面都足夠真到了性能瓶頸再考慮把某個應(yīng)用改寫成原生模塊也不遲。2.2 平臺的五層結(jié)構(gòu)這套微型應(yīng)用平臺架構(gòu)上分成五層每一層各管各的事硬件驅(qū)動層GPIO、I2C、SPI、UART、ADC、網(wǎng)絡(luò)外設(shè)的驅(qū)動。運(yùn)行時層MicroPython 解釋器負(fù)責(zé)執(zhí)行腳本屏蔽底層寄存器和驅(qū)動細(xì)節(jié)。文件系統(tǒng)層基于 Flash 的 LittleFS用來存放應(yīng)用腳本和配置。服務(wù)管理層核心服務(wù)負(fù)責(zé)掃描應(yīng)用清單、提供 HTTP 上傳/列表/運(yùn)行接口、開機(jī)啟動調(diào)度。應(yīng)用層用戶開發(fā)的具體功能腳本比如 LED 控制應(yīng)用、傳感器展示應(yīng)用。手機(jī)會動不動彈“要不要允許這個應(yīng)用訪問傳感器”本質(zhì)是權(quán)限系統(tǒng)。在單片機(jī)平臺上我先不做這么復(fù)雜的權(quán)限控制但通過這種分層已經(jīng)能保證“平臺代碼”和“應(yīng)用代碼”不相干應(yīng)用崩了甚至文件刪了平臺主體還是好的。這一點(diǎn)在后面的容錯設(shè)計(jì)里我會專門講。2.3 硬件準(zhǔn)備與 Flash 地址規(guī)劃我這套用的是一塊最普通的 ESP32 DevKitCFlash 大小 4MB。別小看 4MB對腳本應(yīng)用來說空間非常奢侈。MicroPython 官方固件默認(rèn)會燒寫 bootloader、分區(qū)表、MicroPython 固件然后在末尾自動劃出一個文件系統(tǒng)分區(qū)。查看當(dāng)前文件系統(tǒng)大小可以進(jìn) REPL 執(zhí)行import os print(os.getvfs(/).info())打印出的信息里能看到文件系統(tǒng)總?cè)萘亢褪S嗫臻g。如果嫌官方默認(rèn)分區(qū)給文件系統(tǒng)的空間太小可以自己改分區(qū)表。ESP-IDF 的分區(qū)表是一個 CSV 文件我這邊用一個典型的分區(qū)設(shè)定示例# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x200000, storage, data, fat, 0x300000,0x100000,分區(qū)表里factory是主固件區(qū)如果啟用了 OTA 機(jī)制可以分出ota_0和ota_1來。storage分區(qū)就是留給文件系統(tǒng)的。在設(shè)計(jì)這套“安裝應(yīng)用”的平臺時一個很重要的原則是固件區(qū)是運(yùn)行基礎(chǔ)文件系統(tǒng)區(qū)才是應(yīng)用安裝的位置兩者物理隔離才不怕應(yīng)用寫壞系統(tǒng)。3. 實(shí)操過程從零搭建微型應(yīng)用平臺3.1 燒錄 MicroPython 基礎(chǔ)固件整個過程的第一步是給 ESP32 燒一個帶解釋器的基礎(chǔ)固件。去 MicroPython 官網(wǎng)下載適用于 ESP32 的.bin文件就行選帶SPIRAM或者不帶要看你的板子我手頭這塊沒有 PSRAM就用標(biāo)準(zhǔn)版。燒采用 esptool先擦除整片 Flash再寫入固件esptool.py --port COM3 erase_flash esptool.py --port COM3 --baud 460800 write_flash -z 0x1000 ESP32_GENERIC-20240602-v1.23.0.bin提示0x1000是 ESP32 出廠規(guī)定的 bootloader 起始偏移不要亂改。如果看到A fatal error occurred: Failed to connect to Espressif device大概率是芯片沒有進(jìn)入下載模式按住板子上的 BOOT 鍵再點(diǎn)燒錄即可。燒錄完用 PuTTY 或 Thonny 打開串口復(fù)位一下應(yīng)該能看到的 Python 提示符。到這一步你的 ESP32 已經(jīng)從一臺“需要整體編譯燒錄的設(shè)備”變成了一個“具備腳本運(yùn)行能力的設(shè)備”。后續(xù)再改業(yè)務(wù)邏輯不需要回到編譯器直接在 Thonny 里編輯腳本上傳就行。3.2 網(wǎng)絡(luò)接入給平臺裝上“下載通道”要實(shí)現(xiàn)網(wǎng)頁里“安裝應(yīng)用”網(wǎng)絡(luò)是必須的。ESP32 的優(yōu)勢是自帶 Wi-Fi代碼很短。我通常在main.py里固定寫一個連接 Wi-Fi 并輸出 IP 地址的模塊import network import time def connect_wifi(ssid, password, timeout15): wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, password) start time.time() while not wlan.isconnected(): if time.time() - start timeout: raise RuntimeError(Wi-Fi connect timeout) time.sleep(0.5) print(IP:, wlan.ifconfig()[0]) return wlan.ifconfig()[0]如果你的設(shè)備使用了 LAN8720 以太網(wǎng)模塊那需要走network.LAN接口這個我在后面“常見問題”里專門說因?yàn)榭雍芏?。網(wǎng)絡(luò)通了之后“應(yīng)用平臺”的服務(wù)端就可以綁定在0.0.0.0:80上這樣手機(jī)、電腦在同一個局域網(wǎng)里打開瀏覽器輸入 ESP32 的 IP就能訪問應(yīng)用管理界面。3.3 編寫核心服務(wù)應(yīng)用管理器這個應(yīng)用管理器是整個平臺的心臟我用 MicroPython 的socket模塊手寫了一個精簡 HTTP 服務(wù)不依賴任何第三方網(wǎng)絡(luò)庫方便你在同樣環(huán)境下直接復(fù)現(xiàn)。核心邏輯如下import os import json import socket import gc APPS_DIR /apps MANIFEST /apps/apps.json def load_manifest(): try: with open(MANIFEST) as f: return json.loads(f.read()) except OSError: return {installed: []} def save_manifest(manifest): with open(MANIFEST, w) as f: f.write(json.dumps(manifest)) def install_app(filename, code): if not os.path.isdir(APPS_DIR): os.mkdir(APPS_DIR) path APPS_DIR / filename with open(path, w) as f: f.write(code) manifest load_manifest() for app in manifest[installed]: if app[file] filename: app[version] 1.0 save_manifest(manifest) return update manifest[installed].append({ name: filename.replace(.py, ), file: filename, version: 1.0, autorun: False }) save_manifest(manifest) return install def list_apps(): manifest load_manifest() html htmlheadtitleESP32 App Store/title/headbody html h1ESP32 應(yīng)用管理/h1 html h3已安裝應(yīng)用/h3ul for app in manifest[installed]: html fli{app[name]} - v{app[version]} html f[a href/run?app{app[file]}運(yùn)行/a] html f[a href/uninstall?app{app[file]}卸載/a]/li html /ul html h3上傳安裝/h3 html form methodPOST action/upload html input typetext namefilename placeholderapp_name.py html textarea namecode rows8 cols60 placeholder粘貼腳本代碼/textarea html input typesubmit value安裝 html /form/body/html return html這里我沒有直接把 HTTP 解析寫滿因?yàn)槟憧吹降氖枪羌軐?shí)際操作時還需要在do_GET和do_POST里解析請求行和請求體取到/upload表單里的filename和code字段再交給install_app。為了保證 RAM 不被大請求撐爆上傳的代碼不要太大幾百行以內(nèi)的腳本都沒問題。如果將來想上傳更復(fù)雜的應(yīng)用建議改成按塊寫入或壓縮包方式。3.4 演示應(yīng)用通過網(wǎng)頁遠(yuǎn)程開關(guān) LED平臺有了還要真正“裝上”一個應(yīng)用驗(yàn)證流程。我寫了一個最簡單的 LED 控制應(yīng)用它的功能是提供一個小網(wǎng)頁點(diǎn)按鈕就翻轉(zhuǎn) GPIO2 的電平。這個應(yīng)用本身就是一段獨(dú)立腳本放到/apps/app_led.pyimport machine import socket import network led machine.Pin(2, machine.Pin.OUT) def page(): state OFF if led.value() 0 else ON return fhtmlbodyh1LED 控制/h1 fp當(dāng)前狀態(tài): {state}/p fa href/toggle點(diǎn)我翻轉(zhuǎn)/a/body/html def run_server(): s socket.socket() s.bind((0.0.0.0, 8080)) s.listen(2) while True: conn, addr s.accept() req conn.recv(1024).decode() if /toggle in req: led.value(1 - led.value()) conn.send(HTTP/1.1 200 OK\r\nContent-Type:text/html\r\n\r\n page()) conn.close()安裝這個應(yīng)用的時候通過管理平臺的網(wǎng)頁表單填文件名app_led.py粘貼上述代碼點(diǎn)提交。之后瀏覽器訪問http://esp32-ip:8080就能看到這個應(yīng)用的獨(dú)立控制界面。這個例子的意義在于應(yīng)用和平臺之間的關(guān)系就像手機(jī)裝上了一個獨(dú)立 App。用戶不會感受到底層“Flash 里有幾個文件”他們只知道訪問網(wǎng)頁就能控制 LED。3.5 開機(jī)自啟動與“應(yīng)用入口”的調(diào)度手機(jī)開機(jī)之后你可以手動點(diǎn)開某個 App也可以設(shè)置自啟動。我在平臺里也做了類似機(jī)制main.py里啟動網(wǎng)絡(luò)、啟動管理服務(wù)、然后掃描清單里autorun為True的應(yīng)用。from app_manager import load_manifest, start_http_server def main(): connect_wifi(your_ssid, your_password) start_http_server() manifest load_manifest() for app in manifest[installed]: if app.get(autorun): print(autorun, app[file]) # 通過 importlib 或動態(tài)執(zhí)行機(jī)制加載應(yīng)用 if __name__ __main__: main()動態(tài)執(zhí)行應(yīng)用有很多種方式比如importlib.import_module或者直接exec(open(path).read())。我建議是每個應(yīng)用本身也做成一個函數(shù)入口由管理器在子線程中執(zhí)行這樣某個應(yīng)用阻塞了也不影響整個管理平臺繼續(xù)對外服務(wù)。這里有個容易被忽略的點(diǎn)MicroPython 的線程支持比較基礎(chǔ)多線程時要注意 GIL 和棧空間限制所以在exec那種粗暴方式里要格外小心。4. 常見問題與排查技巧實(shí)錄4.1 LAN8720 以太網(wǎng)模塊的三個高頻坑如果你的設(shè)備是用網(wǎng)線接入局域網(wǎng)的就會用到 LAN8720。這個模塊本身不貴但接 ESP32 時非常容易翻車。我在這里把最有代表性的三個問題講透。第一個坑PHY 芯片的復(fù)位時序。LAN8720 有個nRST引腳如果它一直處于低電平芯片就永遠(yuǎn)處于復(fù)位狀態(tài)網(wǎng)絡(luò)自然起不來。最常見的錯誤是把nRST直接接到 ESP32 的 EN 引腳這樣 ESP32 復(fù)位后 PHY 也跟著復(fù)位但 ESP32 主控初始化網(wǎng)卡時 PHY 可能還沒準(zhǔn)備好問題就隨機(jī)出現(xiàn)了。解決辦法把nRST接到 ESP32 的一個普通 GPIO比如 GPIO18然后在代碼里手動延時并拉高reset_pin machine.Pin(18, machine.Pin.OUT) reset_pin.value(0) time.sleep_ms(100) reset_pin.value(1) time.sleep_ms(150)接線就按 GPIO18 到 LAN8720 的 nRST中間不用加電阻電平兼容性沒問題。第二個坑REF_CLK 時鐘源。LAN8720 支持從外部晶振或從主控輸入 50MHz 參考時鐘。很多模塊板載了 50MHz 晶振有的卻沒有而 ESP32 的 RMII 接口要求ETH_CLOCK_GPIO0_IN或ETH_CLOCK_GPIO17_OUT二選一。如果是板載晶振軟件配置為輸入模式如果是外部輸出要在 GPIO17 引 50MHz。配置錯了現(xiàn)象就是一直連不上錯誤日志里全是TIMEOUT。第三個坑供電。LAN8720 是 3.3V 供電但一些模塊在啟動瞬間電流不小。如果從 ESP32 開發(fā)板的 3V3 引腳直接取電而旁邊又帶著屏幕、傳感器電壓容易跌落網(wǎng)絡(luò)會時好時壞。我第一次遇到就很玄學(xué)剛開機(jī)時能連上一操作外設(shè)就掉線。最后查下來是供電問題建議單獨(dú)用一片 AMS1117 降壓模塊從 5V 取電給 LAN8720。下面是我調(diào)試通過后的接線參考LAN8720 引腳ESP32 引腳VCC3V3或獨(dú)立 3.3V 電源GNDGNDMDCGPIO23MDIOGPIO18RXD0GPIO19RXD1GPIO21CRS_DVGPIO22TX_ENGPIO25TXD0GPIO26TXD1GPIO27nRSTGPIO18注意與 MDIO 區(qū)分建議換一個空閑腳REF_CLK按模塊晶振情況接 GPIO0 或 GPIO17建議真正布線時把 MDIO 和 nRST 分到兩個不同 GPIO我之前圖省事把它們重合導(dǎo)致 PHY 復(fù)位后 MDIO 總線一直異常。4.2 燒錄與文件系統(tǒng)典型故障上傳代碼失敗、文件寫入提示 OSError檢查路徑是否存在MicroPython 的/apps/目錄如果要手動創(chuàng)建用os.mkdir(/apps)。有些固件默認(rèn)文件系統(tǒng)是只讀掛載需要先卸載再重新掛載或者干脆重刷固件。文件系統(tǒng)損壞最直接的表現(xiàn)是啟動后import os; os.listdir()報(bào)OSError: 19。解決方法是擦除 Flash 后重刷然后重新上傳腳本。不要嘗試在設(shè)備上做太多救援操作浪費(fèi)的時間足夠重刷第二遍。應(yīng)用平臺被自己的應(yīng)用影響這一點(diǎn)很關(guān)鍵。我踩過一次很深的坑在做一個睡眠主題應(yīng)用時腳本里寫了machine.deepsleep()結(jié)果平臺管理服務(wù)也被拉下去了。后面我把應(yīng)用執(zhí)行統(tǒng)一放在獨(dú)立線程里并且在啟動應(yīng)用前做了gc.collect()保證內(nèi)存回收及時。4.3 硬件資源限制與運(yùn)行時的“隱藏規(guī)則”ESP32 的 RAM 只有 320KB 左右實(shí)際可用到 MicroPython 堆的也就 100KB 上下。這個體量在“應(yīng)用平臺”層面夠用但你不能指望跑重型計(jì)算。我在實(shí)際測試中一個控制頁面加幾個傳感器讀數(shù)的應(yīng)用堆內(nèi)存占用大約 15KB還能接受。如果你要跑圖像處理或者復(fù)雜的加密邏輯腳本方案就吃力了。另外一定要提 ESP32 的 ADC 問題。用 ADC2 的通道采集時如果 Wi-Fi 同時在工作采集結(jié)果可能一直不對因?yàn)?ADC2 和 Wi-Fi 是共用硬件的。我建議所有模擬量采集盡量走 ADC1GPIO32-GPIO39同時做好電壓分壓校準(zhǔn)畢竟 ESP32 ADC 在中段比較線性靠近兩端會有明顯非線性你需要查原始曲線或者自己多點(diǎn)校準(zhǔn)。4.4 快速排查速查表現(xiàn)象可能原因解決辦法點(diǎn)擊安裝后沒有出現(xiàn)在列表表單字段名不一致導(dǎo)致安裝函數(shù)沒拿到參數(shù)確認(rèn) POST 字段為filename和code訪問管理頁面一直轉(zhuǎn)圈服務(wù)端線程阻塞或卡在某個應(yīng)用執(zhí)行上重啟設(shè)備啟動平臺后再手動觸發(fā)問題應(yīng)用上傳大腳本后設(shè)備重啟接收請求時recv緩沖區(qū)太大導(dǎo)致 RAM 不足限制上傳大小或改用分塊接收Wi-Fi 連接不穩(wěn)定供電不足或信號弱獨(dú)立供電調(diào)整天線方向減少高功率外設(shè)平臺服務(wù)能起來但應(yīng)用自動啟動失敗應(yīng)用腳本有語法錯誤或缺少依賴在 REPL 里單獨(dú)執(zhí)行該腳本確認(rèn)報(bào)錯信息5. 這個平臺還能用在哪些場景5.1 小批量設(shè)備上的“在線增裝”如果你做的是小批量的設(shè)備比如幫朋友做十來個環(huán)境監(jiān)測終端過去每改一次需求都要把所有設(shè)備拆下來燒錄。有了這套應(yīng)用平臺設(shè)備只需要第一次燒好基礎(chǔ)固件后面全部通過網(wǎng)頁更新腳本遠(yuǎn)程運(yùn)維的體驗(yàn)完全不一樣。我身邊有人用這個思路做桌面終端設(shè)備放在客廳人坐在工位上打開瀏覽器就能往終端上裝一個“客流統(tǒng)計(jì)”應(yīng)用或者“白噪音播放”應(yīng)用這就是“ESP32 終端”該有的自由度。5.2 邊緣采集與異常檢測工業(yè)場景里的軸承故障檢測、電機(jī)振動分析搞起來不便宜但 ESP32 加傳感器做輕量級預(yù)篩選完全可行。你可以把傳感器驅(qū)動、特征計(jì)算、告警上傳這些模塊拆成獨(dú)立應(yīng)用基礎(chǔ)平臺只負(fù)責(zé)采集原始數(shù)據(jù)裝一個“FFT 頻譜分析”應(yīng)用就做頻域分析裝一個“閾值報(bào)警”應(yīng)用就只發(fā)告警。應(yīng)用之間互不影響升級某個算法也不用整機(jī)停擺。這種模塊化能力在產(chǎn)品原型驗(yàn)證階段特別有用。5.3 后續(xù)擴(kuò)展方向這套平臺目前只實(shí)現(xiàn)了“文件即應(yīng)用、清單即注冊”的雛形但擴(kuò)展空間足夠大。你可以考慮增加以下能力用 Zip 格式打包應(yīng)用包含代碼、網(wǎng)頁靜態(tài)資源、應(yīng)用圖標(biāo)上傳后自動解壓更像手機(jī)應(yīng)用包。增加簡單鑒權(quán) Token避免局域網(wǎng)內(nèi)任意設(shè)備都能改應(yīng)用設(shè)置。應(yīng)用間通信基于ujson或msgpack通過統(tǒng)一消息總線傳遞數(shù)據(jù)。給文件系統(tǒng)外接 SD 卡讓應(yīng)用區(qū)容量從幾百 KB 擴(kuò)展到幾 GB。極端性能需求下改造部分應(yīng)用為原生二進(jìn)制并通過 loader 動態(tài)加載把解釋型應(yīng)用和原生應(yīng)用混合管理。我之前也試過直接在 ESP-IDF 里寫一個真正的動態(tài)加載器思路和上面的原生二進(jìn)制路線一致但開發(fā)節(jié)奏確實(shí)慢很多目前更穩(wěn)定的組合還是 MicroPython 為主。建議先從腳本應(yīng)用開始把平臺用起來再逐步按需升級。最后再分享一個小技巧平臺入口腳本main.py里盡量給所有可能出錯的調(diào)用都加上try/except讓應(yīng)用級別的問題最多只打印錯誤日志不中斷整個平臺進(jìn)程。我最初就是因?yàn)闆]加保護(hù)一個應(yīng)用語法錯誤直接讓整個 ESP32 陷入反復(fù)重啟排查過程比寫代碼本身還痛苦。把平臺基礎(chǔ)穩(wěn)定住后面再“安裝”多少應(yīng)用都不慌。