在milk-v duo上部署wasm运行环境及udp点灯实例
一、废话前言
wasm,也就是WebAssembly,是一帮搞web前端的在2015年卷出来的鬼东西。
在web上,本来是html、javascript解释性语言的天下,手写几行代码就能玩出花来。但搞web前端的非要往死里卷,搞工程化,弄个东西要下载几个G的工具包。东西越来越大,性能咋办?继续卷,编译,编译了再到web环境运行,就搞出了WebAssembly。光把javascript给编译了还不够,继续卷,把c、c++的代码,甚至把python、rust等的代码,都能给编译了在web环境运行,连opencv都弄了opencv.js。
就像javascript光在前端运行还不够,还要继续卷一个nodejs,前后通吃。
同样的,WebAssembly不停地卷,居然卷到嵌入式的领域了,而且,居然还有不少好处:
- 因为是编译后的,运行速度非常好,和解释性语言相比那不是一条街的。
- 一次编译,到处运行,运行环境还超级小。
- wasm是在沙盒中运行的,对于安全性要求高的场合,非常适用。
java也能做到一次编译到处运行,但相对而言,java那是空天航母,wasm则是独木舟。
不继续废话了,下面来实际的,从基础环境,到在milk-v duo上面,跑起来一个udp点灯实例。
二、开发和运行环境
要在milk-v duo上面运行wasm,需要如下的开发和运行环境:
- 基础编译环境:host-tools,milk-v duo 本身的编译工具链,duo开发必备
- wasm编译环境:wasi-sdk,基于Clang & LLVM的 C/C++ 编译为 WASM工具链
- wasm的运行环境:WAMR,WebAssembly Micro Runtime,一个轻量级的独立wasm运行环境,具有占用空间小、高性能和高度可配置的功能,适用于从嵌入式、物联网、边缘到可信执行环境 (TEE)等。
基本的工作流程为:
- 用host-tools编译WAMR,生成在milk-v duo上面的运行环境。如果是在其他硬件环境上运行,需要为其他环境编译WAMR。
- 用wasi-sdk编译 C/C++ 程序为wasm程序,在 milk-v duo上面 运行。这个是一次编译,就可以在不同的硬件环境上运行。
各工具包的下载地址:
- host-tools
host-tools下载号以后,将其路径添加到PATH中,确保如下的命令可以执行:
riscv64-unknown-linux-musl-gcc --version
- wasi-sdk
wasi-sdk提供了多个环境的预编译包,方便在不同的环境上进行开发工作:
我日常使用macOS进行开发,所以我下载的是 wasi-sdk-20.0-macos.tar.gz,下载解压,搜一个软链接到/opt/wasi-sdk备用:
ln -s /Users/HonestQiao/Project/wasm/wasi-sdk-20.0 /opt/wasi-sdk
ls -l /opt/wasi-sdk/bin/
设置正确的情况下,将输出clang & llvm的工具链文件
- WAMR
WAMR源码,直接使用git克隆即可:
git clone https://github.com/bytecodealliance/wasm-micro-runtime.git
在 wasm-micro-runtime/product-mini/platforms/linux 目录中,提供了克在linux运行的最小wasm环境。
可以直接编译当前普通linux环境的wamr运行环境,但要在milk-v duo上运行,则需要进行一些配置:
在 wasm-micro-runtime/product-mini/platforms/linux/CMakeLists.txt中的set (WAMR_BUILD_PLATFORM "linux")
后添加配置,具体如下:
set (WAMR_BUILD_PLATFORM "linux")
+SET(CMAKE_C_COMPILER riscv64-unknown-linux-musl-gcc)
+SET(CMAKE_AR riscv64-unknown-linux-musl-ar)
+SET(CMAKE_RANLIB riscv64-unknown-linux-musl-ranlib)
+SET(CMAKE_STRIP riscv64-unknown-linux-musl-strip)
+SET(WAMR_BUILD_TARGET "RISCV64_LP64D")
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64")
+SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d")
上述配置中,**+**表示添加的配置。
添加完成后,进行编译:
cd wasm-micro-runtime/product-mini/platforms/linux/
cmake .
make
ls -lh iwasm libiwasm.so
最终生成的iwasm,就是运行环境的入口,可以通过拷贝到(scp)milk-v duo上面/usr/bin/iwasm,然后运行:
iwasm --version
正常输出版本号 iwasm 1.2.3 说明iwasm运行部署环境成功。
然后,就可以写一个hello进行测试了。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
printf("Hello world!\n");
printf("I am wasm!\n");
return 0;
}
- hello
此时编译,需要使用wasi-sdk来进行编译,具体执行的操作如下:
export PATH="/opt/wasi-sdk/bin:$PATH"
clang -O3 -o hello.wasm hello.c
ls -lh *.wasm
正常执行后,会看到编译结果为 hello.wasm 。
如果你为当前运行的电脑环境上,编译了wamr,那么现在就可以执行iwasm hello.wasm
。
或者,你也可以将该wasm文件,上传到milk-v duo上面再执行iwasm hello.wasm
:
iwasm hello.wasm
正常情况下,输出为:
Hello world!
I am wasm!
现在,开发和运行环境已经准备好了,我们可以继续wasm之旅了。
三、udp点灯实例
在开发板上,有一个LED,对应GPIO 440,我们可以通过直接操作 /sys/class/gpio 来控制器亮灭,在 /mnt/system/blink.sh 中有使用shell操作的实例。
我们需要使用vi先对 /mnt/system/blink.sh 做一些手脚:
#!/bin/sh
LED_GPIO=/sys/class/gpio/gpio440
if test -d $LED_GPIO; then
echo "GPIO440 already exported"
else
echo 440 > /sys/class/gpio/export
fi
echo out > $LED_GPIO/direction
+let count=10
-while true; do
+while [ $count -gt 0 ]; do
echo 0 > $LED_GPIO/value
sleep 0.5
echo 1 > $LED_GPIO/value
sleep 0.5
+ let count=count-1
done
+
+echo 0 > $LED_GPIO/value
+echo 440 > /sys/class/gpio/unexport
上述脚本中,+ 表示添加的配置,- 表示去掉的配置。
这样,在亮灭10次后,就会自动退出,不会对其他的操控GPIO 440的程序产生影响。
修改完成后,reboot
一次使得其生效。
在 wasm-micro-runtime/samples/socket-api/wasm-src 中,有关于socket的例子程序,我们可以参考其中的 udp_server.c ,来编写可以控制LED的 udp_light.c,具体内容如下:
#include "socket_utils.h"
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#ifdef __wasi__
#include <wasi_socket_ext.h>
#endif
#include <fcntl.h> //define O_WRONLY and O_RDONLY
#define MAX_CONNECTIONS_COUNT 10000
// LED 引脚
#define SYSFS_GPIO_EXPORT "/sys/class/gpio/export"
#define SYSFS_GPIO_UNEXPORT "/sys/class/gpio/unexport"
#define SYSFS_GPIO_RST_PIN_VAL "440"
#define SYSFS_GPIO_RST_DIR_TPL "/sys/class/gpio/gpio%s/direction"
#define SYSFS_GPIO_RST_DIR_VAL "OUT"
#define SYSFS_GPIO_RST_VAL_TPL "/sys/class/gpio/gpio%s/value"
#define SYSFS_GPIO_RST_VAL_H "1"
#define SYSFS_GPIO_RST_VAL_L "0"
static void
init_sockaddr_inet(struct sockaddr_in *addr)
{
/* 0.0.0.0:1234 */
addr->sin_family = AF_INET;
addr->sin_port = htons(1234);
addr->sin_addr.s_addr = htonl(INADDR_ANY);
}
int
main(int argc, char *argv[])
{
int socket_fd = -1, af, fd;
socklen_t addrlen = 0;
struct sockaddr_storage addr = { 0 };
char reply_message[50] = {0};
unsigned connections = 0;
char ip_string[64] = { 0 };
char buffer[1024] = { 0 };
// gpio变量
char str_sysfs_gpio_rst_pin_val[5] = {0};
char str_sysfs_gpio_rst_dir[64] = {0};
char str_sysfs_gpio_rst_val[64] = {0};
// 从命令行获取gpio编号
if (argc > 1 && strlen(argv[1])<4) {
snprintf(str_sysfs_gpio_rst_pin_val, 4, "%s", argv[1]);
printf("[Server] Info: gpio is config:%s\n", str_sysfs_gpio_rst_pin_val);
} else {
snprintf(str_sysfs_gpio_rst_pin_val, 4, "%s", SYSFS_GPIO_RST_PIN_VAL);
printf("[Server] Info: gpio is defalt:%s\n", str_sysfs_gpio_rst_pin_val);
}
// 设置gpio对应的direction和value文件
snprintf(str_sysfs_gpio_rst_dir, 50, SYSFS_GPIO_RST_DIR_TPL, str_sysfs_gpio_rst_pin_val);
snprintf(str_sysfs_gpio_rst_val, 50, SYSFS_GPIO_RST_VAL_TPL, str_sysfs_gpio_rst_pin_val);
// 打开端口/sys/class/gpio# echo ??? > export
fd = open(SYSFS_GPIO_EXPORT, O_WRONLY);
if (fd == -1)
{
perror("export gpio error.\n");
goto fail;
} else {
printf("[Server] OK: export gpio %s success.\n", str_sysfs_gpio_rst_pin_val);
}
write(fd, SYSFS_GPIO_RST_PIN_VAL, sizeof(SYSFS_GPIO_RST_PIN_VAL));
close(fd);
// 设置端口方向/sys/class/gpio/gpio???# echo out > direction
fd = open(str_sysfs_gpio_rst_dir, O_WRONLY);
if (fd == -1)
{
perror("direction set error.\n");
goto fail;
} else {
printf("[Server] OK: direction set success.\n");
}
write(fd, SYSFS_GPIO_RST_DIR_VAL, sizeof(SYSFS_GPIO_RST_DIR_VAL));
close(fd);
// 输出复位信号: 拉高>100ns
fd = open(str_sysfs_gpio_rst_val, O_RDWR);
if (fd == -1)
{
perror("gpio init error.\n");
goto fail;
} else {
printf("[Server] OK: gpio init success.\n");
}
write(fd, SYSFS_GPIO_RST_VAL_L, sizeof(SYSFS_GPIO_RST_VAL_L));
af = AF_INET;
addrlen = sizeof(struct sockaddr_in);
init_sockaddr_inet((struct sockaddr_in *)&addr);
printf("[Server] Create socket\n");
socket_fd = socket(af, SOCK_DGRAM, 0);
if (socket_fd < 0) {
perror("Create socket failed");
goto fail;
}
printf("[Server] Bind socket\n");
if (bind(socket_fd, (struct sockaddr *)&addr, addrlen) < 0) {
perror("Bind failed");
goto fail;
}
printf("[Server] Wait for clients to connect(max is %d) ..\n", MAX_CONNECTIONS_COUNT);
while (connections < MAX_CONNECTIONS_COUNT) {
addrlen = sizeof(addr);
/* make sure there is space for the string terminator */
int ret = recvfrom(socket_fd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&addr, &addrlen);
if (ret < 0) {
perror("Read failed");
goto fail;
}
buffer[ret] = '\0';
if (sockaddr_to_string((struct sockaddr *)&addr, ip_string,
sizeof(ip_string) / sizeof(ip_string[0]))
!= 0) {
printf("[Server] failed to parse client address\n");
goto fail;
}
printf("[Server] received %d bytes from %s: %s\n", ret, ip_string,
buffer);
if(strcmp(buffer, "on")==0 || strcmp(buffer, "ON")==0) {
snprintf(reply_message, 50, "light on");
write(fd, SYSFS_GPIO_RST_VAL_H, sizeof(SYSFS_GPIO_RST_VAL_H));
}
else if(strcmp(buffer, "off")==0 || strcmp(buffer, "OFF")==0) {
snprintf(reply_message, 50, "light off");
write(fd, SYSFS_GPIO_RST_VAL_L, sizeof(SYSFS_GPIO_RST_VAL_L));
}
else if(strcmp(buffer, "q")==0 || strcmp(buffer, "quit")==0) {
snprintf(reply_message, 50, "quit");
break;
}
else {
snprintf(reply_message, 50, "unknow command");
}
printf("[Server] Info: %s\n", reply_message);
if (sendto(socket_fd, reply_message, strlen(reply_message), 0,
(struct sockaddr *)&addr, addrlen)
< 0) {
perror("Send failed");
break;
}
connections++;
}
if (connections == MAX_CONNECTIONS_COUNT) {
printf("[Server] Achieve maximum amount of connections\n");
}
close(fd);
// 关闭端口/sys/class/gpio# echo ??? > unexport
fd = open(SYSFS_GPIO_UNEXPORT, O_WRONLY);
if (fd == -1)
{
perror("unexport gpio error.\n");
goto fail;
} else {
printf("[Server] OK: unexport gpio %s success.\n", str_sysfs_gpio_rst_pin_val);
}
write(fd, SYSFS_GPIO_RST_PIN_VAL, sizeof(SYSFS_GPIO_RST_PIN_VAL));
close(fd);
printf("[Server] Shuting down ..\n");
shutdown(socket_fd, SHUT_RDWR);
close(socket_fd);
sleep(3);
printf("[Server] BYE \n");
return EXIT_SUCCESS;
fail:
printf("[Server] Shuting down ..\n");
if (socket_fd >= 0)
close(socket_fd);
if (fd >= 0)
close(fd);
sleep(3);
return EXIT_FAILURE;
}
在上述代码中,基于 udp_server.c 和 c 控制 GPIO,实现了 通过UDP发送on、off、quit到milk-v duo,实现点灯、熄灯、退出。
然后,添加相关的编译配置:
# 文件:samples/socket-api/CMakeLists.txt
add_executable(udp_server ${CMAKE_CURRENT_SOURCE_DIR}/wasm-src/udp_server.c)
+add_executable(udp_light ${CMAKE_CURRENT_SOURCE_DIR}/wasm-src/udp_light.c)
# 文件:samples/socket-api/wasm-src/CMakeLists.txt
compile_with_clang(udp_server.c)
+compile_with_clang(udp_light.c)
再进行编译:
cd wasm-micro-runtime/samples/socket-api
mkdir build
cd build
cmake ..
make
ls -l *.wasm
正常执行的情况下,将会输出这个实例中,所有生成的wasm程序。
将其中的 udp_light.wasm 上传到milk-v duo备用。
因为此处的 udp_light 使用了网络功能,而之前的 wasm-micro-runtime/product-mini/platforms/linux 最小环境,没有提供网络功能,因此需要添加配置:
# 文件:wasm-micro-runtime/product-mini/platforms/linux/CMakeLists.txt
+set(WAMR_BUILD_INTERP 1)
+set(WAMR_BUILD_FAST_INTERP 1)
+set(WAMR_BUILD_LIB_PTHREAD 1)
set (WAMR_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
上述脚本中,+ 表示添加的配置。
然后重新编译,将编译后的iwasm上传到milk-v duo的/usr/bin/下备用。
现在,wamr环境运行入口iwasm有了,wasm程序udp_light.wasm也有了,可以在milk-v duo执行了:
iwasm --addr-pool=0.0.0.0/15 --dir=/sys/ udp_light.wasm 440
运行后,具体输出如下:
上述命令最后的440,表示板载的LED对应的GPIO。
另外,–addr-pool规定了可以使用的网络,–dir规定了可以操作的目录。
此时,可以在电脑上,使用网络工具,发送udp包给milk-v duo。我使用的是跨平台的comtool:
在milk-v duo上,则会收到消息,执行对应的动作:
注意接收on、off时,milk-v duo上的LED将会根据指令亮灭。
现在,你可以使用 c/c++ 编写你的程序,使用wasi-sdk编译为wasm,然后部署到milk-v duo上面运行了。编译出来的wasm程序,能直接放到其他部署了iwasm的硬件上,也可以运行了,也就是说,如果不是关联到特定硬件的功能,它就是通用的。
四、常见问题
- 运行
iwasm
时,提示:permission denied- 通常原因为没有设置运行权限,使用命令添加运行权限:
chmod +x iwasm
- 通常原因为没有设置运行权限,使用命令添加运行权限:
- 运行
iwasm
时,提示:not found- 通常原因为,没有添加静态编译参数;注意在前一部分讲解中,第一次编译wamr时,需要进行配置。
- 其他程序类似情况,也可能没有上传需要的库
- 使用
iwasm
运行udp_light时
时,提示:WASM module load failed: integer too large- 没有添加网络功能配置。注意在前一部分讲解中,第二编译wamr时,网络功能部分,需要添加配置,并重新编译运行环境,并上传
- 使用
iwasm
运行udp_light时
时,提示:export gpio error 或者 direction set error- 说明 --dir 没有正确设置,wasm程序无权操作
- 使用
iwasm
运行udp_light时
时,提示:Bind failed: Permission denied- 说明 --addr-pool 没有正确设置,wasm程序无权使用该网络