Skip to content
On this page

凹语言点亮 Arduino Nano 33

凹语言是国内 Gopher 发起的纯社区构建的开源国产编程语言项目(没有公司背景、没有任何赞助)。同时凹语言也是国内第一个实现纯浏览器内编译、执行全链路的自研静态类型的编译型通用编程语言。凹语言不仅仅可以运行在浏览器和各大主流操作系统上,最近凹语言增加了对 Arduino-wasm 平台的支持,从而实现了用凹语言开发 Arduino 应用的目标。

1. Arduino-wasm 是什么

Wasm3 是一个高性能的 WebAssembly 解释器,而 Arduino-wasm 则是 Wasm3 针对 Arduino 的定制版本。Wasm3 最小的硬件依赖是 ~64Kb Flash 和 ~10Kb RAM。

Github 仓库:https://github.com/wasm3/wasm3-arduino

下面是 Wasm3 运行在 iOS 的截图:

Arduino-wasm 则是运行在 Arduino Nano 33 等开发板上的 Wasm3。

2. Arduino Nano 33 开发板介绍

Arduino Nano 33,是 Arduino Nano 的高配版本,是一款基于 nRF52840 SoC ARM 32 位处理器的微型开发板。其中 Arduino Nano BLE Sense 其主控芯片集成了蓝牙低功耗(BLE)。NANO 33 BLE 不仅保留了与经典款 NANO 同样的尺寸与管脚,且在此基础上配有多种高性能传感器等,当然最重要的是满足了 Arduino-wasm 的最低硬件要求。

目前(2022年底),淘宝的价格大约在200元以上。

3. 编写 Arduino 的闪灯例子

代码逻辑比较简单,只是换成了凹语言来写:

wa
# 版权 @2022 凹语言 作者。保留所有权利。

import "syscall/arduino"

var LED = arduino.GetPinLED()

fn init() {
	arduino.PinMode(LED, 1)
	arduino.Print("凹语言(Wa)/Arduino is running ...\n")
}

fn main() {
	for {
		arduino.DigitalWrite(LED, arduino.HIGH)
		arduino.Delay(100)
		arduino.DigitalWrite(LED, arduino.LOW)
		arduino.Delay(900)
	}
}

我们直接使用了 syscall/arduino 包来使用 Arduino 的功能。

4. syscall/arduino 包介绍

让我们看看 syscall/arduino 包的代码

wa
# 版权 @2022 凹语言 作者。保留所有权利。

var (
	LOW  :i32 = 0
	HIGH :i32 = 1

	INPUT        :i32 = 0
	OUTPUT       :i32 = 1
	INPUT_PULLUP :i32 = 2
)

#wa:import arduino millis
fn Millis() => i32

#wa:import arduino delay
fn Delay(ms: i32)

#wa:import arduino pinMode
fn PinMode(pin, mode: i32)

#wa:import arduino digitalWrite
fn DigitalWrite(pin, value: i32)

#wa:import arduino getPinLED
fn GetPinLED() => i32

#wa:import arduino print
fn PrintRawString(ptr: i32, len: i32)

fn Print(s: string) {
	print(s)
}

fn Println(s: string) {
	println(s)
}

主要是将常用的函数通过 WASM 方式导入到了代码空间,大部分函数并不在凹语言中实现。

5. 编译到 Arduino-wasm 平台

在编译时需要通过 -target=arduino 参数指定目标类型:

$ wa build -target=arduino app.wa
$ wat2wasm a.out.wat -o app.wasm
$ xxd -i app.wasm > app.wasm.h

第一个命令是编译为文本格式的 WASM 文件 a.out.wat,然后通过 wat2wasm 命令编译得到二进制格式的 WASM 文件,最后通过 xxd 将二进制的 WASM 文件转换为 C 语言的头文件。

app.wasm.h 文件内容如下:

c
unsigned char app_wasm[] = {
  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x2a, 0x08, 0x60,
  0x01, 0x7f, 0x00, 0x60, 0x02, 0x7f, 0x7f, 0x00, 0x60, 0x00, 0x01, 0x7f,
  ...
  0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x0a
};
unsigned int app_wasm_len = 1543;

然后 Arduino 工程就可以通过 #include "app.wasm.h" 方式引用这个 WASM 程序,最终和 Wasm3 一起编译。

6. Arduino 平台胶水代码初探

最终还需要一个 arduino.ino 代码(其实就是针对 Arduino 简化的 C++ 程序)。代码结构如下:

c
#include <wasm3.h>
#include <m3_env.h>

// 定义 WASM 和 本地栈大小
#define WASM_STACK_SLOTS  1024
#define NATIVE_STACK_SIZE (32*1024)

// WASM 最大内存限制, 一般不得超过 64KB 大小
#define WASM_MEMORY_LIMIT 8096

// 导入 凹语言 生成的 WASM 文件对应的二进制头文件
#include "app.wasm.h"

开头保护 Wasm3 和 WASM 程序对应的头文件,同时定义以下栈的大小。

然后看看代码主体结构:

cpp
// 执行 WASM 的函数
void wasm_task(void*) { ... }

// setup 作为 main 函数用户
void setup() {
	// 串口初始化
	Serial.begin(115200);
	delay(100);

	// 等待串口初始化完成, 必须是 USB 串口
	while(!Serial) {}

	// 阻塞执行 wasm 程序, 不会返回
	wasm_task(NULL);
}

// 该函数不会被执行
// 定义该函数只是为了确保 Arduino 编译通过
void loop() {
	delay(100);
}

Arduino 的常规代码只有 setup 和 loop 两个函数。不过这里只用到了 setup 函数。在 setup 函数中首先初始化串口(方便打印调试信息),最后调用 wasm_task 执行凹语言写的亮灯代码,其中会加载 WASM 模块并执行。wasm_task 看起来是一个比较复杂的程序,不过核心逻辑和普通的 WASM 执行流程类似,细节可以下次文章再展开。

总体来说,以上这些胶水代码是相对固定的。后面会自动生成全部这些代码,同时去掉对外部其他工具的依赖。目标是生成的 Arduino 工程文件可以直接打开构建。

7. Arduino 构建 & 执行

如果是第一次使用 Arduino Nano 33 开发板,打开 IDE 后会提示安装必要的工具。然后需要在库管理菜单手动安装 Wasm3 包:

然后编译后上传的效果:

执行的效果,除了可以看到 LED 闪烁,串口还可以看到输出信息:

一切正常!

8. 本地模拟执行

为了方便测试简单程序,凹语言命令行提供了模拟执行 Arduino 的方式:

目前模拟还比较简单,只是简单打印调用 API 的名字和参数。

9. 总结展望

目前流行 Arduino 单片机的配置还是比较低的,可能难以运行 WASM 程序。不过可以乐观估计 Arduino Nano 33 将会很快普及。而且,Wasm3 不仅仅可以支持 Arduino,还可以支持树莓派 Pico(淘宝价格30元)。因此,从长远看 WASM 是一个兼具灵活性和性价比的可选方案。如果确实需要在非常受限的环境秩序,也可以尝试 LLVM 到 AVR 单片机的路线。