过去,我们曾在文章《硬件实现:在 ESP32-C6 单片机上运行 MoonBit WASM-4 小游戏》中,展示了如何通过 WebAssembly (WASM) 将 MoonBit 程序移植到物理硬件,初步探索其在嵌入式领域的潜力。 而如今,随着 MoonBit Native 后端 的正式发布: MoonBit 支持 Native 后端,MoonBit 迈出了关键一步:无需再依赖 WebAssembly 作为中间层,代码可以直接以原生形式运行在嵌入式硬件之上。 这项进步不仅显著提升了性能和资源利用效率,也为 MoonBit 深度整合进嵌入式与物联网生态、直接控制硬件设备,奠定了坚实基础。
为了具体展示 MoonBit Native 在嵌入式开发中的优势,本文将通过一个经典实例——在乐鑫 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 = -1
pub const ROWS : Int = 240
pub const COLS : Int = 240
let 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
)又安全(错误自动向上传播)。与 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,
[email protected],
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")
}
此外,读者可以注意到,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>
cd game-of-life/qemu
moon install
make set-target esp32c3
make update-deps
make build
make qemu
以下为运行效果
此外,我们还提供了一份使用 C 实现的代码,位于 game-of-life/qemu-c
目录,测试结果表明,使用 C 实现的生命游戏每帧的计算时间与 MoonBit 版本相同,均为 30.1ms 左右。
此版本在真实的 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?
1
sthwrong 132 天前
我记得 moonbit 的异步方案还没成熟?看起来前景不错,但感觉缺了这个还不能真拿来干活。
|
![]() |
2
pursuer 132 天前
现在大模型靠算力力大砖飞,让我觉得现在才出现的新语言前景更微妙了。
|
![]() |
3
Gilfoyle26 132 天前
大胆预测,人类最后一个比较火的底层语言是 rust ,因为量子计算的时代马上要到了,以后估计是量子计算机的编程语言了。
|
4
w568w 132 天前
@Gilfoyle26 通用量子计算还没实现,现在的量子计算和冯·诺依曼提出的「计算机」完全不是一个东西,先了解一下物理学再大胆预测吧。
现在的量子计算是非常 specific 的设备,只能解决某些特定问题。对绝大部分算法,尤其是那些非概率性过程,量子计算机只会更慢而不是更快。例如,quantum memory 永远只能同时访问所有位,而且在物理上不可复制。在安全通信领域这是优势,但在其他算法领域,你肯定不会想要一个没有 memcpy 的计算机。 量子计算不是魔法,不会神奇地让数值求和、网页渲染加速 100 万倍。 |
5
v2exgo 132 天前
@Gilfoyle26
![]() |
![]() |
6
murmur 131 天前
这个东西给我的理解又是 java 和 go 的区别
这个东西给我的感觉是卡在个很奇怪的位置 一是嵌入式的底层开发相当成熟,各种硬件都有公版方案,除非是那种很特殊的定制写驱动,也不缺 c 和 asm 工程师 如果是性能稍微好点的,就都有操作系统了,为啥要用 webasm ,qt 不香了吗 |
![]() |
7
muooOOO 131 天前
这些年碰瓷嵌入式的语言 python ,js ,lua 甚至是 c++都没法影响 c 的地位。这一行钱少事多,不适合天天整活。目前我看好 rust ,但前提是 rust 能做到在 linux 源码中占比超过 50%
|
8
mayli 131 天前
话说啥时候 esp32 有 jre 就完事了吧。
|
![]() |
9
jazzsama 131 天前
国产 Soc 官方会给你提供 Moonbit SDK ?看不到可能性
|
![]() |
10
wanjun 131 天前
是不是基于 rust 二次开发的
|