MoonBit支持國產芯片開發–性能媲美C
在 ESP32-C3 上實現生命游戲
過去,我們曾在文章《硬件實現:在ESP32-C6單片機上運行MoonBit WASM-4小游戲》中,展示了如何通過 WebAssembly (WASM) 將 MoonBit 程序移植到物理硬件,初步探索其在嵌入式領域的潛力。
而如今,隨著 MoonBit Native 后端 的正式發布: MoonBit 支持 Native 后端,MoonBit 邁出了關鍵一步:無需再依賴 WebAssembly 作為中間層,代碼可以直接以原生形式運行在嵌入式硬件之上。
這項進步不僅顯著提升了性能和資源利用效率,也為 MoonBit 深度整合進嵌入式與物聯網生態、直接控制硬件設備,奠定了堅實基礎。
為了具體展示 MoonBit Native 在嵌入式開發中的優勢,本文將通過一個經典實例——在樂鑫 ESP32-C3 芯片(或其 QEMU 仿真環境)上實現“康威生命游戲”——來進行探討。
- 生命游戲是一個非常經典的細胞自動機,在一個二維網格上模擬細胞的生死演化。每個細胞的狀態(存活或死亡)根據其周圍 8 個鄰居的狀態,在離散的時間步中同時更新。基本規則包括:存活細胞在鄰居過少(<2)或過多(>3)時死亡,在鄰居適中(2或3)時繼續存活;死亡細胞在正好有 3 個存活鄰居時“復活”。
- ESP32-C3 是一款流行的低成本、低功耗 RISC-V 微控制器,資源相對有限。生命游戲對計算和內存訪問有一定要求,使其成為檢驗嵌入式系統性能和編程語言效率的理想模型。我們的目標是利用 MoonBit 編寫生命游戲的核心邏輯,并將其運行結果顯示出來。不僅支持在連接了 ST7789 LCD 的真實 ESP32-C3 硬件上運行,也支持在開發機上通過 QEMU 仿真環境運行,以方便開發和驗證。
準備工作
在開始之前,請確保你的系統中安裝了樂鑫官方提供的鏈接: ESP IDF 開發框架。請參考鏈接: ESP官方文檔,當前支持的ESP-IDF(樂鑫物聯網開發框架)版本為 v5.4.1。若希望使用 QEMU 仿真,請參考樂鑫官方文檔中的鏈接: QEMU模擬器章節安裝相關工具鏈。
本文使用MoonBit native后端生成C代碼,并且借助鏈接: moonbit-esp32 包將MoonBit項目打包為靜態鏈接庫,以嵌入到標準的ESP-IDF項目中。 moonbit-esp32
庫的核心功能體現在其 components
包中。這個目錄扮演著關鍵的橋梁角色,專門負責提供 MoonBit 語言到 ESP-IDF中各種核心組件功能的綁定。具體來說,該目錄下包含了多個子模塊,例如 gpio
(通用輸入輸出)、 spi
(串行外設接口)、 lcd
(液晶顯示屏控制,包含通用接口和特定驅動如 ST7789 的封裝)、 task
(封裝 FreeRTOS 任務管理)以及 qemu_lcd
(針對 QEMU 仿真環境的 LCD 接口)。每一個子模塊都封裝了對應的 ESP-IDF C API,允許開發者使用類型安全、更符合 MoonBit 語言習慣的方式來直接操作 ESP32 的硬件外設和系統服務,從而將 MoonBit 的現代語言特性帶入底層嵌入式開發。
本文相關代碼位于 鏈接 倉庫中的 game-of-life 子目錄中。
注意:本文所述的開發與測試流程僅在 macOS 系統上驗證通過,其他操作系統用戶可能需要根據平臺差異自行進行調整。
生命游戲邏輯的實現
相關代碼位于game.mbt
文件中。
- 首先我們定義
DEAD
定義為0
,ALIVE
定義為Int16
類型的-1
。這里定義ALIVE
為-1
而不是1
是一個巧妙的優化:在 RGB565 顏色格式下,0
(0x0000
)恰好代表黑色,而-1
(0xFFFF
)恰好代表白色。這樣,在將游戲狀態數據傳輸給 LCD 時,可以省去顏色轉換的步驟。 - 網格大小由
ROWS
和COLS
(均為 240) 定義。cells
使用一個一維FixedArray
全局變量來存儲整個二維網格的當前狀態。FixedArray
是固定大小的數組,大小為 112.5KB(240 * 240 * 2 / 1024)。因為ESP32-C3可用內存約有320KB,且并不是連續的,所以next
數組沒有定義成完整的下一個狀態副本,而是定義為一個 3 行滾動緩沖區,大小約為1.4KB。我們通過模運算循環使用這三行空間來存儲計算中的下一代狀態,并采用延遲提交策略將計算完成的舊行寫回cells
,以降低內存使用峰值。
const DEAD : Int16 = 0
const ALIVE : Int16 = -1pub const ROWS : Int = 240
pub const COLS : Int = 240let cells : FixedArray[Int16] = FixedArray::make(ROWS * COLS, 0)
let next : FixedArray[Int16] = FixedArray::make(3 * COLS, 0)
在計算下一個狀態時,核心代碼如下:
let live_neighbors = live_neighbor_count(row, col)
let next_cell = match (cell, live_neighbors) {(ALIVE, _..<2) => DEAD // 優雅地用范圍模式匹配鄰居數小于2的情況(ALIVE, 2 | 3) => ALIVE // 使用或模式, 將鄰居數為2或3的情況合并在一個分支(ALIVE, 3..<_) => DEAD // 用范圍模式匹配鄰居數大于3的情況(DEAD, 3) => ALIVE(otherwise, _) => otherwise // 必須考慮所有情況, 編譯器會進行窮盡性檢查
}
這是實現生命游戲規則的核心,充分展現了 MoonBit 模式匹配的強大和優雅。生命游戲的規則被近乎直譯地映射到 MoonBit 的 match
模式匹配語句中,代碼清晰、直觀且不易出錯。
此外,在需要性能的場景,MoonBit提供了對數組的unsafe
操作。這在性能關鍵路徑上消除了數組邊界檢查的開銷,是嵌入式開發中為了榨取極致性能的常用手段,但需要開發者確保邏輯的正確性。
最后,對比 QEMU(仿真)版本和 ST7789(硬件)版本的 main.mbt
文件。
與 QEMU 虛擬 LCD 面板相關的邏輯位于 game-of-life/qemu/src/main/main.mbt
,關鍵代碼摘錄如下:
let panel = @qemu_lcd.esp_lcd_new_rgb_qemu(@game.COLS, @game.ROWS, BPP_16)..reset!()..initialize!()
@game.init_universe()
for i = 0; ; i = i + 1 {let start = esp_timer_get_time()panel.draw_bitmap!(0, 0, @game.ROWS, @game.COLS, cast(@game.get_cells()))@game.tick()let end = esp_timer_get_time()println("tick \\\\{i} took \\\\{end - start} us")
}
這段代碼借助MoonBit ESP32 binding,首先初始化一個用于 QEMU 仿真的虛擬 LCD 面板,并對其進行復位和初始化設置。這段 MoonBit 代碼利用了級聯運算符 (..
) 和錯誤處理(!
)。
- 首先創建
panel
對象。 - 接著,
..reset!()
使用級聯運算符,在panel
上調用reset
方法;由于 reset 函數的返回類型可能包含錯誤,末尾的!
表示:如果reset
成功,則繼續執行;如果reset
引發了一個錯誤,!
會立即將該錯誤重新拋出,中斷后續操作。 - 同理,
..initialize!()
只有在reset!
成功后才會執行,并且同樣帶有自動錯誤傳播的!
。這種組合使得對同一對象執行一系列可能失敗的操作時,代碼既簡潔(避免重復寫panel
)又安全(錯誤自動向上傳播)。 - 接著,它初始化生命游戲的世界狀態。
- 最后,代碼進入一個無限循環:在每次循環中,它將當前的游戲狀態繪制到虛擬 LCD 上,然后計算并更新游戲到下一代狀態,同時測量并打印出每次繪制和更新操作所花費的時間。
與 ST7789 LCD 相關的邏輯位于 game-of-life/st7789/src/main/main.mbt
,關鍵代碼摘錄如下:
let spi_config : @spi.SPI_BUS_CONFIG = { ... }
@spi.spi_bus_initialize(@spi.SPI2_HOST, spi_config, SPI_DMA_CH_AUTO) |> ignore...let panel = @lcd.esp_lcd_new_panel_st7789(io_handle~,reset_gpio_num=PIN_RST,rgb_ele_order=@lcd.BGR,bits_per_pixel=16,)..reset!()..initialize!()..config!(on_off=true, sleep=false, invert_color=true)
@game.init_universe()
for i = 0; ; i = i + 1 {let start = esp_timer_get_time()panel.draw_bitmap!(0, 0, @game.ROWS, @game.COLS, cast(@game.get_cells()))@game.tick()let end = esp_timer_get_time()println("tick \\\\{i} took \\\\{end - start} us")
}
- 這段代碼首先定義了與物理 ST7789 LCD 連接所需的 GPIO 引腳和 SPI 時鐘速度等常量。
- 接著,它執行了一系列硬件初始化步驟:配置并啟動 SPI 總線,創建基于 SPI 的 LCD IO 句柄,然后使用特定驅動初始化 ST7789 面板并設置其顯示狀態(復位、初始化、開啟顯示、喚醒、顏色反轉)。
- 完成硬件準備后,它初始化生命游戲的狀態。
- 最后,程序進入一個無限循環,在循環中不斷將當前游戲畫面繪制到物理 LCD 屏幕上,計算游戲的下一代狀態,并測量打印每次循環所需的時間。
此外,讀者可以注意到,MoonBit支持標簽參數。無論是在 esp_lcd_new_panel_st7789
的調用中(如reset_gpio_num=PIN_RST, rgb_ele_order=...)
,還是在后續鏈式調用的 ..config!(on_off=true, ...)
中,都明確地將值與其參數名稱關聯起來。這種方式極大地提高了代碼的可讀性和自文檔性,使得參數的意圖一目了然,并且也無需關心參數順序。
我們可以發現:生命游戲的計算 (@game.tick()) 和主循環結構完全相同。主要的區別在于與顯示設備的交互層。ST7789 版本需要進行詳細的物理硬件配置(定義 GPIO 引腳、配置 SPI 總線、初始化特定 LCD 驅動);而 QEMU 版本則直接與仿真環境提供的虛擬 LCD 接口交互,初始化過程相對簡單。
運行實例代碼
您可以通過克隆示例代碼倉庫來親自體驗:
git clone <https://github.com/moonbit-community/moonbit-esp32-example.git>
- QEMU 版本
為了方便讀者不借助硬件復現,我們提供了QEMU版本,代碼位于 game-of-life/qemu 目錄,在確保環境配置無誤后,運行以下命令便可看到結果。33.1 FPS (每幀約 30.2 ms)
cd game-of-life/qemu
moon install
make set-target esp32c3
make update-deps
make build
make qemu
以下為運行效果
MoonBit 支付國產芯片-Part1
此外,我們還提供了一份使用C實現的代碼,位于 game-of-life/qemu-c
目錄,測試結果表明,使用C實現的生命游戲每幀的計算時間與MoonBit版本相同,均為30.1ms左右。
- ST7789 版本
此版本在真實的 ESP32-C3 開發板和 ST7789 LCD 上運行。實測幀率約 27.1 FPS (每幀約 36.9ms)。
cd game-of-life/st7789
moon install
make set-target esp32c3
make build
make flash monitor
此外,我們還提供了一份使用C實現的代碼,位于 game-of-life/st7789-c
目錄,測試結果表明,使用C實現的生命游戲每幀的計算時間與 MoonBit 版本幾乎相同,為36.4ms。
對于具體的測試方法,詳見鏈接: 倉庫
總結
通過在 ESP32-C3 上運行生命游戲的實例,我們展示了 MoonBit 的 Native 后端在嵌入式開發中的應用。MoonBit生命游戲代碼經過優化,能到達與C幾乎相同的速度。同時,MoonBit 的模式匹配等現代語言特性有助于提升代碼的可讀性和開發體驗。結合其與 ESP-IDF 等生態系統的無縫集成能力,MoonBit 為ESP32嵌入式開發提供了一種將原生級執行效率與現代化開發體驗相結合的高效解決方案。
New to MoonBit?
- Download MoonBit.
- Explore MoonBit Beginner’s Guide.
- Play with MoonBit Language Tour.
- Check out MoonBit Docs.
- Join our Discord community.