简介这份源码资源完整呈现了一台基于Jetson Orin NX(8GB)与STM32协同控制的移动机器人项目面向正在入门ROS机器人开发、嵌入式运动控制或激光雷达与视觉融合应用的开发者。包内包含ROS工作空间、STM32控制逻辑、机械臂与车身运动控制相关代码以及思岚C1激光雷达与奥比中光Gemini相机的驱动配置与启动文件能帮助读者理解从底层驱动到上层感知的完整工程结构。资源包共1884个文件以make与cmake构建脚本为主辅以头文件、launch启动脚本、cpp与py源码并包含srv/msg自定义通信接口便于按模块研读和部署。压缩包仅2.39MB版本简洁适合在Jetson平台快速参考。目前已有1309人学习下载适合具备一定Linux与ROS基础、希望参考一套多传感器融合实车源码来搭建或改进自己机器人项目的开发者。1. 双芯片机器人源码包Jetson Orin NX 和 STM32 是怎么各司其职的第一次用这套源码包搭移动机器人我把电机 PID 直接放在 Jetson 上跑视觉模型一推理速度指令就开始抽风轮胎的转速听起来像在打拍子。后来把底盘控制全部下放给 STM32Jetson Orin NX8GB只干感知和规划问题才彻底消失。这套源码包就是按这个分工设计的Jetson 端接管 Gemini 深度相机和激光雷达的驱动跑 SLAM、障碍物检测与全局路径规划STM32 端负责电机速度环、编码器采集、急停和电量监控两边通过串口或 CAN 交换速度指令与状态反馈。适合正在做室内移动底盘、巡检机器人、服务机器人的开发者。新手可以照着章节把系统串起来老手则能直接参考协议帧和线程结构省掉从零写底层驱动的时间。它不是什么黑匣子而是把一台机器人的“脑子”和“小脑”拆开各自干最擅长的事。2. 硬件协同与选型为什么是“Jetson 算STM32 控”选型不是拍脑袋。移动机器人里感知和运动控制对计算平台的要求完全相反感知需要大算力但能容忍几十毫秒抖动运动控制需要硬实时但计算量不大。用一块 Jetson 跑 Linux 做全部事情会遇到调度延迟和中断响应不确定的问题。源码包把两者拆开本质上是把一个实时性问题拆成“算力不够”和“实时性不够”两个独立问题分别用不同芯片解决。2.1 双芯片分工的依据算力、实时性与功耗的权衡Jetson Orin NX 8GB 在嵌入式平台里算力并不弱跑轻量级 YOLO 模型、运行 2D SLAM 都没有压力。但它是 Linux 环境GPU 调度、内存交换、后台进程都可能造成毫秒级抖动。电机速度环需要确定的 1kHz 控制周期一旦某个周期被延迟电流和转速就会出现毛刺。而 STM32 这类微控制器用硬件定时器中断可以保证周期误差在微秒级所以分工的第一条原则是不是看“能不能跑”而是看“能不能按时跑”。我一般把任务分成两类源码包也是按这个思路组织的。功能模块归属芯片原因Gemini 深度图、点云生成Jetson依赖 Linux CUDA SDK激光雷达驱动、SLAM、代价地图Jetson需要 ROS 生态和大内存路径规划、速度指令发布Jetson与传感器数据耦合紧密电机速度环、位置环STM32需要 1kHz 硬实时控制编码器倍频解算STM32中断频率高、定时器精度要求高急停、限位开关、电量监控STM32必须独立于 Linux 运行功耗也是现实约束。Jetson 模块跑起视觉推理整机功耗经常到几十瓦STM32 加电机驱动板待机只有几十毫瓦。做电池供电的巡检机器人时让 STM32 负责睡眠唤醒和低功耗巡检Jetson 深度睡眠续航能明显提升。源码包里虽然没有把整个电源管理做完但底层接口已经留好了。2.2 激光雷达与 Gemini 深度相机的数据接入方式常见的传感器接法是激光雷达和 Gemini 深度相机都接 JetsonSTM32 不直接读传感器数据。原因很简单避障算法需要把激光点云和深度点云放在同一时间戳下融合数据都走 Jetson才能用消息同步机制对齐。STM32 只接收 Jetson 下发的最终速度指令这样底层不会被传感器数据淹没。室内移动机器人常用单线激光雷达大部分通过 USB 转串口出数据ROS 驱动会发布 /scan 话题。Gemini 深度相机走 USB 3.0输出 RGB、深度图和点云这类相机提供的 SDK 依赖 Linux 和 CUDA所以把它接在 Jetson 上是唯一合理选择。这里有一个容易忽略的带宽问题Jetson 的 USB 控制器虽然支持 USB 3.0但同时挂载深相机、激光雷达、4G 模块时带宽会争抢。我的习惯是 Gemini 相机独占一路 USB Controller激光雷达挂在另一路避免同时大量传输时出现深度帧掉帧。源码包的设备启动脚本里会做 USB 端口绑定就是为了减少这种软性丢帧。2.3 电源与通信12V 供电、串口选择和共地问题电源设计是整个系统最容易翻车的地方。Jetson Orin NX 载板一般需要 DC 12V 供电STM32 开发板常用 5V激光雷达和 Gemini 相机也吃 5V。如果全从一个开关电源拉电要注意瞬态电流Gemini 相机 USB 供电在启动瞬间可能超过 1AUSB 口压降一大就直接掉线。所以我的做法是给相机独立供电数据线只传信号不传电源。Jetson 与 STM32 之间常用串口或 CAN。UART 接线少、协议简单波特率 115200 或 460800 足够传递速度指令CAN 抗干扰强、支持多节点适合底盘带多个电机控制板的情况。无论选哪种通信线都要尽量远离电机动力线否则电机电流突变时容易出现误码。还有一个必须注意的点Jetson 的 GND 和 STM32 的 GND 必须共地。有些人只看 RX/TX 两根线结果串口乱码最后发现是两个板子的地电平不一致加了根地线就正常了。源码包的协议设计里带了校验字段也是为了抵抗这些噪声带来的偶发错帧。3. 源码结构拆解从“一个包”到“能跑起来”的逻辑拿到源码包先别急着编译看清楚目录划分再动手。源码包不是把所有代码堆在一起而是按“感知、决策、底层控制”三条线分开。理解目录结构能让你知道改哪个文件、启动哪个节点以及遇到问题时该去哪里查日志。3.1 目录划分每个文件夹解决什么问题解压后是典型的工作空间布局我用一个简化版本说明robot_ws/ ├── jetson_nx/ # 运行在 Jetson 上的 ROS 功能包 │ ├── perception/ # Gemini 相机与激光雷达驱动封装 │ ├── navigation/ # SLAM、代价地图、路径规划 │ ├── decision/ # 避障策略与目标点管理 │ └── serial_bridge/ # 与 STM32 通信的串口/ CAN 桥接节点 ├── stm32_firmware/ # STM32 裸机工程 │ ├── Core/ # 主循环、中断回调 │ ├── Drivers/ # HAL 库与底层驱动 │ └── App/ # 电机 PID、协议解析、急停逻辑 └── scripts/ # 设备权限、开机自启、录包脚本perception 和 navigation 分开是有原因的。感知模块只管把雷达 /scan 和 Gemini 的深度图转成可供决策的障碍信息navigation 只消费处理后的数据不关心 Gemini 的 SDK 怎么安装。将来想换一款相机或者加一个超声波传感器只改 perception 就够了。serial_bridge 是 Jetson 端连接 STM32 的唯一通道它订阅 /cmd_vel把线速度和角速度打包成二进制帧通过串口发给 STM32。这样做的好处是上层程序不需要关心底层协议STM32 也不需要在 ROS 环境里跑。3.2 核心数据流从传感器到电机命令整个系统的数据流可以概括成一条链激光雷达和 Gemini → 障碍物检测 → 决策节点 → /cmd_vel → serial_bridge → STM32 → 电机。下面这段代码可以理解决策节点的工作逻辑#!/usr/bin/env python3 # 决策节点订阅 /scan计算最近障碍距离输出速度指令 import rospy from sensor_msgs.msg import LaserScan from geometry_msgs.msg import Twist pub rospy.Publisher(/cmd_vel, Twist, queue_size1) def scan_cb(msg): tw Twist() valid_ranges [r for r in msg.ranges if 0.1 r 3.5] if not valid_ranges: rospy.logwarn(激光雷达有效范围为空保持停止) pub.publish(tw) return min_dist min(valid_ranges) if min_dist 0.5: tw.linear.x 0.0 tw.angular.z 0.5 # 原地向右转 else: tw.linear.x 0.3 tw.angular.z 0.0 pub.publish(tw) rospy.init_node(decision) rospy.Subscriber(/scan, LaserScan, scan_cb) rospy.spin()这段代码的关键在于queue_size1。发布 /cmd_vel 时如果队列太长旧指令会堆积STM32 拿到的是迟到的速度导致电机反应迟钝。设为 1 表示只保留最新指令丢掉没来得及处理的旧指令这是低速移动机器人最常用的策略。实际源码包里的决策节点会更复杂可能会融合 Gemini 点云生成 3D 障碍栅格但数据流主线就是这个链路。serial_bridge 把 /cmd_vel 转成下面的帧格式再发送给 STM32typedef struct __attribute__((packed)) { uint8_t head; // 帧头 0xAA 0x55 uint8_t id; // 0x01 表示速度指令 int16_t linear_x; // 线速度单位 mm/s int16_t angular_z; // 角速度单位 0.01 rad/s uint8_t checksum; // 累加和校验 } SpeedFrame;用 int16_t 传速度而不是浮点是因为 STM32 端解析整数快也不需要引入浮点库。线速度单位是 mm/s角速度单位是 0.01 rad/s这样可以保留一位小数精度又避开浮点误差。校验和字段必须保留电机噪声和地线波动产生的偶发错帧靠它才能被识别丢弃。3.3 关键参数表波特率、坐标系与速度上限移植到自己的机器人时最优先检查的是下面这张表参数几乎都在配置文件中能找到参数名默认值说明serial_port/dev/ttyTHS1Jetson 与 STM32 的串口设备号baud_rate460800串口波特率与 STM32 端保持一致scan_frame_idlaser_link雷达坐标系camera_frame_idgemini_link深度相机坐标系base_frame_idbase_link机器人底盘坐标系max_linear_speed0.5 m/s线速度上限max_angular_speed1.0 rad/s角速度上限pid_rate1000 HzSTM32 速度环频率坐标系是新手最容易栽的地方。laser_link、gemini_link、base_link 三者之间的 TF 变换必须和真实物理安装位置一致否则导航模块以为自己看到了墙实际上雷达看的是自己的支架。源码包给了一个 tf2 静态变换发布脚本改了安装位置以后记得同步改这里的数值。4. 从零跑通环境配置、编译与联调步骤源码包能不能跑起来取决于环境是否对齐。Jetson 端需要 ROS 环境、相机 SDK、雷达驱动STM32 端需要编译链和烧录工具。下面按“先 Jetson再 STM32最后联调”的顺序讲每一步都有验证方法。4.1 Jetson 端环境刷系统、安装 ROS 与编译工作空间拿到 Jetson 模块以后先刷对应版本的 Ubuntu 镜像。刷机工具会预装 Linux 内核、CUDA 和 TensorRT不需要手动装 NVIDIA 驱动这一步能省掉很多麻烦。刷完系统以后安装 ROS 基础环境# 安装 ROS 基础组件这里以 ROS2 的某个 LTS 版本为例 # 具体版本号请和源码包 README 保持一致 sudo apt update sudo apt install -y ros-${ROS_DISTRO}-desktop sudo apt install -y ros-${ROS_DISTRO}-serial sudo apt install -y ros-${ROS_DISTRO}-tf2 # 创建工作空间并编译源码包 cd ~/robot_ws rosdep install --from-paths src --ignore-src -r -y colcon build --symlink-install source install/setup.bash${ROS_DISTRO}是环境变量实际使用时需要根据安装的 ROS 版本替换比如 humble 或 foxy。--symlink-install表示用符号链接方式安装改 Python 脚本不用重新编译调试阶段特别有用。rosdep 会自动拉取未知依赖但国内网络环境下偶尔会卡住遇到超时可以手动安装对应包我后面会讲一个和权限相关的坑。4.2 STM32 端固件工程用 STM32CubeMX 生成并烧录STM32 端的工程建议从源码包里提供的 CubeMX 配置开始不要自己重新建工程。用 STM32CubeMX 打开 ioc 文件检查 UART 引脚是否和 Jetson 串口对应然后重新生成代码。生成后编译用 ST-Link 烧录# 单独编译固件输出 bin 文件 make -j4 # 通过 ST-Link 烧录到 Flash 起始地址 st-flash --connect-under-reset write build/firmware.bin 0x8000000如果手边没有 ST-Link也可以直接用串口 DFU 烧录但需要先把 BOOT0 引脚拉高速度比较慢。烧录完成后STM32 端的板载 LED 常亮代表主循环正常如果 LED 闪烁则可能是协议解析错误需要通过串口回环检查接线。4.3 雷达与 Gemini 相机驱动验证先看 topic再测数据启动驱动的先后顺序有讲究。先启动相机和雷达再启动导航最后再开启 serial_bridge。这样节点启动时就能拿到传感器数据不会因为 /cmd_vel 无人订阅而报警。# 查看 USB 设备是否识别到 Gemini 和雷达转接器 lsusb # 启动 Gemini 相机 ROS 驱动 roslaunch gemini_camera gemini_node.launch # 启动激光雷达驱动具体包名以源码包 README 为准 roslaunch lidar_lib lidar_node.launch # 分别检查话题发布频率 rostopic hz /scan /camera/depth/image_rawrostopic hz输出每秒消息数雷达一般在 10~15HzGemini 深度图根据分辨率不同可能有 15~30Hz。如果某个话题频率是 0说明驱动没正常起来先去看对应节点的日志不要急着调导航参数。lsusb是最直观的设备排查手段确认设备是否被系统识别。5. 联调避坑时间同步、串口丢包与权限问题这一章是实车调试遇到过的“血泪经验”。下面这些坑看起来都很小但每一个都可能导致整台机器人原地罢工。我把它们按“现象 → 原因 → 解决”写成记录方便直接对号入座。5.1 高频踩坑记录现象 → 原因 → 解决现象 1串口发给 STM32 的速度指令时断时续电机一顿一顿地转。 原因Jetson 端串口缓冲区溢出STM32 端没有做环形缓冲单字节中断又丢失数据。 解决STM32 端切换到 UART DMA IDLE 不定长接收用空闲中断判断一帧结束Jetson 端提高串口 buffer 大小并校验帧头帧尾丢弃错帧。现象 2Gemini 深度相机在 USB 3.0 下运行一段时间后掉线重启节点能恢复。 原因USB 供电不足或者 Jetson 的 USB 电源管理自动挂起设备。 解决给 Gemini 独立 5V 供电并在系统里禁用 USB 自动挂起。# 禁用 USB 自动挂起将下面配置写入 /etc/rc.local 或 systemd 服务 echo on /sys/bus/usb/devices/1-1/power/control现象 3激光雷达和深度相机融合后障碍物边缘出现明显重影避障时频繁急停。 原因两个传感器数据帧的时间戳不一致导航节点拿到的是“错位的世界”。 解决用 message_filters 做时间同步。如果时间差异大于 20ms则需要考虑硬件触发或者调整发布频率。现象 4STM32 的 PID 频率调到 1kHz 后急停按钮反应很迟钝有时候撞到墙才停下。 原因PID 放在 main loop 的 while 循环里急停逻辑也要等循环跑完才能执行。 解决把急停用外部中断实现并设为 STM32 最高抢占优先级PID 放在定时器中断里优先级低于急停。现象 5设备每次重启以后激光雷达的串口设备号从 /dev/ttyUSB0 漂到 /dev/ttyUSB1启动脚本找不到设备。 原因linux 按枚举顺序分配设备名换个 USB 口或者启动顺序变了名字就变了。 解决通过 udev 规则绑定设备 vendor/product ID固定符号链接到 /dev/lidar脚本里只引用这个固定路径。5.2 硬件时序与中断优先级为什么急停必须最高STM32 端的中断优先级设置直接影响安全。这里给出典型的优先等级配置// 急停外部中断抢占优先级最高 HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0); // 电机 PID 定时器中断比急停低一级 HAL_NVIC_SetPriority(TIM6_IRQn, 1, 0); // 串口 DMA 空闲中断优先级最低 HAL_NVIC_SetPriority(USART3_IRQn, 2, 0);急停不能放在主循环里用if (GPIO_ReadPin())来判断因为在 while 执行其他阻塞代码时灾难已经发生了。必须用硬件外部中断才能保证任何时刻都能打断当前任务。PID 定时器中断虽然也有实时要求但低于急停。这个优先级顺序写进源码包里不建议为了 PID 响应速度去抢占急停的优先级。5.3 软件层面的日志与诊断工具系统出问题时第一步不是改代码而是看日志。Jetson 端我常用下面几条命令定位# 检查 USB 设备是否异常断开 dmesg | grep -i usb\|gemini # 查看关键话题是否持续发布 rostopic hz /scan /camera/depth/image_raw # 查看 Jetson 的 CPU/GPU 占用排除算力瓶颈 sudo jtopdmesg 能看到 USB 设备的反复插拔记录往往能直接定位相机掉线。rostopic hz能确认是驱动问题还是设备问题。jtop 可以看 GPU 和 CPU 占用如果决策节点和大模型同时跑导致 GPU 满载也会让导航降频看起来像电机反应慢。把这些日志按时间对比基本能判断问题出在感知、决策还是底层控制。6. 进阶技巧用 rosbag 回放代替实车调试先把参数调明白实车调试最烦的是每次改一个参数都要重新跑一圈耗时又危险。我的习惯是把传感器原始数据录下来回到实验室用 rosbag 回放调算法等参数基本收敛再上车。这个方法能省掉一大半实车调试时间。先在实车开一圈同时录制必要的话题# 记录雷达、深度点云、里程计和速度指令 rosbag record -O indoor_lidar.bag \ /scan \ /camera/depth/image_raw \ /camera/depth/points \ /odom \ /cmd_vel录制时带上 /cmd_vel这样回放时能看到当时的控制指令。回到实验室回放时不要发布 /cmd_vel否则机器人会以为自己已经被控制了# 只回放传感器和里程计不发布 /cmd_vel rosbag play --pause \ --topics /scan /camera/depth/image_raw /camera/depth/points /odom \ indoor_lidar.bag # 打开 RViz观察代价地图和路径变化 roslaunch rviz rviz暂停播放每改一个导航参数后再继续播放几秒立刻能看到路径和代价地图形状变化。以前调局部代价地图的膨胀半径实车跑了两个多小时没试对回放时用这个方法二十分钟就确定阈值膨胀半径太小机器人贴着墙走太大狭窄通道直接封死。从那以后我每次动控制参数都强制走一遍“先录后放”的流程调出来再实车验证。需要注意rosbag 回放解决的是感知和规划问题电机响应延迟、轮胎打滑这类问题还是要实车测。回放时也没有真实碰撞风险参数可以大胆试这也是我推荐它的原因。希望这个习惯能帮你在联调时少走几步弯路。本文还有配套的精品资源点击获取