做嵌入式视觉、尤其是拿到一块陌生的 ARM 开发板时,“移植 OpenCV”一定避不开。这一篇循序渐进的实操记录人数,我不单独展开,直接从我在一块 Cortex-A 级别 ARM Linux 板子上移植 OpenCV 4.x 的过程中抽出的核心步骤,按环境、依赖、编译、上线运行、性能调优的顺序写。先交代两个前提:宿主机器是 x86_64 的 Linux 环境,目标板是 ARM 架构的 Linux 系统;这是目前最常见的“宿主机编译、目标板运行”的嵌入式开发场景。如果你目标是 AArch64,也就是 arm64 架构,除了工具链前缀和个别参数不同,流程完全一样。
想强调的是,移植不等于把 OpenCV 源码放到板子里直接 make。ARM 板大多受限于存储空间和处理能力,在板上编译既慢又容易卡死,更可靠的手段是交叉编译,在性能强大的宿主机器上生成目标板可执行的库文件与可执行程序,最后拷贝到板子上验证。这篇文章会陪你走完全部流程,最后一小节还有我自己反复踩过几轮的调优心得。系列前一篇已经提过几处通用“经验包”,这一篇则是完整版,以一次独立的板级移植作为眼下唯一主线。1. 移植前必须确认的硬件与工具链事实
拿到一台 ARM 板子,多数人第一反应是找 datusheet,看处理器能跑多快、内存多大这当然对,但开始动 CMake 之前还有更关键的问题:工具链与板子是否匹配。工具链版本、处理器架构、跟浮点运算相关的 ABI 约定,任何一项对不上,都会让后续的编译过程出现一系列怪异错误。1.1 交叉编译是移植的起点,不是唯一选项
交叉编译的原理很简单:在主机上用一套针对 ARM 目标的编译器生成代码,目标板只负责运行。OpenCV 本身是 C/C++ 项目,源码用到的编译器特性不会特别新,但依赖的第三方库在交叉编译时是真正的难点,因为每个库都需要用目标层的工具链重新编译一遍,这是很多人开始没有心理准备的地方。 为什么不直接到板子上编译?我在某型号 ARM 板子试过,4 核处理器,2GB 内存,编译一个完整 OpenCV 大概需要 poke 近半小时,而且编到中途内存耗尽常见得很。板子系统里还没有完整 build-essential 工具链的也大有人在,所以交叉编译“更大几率”是对的做法。当然,如果板子内存充足、系统里已有可用的编译环境,直接在板上编也是可行的,但如果你做的是产品预研,后续需要反复改配置、加模块,交叉编译的整个流程更可控。1.2 建立符合目标板的根文件系统
交叉编译的 OpenCV 编译期环境需要基础 C/C++ 库和系统头文件,这些来自目标板的系统根目录。建议构造一个专门的主机目录,例如 /opt/arm-rootfs,用板子的根文件系统内容填充它,至少要包含 lib、usr/lib、usr/include 这几个关键目录。在这个目录里,目标板的 libc.so、pthread、dl 等基础库和头文件都会使用到。 很多供应商发布的 SDK 包里已经自带根文件系统或工具链目录,用起来也比较稳妥。如果没有现成的,就从板子上直接把整个根文件系统打包过来,再解包到这个目录。拿板子系统时要注意:不要只拷贝 /lib 和 /usr/lib,因为很多库和头文件在 /usr/local 下,尤其是你自己之前装过的依赖。干脆把整块根文件系统镜像挂载或解包,避免遗漏。1.3 工具链接口与处理器特性的常见误区
我犯过也用一件事:编译器用 arm-linux-gnueabihf 开头,代表 ARMv7 硬浮点;如果板子其实是 AArch64,那么必须使用 aarch64-linux-gnu 前缀。多个工具链前缀不要乱挑,最好的办法是检查板子的内核信息或者供应商 SDK 文档,一次性给自己记录下面这些参数。| 检查项 | 命令/方法 | 对 OpenCV 的影响 |
|---|---|---|
| 处理器架构 | 板内执行uname -m | 决定工具链前缀与 -march 参数 |
| 是否支持 NEON | 查看 /proc/cpuinfo 里的 Features,包含 neon 才行 | 影响 CPU_BASELINE 与 NEON 使能 |
| 浮点 ABI | 工具链名称含 hf 通常是 hardfp,但最好用readelf -A验证 | 决定链接时是否报 VFP 寄存器错误 |
| 根文件系统 libc 版本 | 板内执行ldd --version | 工具链 sysroot 的 libc 不能比板子新太多 |
工具链和系统根目录还有两个容易踩进去的坑。第一个是工具链自带的 sysroot 与板子的 libc 版本不匹配,通常会出现 `cannot find -lstdc++` 或 `GLIBC_2.34 not found` 这类运行时报错,解决方法是尽量采用供应商配套 SDK。第二个是不要用主机的 `/usr/lib` 来满足交叉编译的查找,种下的错误会在链接后期才爆发,后面第四部分会专门展开。2. 依赖库的顺序排列与版本锁定
刚开始接触 OpenCV 时,我一直以为它是个自包含的库,直到有一天在板子上跑 imread 读取常规图片,返回的 Mat 是空的,才明白它如果想读 jpg/png,就必须在编译期看到对应的解码后端。这个道理在交叉编译场景里更严峻:主机的这些后端库不能直接复用,必须在编译 OpenCV 前为目标板准备一套 ARM 版本。2.1 先弄清楚完整依赖图谱再动手
OpenCV 的模块化结构决定了依赖“按需引入”的魅力。不开 DNN 就不用碰 protobuf;不开 GUI 窗口就不用编译高层的图形工具库;不加视频采集功能就不需要外接视频后端。先做一个清单,再决定每个模块要不要。 我日常最常保底的一组是核心、图像处理、图像编码、视频采集和窗口显示模块。围绕它们,依赖可分为以下几类:基本压缩库:任何带压缩性质的图片格式都绕不开的底层库,先编它,后面的依赖都直接或间接需要它。
图片格式解码库:PNG、JPEG、TIFF 三个常用格式的解码库,编码解码都靠它们完成。
摄像头采集后端:V4L2 相关接口库,Linux 下摄像头帧采集的标准入口。
并行库选项:TBB 这类库属于可选项,不开启也可以用 pthread 完成并行,但板子核多且性能关键时往往会加它。
我的建议是:为了减少环境不确定,第一版编译只保留核心、图像处理、图像编码、视频采集这四个模块,将界面和 DNN 全部关闭。等版子在板上能跑通最小 Demo,再往里加其他模块。编译期减少依赖,验证周期短,出问题时定位也大幅度快于一次性全量。
2.2 交叉编译依赖库的通用操作流程
依赖库的构建方式各不相同,但大体遵循一个固定姿势:先 configure,再 make,再 install 到根文件系统目录。以常见的图片格式解码库为例,通常你会见到这样的功能步骤:tar -xzf libxxx.tar.gz cd libxxx ./configure --prefix=/opt/arm-rootfs/usr --host=arm-linux-gnueabihf --enable-shared make -j4 make install有一部分库用 cmake 构建的也很常见,只要注意两点:指定 CMAKE_INSTALL_PREFIX 指向系统根目录,并让 cmake 找到同一套交叉工具链。在做这一步时,我习惯用一个临时环境变量把工具链地址带出来,既避免反复写活活的路径,也能保证依赖库之间使用同一条编译链。 编译顺序基本按依赖箭头反向来:最底层的基础压缩库最先编,图片格式解码库在上层,OpenCV 最后。贪方便打乱顺序,常常导致链接阶段 libxxx.so 引用了一堆 undefined reference。如果你发现一个库链接时依赖另一个库,就回头重新排一下顺序,没有捷径。2.3 版本选择的三个原则
依赖库版本与 OpenCV 版本之间的兼容问题,比大多数人想象的更关键。某次我为了性能直接选了很新的图片格式解码库,结果 OpenCV 的接口调用方式跟不上,报了一堆隐晦的 `undefined symbol`。后来换回板子上常用的大版本旧一点的分支,立刻安静下来。总结出三条可行的原则:第一,依赖库版本不要超过 OpenCV 源码官方分支里对应的 README 推荐版本。第二,以板子系统里已有库的版本为中心,优先匹配主版本一致;如果板子已经有人验过某个版本,你直接沿用是最安全的。第三,编译依赖库时保留窗口的configure参数日志,避免日后需要重建时无据可依。
环境变量也可以帮上大忙。交叉编译时把 `PKG_CONFIG_LIBDIR` 指向 `/opt/arm-rootfs/usr/lib/pkgconfig`,并且让它只定位到系统根目录里的 `.pc` 文件,这样 OpenCV 在查依赖时就不会撞到主机上的同名文件。在我某次操作中,这个变量一度是脚本里最关键的一行,少了它,再标准的前缀设置都可能前功尽弃。3. 用构建系统配置 OpenCV 的关键参数
OpenCV 从 3.x 时代开始全面转向 CMake,到了 4.x 几乎只有 CMake 一条路。交叉编译时,CMake 的配置参数直接决定生成的库是给 x86 用的,还是给 ARM 用的。很多第一次搞的人打开 CMake 界面就蒙,我就把最关键的一组参数与一个可复用的配置文件写下来,这套配置翻过整个 ARM 团队后还会继续用到。3.1 一份可用的交叉编译配置文件
在 OpenCV 源码目录之外放一个单独的文件,名字就叫 toolchain.arm.cmake,内容大致如下:set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(TOOLCHAIN_PREFIX arm-linux-gnueabihf) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++) set(CMAKE_SYSROOT /opt/arm-rootfs) set(CMAKE_FIND_ROOT_PATH /opt/arm-rootfs) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)也许你会问这第九行是什么意思:前三行明确目标系统是 ARM Linux,告诉 CMake 不要走 x86 的文件路径;第四到第六行把编译器切换成交叉工具链;第七行设置系统根目录;最后四行限定查找程序、库、头文件、包时的路径模式。`PROGRAM NEVER` 保证它去找主机命令,而不是在根文件系统里找开发工具;后面三个 `ONLY` 则强制所有库和头文件只从目标根文件系统里找,隔离主机干扰。 如果用 AArch64 板子,只需要把 TOOLCHAIN_PREFIX 换成 aarch64-linux-gnu。关于 `arm-linux-gnueabihf` 和 `arm-linux-gnueabi` 的区别,简单讲,gnueabi 对应软浮点,gnueabihf 对应硬浮点,绝大多数现代 ARM Linux 系统都用硬浮点工具链,但老版本内核和旧板子可能只有软浮点版本,这从 boot 时打印的 CPU 信息能看出来。3.2 决定最终产物的模块选项
配置的时候,OpenCV 默认会尝试把能编的模块都编上,这可能不是你想要的。用 BUILD_LIST 压缩模块集合,往往能省下大量时间与磁盘空间,还能减少被依赖的第三方库范围。我的这组参数一直拿来反复用:cmake -S . -B build-arm \ -DCMAKE_TOOLCHAIN_FILE=../toolchain.arm.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr/local/opencv-arm \ -DBUILD_LIST=core,imgproc,imgcodecs,videoio,highgui \ -DWITH_OPENCL=OFF \ -DWITH_OPENMP=OFF \ -DWITH_TBB=OFF \ -DWITH_GTK=OFF \ -DWITH_V4L=ON \ -DBUILD_SHARED_LIBS=ON关键选项的含义我列个表,比事后猜含义方便:| 参数 | 取值 | 作用与理由 |
|---|---|---|
| BUILD_LIST | core,imgproc,... | 只编译必要的模块,减少依赖与体积 |
| WITH_GTK | OFF | 不编 GUI 后端,绝大多数无屏幕板子用不到 |
| WITH_V4L | ON | 打开 Linux 视频采集后端,摄像头场景需要 |
| BUILD_SHARED_LIBS | ON | 默认生成 .so,方便排查问题;后面可再改静态方案 |
| WITH_OPENMP | 按需 | 板子核心多且追求速度才开,多核小内存慎用 |
| WITH_TBB | 按需 | 引入并行库会带来额外依赖,先 OFF,跑通后再试 |
如果你之后需要用 OpenCV 的 DNN 模块跑神经网络,还得把 DNN 加入 BUILD_LIST,同时准备 protobuf 依赖。建议第一版全部关闭,稳字当头。3.3 指令集优化:NEON 与 VFPv4 的启用边界
ARM 平台的性能优化往往绕不开 NEON 指令集,OpenCV 4.x 在 CMake 里对 ARM 优化有比较直观的控制方式。局部板子若是 ARMv7,通常会在命令行加上 `-DCPU_BASELINE=NEON,VFPV3` 或类似参数,如果板子支持 VFPv4,则写成 `NEON,VFPV4`。过早地全局泛化会出问题,尤其当你手里的板子其实不支持某些指令时,贸然打开 NEON 会让编译出的库在运行阶段直接非法指令。 我有一个稳妥做法:先在板内看 /proc/cpuinfo 的 Features 是否包含 `neon`,再决定要不要开启。若包含,在 CMake 参数里加上 NEON;若不包含,宁可什么也别动,用通用编译参数。对于 AArch64 架构,NEON 是默认启用且没有选择的余地,再去手动加反而多余。 还要特别提醒一个坑:不要在交叉编译环境里使用 `-mcpu=native`,它会把编译器所在的主机 CPU 特性当成目标 CPU 特性,酿成的在板子上必崩无疑。目标板具体用什么 CPU,就写什么 CPU,比如明确指定 `-mcpu=cortex-a7`,而不是依赖宿主机自动探测。4. 编译链接阶段最容易踩的坑
依赖就绪、CMake 参数入湖,进入真正 make 阶段,好戏才刚开始。在 x86 上非常正常的命令,到了 ARM 交叉编译环境却可能冒出一整套秀存在感的报错。这一节记下我自己遇到的三类真问题,不看报错本身,先看环境因素。4.1 编译崩溃不一定是内存不足
我第一次在笔记本电脑上跑 OpenCV 全量交叉编译时,顺手用了 `make -j12`,结果编到中段 GCC 进程莫名被杀死,终端输出一栏一栏的 `Killed`。当时下意识觉得是磁盘空间问题,后来用 `free -h` 看到是内存耗尽,大量并行编译进程占满内存,系统开始把编译进程杀掉,控制台却不报详细的编译错误。 交叉编译对主机内存的要求往往被低估,尤其 OpenCV 这类 C++ 模板大户,每个编译单元都可能让人内存达到一两百 MB。解围办法很简单,先降到 `make -j4`,如果还是被杀死就降 `-j2`,同时确认 `temp` 分区剩余内存足够。后续我组包里特意保留自主板的多次 `-j4`,极少再出问题。附加建议:关闭其他大量占用内存的应用,再用 `make -j$(nproc)` 直接胡来的情况,必须忍住,只在你确认内存充裕时才用。4.2 链接 error 时先检查文件属性而非看错信息
提示错误大多数是 `undefined reference to xxx`,或者 `cannot find -lxxx`。这俩时候第一反应是去补什么包,那就容易误判。真正原因经常是:它在系统根目录里找到的库文件其实是 x86 版本,或者是两个不同架构的库被 CMake 串在一起。 我的排查顺序:先定位报错里涉及的 `.so` 文件在哪个目录,然后执行 `file /路径/libxxx.so`,看输出的架构描述。它应该是类似 `ELF 32-bit LSB shared object, ARM, EABI5` 这样的结果。如果看到 `x86-64`,就说明库是从主机带进去的,交叉编译过程根本不该用。此时回到依赖库编译安装那一步,重新在系统根目录里安装 ARM 版本。 第二个反复遇到的链接问题是:某个依赖库确实编好了,但 OpenCV 仍然说找不到。这通常是 `CMAKE_FIND_ROOT_PATH` 没设置,或者设置了配置里少加了 `ONLY` 模式。系统根目录里明明有对应版本却找不到,优先回看 3.1 节里的配置文件,把查找模式的四个参数一个一个对齐。4.3 运行时非法指令常见诱因
编译链接顺利通过并不代表万事大吉。复制到板子上一分钟跑,程序直接崩掉,后台常常给出 `Illegal instruction (core dumped)`,这就是典型的编译时启用了目标板不支持指令的表现。我经历过一次狼狈。某块核芯其实支持 Cortex-A7,但我在编译参数里写了更适合更新处理器的 NEON 变体,运行时直接打到非法指令上。 这类问题的排查逻辑是反向的:先确认编译时开过哪些指令优化参数,再核对板子 /proc/cpuinfo 里的 Features。如果编译时启用 NEON,但板子的 Features 里连 neon 都没有,那么程序大概率就是这样崩,去掉相关参数重编即可。另一个更省事的办法,在板子上执行 `readelf -A 可执行文件名`,查看 Tag_CPU_arch 和 Tag_NEON_preserved 之类的标记,对比板子的实际能力,你很快就知道自己编出了什么水平的产物。 有时候非法指令与可用变量不够有关系,比如低版本内核配高版本编译库,但这很少见。先从指令集、从工具链和板子的匹配度排查,是永远用在最前面的方向。5. 部署到 ARM 板上的验证方法
等 make 完成,你的构建目录里会生成一批 .so 和可执行程序。不要急着把整个目录复制到板子上,更好的思路是设定一个清晰的目标目录结构,把需要的东西统一收集起来,再同步过去。这一节讲讲安装阶段和上板验证的细节。5.1 两个安装路径的区别
在 CMake 配置里有两个容易弄混的概念:`CMAKE_INSTALL_PREFIX` 决定安装到哪个前缀目录,`DESTDIR` 是临时把一个安装过程重定向到指定根目录的手段。 我的习惯是先指定 `CMAKE_INSTALL_PREFIX=/usr/local/opencv-arm`,然后在 make 完成后的安装阶段使用临时目录:make install DESTDIR=/tmp/opencv-arm-target/这会在 /tmp/opencv-arm-target/usr/local/opencv-arm 下生成完整的头文件与动态库。然后我只需要把 /tmp/opencv-arm-target/ 里的内容按目录结构拷贝到板子根目录,再在板子上把 /usr/local/opencv-arm/lib 加入动态库搜索路径即可。 当初一开始用的是直接把安装前缀指向系统根目录 /usr,后来发现这样极易污染宿主机的环境,也不方便多版本管理。用 DESTDIR 的临时目录做法,宿主机完全隔离,板子端也随时可以删掉整个目录,不需要重装系统验证。5.2 编译一个最小验证程序
到了这一步,最急迫的就是用一段足够简单、又能覆盖基础功能的程序验证 OpenCV 是否真正可用。我多次用来验证的就是图像读取加灰度转换:#include <stdio.h> #include <opencv2/opencv.hpp> int main(int argc, char** argv) { if (argc < 2) { printf("usage: %s image.jpg\n", argv[0]); return 1; } cv::Mat img = cv::imread(argv[1]); if (img.empty()) { printf("imread failed\n"); return 1; } cv::Mat gray; cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY); printf("image size: %d x %d\n", img.cols, img.rows); printf("gray pixel[0,0]: %d\n", gray.at<uchar>(0, 0)); return 0; }编译时直接指定 OpenCV 的头文件和库目录,至少这种明确路径,不依赖板子上的 pkg-config,便于排查问题:arm-linux-gnueabihf-g++ demo.cpp \ -I/tmp/opencv-arm-target/usr/local/opencv-arm/include/opencv4 \ -L/tmp/opencv-arm-target/usr/local/opencv-arm/lib \ -lopencv_core -lopencv_imgproc -lopencv_imgcodecs \ -o vision_demo把 vision_demo 与一张普通图片 copy 到板子,执行 `/vision_demo test.jpg`。如果打印 output 正常,说明最核心的 core、imgproc、imgcodecs 模块二次链接无误;到这一步,Arm 移植流程就基本跑通了。5.3 运行时依赖是否齐全怎么判断
上板运行也常有看到 `error while loading shared libraries: libopencv_core.so.408: cannot open shared object file` 之类输出。这并不一定是 OpenCV 没安装到位,而是系统动态链接器没找到库路径。 在板子上执行 `ldd vision_demo`,会列出程序依赖的每个动态库及搜索到的位置。像 `libopencv_core.so.408` 或 `libstdc++.so.6` 显示 `not found`,就证明路径配置不完整。简单直接的方案是在板子上的 `/etc/ld.so.conf` 里加入一行 `/usr/local/opencv-arm/lib`,再执行 `ldconfig`,让系统重新扫描并建立链接缓存。 用 `ldd` 时顺带看一下 `librt`、`libpthread` 等依赖的指向,如果某些基础库指向宿主机的 x86 版本,抓取到板子上无意义,需要回根文件系统里替换。判定是否标准,首先要看目标板系统根目录里的同名库版本,与工具链自带的 sysroot 是否同源;如果版本不一致,重新编译对应依赖库是稳妥路线。6. 围绕性能与二次开发的实际调优建议
一旦最小 Demo 跑通,剩下的问题就转到“怎么更快、更省地方”。OpenCV 在 ARM 上有不少可以挤压的余地,但前提是要尊重板子本身的能力,不要盲目跟风开优化选项。6.1 先测量再开启优化
我有一次非常典型的经历:在项目里听说 NEON 对图像处理加速效果明显,直接全量 NEON 编译,满心以为板子上的图像缩放性能会翻倍,结果实测发现提升比预期低很多,因为算法大部分时间卡在内存拷贝上,甚至有些场景因随机访问反而更慢。 所以任何优化都要先有一个基准数据。在板子上执行一轮简单的计时,比如对同一张 720x480 图片做 100 次缩放或滤波,记录平均耗时。之后开启优化选项,再跑同一套数据,对比才有效。不要依赖直观感受,图像缩放这类操作在 ARM 上用 NEON 确实能快不少,但如果瓶颈在内存带宽,盲目 NEON 不会带来质变。 并行方面也有同样的原则。TBB 或 OpenMP 都要产生线程管理成本,若板子是二核四核且算法图像较小,你需要考虑开并行带来的收益。我一般先关掉并行,测试一次,再打开并行对比一次,确认真正快了才会保留配置。这一原则对嵌入式视觉相当有效。6.2 裁剪模块与控制动态库数量
嵌入式产品发布时,最关心可执行产物体积与依赖数量。完整版 OpenCV 动态库非常多,安装目录动辄几百 MB 甚至更大,这在医疗、工业级设备上可以接受,但在存储紧张的板子上就遭罪了。我的建议:根据项目实际算法逻辑精确设计模块剪纸,凡是没用到的模块一律不要编进去。 BUILD_LIST 就是裁剪的有效入口。我自己的例子,只留 core、imgproc、imgcodecs、videoio、highgui 五个模块后,安装目录体积能下降到几百 MB 的四分之一左右。若后续不需要视频捕获,把 videoio 去掉还能再少一截。DNN 如果不涉及神经网络推理,坚决不纳入。 动态链接库太多也影响启动时间。产品最终形态可以把 OpenCV 与主程序一起静态链接,生成单个可执行文件,方便拷贝部署,但开发阶段保持动态库方式更舒服,出错时不必反复重编。我在验证阶段用动态库,出产品时再默认调成静态是相对平衡的方案。6.3 值得固化的移植流程
走过的弯路都折腾完后,建议把所有步骤沉淀成脚本,否则三个月后再升级版本,又要重新踩一遍。参考下列一个极简脚本结构,把主干留下来:#!/bin/bash export ROOTFS=/opt/arm-rootfs export TOOLCHAIN=arm-linux-gnueabihf export PKG_CONFIG_LIBDIR=${ROOTFS}/usr/lib/pkgconfig build_dep() { cd $1 && ./configure --prefix=${ROOTFS}/usr --host=${TOOLCHAIN} make -j4 && make install } build_dep zlib build_dep libpng build_dep libjpeg # 其余依赖类似 cmake -S opencv -B build-arm \ -DCMAKE_TOOLCHAIN_FILE=toolchain.arm.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_LIST=core,imgproc,imgcodecs,videoio,highgui \ -DWITH_V4L=ON -DWITH_GTK=OFF cmake --build build-arm -j4 make install DESTDIR=/tmp/opencv-arm-target再配合拉一页简单的记录,写明各依赖库的版本号、CMake选项、测试结果,以后在不同板子间复用摩目时会顺很多。我上一轮完全照着这套脚本从一个平台复刻到另一个相同架构的板子,除了工具链路径略改,几乎没有再花一个下午一起去调环境。如果你手头还有几块不同板子正等着部署,可以把这个流程当作继续往下走的模板。