ESP32 不編譯、不燒錄:MicroPython 讓我一夜從零寫出四種 LED 動畫引擎

作者: Calpa Liu
字數:1584
出版:2026 年 11 月 3 日
#MicroPython#ESP32#Web Serial#WS2812#IoT
MicroPython 開發 ESP32 的真實一夜實錄:不用編譯、不用燒錄,REPL 改一行 0.5 秒生效。從三行點亮 WS2812、設計序列協議、心跳動畫的雙峰曲線,到序列埠佔用與 esptool 咬死 USB-JTAG 的踩坑全記錄,附完整程式碼。

一句話總結:MicroPython 把 ESP32 的開發循環從「編譯三十秒加燒錄一分鐘」壓到「改一行 0.5 秒生效」,一個晚上從裸板寫到四種動畫引擎。這篇文章是那一夜的實錄,含完整程式碼與踩坑清單。

半年前我做嵌入式開發的時候,一天裡有大量的時間在等。改一行 C,等編譯;編譯完,等燒錄;燒錄完,看著板子重開機,然後發現改錯地方,重來一次。真正的開發時間被切成碎片,每一次等待都在把腦中剛成形的假設放涼。

電路板特寫:嵌入式開發的日常風景

前天晚上十一點多,我拿出塵封的 ESP32-S3 和一顆 WS2812 RGB 燈,決定換一種做法。這次的目標很簡單:不寫 C,改用 MicroPython,看看一個晚上能走多遠。結果是,凌晨一點半的時候,我有了自己的韌體協議、四種動畫引擎、一個瀏覽器調色盤前端,還有另一個 repo。

為什麼是不用編譯、不用燒錄

傳統嵌入式的循環是「改、編譯、燒、重開」。MicroPython 把前三步砍到只剩「改」,而且改的地方不是本機檔案,是板子上正在跑的直譯器。

打開序列埠的 REPL,直接敲三行,燈就亮了:

>>> from machine import Pin
>>> from neopixel import NeoPixel
>>> np = NeoPixel(Pin(48, Pin.OUT), 1)
>>> np[0] = (255, 128, 0)
>>> np.write()

藍色調的開發板:硬體開發的起點

沒有 toolchain,沒有 linker 錯誤,沒有等。想看不同顏色?改那組數字再 np.write() 一次。想調呼吸燈的頻率?改完參數 0.5 秒內生效。這種回饋速度改變的不是效率,是嘗試的勇氣:敢試的次數變多,找到對的參數就變成必然。

從點燈到設計自己的韌體協議

點燈只是入場券。那天晚上真正好玩的部分,是替 LED 設計一個序列協議,讓瀏覽器可以直接對板子下指令。

協議設計得很克制,一行一個指令,換行結尾:

# R,G,B                      靜態色 0-255(同時停止動畫)
# red/green/blue/white/off   具名色 / 關燈
# stop                       停止動畫(保留目前色)
# anim,NAME,R,G,B,PERIOD_MS  動畫:breath / rainbow / strobe / heart

綠色印刷電路板:協議與邏輯的家

四種動畫引擎裡,我最喜歡的是心跳。它不是簡單的明暗交替,而是模擬真實心跳的雙峰:一次比較重的「咚」,停頓零點零八秒,再來一下比較輕的「咚」。寫成程式只有幾行:

def f_heart(t):
    r, g, b = color
    p = (t % period) / period
    if p < 0.12:  # 咚
        k = math.sin(math.pi * p / 0.12)
    elif 0.2 <= p < 0.35:  # 再咚一下
        k = math.sin(math.pi * (p - 0.2) / 0.15)
    show(r * k, g * k, b * k)

主迴圈是整個韌體的心臟:8 毫秒輪詢一次序列埠,有新指令就處理;動畫引擎則鎖在 16 毫秒一幀,大約 60fps。命令的低延遲與動畫的穩定幀率,兩者互不干擾:

while True:
    if poll.poll(8):  # 8ms 輪詢,命令低延遲
        line = sys.stdin.readline()
        if line:
            handle(line)
    now = time.ticks_ms()
    if anim and time.ticks_diff(now, last_frame) >= 16:  # ~60fps
        last_frame = now
        ANIMS[anim](now)

踩坑實錄:便利的背面

一夜順利的背後,該踩的坑一個沒少。這些都記在 repo 的 AGENTS.md 裡,這裡挑四個最有價值的:

序列埠佔用有四種來源。mpremote 連線中算一種,本頁的 Web Serial 連線算一種,其他瀏覽器分頁開著的 Web Serial 也算一種,最後是 FTDI 橋接。用 lsof /dev/cu.usbmodem101 查,會看到 Google 程序握著埠,那其實是另一個分頁。

esptool 的重置序列會咬死 USB-JTAG。監聽器運行中直接燒錄,裝置會進入僵死狀態,解法是先進 machine.bootloader() 再用 no-reset 參數。真的救不回來,拔插 USB 線斷電,物理治療最有效。

mpremote exec 之後裝置會停在 REPL,main.py 的監聽器已經死了。判斷方法:開埠讀兩秒,看到 PALETTE-LISTENER READY 是監聽中,看到 >>> 就是卡住了。

MicroPython 是裁剪版 Python。print() 沒有 flush、str 沒有 ljust、array 沒有 itemsize。從 CPython 的習慣直接搬過來會撞牆,這份裁剪清單值得先讀一遍。

工作桌上的工具與零件:每個坑都是學費

瀏覽器直連板子,免伺服器

韌體有了協議,下一步是前端。Web Serial API 讓瀏覽器直接開序列埠,配上前端的調色盤,選一個顏色,WS2812 就變色。整條鏈路是「瀏覽器、USB、板子」,沒有中間伺服器,沒有雲端轉發,延遲低到像是板子長在螢幕旁邊。

前端用 Vue 3 加 Vite 堆了調色面板、Pinia 狀態和一個 2D 調色板,四種動畫引擎都能從網頁觸發。硬體工程和前端工程在同一晚被同一條 USB 線串起來,這種體驗在編譯式的世界裡很難發生。

理性與情緒的拉扯

理智上我知道這個選擇的代價。後來的瓶頸分解實驗顯示,MicroPython 的逐元素運算有 69% 是直譯器開銷,這是語言的骨子,不是程式碼能修的。同樣的計算工作交給編譯式語言,數字會漂亮很多。

但那晚我看著呼吸燈在 0.5 秒內改變頻率,看著心跳動畫的雙峰曲線隨著我調整參數而變化,那個當下我明白了一件事:原型階段最貴的不是運算,是假設死掉的速率。直譯器的稅金買回來的,是假設可以活著被驗證。

後來我也真的用 Rust 在同一塊板子上寫了一個 no_std 的核心,成功跑起來了。但那次體驗充滿了插線、換埠、等燒錄的循環,兩種語言的定位在一夜之間變得非常清楚:Rust 負責需要性能與確定性的地方,MicroPython 負責探索與業務邏輯。它們不是競爭者,是同事。

給想入門的人三個建議

第一,韌體選對等於容量翻倍。我這塊板子換上支援 octal PSRAM 的韌體後,可用記憶體從 220KB 變成 8.32MB,裝置能處理的資料量直接差兩個數量級。

第二,序列埠的佔用清單貼在工作桌上,每次插線前對一遍。省下來的不是時間,是脾氣。

第三,把踩坑寫進文件。那晚的四個坑如果只存在記憶裡,下週就會用兩倍時間再踩一次。寫下來,它們就變成文件,變成這篇文章,變成別人的捷徑。

復古電腦與硬體:從零開始的 Maker 之夜

創意的半衰期很短,工具的第一責任是讓它活過今晚。MicroPython 沒有讓我的程式跑得更快,它讓我的想法跑得更快,而開發這件事,追的從來都是後者。


如果你的專案也在 ESP32 或 MicroPython 的世界裡,歡迎寫信到 partner@calpa.me 交流踩坑經驗,或到 GitHub 看更多實驗。

關於 Calpa

Calpa 擅長使用 TypeScript、React.js 和 Vue.js 建立 Responsive Website。

他積極參與開源社區,曾在 2019 年的香港開源大會上擔任講者,提供工作經驗和見解。此外,他也在GitHub 上公開分享個人博客程式碼,已獲得超過 300 顆星星和 60 個分支的支持。

他熱愛學習新技術,並樂意分享經驗。他相信,唯有不斷學習才能跟上快速演變的技術環境。

熱門文章

最新文章