首页 / 资讯中心 / 文章详情

micro-ROS打通六轴机械臂ROS 2通信:关节状态与RViz可视化实践

micro-ROS打通六轴机械臂ROS 2通信:关节状态与RViz可视化实践 ★ FEATURED ARTICLE
如果你也在做一台六轴机械臂并且想让它真正接入 ROS 2 生态那么 micro-ROS 是你绕不开的关键一环。最近我把一台自制六轴机械臂从单片机裸机控制升级成ROS 2 可感知状态核心就是打通关节状态通信并让 RViz 里的模型按真实角度动起来。这篇文章不聊理论书就聊我怎么把 micro-ROS 跑通、把 /joint_states 话题传上来、再让 RViz 认账的全过程适合正在做嵌入式机械臂但第一次碰 ROS 2 的朋友。先说结论整体链路并不复杂但如果没有理解 micro-ROS 与 ROS 2 的边界、QoS 匹配和 TF 需求很容易卡在数据明明在发RViz 里却纹丝不动的怪圈里。下面我按照实际项目的推进顺序展开从选型、固件、桥接、可视化到排坑全程给可抄的作业。1. 为什么选择 micro-ROS6轴机械臂关节状态上云的最后一公里1.1 关节状态通信在整个系统里的角色一台 6 轴机械臂不管你是用舵机还是步进电机最终都要回答一个问题当前每个关节到底转到了哪个角度在 ROS 2 的世界里标准答案是发布sensor_msgs/msg/JointState消息。这个消息包含了name、position、velocity、effort四个数组分别对应关节名、当前角度弧度、速度、力矩。只有把关节状态发出来上层才有机会做三件事第一RViz 里实时还原机械臂姿态第二状态机或安全逻辑能判断当前动作是否合法第三MoveIt 这类运动规划库才能拿到真实反馈做闭环。没有关节状态机械臂就是一个盲执行器。问题在于大多数机械臂的底层还是一个单片机可能是 STM32、ESP32或者更小众的微控制器。而 ROS 2 的完整运行时依赖 Linux、DDS 实现和大量库直接塞进微控制器显然不现实。于是 micro-ROS 出现了。它把 ROS 2 的发布、订阅、服务、action 等通信能力做了嵌入式裁剪运行在单片机上的 micro-ROS client 通过串口、Wi-Fi 或以太网与主机侧的 micro-ROS agent 通信agent 再作为桥接节点把数据转成标准 ROS 2 话题。简单说micro-ROS 就是让单片机接入 ROS 2 的末梢神经。1.2 micro-ROS 与 ROS 2 的分工很多第一次接触的人会混淆micro-ROS 是 ROS 2 的迷你版吗不完全。它的核心不是把 ROS 2 的所有功能塞进单片机而是提供一棵够用的通信树。我方侧是单片机上运行 micro-ROS client它只负责组织好消息并发送主机侧运行 micro-ROS agent负责把 client 传来的数据翻译成标准 DDS 通信让普通 ROS 2 节点订阅。从系统架构看大致是6 个关节的电机和编码器由单片机直接读取。单片机按固定周期计算关节角度、速度填入 JointState 消息。micro-ROS client 将消息通过串口发出去。主机上的 micro-ROS agent 把串口数据桥接为 ROS 2 话题 /joint_states。其他节点如 robot_state_publisher订阅该话题更新 TFRViz 读取模型和 TF 可视化。这个分工非常重要不要在单片机里做复杂计算也不要试图在单片机里运行 RViz。我见过有人想用一块开发板跑全部功能结果内存吃紧、调度紊乱。正确的做法是各司其职单片机关心实时采集主机关心可视化和规划。2. 开发环境与固件侧准备从电机驱动到关节角度计算2.1 选型与开发环境ESP32 还是 STM32我首选 ESP32原因很现实micro-ROS 针对 ESP32 的 Arduino 库很成熟支持 WiFi 和 UART 两种传输方式调试时用串口最简单。而且 ESP32 性能足够同时读取 6 路编码器信号并跑 micro-ROS 发布线程。如果你手里是 STM32 也没问题可以走 micro-ROS 的 CMake 串口方式。我用过的经验是如果只是想快速验证关节状态发布ESP32 Arduino micro-ROS 库的上手速度最快如果后续要上实时电机控制STM32 的定时器和 PWM 精度更可控micro-ROS 同样可以挂在后台跑。开发环境方面主机我装的是 Ubuntu 22.04 ROS 2 Humble。微控制器一侧我用 PlatformIO 管理 ESP32 工程依赖项中加入micro_ros_arduino库。记得先把Tools Board ESP32 Dev Module选好USB 驱动装好比如 CP2102 或 CH340 的驱动。这一步听起来简单但我见过不少人卡在端口找不到上其实只是没装驱动。2.2 固件代码发布 /joint_states 的最小实现先给一个最小可用的固件思路不追求完美先打通链路。核心步骤是初始化 micro-ROS、创建发布者、周期性填充 JointState 消息。代码如下简化版#include micro_ros_arduino.h #include rcl/rcl.h #include rclc/rclc.h #include sensor_msgs/msg/joint_state.h rcl_publisher_t joint_pub; rclc_executor_t executor; rclc_support_t support; sensor_msgs__msg__JointState msg; const char *joint_names[6] {joint1, joint2, joint3, joint4, joint5, joint6}; void setup() { // 串口通信参数注意与 micro-ROS agent 启动参数一致 Serial.begin(115200); set_microros_serial_transports(Serial); rcl_allocator_t allocator rcl_get_default_allocator(); rclc_support_init(support, 0, nullptr, allocator); rclc_init_node(support, arm_controller_node, , arm); rclc_publisher_init_best_effort( joint_pub, support.node, ROSIDL_GET_MSG_TYPE_SUPPORT(sensor_msgs, msg, JointState), /joint_states); msg.name.data (char **)joint_names; msg.name.capacity 6; msg.name.size 6; msg.position.data (double *)malloc(6 * sizeof(double)); msg.position.capacity 6; msg.position.size 6; msg.velocity.data (double *)malloc(6 * sizeof(double)); msg.velocity.capacity 6; msg.velocity.size 6; msg.effort.data (double *)malloc(6 * sizeof(double)); msg.effort.capacity 6; msg.effort.size 6; } void loop() { // 假设 readJointAngles() 返回 6 个关节的弧度值 readJointAngles(msg.position.data); // 可选用相邻周期差除以时间算速度 computeJointVelocity(msg.velocity.data); // 力矩如果暂时没数据至少填 0 for (uint8_t i 0; i 6; i) msg.effort.data[i] 0.0; msg.header.stamp.sec 0; msg.header.stamp.nanosec 0; rclc_executor_spin_some(executor, 100); rcl_publish(joint_pub, msg, nullptr); delay(20); // 20ms即 50Hz 发布 }这段代码里我特意把发布者初始化成了best_effort是为了避免后面第 5 节讲的那个 QoS 坑。如果你第一次跑没收到数据先检查这里。实际项目中readJointAngles()要根据你的电机类型实现。如果用的是带霍尔编码器的直流减速电机需要读取正交编码器计数乘以每圈脉冲数和减速比得到角度。若是步进电机则按步数换算。但无论如何最终单位必须是弧度。2.3 编码器角度换算与坐标系约定在写换算之前必须先定坐标系的零位和正方向。我犯过的错误是单纯把编码器读到的最低值当作 0导致机械臂在 RViz 里永远不在合理的初始姿态。正确做法先把每个关节手动调整到机械零位记录此时编码器计数。然后在程序里用当前计数减去零位计数再乘以2 * PI / (PPR * gear_ratio)得到弧度。注意如果编码器正转时计数增加那么正方向才是正角度否则需要取反。另外JointState 里的关节顺序要和你后续 URDF 里的关节顺序保持一致。我先规定joint1到joint6从底座到末端依次排列固件里也按这个顺序填充。很多可视化问题其实不是数据没到而是顺序错了导致每个关节张冠李戴。3. 主机侧 bridge 配置与话题数据核对3.1 启动 micro-ROS Agent 的两种方式micro-ROS agent 是连接单片机与 ROS 2 的桥。启动方式取决于你的传输方式。如果是串口命令很直接ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200注意-b 115200要和 ESP32 固件里Serial.begin(115200)一致。我曾经一会儿用 115200一会儿用 921600结果 agent 启动时静态信息能收到动态话题数据却断断续续排查了半天。如果用的是 WiFi 传输agent 命令会变成ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888单片机端要配置好 WiFi SSID、密码和 agent 的 IP、端口。调试阶段我更推荐串口没网络干扰抓包也简单。启动 agent 后它会打印发现的 micro-ROS client 信息。此时在另一个终端执行ros2 topic list应该能看到/joint_states、/rosout等话题。如果此时做了第 2 节的固件正常发布话题列表里出现/joint_states就说明桥已经通了。3.2 用 ros2 topic 诊断关节数据是否正确看到话题不等于数据正确。我会按顺序跑三条命令ros2 topic echo /joint_states --once ros2 topic info /joint_states -v ros2 topic hz /joint_statesecho --once用于看单条消息内容检查name数组长度和顺序、position是否在合理范围。比如初始状态各关节应该是 0.0 左右如果出现几百的数字十有八九是单位或换算问题。info -v用来查看发布端和订阅端的 QoS 设置。这一步非常关键。micro-ROS 默认不一定用 best_effort如果你在固件里用rclc_publisher_init_best_effort那么订阅端比如 robot_state_publisher也必须是 best_effort或者至少兼容。否则会出现话题列表里能看到但 echo 一直不输出的诡异现象。hz用来确认发布频率。我的目标 50Hz实测能达到 49-50Hz。如果频率乱跳可能原因很多但优先检查串口波特率和 USB-TTL 转换芯片的质量。劣质转换芯片在长时间高速传输时会丢字节micro-ROS 没有完整校验帧一旦丢字节数据就会断。4. RViz 可视化与 URDF/TF 树的设计4.1 画一个能展示 6 轴姿态的 URDFRViz 本身不认识你的机械臂它需要两个东西一个机器人模型描述URDF一组 TF 坐标变换。URDF 里定义连杆长度、质量、关节轴方向。对于关节状态可视化不需要精确物理属性但几何尺寸要大致正确否则看起来会比例失调。我建议先画一个简单的 6 自由度 URDF每个关节是一个 revolute 类型轴方向按实际机械结构填。比如基座到第一个关节通常是绕 z 轴旋转第二个关节绕 y 轴或 x 轴这取决于你的机械设计。URDF 的 origin 里的 xyz 表示连杆之间的平移rpy 表示旋转。为了让 RViz 动起来时符合真实运动一定要先确认每个关节的旋转轴和正方向。下面是简化后的 URDF 片段link namebase_link visual geometrycylinder length0.1 radius0.05//geometry /visual /link joint namejoint1 typerevolute parent linkbase_link/ child linklink1/ origin xyz0 0 0.05 rpy0 0 0/ axis xyz0 0 1/ limit lower-3.14 upper3.14 effort10 velocity1/ /joint link namelink1 visual geometrybox size0.04 0.04 0.2//geometry /visual /joint我用的是-3.14到3.14的限位后期如果关节角度超限在 RViz 里会表现成模型抽搐。URDF 不写 limit 也行但有些工具会警告。4.2 robot_state_publisher 与 TF 的配合RViz 显示模型时默认 fixed frame 是base_link。模型图标会显示在 TF 树中有正确变换的连杆上。而 TF 树由robot_state_publisher节点从 URDF 和 /joint_states 联合计算出来。所以启动流程是ros2 launch robot_state_publisher robot_state_publisher.launch.py model:path/to/arm.urdf或者写在 launch 文件里from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node(packagerobot_state_publisher, executablerobot_state_publisher, parameters[{robot_description: open(arm.urdf).read()}]) ])这个节点订阅/joint_states根据每个关节的角度计算 link 间的变换然后发布 TF。RViz 里添加 RobotModel 后它会跟着 /joint_states 的数据动。如果模型没动最常出现的是 TF 树的base_link与 RViz 的 fixed frame 不一致。RViz 左下角会有红色警告提示base_link或link1不存在。这时去检查 URDF 的 parent/child 命名是否和 /joint_states 里的 name 一致。如果模型动了但方向和实际相反那就要回到第 2.3 节的坐标系约定调整编码器正方向或 URDF 的 axis 符号。5. 实测踩坑记录QoS 衰减、时间戳与单位换算5.1 QoS 不匹配导致的话题静默这个坑我差点放弃。现象是ros2 topic list能看到/joint_states但ros2 topic echo /joint_states --once一直挂起不输出。我当时以为串口数据没上来回头查固件查接线查 agent 日志全正常。后来用ros2 topic info /joint_states -v才看到发布端 QoS 是best_effort而订阅端robot_state_publisher默认是reliable。两者不兼容导致订阅端完全收不到数据。ROS 2 的 QoS 不是尽力而为兼容而是发布和订阅必须有一组匹配的 profile。解决办法有两个要么把固件端发布者改成 reliable代价是可能因网络或串口丢包导致阻塞要么让所有订阅端都用 best_effort。对关节状态这种周期性的低速数据reliable 其实更合适。但是 micro-ROS 的 client 资源有限如果你既想可靠又不想占内存可以在 agent 启动后通过参数设置让客户端使用可靠 QoS。不过最简单的还是在固件初始化时rclc_publisher_init_default(joint_pub, support.node, ...);这样默认就是 reliable。如果你实在想用 best_effort记得后续所有订阅节点都要显式设置subscriber node.create_subscription( JointState, /joint_states, callback, qos_profileQoS(10).best_effort())排查任何话题有列表、无数据的问题第一步永远是ros2 topic info -v不要猜。5.2 时间戳不同步带来的模型抖动第二个坑是 RViz 模型每隔几秒就抖动一下像抽搐。现象是数据频率 50Hz但 TF 偶尔会跳到过去或未来。根源在 JointState 消息里的header.stamp。我最初在图里填sec0, nanosec0表示未知时间。micro-ROS agent 会尝试把时间戳同步到主机时钟但如果你自己手动给 0会导致 TF 缓冲区认为消息时间混乱。RViz 的 TF 有一个数据缓冲如果收到一条过去 100 秒的消息它会认为这是回退导致变换跳变。解决方法是让 micro-ROS 自动同步时间。在固件中可以通过调用rmw_uros_sync_session(timeout_ms);来建立时间同步。或者在设置传输后agent 会定期发布时间参考micro-ROS client 会自动更新时钟。然后填充时间戳时用rclc_get_timestamp(msg.header.stamp);这个函数会返回同步后的 ROS 2 时间。如果因为没有联网而无法同步至少保证时间戳单调递增不要总是 0。一个简单的替代方案是用millis()换算成秒和纳秒填进去但要记住 ROS 2 对时间基准有要求最好还是靠 agent 同步。5.3 角度单位错乱造成的姿态异常第三个坑非常隐蔽微控制器里很多编码器库默认返回度比如readAngle()返回 0-360。我一开始没注意直接把度数填进去。RViz 模型在旋转时看起来速度很快且乱转因为比如 90 度被当成 90 弧度而 90 弧度大约是 14.3 圈。排查时我在echo里看到 position 数值动辄 30 或 90正常弧度应该在 -3.14 到 3.14 之间。我立刻意识到是单位问题。解决办法是统一乘以M_PI / 180.0并且顺手把代码里所有角度变量名加上_rad后缀。顺带提醒URDF 里的limit单位也是弧度很多人在写 URDF 时写成 180 或 360导致限制范围完全不对。URDF 没有度的说法全部用弧度。6. 小技巧与扩展方向6.1 快速验证 micro-ROS 链路的办法如果你已经能跑通上面的链路我建议在主机上保留一个心跳验证脚本方便以后快速定位问题。micro-ROS agent 里其实有一个 ping 服务可以用ros2 service call /micro_ros_ping micro_ros_interfaces/srv/Ping如果 client 在线会返回响应。没有响应就说明 agent 与 client 连接断了先查串口线和电源。另一个技巧是在 RViz 中添加一个使用/joint_states的图表插件或者直接用plotjuggler查看各关节角度曲线这比看数字直观得多。6.2 从状态可视化走向闭环控制关节状态通信打通后这只是第一步。再往后你可以给每个关节加力矩反馈把effort数组填上真实电流数据用于碰撞检测也可以把 micro-ROS 的传输从串口换成 WiFi这样机械臂可以脱离工控机的物理束缚。我目前正在尝试在同样的链路上增加一个/arm_controller服务让上层可以下目标角度走 micro-ROS 的 service 而不是 topic这样控制语义更明确。不过在你做这些之前先把当前的基础链路稳定跑一周记录/joint_states的丢包率和时间戳抖动。我现在的项目中实际长期运行的发布频率稳定在 50Hz丢包率低于 0.1%RViz 模型与实体机械臂动作基本同帧。这个状态下的机械臂才算真正接入了 ROS 2 生态。
阅读完成 · 觉得有帮助?
咨询建站