做机器人这行的朋友应该都有过这种经历:拿到一块低功耗的板子,想让它跑 ROS,结果光是装环境就被存储和内存卡了个半死。我最近就在一颗 RK3308 上重新搭了一遍 ROS 运行环境,把它当作一个小型底盘的控制器,并且把编译工作全部搬到了 x86 主机上进行交叉编译。整个过程踩了不少坑,但也把跨架构移植的套路彻底理清了。这里把完整的流程和踩坑记录写出来,涵盖 rootfs/sysroot 准备、工具链选择、CMake 交叉文件、catkin 工作区处理,以及部署到 RK3308 之后的运行细节。做同类嵌入式 ROS 开发的朋友可以直接对照着操作。
1. 项目概述与整体技术路线
1.1 为什么在 RK3308 上跑 ROS 需要提前规划
RK3308 是一颗四核 Cortex-A35 的低功耗应用处理器,常见场合是语音交互、边缘摄像、简单工业控制,很少有朋友直接把它和 ROS 联想到一起。但用来跑轻量级 ROS 节点其实很合适,比如读取 IMU、收发串口、做简单的运动学换算。这类任务占用不高,板子长时间低功耗运行也没压力。
不过真要去跑rviz、点云可视化那套重负载,就得做好心理准备。ROS 本身不是单文件程序,它有roscpp、消息生成工具、Python 绑定等一系列依赖。对一个 512MB 内存的 RK3308 板子来说,常见的桌面版 Linux 配合完整 ROS 完全不合适;哪怕是精简的 ros-base,也要检查剩余内存和 flash。我给这块板子的规划是“底盘控制器”,只跑roscore和几个自己写的驱动节点。这和标题里的“运行环境搭建”很贴合:先有一个能常驻运行 ROS 的根目录,再去裁剪和交叉编译我们需要的软件。
1.2 板载编译与交叉编译怎么选
很多朋友第一反应是:直接在板子上apt installROS,然后catkin_make自己的工作区,这不是最自然的吗?如果编译的包很小,板载编译完全没问题;但一旦涉及 PCL、OpenCV 这类依赖,RK3308 的 CPU 会很痛苦,内存也不够。我试过在板子上编译一个带几个依赖的功能包,结果g++直接被 OOM 杀掉,连 swap 都没来得及反应,最后只能重启。
交叉编译的核心思路是:在 x86 主机上用目标架构的编译器编出 ARM64 二进制,再拷贝到 RK3308 上运行。难点不是“用哪个编译器”,而是“编译时看到的头文件和库必须和运行时一致”,这就是 sysroot 存在的意义。sysroot 等价于把目标板上的根文件系统复制一份到主机,让编译器在里面找/usr/include、/usr/lib的依赖。只要这一步做好,交叉编译的成功率会非常高。
1.3 我采用的整体技术链路
我的最终方案是混合式的:
- 基础 ROS 环境(
ros-noetic-ros-base)直接在目标系统的 rootfs 里安装,因为官方有 arm64 现成二进制; - 自研的传感器驱动、运动学算法这些 C++ 功能包,在 x86 主机上用
aarch64工具链交叉编译; - 编译时通过 toolchain 文件指定 sysroot,通过 pkg-config 的 cross 模式让 CMake 找到目标板上的 ROS 头文件;
- 生成的可执行文件连同
devel环境一起同步到板子,使用roslaunch/rosrun启动。
这样既能避免为整个 ROS 源码做交叉编译的折腾,又能保证所有自研代码复用主机的编译性能。下面按这个链路逐步展开。
2. 交叉编译环境准备:sysroot、工具链和 CMake 配置
2.1 先搞定目标板 rootfs
交叉编译第一步,是拿到一份和目标板软件环境基本一致的根文件系统。如果这是某个厂商 SDK 里带的板子,通常会附带 rootfs,可以直接拿它来做 sysroot。我手里这块板的 BSP 采用 Debian 风格根文件系统,所以我把目录同步到了主机上:
mkdir -p /opt/rk3308/sysroot rsync -av --exclude='proc' --exclude='sys' --exclude='dev' \ debian_rootfs/ /opt/rk3308/sysroot/这里要注意,不需要把整个 rootfs 全部塞进去,重点是/usr/include、/usr/lib、/usr/share、/lib这几个目录必须完整。如果 rootfs 是用debootstrap创建的,那更好,所有库文件都是齐的。
提示:不要图省事直接拷贝主机上的
/usr作为 sysroot。宿主机是 x86 的,.so和头文件版本都可能跟目标系统不一致,最后出来的文件要么链接失败,要么运行时报GLIBC_2.XX not found。sysroot 必须来源自目标架构的完整 rootfs。
2.2 工具链安装与环境变量
我用的是主线gcc-aarch64-linux-gnu和g++-aarch64-linux-gnu,在 x86 主机上直接 apt 安装:
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu如果 BSP 提供了自定义工具链,也建议先验证一下它使用的 glibc 版本。很多时候自定义工具链是把 glibc 的兼容层打包在 sysroot 里的,那样出问题的概率更低。但我用主线工具链也一切正常,只要链接时指定了--sysroot。
建议把下面的环境变量写到一个env.sh里,方便每次编译时 source:
export RK3308_SYSROOT=/opt/rk3308/sysroot export CROSS_PREFIX=aarch64-linux-gnu- export CC=${CROSS_PREFIX}gcc export CXX=${CROSS_PREFIX}g++ export LD=${CROSS_PREFIX}ld export AR=${CROSS_PREFIX}ar export RANLIB=${CROSS_PREFIX}ranlib export PKG_CONFIG_PATH= export PKG_CONFIG_LIBDIR=${RK3308_SYSROOT}/usr/lib/pkgconfig:${RK3308_SYSROOT}/usr/share/pkgconfig:${RK3308_SYSROOT}/opt/ros/noetic/lib/pkgconfigPKG_CONFIG_LIBDIR是 pkg-config 查找.pc文件的唯一路径。这里把 sysroot 里的/opt/ros/noetic也加进去,目的是让后续能找到roscpp自带的 pkg-config 信息。
2.3 CMake 工具链文件的编写
CMake 不会自动意识到你在交叉编译,必须提供 toolchain file。我的rk3308.toolchain.cmake内容如下:
set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) set(CMAKE_AR aarch64-linux-gnu-ar) set(CMAKE_RANLIB aarch64-linux-gnu-ranlib) set(CMAKE_SYSROOT $ENV{RK3308_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 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) set(CMAKE_EXE_LINKER_FLAGS_INIT "--sysroot=${CMAKE_SYSROOT}") set(CMAKE_SHARED_LINKER_FLAGS_INIT "--sysroot=${CMAKE_SYSROOT}") set(CMAKE_MODULE_LINKER_FLAGS_INIT "--sysroot=${CMAKE_SYSROOT}")其中CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER意味着查找可执行程序时不去 sysroot 里找,避免 CMake 查找python3时撞上 ARM 版 Python,然后直接拿它来执行代码生成脚本。后面的LIBRARY、INCLUDE、PACKAGE全部设为ONLY,能保证头文件和库只来自 sysroot,不会混入主机上的/usr/lib。
如果是普通 CMake 项目,编译时只需要:
cmake .. -DCMAKE_TOOLCHAIN_FILE=./rk3308.toolchain.cmake对于 catkin 工作区,我会在第 4 章给出完整命令。
2.4 为什么FIND_ROOT_PATH_MODE_PROGRAM要设成 NEVER
这个点值得单独说明。ROS 在编译过程中会调用很多 Python 脚本去生成消息代码,比如genmsg、genpy、gencpp。这些脚本执行时不受交叉编译影响,它们只需要在主机上有对应 Python 环境即可。但 CMake 为了找python3,默认会搜索CMAKE_FIND_ROOT_PATH。如果MODE_PROGRAM也设成ONLY,CMake 就能找到 sysroot 里的usr/bin/python3——那是面向 ARM 的二进制,在 x86 主机上根本跑不起来,编译直接报错。所以工具链里的NEVER是保证“编译期生成器跑在主机上,链接期目标文件面向板子”。
3. ROS 基础环境与目标板上的准备
3.1 安装 ros-base 而不是桌面完整版
在 RK3308 的最终系统里,我优先选择安装ros-noetic-ros-base,而不是desktop-full。一个干净的精简安装,包含roscpp、rospy、std_msgs、sensor_msgs、roslaunch这些,足够支撑绝大多数嵌入式节点。
sudo apt update sudo apt install ros-noetic-ros-base如果你的 rootfs 是在主机上用chroot方式操作,需要先挂载一些虚拟文件系统,否则 apt 可能无法联网或用不了 DNS:
sudo mount --bind /proc /opt/rk3308/sysroot/proc sudo mount --bind /dev /opt/rk3308/sysroot/dev sudo mount --bind /sys /opt/rk3308/sysroot/sys sudo chroot /opt/rk3308/sysroot /bin/bash装完后,确认/opt/ros/noetic/setup.bash存在,同时echo $ROS_DISTRO能输出版本。这一步很关键,因为后续交叉编译自研包时需要这个目录里的.so和头文件。
3.2 将 ROS 基础库纳入 sysroot 搜索路径
如果是在板子 rootfs 里直接装 ROS,那/opt/ros/noetic自然就在 sysroot 中。交叉编译时,CMake 的find_package(catkin)会到 sysroot 的/opt/ros/noetic/share中找 catkin 的 CMake 模块。只要CMAKE_SYSROOT设置正确,不需要额外手动复制。
但如果你的 sysroot 是从 SDK 拷贝出来的,里面没装 ROS,那就需要把目标板上/opt/ros/noetic整个目录同步到/opt/rk3308/sysroot/opt/ros/noetic,同样还要带上/usr/lib/aarch64-linux-gnu下所有.so。否则即使 toolchain 指向 sysroot,也可能在find_package(roscpp)的时候提示找不到。
注意:ROS 的
.pc文件在/opt/ros/noetic/lib/pkgconfig下,PKG_CONFIG_LIBDIR里必须包含这个目录。有些文章推荐把PKG_CONFIG_PATH指向主机/opt/ros/noetic,这会导致链接时把 x86 的libroscpp.so找出来。那种方法就算链接过了,把包拷到目标板上也跑不了。
3.3 使用 qemu 和 chroot 在宿主上安装 ROS 的备选方案
如果你的板子没有联网,又想得到一个“已装好 ROS 的 rootfs”,还有一个非常实用的备选方案:在 x86 主机上用qemu-user-static模拟 ARM,对 rootfs 直接执行chroot,然后在模拟环境中用 apt 安装 ROS。操作大致是:
sudo apt install qemu-user-static binfmt-support sudo cp /usr/bin/qemu-aarch64-static /opt/rk3308/sysroot/usr/bin/ sudo chroot /opt/rk3308/sysroot /bin/bash在这个模拟环境里,apt会按照目标架构 arm64 进行安装。装完后把qemu-aarch64-static从 sysroot 中删除,避免影响后续 sysroot 的干净程度。这个方式的优点是不依赖板子当前是否运行,缺点是模拟环境安装速度很慢,尤其 ROS 这种依赖极多的东西,可能要半小时以上。通常我还是推荐直接在板子上 apt 装,再通过rsync把/同步回主机作为 sysroot源。
4. 交叉编译自研 ROS 驱动和算法包
4.1 用 catkin_make 配合交叉工具链
自研包一般放在独立的工作区里。我的目录结构大致是:
catkin_ws/ src/ rk3308_chassis/ CMakeLists.txt package.xml src/ chassis_node.cpp rk3308.toolchain.cmake编译时不是直接无脑catkin_make,而是把 toolchain 文件路径传给 CMake。我试过下面这组参数,能稳定处理大多数情况:
cd catkin_ws catkin_make -DCMAKE_TOOLCHAIN_FILE=$(pwd)/rk3308.toolchain.cmake \ -DCMAKE_PREFIX_PATH=${RK3308_SYSROOT}/opt/ros/noetic \ -DCMAKE_BUILD_TYPE=Release因为已经让CMAKE_SYSROOT指向 sysroot,理论上-DCMAKE_PREFIX_PATH不用手动指定。但 catkin 内部的包搜索有时会额外受主机变量影响,显式声明CMAKE_PREFIX_PATH能降低“混到宿主 ROS”的概率。编译完成后检查生成的可执行文件:
file devel/lib/rk3308_chassis/chassis_node如果输出里有ELF 64-bit LSB executable, ARM aarch64而不是x86-64,说明架构对了。
4.2 消息生成与依赖问题
ROS 的消息生成脚本是从源码仓库里找到并执行的,在主机上有 Python 3 和genmsgPython 包即可。它们生成的是 C++/Python 文件,不参与目标板链接,所以没有架构问题。唯一要留意的是某些老教程依赖 Python 2 的genmsg,在新主机上会直接失败;Noetic 已经统一到 Python 3。
如果自研包里引用了roscpp、std_msgs、sensor_msgs等,CMake 的find_package会通过 sysroot 找到。问题往往出现在引用第三方库时,比如 PCL、OpenCV。这些库若在目标板是通过 apt 预装的,头文件已经躺在 sysroot 里,toolchain 能自动找到;若你是从源码构建的,则要单独指定安装前缀。
4.3 用 pkg-config 包装脚本解决.pc文件查找
为了让 autotools 或某些 CMake 项目正确找到 sysroot 里的依赖,可以写一个aarch64-pkg-config包装脚本:
#!/bin/bash export PKG_CONFIG_LIBDIR=${RK3308_SYSROOT}/usr/lib/pkgconfig:${RK3308_SYSROOT}/usr/share/pkgconfig:${RK3308_SYSROOT}/opt/ros/noetic/lib/pkgconfig export PKG_CONFIG_PATH= exec /usr/bin/pkg-config "$@"记得给脚本加可执行权限,然后在编译第三方库时用它替代系统pkg-config:
./configure --host=aarch64-linux-gnu \ PKG_CONFIG=/path/to/aarch64-pkg-config \ --prefix=${RK3308_SYSROOT}/usr这部分对自研 ROS 包不一定必需,但如果板子上部署的 ROS 节点还要用到 Boost、OpenSSL 或自己写的 C 库,这个包装脚本能省去很多环境变量上的折腾。
4.4 交叉编译时也要注意编译选项
针对 RK3308 的 Cortex-A35 核心,我建议在 CMake 里加上:
add_compile_options(-march=armv8-a -mtune=cortex-a35 -O2)没有-march时,编译器默认按基础 ARMv8 指令集生成,功能没问题,但性能会打折扣。加上-mtune=cortex-a35之后,无论是系统库还是自研节点,性能都能压榨到比较理想的状态。对于传感器预处理节点,开启-ffast-math前要慎重,它会在某些边界情况下改变数值比较逻辑,可能导致滤波器不稳定。我一般只用-O2 -march=armv8-a,不做激进优化。
5. 部署到 RK3308 并让 ROS 节点跑起来
5.1 把编译产物同步到目标板
交叉编译完成后,部署不是把devel整个目录拷过去就完事,也不是只拷一个可执行文件。一个稳妥的做法是同步工作区里的devel和src:
rsync -av ./devel/ target@<board_ip>:~/catkin_ws/devel/ rsync -av ./src/ target@<board_ip>:~/catkin_ws/src/关键在于主机和板子上这个工作区的相对路径要保持一致,因为setup.bash会根据自身所在路径计算 ROS 环境前缀。如果路径变了,轻则source报错,重则节点启动时找不到catkin_generated里的配置。同步之后在板上执行:
source /opt/ros/noetic/setup.bash source ~/catkin_ws/devel/setup.bash这里不要试图在板上重新catkin_make。交叉编译缓存和板载原生 CMake 缓存不一致,一旦触发重新配置,大概率会把整个工作区按本机架构重新编译一遍,耗时巨大。直接使用同步过去的devel是最快的。
5.2 正确初始化 ROS 环境
在 RK3308 上,开机后建议把下面的内容写入.bashrc:
source /opt/ros/noetic/setup.bash source ~/catkin_ws/devel/setup.bash export ROS_MASTER_URI=http://localhost:11311 export ROS_HOSTNAME=localhost如果机器人的 ROS Master 运行在另一台主控上,ROS_MASTER_URI就要指到那台机器的 IP,比如http://192.168.1.100:11311。这里的ROS_HOSTNAME如果只是单机运行,设成localhost最省事;如果 ROS 节点要和其他机器通信,建议设成 RK3308 的内网 IP,否则roscore可能只监听回环地址,出现“节点拿不到 master 数据”的奇怪现象。
5.3 冒烟测试
部署完成后先做一个小测试:
source /opt/ros/noetic/setup.bash source ~/catkin_ws/devel/setup.bash roscore & rosrun rk3308_chassis chassis_node如果启动正常,能在rosnode list中看到节点。再跑一个rostopic echo /chassis/status验证消息是否能持续发布。如果出现error while loading shared libraries,先用ldd chassis_node检查缺失的.so,绝大多数情况是因为 sysroot 里没有同步某个库,或者把主机 ROS 的链接路径带到了 sysroot 之外。
6. 常见问题与排查技巧实录
6.1 编译时报cannot find -lroscpp怎么办
这个报错最常见的两个原因:第一,CMAKE_SYSROOT没有设置正确,导致find_path/find_library找不到/opt/ros/noetic/lib;第二,ROS 库虽然是.so文件,但内部还有符号链接,rsync同步时如果没有保留符号链接,libroscpp.so的软链就会断掉。用ls -l /opt/rk3308/sysroot/opt/ros/noetic/lib/libroscpp.so查看,如果是红色断链,说明源 rootfs 里的链接关系没保留。重新用cp -a或者保留链接的rsync -avH同步一次。
6.2 链接成功后板载运行报GLIBC_2.32 not found
这是典型的“宿主机链接到了宿主 glibc”症状。比如编译时没有加--sysroot=/opt/rk3308/sysroot,而是在/usr/lib/x86_64-linux-gnu里找到了 glibc,生成的二进制就会携带宿主机 glibc 版本要求。解决方法是回到编译目录,彻底清理缓存后重新用 toolchain 文件配置。注意有些时候make -j没有重跑 CMake,链接器仍沿用旧参数,所以一定先rm -rf CMakeCache.txt CMakeFiles再重编。
6.3 qemu 与 binfmt 的坑
在 x86 主机上 chroot 安装 ROS 时,如果发现apt安装的都是 x86 包,先检查目标 rootfs 的架构是否真是aarch64。有些 SDK 的 rootfs 其实是 32 位armhf,对应要拷贝qemu-arm-static,工具链前缀也要改成arm-linux-gnueabihf-。我第一次操作就把 64 位 rootfs 和 32 位工具链混用了,结果编译器报 “selected processor does not support mode” 一类的错误。确认架构最简单的方式是看根文件系统里有没有/lib/ld-linux-aarch64.so.1,有就是 64 位;如果只有/lib/ld-linux-armhf.so.3,就是 32 位。
6.4 catkin_make 跑得太慢或者被杀
如果某个 ROS 包装了 OpenCV 这类重型依赖,交叉编译时耗时依然不短。建议把make -j并发数调低一点。其实交叉编译还是 x86 主机在做,-j8通常没问题;但后台跑着容器或虚拟机,内存不够时也会有交叉编译进程被 OOM 杀掉。更常见的问题是 CMake 配置阶段反复试探 sysroot 里的库,如果添加了一堆-D参数让配置反复变化,每次都会重新探测。最好一次把参数写全,不要一条命令多跑几遍。
6.5 ROS 参数服务器上的时间同步问题
嵌入式板卡的时钟经常不准,ROS 节点启动后如果发现tf时间跟不上,先检查板子是否启用了 NTP。我遇到过一次ros::Time::now()经常跳变,后来发现是时间同步服务没启用。这个问题和交叉编译无关,但很容易在使用过程中被误认为是“驱动节点编坏了”。搭环境时顺手把时间同步配置好,能省掉很多后续调试。
7. 善后与长期维护心得
7.1 最容易忽略的三个维护点
第一,不要把“交叉编译”和“部署”解耦开。交叉编译只是产出目标板上的二进制,最终验证永远在目标板上。所以每次改完代码,我都会用rsync增量同步到板上,在板上跑一个快速回归测试,而不是只看主机编译结果。
第二,工具链和 sysroot 里库的版本一定要对齐。很多朋友拿到一个 prebuilt 的 ARM 库,直接放进 sysroot,结果编译时符号对不上。最简单的做法是用一套完整 rootfs,不要东拼西凑“最小 sysroot”。完整 rootfs 能保证/usr/lib下的符号依赖一致,也不会破坏.so之间的软链接关系。
第三,RK3308 不适合跑桌面级 ROS 开发环境,但作为底盘控制器、传感器网关这类固定任务节点很稳。我最后把roscore放在板子上,自研节点全部做成开机自启,并控制采样周期。整机功耗和稳定性都比较满意。
7.2 这套环境后续还能怎么扩展
如果后续有更多算力需求,可以考虑把重负载节点移到上层主控上,RK3308 只保留实时性要求高的轻量节点。或者把 ROS 通信中间层换成更轻量的本地总线,只在边界处与 ROS Master 对接。这套交叉编译框架本身不做变化,新增驱动、新增算法包时只需要在 catkin 工作区里继续编进去,再同步到板子上即可。这也是在资源受限设备上部署 ROS 的一条比较清晰的长期演进路线。