实在想不出标题了,这次就原谅我吧。

暑假多接触了些嵌入式设备,加上 AOSCC 2023 上各路神仙带来的机器,已经可以说是让我大开眼界了。半年前确实完全想不到自己还会重新去碰这鬼东西……

我手里能用嵌入式 Linux 的嵌入式设备多半还是些有一定计算属性、和单片机开发板比更接近一般 PC 的机器。C51 或者 STM32 之类偏工业控制的东西我是完全玩不懂的。

本来打算九月之前写完上传的,结果开学后就忙得要命。恍惚之间,仿佛看到……算了,这个引用再拿来用就无聊了。总之很难抽出时间深入了解下这些硬件。

硬件

Raspberry Pi 4B

树莓派的知名度之高,在嵌入式领域之外也能看出来。RPi 4B 搭载 BCM2711,28nm,稍稍超个频就很烫。手上的设备是学校实验室财产,所以也不好给它原有的散热器拆掉粘个新的上去,就稍微改了下原有的可拆卸的散热器,下面会具体讲。

这家伙 CPU 性能一般般,GPU 性能很弱,带起 Minecraft 来比较艰难,各方面硬件都给不了人惊喜。好在树莓派基金会比较重视社区建设,资料也好找,用起来不折腾,新的 Raspberry Pi OS 12 在驱动上提升也比较明显。话说 RPi 还有 EFI 固件,不过我还没试。

我在树莓派上用的是 Raspberry Pi OS,主要还是因为其他发行版表现不是特别好。

X96 Max Plus V5.1

一台电视盒子,价值在于能运行第三方 Armbian 之类的发行版。

二百块钱的设备能有主线内核和爆杀树莓派的 CPU 性能,还带 NPU(虽然我不会用),说实话已经很难得了。缺点是文档几乎没有,做工比树莓派差(这当然并不是在说树莓派的做工好),网卡之类外围设备有时候会出问题。它的原厂系统特别难用,单纯当电视盒子的话我肯定不愿意花二百块钱买它。

盒子搭载 12nm 的 AmLogic s905x3,CPU 有负载也比较凉快,用 GPU 就比较容易过热了。最好把原厂的散热拆掉,换个好的上去。

s905x3 CPU 性能几乎是 RPi 4B 的两倍,GPU 也强一点(不过似乎在新驱动上被 RPi 4B 压下去了),运行 KDE 会明显比 RPi 4B 顺滑,可惜图形有概率出现和我 PinePhone 一样不稳定的情况,不知道是不是 Mali 系列 GPU 的通病。

电视盒子同一型号常有不同版本,安装系统要麻烦得多,我建议你直接把壳子打开(反正也别苛求能在大陆享受外贸盒子的售后),对着主板上的丝印和芯片型号找找有没有人适配第三方 Armbian。有的话,下载系统镜像,写入存储盘。这时候先别插盘上电,先在电脑上挂载存储盘,按照说明设定正确的 DTB,选择合适的 U-boot,之后再把存储盘插盒子上,按住盒子里隐藏的、允许盒子从外接存储盘启动的按钮,插电。以后启动就不需要再按那个隐藏的按钮了。

这机器默认从 eMMC 启动,所以如果把 eMMC 上的引导程序搞坏了,就算有制作好的启动盘也没法正常启动。我嫌 U 盘慢就把系统覆盖到 eMMC 上,砖了。我手上这台从 SD 卡启动就总是会有问题,eMMC 与 SD 同源,所以系统在 eMMC 上不正常也说得通。引导程序坏了,要用镊子短接主板上的一对触点,用一根 USB-A 公对公线从盒子的 USB 3.0 口连接电脑,用 AmLogic 提供的工具来恢复。

我的盒子 eMMC 上装了 SlimBox,第三方 Armbian 就用 U 盘来启动了。我用的 Armbian 默认给 CPU 频率超到了 2.2 GHZ。

Visionfive 2

现在确实不是入手 RISC-V 的好时候。Visionfive 2 用了一颗 JH7110,不支持 RVV (RISC-V Vector Extension),不支持硬件虚拟化,没有 NPU。制程我没查到,主观感觉芯片比 RPi 4B 的凉一点。

RVV 和 x86/ARM 上 SIMD 实际解决的问题差不多,没 RVV 就有点类似于用没 AVX (或者极端点,SSE MMX 都没有)的 x86电脑,基本要和多媒体、游戏、科学计算 say goodbye~ 了;硬件虚拟化暂时不算刚需,不过没有它的确是少了很多乐趣。

我刚开完箱就被 Revy 全身上下吐槽了一遍,这才知道 LicheePi 4A 的 TH1520 性价比要高得多,支持 RV 0.7.1,还带 NPU,不过我想等同时有 RVV 1.0 以上和硬件虚拟化的芯片多起来再买新机器。

当然 Visionfive 2 当实验材料以及拿来展示的效果应该说得过去,不至于吃灰,只是七百多块的实验材料的确太贵了。

JH7110 的 PowerVR GPU 动画很顺滑,不过 Visionfive 2 的 HDMI 兼容性大约不太好,我用 HDMI-DVI 转换器和 HDMI 采集卡都没反应,插 HDMI-VGA 转换器上才亮起来。

Visionfive 2 官方提供的系统是 Debian Sid,其中他们主要改动的是 Mesa……总之很不稳定就是了。

Milk-V Duo

这玩意在这几个里算比较特殊,它是块核心板,有点像树莓派 Pico,拿出去别人可能会以为是 STM32 那种单片机。

拿到 Duo 当然也是一分钱没花(笑),社团的五块里还有两块都没拆封。Duo 主板上只有一颗芯片(CV1800B,它的 SoC),大概是把内存之类的也集成在一起了。芯片是 7mm*7mm,和常见的 8 脚 SPI 闪存差不多大,完全没法装散热器,不过机器功耗足够低,所以没关系。

Duo 的一个核心是 1GHz,另一个是 700MHz,Linux 下低频的那个核心好像用不了。文档说低频的那颗只能用来跑 RTOS,又说 Linux 和 RTOS 可以同时运行,我不知道是什么原理。我离了 Linux 就什么也不会,就把它当单核来用了。这核心有 RVV 0.7.1,这下更显得花七百多块买 Visionfive 2 的我是在自取其辱了。Duo 还有个 TPU(不知道和 s905x3 上的 NPU 有什么区别),我一样不会用。

Duo 有 64M 内存(不错,比我那台 armv4l 强),默认系统镜像里要留一半内存给多媒体处理,如果不需要相关功能的话,可以改 Duo Buildroot SDK 里的参数把这部分内存要回去。这么小的内存也没什么运行带包管理器的主流发行版的可能,装软件就从 Buildroot 里选吧,自己编译也行。入门要先学会 Buildroot,这个确实让它显得比别的开发板更难上手,再加上文档的各种问题……有厂商会给我那末流本科供应开发板也就说得通了。

散热

学校的 RPi 4B 和我的 Visionfive 2 都有配主动散热器:一个风扇,对着金属鳍片吹。这种散热器本身应该能轻松压住开发板的发热,但散热器是用一片导热硅胶和 SoC 贴在一起的,这样压力全在导热性能上了。我以为会有人给这些开发板设计带弹簧、直接涂硅脂来用的散热器,哪知没有,只能自己解决导热问题了。

要是直接把厚厚一片导热硅胶换成涂上硅脂的金属片,搞不好散热器的弹力会把开发板压坏。我买了一组不同厚度的铜片,选择厚度略小于散热器与 SoC 间距的一片,在散热器上涂上导热硅胶,SoC 上涂硅脂,之后压在一起。导热硅胶黏度比较高,刚涂上时会顶着铜片凸出来,让硅脂-铜片-导热硅胶的厚度略大于散热器与 SoC 的间距,之后导热硅胶一层受压变扁,就能保证散热器与 SoC 间刚好被导热硅胶和硅脂紧密填充了。

gel

这是用导热硅胶粘在散热器上的铜片。因为一开始涂得有点多,所以刮掉了一点,免得多余的部分被挤出来粘到 SoC 上拿不掉。

grease

涂上硅脂压实后稍微抬起一点,大概就是这样。

控制设备

RPi 4B、X96 Max Plus 和 Visionfive 2 这样带 HDMI、USB 口的设备可以直接插键盘鼠标显示器操作。如果需要高速连接,我有时候也直接用以太网线连接开发板:NetworkManager 有个一键在笔记本上启动个 DHCP 服务器,把笔记本当成路由器的功能,比手动设置 DHCP 方便了不少。这个功能在修电脑的时候也很好用,只是有时候会不停断线重连,不知道是硬件还是软件有问题。

Milk-V Duo 就不能直接插显示器了,官方推荐的做法是直接用 USB 线连接电脑,开发板默认会通过 usb_gadget 的 RNDIS 功能把自己模拟成一张网卡,这样就能在电脑上 ssh 登录开发板了。openSUSE Leap 15.5 某次更新之后默认不加载 rndis_host 模块了(改之前也不和我说一声,我还以为开发板坏了),应该和 Linux 移除 RNDIS 的计划有关,用的时候手动加载一下模块就好了。

当然这里除了 X96 Max Plus 外,连接 UART 也都比较容易,只是我平时不想拖这么多硬件。连接 UART 时我用的是 PuTTY。

系统引导

我并不是很懂计算机的底层原理,只能按照对 x86 系统的认知稍微理解一下这几块开发板的引导过程。我手里的开发板都可以用 U-boot,系统启动的时候由某种更底层的程序加载 U-boot,之后 U-boot 搜索存储盘,找到、加载 Linux,完成启动;U-boot 还能当 EFI 固件来着,所以 U-boot 至少同时有我的 x86 电脑上 UEFI BIOS、GRUB(x86) 两层引导程序的地位。

话说 RPi 4 上 openSUSE 是由 U-boot 加载 GRUB,GRUB 再加载 Linux 来引导的,感觉很是神奇。

Buildroot 和主流发行版都给引导程序的配置封装好了,一般情况下不用太关心引导问题,我也就不班门弄斧了。

GPU 的 3D 加速与通用计算

我并不打算日常用这些开发板打游戏,所以还是更关心通用计算的能力,至于 3D 加速的部分简单用 glmark2 测试下就是了。

RPi 4B、X96 Max Plus 和 Visionfive 2 理论上都能支持 Vulkan Compute,理论上能搞 GPGPU,实际效果就说不定了。关于对性能要求高的通用计算,我对机器学习的理解程度只达到了能看懂儿童科普读物的水平,其他领域也一概不会,只能跑跑基准测试。腾讯有个叫 NCNN 的机器学习框架,对 SIMD、RVV 和 Vulkan Compute 的支持都有,就拿它自带的工具来测试好了。

RPi 4B

RPi 4B 在 Raspberry Pi OS 12 上的 vc4/v3d 驱动表现不错,图形 API 原先支持的版本不高,现在已经能检测到 OpenGL 3.1/GLES 3.1/Vulkan 1.3 了,可惜它 GPU 性能太低,上 Vulkan Compute 还要比 CPU 慢得多……关于 Vulkan 渲染,我试着用 Zink 运行下 glmark2,画面是卡住的;vkcube 倒是能正常显示。不加载 Zink,glmark2(Wayland)有 780 分。

rpi4b-vkcube

vc4/v3d 用户空间驱动是开源的,但用 openSUSE 启动就没法正常硬件加速,在 Raspberry Pi OS 上启动 openSUSE 容器(把 /dev/dri 通进容器)却没问题,不知道是 openSUSE 没配好,还是原厂系统上存在一些闭源的内核驱动。

关于内核驱动和用户空间驱动,感兴趣的同学去看看 Linux DRI 就是了……反正我也完全不懂,只是外界的评价(见第 53 页)似乎并不是很高。用 N 卡的时候能在宿主机上只装开源内核驱动,而在容器里装闭源用户空间驱动的话确实要方便一点,但是装个 Flatpak 要同时装两个显卡驱动、用 VirGL 图形性能会受内核-用户态间多次转换而打骨折的问题也十分棘手……跑题了。

Vulkan Compute 当然就用 NCNN 测了,同时也测下用 CPU 运算的性能。我编译 NCNN 的时候给 CMake 加了 -DCMAKE_BUILD_TYPE=Release。NCNN 基准测试用的模型参数在源代码路径下,要在这个路径下 benchncnn 程序才能找到参数文件……benchncnn 输出的结果是时间,所以越小越好。

测试完发现 Vulkan Compute 真变成硬件减速了……

Model Vulkan CPU Ratio
squeezenet 298.37 46.76 15.67%
squeezenet_int8 43.36 40.27 92.87%
mobilenet 371.72 63.96 17.21%
mobilenet_int8 37.41 35.29 94.33%
mobilenet_v2 273.21 61.78 22.61%
mobilenet_v3 266.29 46.65 17.52%
shufflenet 171.36 33.83 19.74%
shufflenet_v2 226.22 24.15 10.68%
mnasnet 274.26 51.98 18.95%
proxylessnasnet 286.06 54.81 19.16%
efficientnet_b0 483.66 77.36 15.99%
efficientnetv2_b0 720.6 82.9 11.50%
regnety_400m 343.93 73.59 21.40%
blazeface 82.57 8.79 10.65%
googlenet 794.13 126.45 15.92%
googlenet_int8 103.21 99.97 96.86%
resnet18 854.74 126.29 14.78%
resnet18_int8 81.53 75.65 92.79%
alexnet 482.72 82.27 17.04%
vgg16 4529.85 571.87 12.62%
vgg16_int8 400.62 395.42 98.70%
resnet50 1961.75 280.65 14.31%
resnet50_int8 213.19 199.2 93.44%
squeezenet_ssd 1136.81 129.67 11.41%
squeezenet_ssd_int8 106.2 97.81 92.10%
mobilenet_ssd 915.37 126.24 13.79%
mobilenet_ssd_int8 75.44 73.2 97.03%
mobilenet_yolo 1826.17 286.25 15.67%
mobilenetv2_yolov3 893.05 191.85 21.48%
yolov4-tiny 1654.64 218.29 13.19%
nanodet_m 523.85 71.37 13.62%
yolo-fastest-1.1 235.75 45.21 19.18%
yolo-fastestv2 209.61 36.18 17.26%
vision_transformer 45802.37 2547.61 5.56%
FastestDet 215.75 36.24 16.80%

X96 Max Plus

s905x3 用的 Bifrost GPU 有 Panfrost OpenGL/GLES 开源驱动。如果 RPi 4B 用 Raspberry Pi OS 11 的话,s905x3 上的 OpenGL 版本要比 RPi 4B 上的高一些,性能也稍好;RPi 4B 换成 Raspberry Pi OS 12 ,s905x3 的 glmark2 分数竟然就被压下去了,不过主观上还是 s905x3 的视觉效果更顺滑一点。

s905x3-glmark2-wayland

glmark2(Wayland)得了 526 分。截图是为了看视觉效果,我在实际跑分的时候没有乱动机器。

需要 Vulkan 就得用 PanVk,目前版本的 Armbian 没提供,要自己编译 Mesa。NCNN 主要作者也有关于这的教程。我在编译完设好 VK_ICD_FILENAMESPAN_I_WANT_A_BROKEN_VULKAN_DRIVER 之后,编译 NCNN 之前还试了下 vulkaninfovkcube,结果 vkcube 说检测不到 VK_KHR_surface,显示不出画面;vulkaninfo 倒是能检测到 Vulkan 1.0 了。总之硬件加速支持比 RPi 4B 上差得多,不过 NCNN 对 Vulkan 的版本要求不高,所以也没大问题。

之后分别用 CPU 和 GPU(Vulkan Compute) 运行下 NCNN 基准测试,同样也是用 -DCMAKE_BUILD_TYPE=Release 编译,用 Vulkan Compute 的时候给 benchmark/benchmark.cpp 做了和教程里同样的改动;用 CPU 时没改。

之后发现用 Vulkan Compute 比 CPU 还慢的现象比在 RPi 4B 上还要明显……

Model Vulkan CPU Ratio
squeezenet 294.09 20.79 7.07%
squeezenet_int8 28.21 20.45 72.49%
mobilenet 286.56 26.93 9.40%
mobilenet_int8 33.79 23.47 69.46%
mobilenet_v2 199.89 27 13.51%
mobilenet_v3 471.68 21.55 4.57%
shufflenet 161.8 17.56 10.85%
shufflenet_v2 143.47 14.66 10.22%
mnasnet 201.17 23.24 11.55%
proxylessnasnet 220.43 27.5 12.48%
efficientnet_b0 304.8 34.59 11.35%
efficientnetv2_b0 607.84 42.48 6.99%
regnety_400m 257.59 35.87 13.93%
blazeface 43.3 5.39 12.45%
googlenet 1215.24 69.09 5.69%
googlenet_int8 79.82 66.21 82.95%
resnet18 1591.02 55.16 3.47%
resnet18_int8 52.33 47.55 90.87%
alexnet 609.33 48.83 8.01%
vgg16 7785.7 303.21 3.89%
vgg16_int8 368.11 343.21 93.24%
resnet50 2744.06 124.75 4.55%
resnet50_int8 145.53 111.35 76.51%
squeezenet_ssd 1163.9 67.84 5.83%
squeezenet_ssd_int8 74.34 64.68 87.01%
mobilenet_ssd 603.74 62.19 10.30%
mobilenet_ssd_int8 59.77 49.38 82.62%
mobilenet_yolo 1291.76 136.28 10.55%
mobilenetv2_yolov3 648.86 89.2 13.75%
yolov4-tiny 2914.02 118.35 4.06%
nanodet_m 313.59 43.58 13.90%
yolo-fastest-1.1 130.84 21.55 16.47%
yolo-fastestv2 119 18 15.13%
vision_transformer 31808.15 2220.99 6.98%
FastestDet 139.44 17.76 12.74%

算了,我就当它没 Vulkan……

话说之前有群友先是好奇 PanVk 为什么没法正常渲染却能做 Vulkan Compute,随之又查了下 Khronos 官网,说意思大概是 Vulkan Compute 是 Vulkan 的一等公民吧。

Visionfive 2

Visionfive 2 上驱动显示是 pvr,应当是闭源驱动。总之图形性能很强,但是只给 GLES,也用不了 Vulkan。

缺 OpenGL 的问题本来可以用 gl4es 解决,但我的机器上 gl4es 总是 Segfault,明明社区的人和 gl4es 原作者都曾经在这开发板上正常跑起来过的。考虑到现在很难有时间持续跟进,就没去开 Issue……不知道等 Vulkan 支持后直接去用 Zink 会不会现实一点。

至于测试 NCNN……没意义,真的。

User-Mode Linux

没有的不存在的。现在没有这种适配。

QEMU

KVM

ARM 上运行 QEMU/KVM 并没有太复杂,只是各大发行版的文档太少,加上 QEMU 运行 ARM 虚拟机默认不给键盘鼠标显示器,初次上手会让人有点紧张,有种“人生地不熟”的刺激感。openSUSE 上有部分页面简单介绍了下系统在 ARM 上的用法,页面整体有点老,不过没关系。

RPi 4B 和 X96 Max Plus 都可以正常运行 QEMU/KVM 虚拟机。RISC-V 上……先前提过 Visionfive 2 不支持硬件虚拟化,我这边试不了。

安装 QEMU,qemu-system-aarch64 指定 -accel kvm-machine 我直接选了 virt-cpu 我总是 host,想开多核就 -smp cores=[数字](不含方括号)。想要键盘鼠标显示器就手动指明 -device qemu-xhci(没错默认连 USB 都不给)、-device usb-kbd-device usb-tablet-device virtio-gpu-pci。不用键盘鼠标显示器也没关系,系统启动之后直接用串行终端也行。

想要像 PC 一样能插安装盘、能用键盘调设置的体验,就用 -bios [位置] 加载个 UEFI 固件,这里是 TianoCore EDK 2。关于固件的位置,openSUSE 上在 /usr/share/qemu/aavmf-aarch64-code.bin,Debian 系上多在 /usr/share/qemu-efi-aarch64/QEMU_EFI.fd 下。

s905x3-kvm

进 EDK 2,按 F12 就是很熟悉的界面:

kvm-edk2

想玩 U-boot 也可以自己编译一个,同样用 -bios [位置] 加载它。U-boot 默认要从串行终端进行调试,把 QEMU 视图切换到 serial0 就可以控制它了。

kvm-uboot

如果想在虚拟机里装的发行版提供了 ARM UEFI 镜像,那么直接用 -cdrom [位置] 加载安装盘就好了。虚拟硬盘往往可以直接用 VirtIO,具体取决于你要装的发行版支持情况。U-boot 和 EDK 2 都可以启动 UEFI 启动镜像……相较于同 Device Tree 斗智斗勇,用 UEFI+ACPI 确实要轻松愉快得多。

EDK 2 加载 openSUSE LiveCD 就会进 openSUSE LiveCD 上抹茶味道的很漂亮的 GRUB:

opensuse-edk2

U-boot 加载就要进串行终端操作启动菜单了。

opensuse-uboot

当然这两种都还是 EFI 启动,从 kmsg 里能看到:

opensuse-efi-by-edk2

opensuse-efi-by-uboot

你要是恰好有个 Ventoy 盘,也可以试一下从它上面启动(笑)。

kvm-ventoy

有时候 Debian 系发行版要装 qemu-system-x86 才肯安装 VirtIO 相关文件。有种拆包都不会拆的感觉……

话说 ARM 上跑 KVM 干嘛……放 CI 上拿来测试实验品的系统应该很有用吧。不过这是借口,我根本玩不懂 CI。

嵌套虚拟化

没有的不存在的。搜一下 ARM 的嵌套虚拟化,第一页出来的超过一半是论文。

x86 上嵌套虚拟化就显得轻松自如,不知道是什么原理。

TCG

你若是能忍受 QEMU/TCG 的低性能,也可以在普通 x86 PC 上模拟 ARM 设备,只要把 -accelkvm 改成 tcg 就行了,还可以指定个多线程,合起来是 -accel tcg,thread=multi。用 TCG 的话就不能选 -cpu host 了,我一般选 max 或者 cortex-a53

模拟 RISC-V 也差不多,只是没有很多发行版提供现成的镜像,现成的文档也更少……

Buildroot

Buildroot 是一组给嵌入式设备定制系统的工具,Milk-V Duo 官方只支持用 Buildroot 构建 Linux,其他开发板用了 Buildroot 定制系统也会方便一点,所以学会这个还是很重要的。

简单来说,先获取 Buildroot 工具,之后按文档配置构建选项,之后等着想要的镜像构建出来就可以了。Buildroot menuconfig 下能定制的内核选项很少,实在需要改的话要自己导入改好的配置;选项间常有互斥或者依赖关系,所以很可能要来回翻菜单修改设置;Buildroot 会检查的东西相对较少,检查配置是否正确最好还是自己多看两眼。

改完 Buildroot 配置后有时得做一次完整的编译,要是比较勤劳,一天多编译几次的话硬盘说不定两年就写坏了,我视情况会给 Buildroot output 目录挂载到 tmpfs 上。编译用的机器尽量至少有 16GiB 内存吧。

给 Duo 编译的话,下载 Duo Buildroot SDK,进入 buildroot 目录,选择 Milk-V Duo 的默认配置(make milkv_duo_musl_riscv64_defconfig),按照需要 menuconfig,之后 .config 似乎会被 Duo 的构建脚本覆盖……我直接把 configs/milkv_duo_musl_riscv64_defconfig 用生成的 .config 覆盖了。很暴力,但是确实有效果。

改内核配置的话,回到 SDK 目录,给 build/milkvsetup.shbuild_all() 里加上 menuconfig_kernel,启动编译脚本之后等待 menuconfig 界面出现就是了。大概也可以手动选择 .config,但我没找到相关文档。我想用 Bubblewrap,所以在程序炸了几次之后找到并启用了 Control Group supportNamespaces supportEnable signalfd() system call 和 OverlayFS。如果对 cgroup、namespace 相关配置有疑问,可以去看 check-config.sh

duo-bubblewrap

之后发现 lxc-images 里没有 RISC-V 容器镜像,没法偷(不是)现成镜像来用,要自己 bootstrap 一个。确保电脑上正确配置了 qemu-user 和 qemu-binfmt,然后找个干净的目录,debootstrap --arch=riscv64 sid rootfs https://mirrors.tuna.tsinghua.edu.cn/debian。用 Sid 是因为稳定版里还没有 RISC-V 支持。

容器里 APT 不知怎么在连接网络时会有 Illegal Instruction,就传了个 neofetch 包用 dpkg 装了一下。

duo-debian-neofetch

RVV

感谢 Revy 和 Robin Lu 的指导。

简单玩一下 Duo 上的 RISC-V Vector Extension。关于 RVV 和 x86/ARM SIMD 的区别,这里还不需要过度关注,感兴趣的话可以去读些科普文章

如果有一组数据需要同时进行同一类型的运算,比如做矩阵乘法的时候(如果还没学线性代数,我比较推荐看一下 Linear Algebra Done Right ),要频繁做两组数据中,一组里的每个数都乘以另一组中一个数的操作,这种情况下用 Vector Extension 相比不用会快一点。

用 RVV 的方法有几种,可以手写汇编,或者用 Intrinsic,也可以让编译器根据程序的语义自动向量化。手写汇编比较恐怖;RVV Intrinsic 是一系列包装好的汇编函数,可以在更高级的语言中安全地使用,用起来就友好一点了。

做计算的时候,要先从内存中取出一些要操作的数,放到向量寄存器里,之后用指令进行算术运算,再从向量寄存器中取出结果,放回内存,重复……

先用 Intrinsic 来个向量加法:

#include <riscv_vector.h>

void rvv_float32_add(const float *a,const float *b,float *ans,size_t length)
{
	//先说好,我们把 a+b 写到 ans 里。length 是向量的维数
	//不直接把运算结果写回 a 或者 b 里是为了方便检查运算的正确性
	size_t vl;//寄存器的空间是十分有限的,我们一次只操作 vl 个元素
	vfloat32m1_t va,vb;//设置两个向量寄存器,用来存放一轮操作的几个元素
	//元素类型是单精度(32位)浮点数,LMUL 为 1
	//LMUL 是指一组向量寄存器的大小,调大 LMUL 可以让几个向量寄存器组成一组,可以少进行几轮循环
	for(;length>0;length-=vl)//length 为 0,就说明已经把向量的所有维都算完了
	{
		vl=vsetvl_e32m1(length);//取得这次要操作多少个元素
		va=vle32_v_f32m1(a,vl);//从 a 中加载 vl 个元素,存到 va 里
		//可以看出,元素必须是紧挨在一起的
		vb=vle32_v_f32m1(b,vl);//从 b 中加载,存到 vb 里
		va=vfadd_vv_f32m1(va,vb,vl);//va 与 vb 相加,结果写到 va 里
		//即 va[i]=va[i]+vb[i]
		vse32_v_f32m1(ans,va,vl);//从 va 中读取运算结果,存到 ans 里
		a+=vl;//设置偏移量,为下一轮循环作准备
		b+=vl;
		ans+=vl;
	}
}

之后写个主函数调用这个函数,用 Duo APP SDK 里的工具链编译一下:

path-to-duo-app-sdk/riscv64-linux-musl-x86_64/bin/riscv64-unknown-linux-musl-gcc -o vadd vadd.c -mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d

编译开关是我从 duo-examples 里抄的。编译完 sftp 上传到 Duo,运行一下看看结果。

现在我们已经学会怎么用 RVV 加速向量加法了,再来看一下矩阵乘法。计算矩阵 a × b 的话,得到的结果中第 [i][j] 项就是 a[i][0]*b[0][j] + a[i][1]*b[1][j] + ... + a[i][n]*b[n][j],其中 a[i][0]...a[i][n] 在内存中是连续的,b[0][j]...b[n][j] 彼此之间却要隔一段,不利于我们从内存中取数,所以先给 b 转置一下,记为 bt,原本的表达式就变成了 a[i][0]*bt[j][0] + a[i][1]*bt[j][1] + ... + a[i][n]*bt[j][n]

先用标量运算实现一个。

void matrix_mtpl(float **a,float **bt,float **ans,int m,int n,int o)
{
	//a * b,bt 是 b 的转置。a 是 m*n 矩阵,b 是 n*o
	for(int i=0;i<m;i++)
	{
		for(int j=0;j<o;j++)
		{
			ans[i][j]=0;
			for(int k=0;k<n;k++) ans[i][j]+=a[i][k]*bt[j][k];
		}
	}
}

之后对着改成用 RVV:

void rvv_matrix_mtpl(float **a,float **bt,float **ans,int m,int n,int o)
{
	size_t vl;//和上面的 vl 含义一致
	size_t vlmax=vsetvlmax_e32m1();//vl 最大可能的值,相当于下面用到的向量寄存器里,最多存储的元素个数
	vfloat32m1_t vzero,vans;
	vzero=vfmv_v_f_f32m1(0.0,vlmax);//设置一个全 0.0 填充的向量寄存器,下面有用
	for(int i=0;i<m;i++)
	{
		for(int j=0;j<o;j++)
		{
			vans=vzero;//vans 存储 a[i][0]*bt[j][0]..a[i][n]*bt[j][n] 的和
			//计算开始前将 vans 置零
			float *pa=a[i],*pbt=bt[j];//指针指向将要被计算的第一个元素
			//初始状态 pa 指向 a[i][0],pbt 指向 bt[j][0]
			for(size_t length=n;length>0;length-=vl)
			{
				vfloat32m1_t va,vb;
				vl=vsetvl_e32m1(length);//同样是一次处理 vl 个元素
				if(vl<vlmax)
				{
					va=vzero;//给 va 置零
					vb=vzero;
				}
				va=vle32_v_f32m1(pa,vl);//va 存储 a[i][x],a[i][x+1],...
				vb=vle32_v_f32m1(pbt,vl);//vb 存储 bt[j][x],bt[j][x+1],...
				//如果 vl < vlmax,剩下的没被设定的部分是 0
				//最新 rvv-intrinsic-doc/examples 是没有给 va vb 置零这一步的,可能是因为新版 vfmacc_vv 的行为不太一样
				vans=vfmacc_vv_f32m1(vans,va,vb,vlmax);//向量乘加的操作
				//此处 va 中的每个元素分别与 vb 中同一下标的元素相乘
				//得到的向量再与 vans 相加,即 vans[i]=vans[i]+va[i]*vb[i]
				//如果写成 Octave 语法,是 vans=vans+va*vb
				//如果vl=4,可以看出 vans[0] 最终会等于 a[i][0]*bt[j][0] + a[i][4]*bt[j][4] + ...
				//
				//需要说明的是,最新 rvv-intrinsic-doc/examples 里 vfmacc_vv 最后一个参数是 vl(而不是vlmax)
				//我这里如果设成 vl,且 vl < vlmax 的话,vans 后一部分的数据(vans[vl,vl+1,...])似乎会丢失
				//所以把操作元素数改成了 vlmax。前面的 va vb 置零是为了保证 va,vb 的 [vl,vl+1,...] 是 0

				pa+=vl;//设置偏移量
				pbt+=vl;
			}
			vfloat32m1_t vsum;
			vsum=vfredusum_vs_f32m1_f32m1(vsum,vans,vzero,vlmax);//求向量vans中各个元素的和(得到标量),
			//再与vzero[0](标量,这里是0)相加,写入vsum[0]
			//相当于是 vsum[0]=vzero[0]+vans[0]+vans[1]+...
			//这就得到 a[i][0]*bt[j][0] + a[i][1]*bt[j][1] + ... 的和
			//
			//vfredusum_vs 的第一个参数我也不知道是什么意思,实际上好像填什么都不影响运算
			//最新 RVV Intrinsic 里是没有第一个参数的
			//有些代码在第一个参数处写即将被覆盖的寄存器,所以我也这么写了
			ans[i][j]=vfmv_f_s_f32m1_f32(vsum);//取出 vsum[0],写入内存
		}
	}
}

写完之后还可以比较下用 RVV Intrinsic 与标量运算的耗时,我把实验用的代码放 Github 上了。

Duo 的工具链自动向量化效果不好,NCNN 用的 Intrinsic 版本看样子和 Duo 工具链不兼容,所以都不测了。

写 RVV 0.7.1 的体验不佳,如果有同学想上手但学校不提供免费开发板的话……就再等一两年吧。

题外话

嵌入式确实是坑。算法不是,算法是深渊。这么想的话,“坑”大抵是一种赞美。

玩嵌入式 Linux 和玩 Minecraft 有一点类似,就是不想玩的时候碰都不想碰,但玩上就有点容易上瘾。

计算机行业的发展史,就是一代又一代从业者被淘汰的历史。我现在学的很多东西五年之后全部作废也是完全有可能的吧。

不写了,我要去写线性代数作业了。