机器人运动学 C++20 头文件库。基于 Craig 改进 DH(Modified DH)参数描述串联机器人, 提供正运动学、几何雅可比、伪逆雅可比与逆运动学求解,并附完整单元测试与数学推导文档。
- 头文件实现,无编译产物,
#include即用 - 仅依赖 Eigen(仓库内
3rd/Eigen已内置) - 关节角度单位可选(角度 / 弧度),长度单位统一为 mm
- 217 个单元测试(56 个套件,含 6R / 3R / 移动关节 / 7 自由度冗余等机型与批量扫描、契约矩阵、精度交叉),核心模块行覆盖率 >99%
robospace/
├── README.md 本文件
├── CMakeLists.txt 顶层构建脚本(可选 demo 示例 + 测试开关)
├── robospace/ 运动学库本体(头文件库)
│ ├── robotype.h 基础类型与别名:AngleUnit、PoseVec/JointVec/VecX/MatX、DYNAMIC
│ ├── robomath.hpp 数学工具:角度换算、欧拉角/旋转矩阵/四元数/齐次变换互转、秩、伪逆
│ ├── robolink.hpp 单连杆模型:关节类型、改进 DH 参数、关节状态与限位
│ ├── robochain.hpp 核心:机器人定义(MDH 连杆与关节、限位、关节状态、拓扑、变换缓存)
│ │ 与运动学算法接口(虚函数)及默认算法实现(正运动学 tr / fk、几何雅可比
│ │ jacob / pinvjacob、逆运动学 ik,位于内部命名空间 robospace::detail,可整体替换,见 §2.4)
│ ├── robodynamic.hpp 动力学与逆动力学(占位,接口规划见文件头)
│ ├── robotensor.hpp 张量工具:Kronecker 积、AX = YB 约束矩阵(手眼标定用)
│ └── robospace.h 聚合头文件:只 include 各模块,不实现任何功能
├── test/ 单元测试(GoogleTest + GoogleMock)
│ ├── CMakeLists.txt 测试与覆盖率构建脚本
│ ├── test_helpers.h 测试便捷包装(避免断言宏与模板逗号冲突)
│ ├── test_models.h 测试模型、独立参考实现、数值雅可比
│ ├── robomath_test.cpp 数学工具测试
│ ├── robolink_test.cpp 连杆测试
│ ├── robochain_test.cpp 运动学链 API / 生命周期 / 虚函数派发与算法外部替代测试
│ ├── kinematics_test.cpp 运动学数值正确性测试(解析解、参考实现、有限差分)
│ ├── ik_test.cpp 逆运动学测试(7 自由度冗余机型)
│ ├── robot_arm6_test.cpp 6 自由度机械臂测试
│ ├── robot_arm3_test.cpp 3 自由度位置控制臂测试(任务维数 3)
│ ├── robot_prismatic_test.cpp 含移动关节的机型测试(直角坐标龙门 / 龙门+手腕)
│ ├── robot_arm7_test.cpp 7 自由度冗余机械臂测试(S-R-S 构型)
│ ├── robolink_stdh_test.cpp 标准 DH → MDH 转换测试(PUMA560 / UR5 / 随机构型)
│ ├── sweep_test.cpp 参数化批量扫描(FK / 雅可比 / IK 成功率与最坏残差)
│ ├── contract_test.cpp 异常与契约矩阵(异常类型与消息关键字)
│ ├── precision_test.cpp 单位 × 精度 × 类型交叉(DEG/RAD × float/double)
│ ├── algorithm_swap_test.cpp 核心算法外部替代测试
│ ├── robotensor_test.cpp 张量工具测试
│ └── gcov_report.py 覆盖率汇总脚本(gcovr 的轻量替代)
├── doc/ 数学原理与算法文档与示例说明(中文,详见 §7)
├── demo/ 独立示例程序与 MuJoCo 可视化(不属于库本体,可整体删除)
│ ├── demo_models.h **真实机型单一来源**:7 个机型的 DH 表、参数来源与外部核验、初始位姿与工具路径
│ ├── demo_runner.h 示例共用的打印与「初始位姿 -> 工具路径 -> 运动过程」运行器
│ ├── demo_ur5.cpp Universal Robots UR5(6R,位姿任务)-> 目标 demo_ur5
│ ├── demo_iiwa7.cpp KUKA LBR iiwa 7 R800(7R 冗余)+ 其前 3 关节子链(demo_iiwa7)
│ ├── demo_puma560.cpp Unimation PUMA 560(标准 DH 转换)+ 工具连杆(demo_puma560)
│ ├── demo_scara.cpp AdeptOne SCARA(R-R-P-R,位置任务)(demo_scara)
│ ├── demo_cartesian.cpp 三轴直角坐标机器人(3 移动关节)(demo_cartesian)
│ ├── mj_export.cpp 导出 MDH 表 + 工具路径 + 运动轨迹为 JSON(可选目标 mj_export)
│ ├── run_demo.py 一键入口(跨平台 Python):构建 → 导出 → 渲染,有显示则开交互窗口
│ ├── pyproject.toml demo 的 Python 环境(由 uv 管理:mujoco / numpy / pillow)
│ ├── robot/ 机器人描述文件:每个机型一个 MuJoCo MJCF(<id>.xml)
│ └── mujoco/ Python 渲染器(把描述文件拼进场景并渲染,细节见其中 README.md)
└── 3rd/ 第三方依赖(Eigen、nlohmann/json,均为 header-only)
demo/与库本体完全解耦:robospace/、test/、doc/不引用其中任何文件, 删除或通过-DROBOSPACE_BUILD_DEMO=OFF关闭均不影响库的使用与测试。 唯一例外是test/real_robot_test.cpp,它 刻意 包含demo/demo_models.h:这样测试验证的 就是 demo 与可视化实际使用的那份真实机型参数表(单一事实来源,避免「测试和 demo 用了两份表」)。
| 模块 | 职责 | 关键接口 |
|---|---|---|
robotype.h |
类型基础设施 | AngleUnit{DEG,RAD}、PoseVec<FP>、JointVec<N,FP>、VecX<N,FP>、MatX<R,C,FP> |
robomath.hpp |
位姿表示与线性代数 | deg2rad/rad2deg、eul2rot/rot2eul、quat2rot/rot2quat、eul2quat/quat2eul、tform2trvec/trvec2tform、itform、rank、pinv |
robolink.hpp |
单连杆建模 + 标准 DH 转换 | RoboLink<FP,UNIT>:DH 恒定量(alpha/a/d/theta)、关节偏移(offset)、限位(qlim)、状态(q)、类型(jtype) |
robochain.hpp |
核心层:机器人定义 + 算法接口与默认实现 | RoboChain<TASK_DIM,LINK_NUM,FP,UNIT>:create/initialized/name/links/joints/offsets/topology/tr() 变换缓存,虚接口 tr/fk/jacob/pinvjacob/ik,RoboSpace 占位类型;默认算法(MDH 正运动学、几何雅可比、阻尼最小二乘 IK)及内部工具 task_error/clamp_joints/step_to_joint_unit 位于内部命名空间 robospace::detail |
robodynamic.hpp |
动力学与逆动力学 | 占位头文件,规划接口见文件头注释(RNEA / CRBA / 正动力学 / 回归矩阵) |
robotensor.hpp |
张量/标定 | kron(A,B)、vecmat(A,B) |
robospace.h |
聚合入口 | 只 #include 各模块头文件,不含任何类或函数定义 |
以下模块位于 demo/,属于示例与可视化设施,不属于库本体:
| 模块 | 职责 | 关键接口 |
|---|---|---|
demo/demo_models.h |
真实机型单一来源 | 7 个机型的 DH 表 + 参数来源 + 外部核验 + 初始位姿 + 工具路径(直线段 / 圆弧回转段)+ 路点生成 |
demo/demo_runner.h |
示例共用呈现 | 参数表/位姿打印、run_point_to_point(逐路点 ik() 并追踪残差) |
demo/demo_*.cpp |
独立示例程序 | demo_ur5 / demo_iiwa7 / demo_puma560 / demo_scara / demo_cartesian,各自自包含 main() |
demo/mj_export.cpp |
导出可视化数据 | 目标 mj_export:导出各机型 MDH 表 + 工具路径 + 运动轨迹为 build/mj_export.json |
demo/robot/<id>.xml |
机器人描述文件 | 每个机型一个 MuJoCo MJCF(运动学 + 图元几何 + 关节限位 + ee 末端 site),单独存放,可直接被 MuJoCo 加载 |
demo/run_demo.py |
一键运行(跨平台) | 构建 → 导出 JSON → 检查描述文件 → 渲染,支持 --demo NAME / --robot NAME / --list / --gui / --no-gui / --jobs N |
demo/pyproject.toml |
Python 环境 | uv 管理:uv sync 装 mujoco / numpy / pillow,无需系统 pip |
demo/mujoco/ |
MuJoCo 渲染 | 把 demo/robot/<id>.xml 拼进场景并渲染 GIF/PNG(绿方块 = 初始位姿、红方块 = 终点、黄点 + 红/绿/蓝三轴 = TCP 工具坐标系、淡蓝带 = TCP 轨迹) |
| robot id | demo | 真实机型 | DH 约定 | 任务 | 外部核验 |
|---|---|---|---|---|---|
ur5 |
demo_ur5 |
Universal Robots UR5 | 标准 DH → form_sdh |
6 维位姿 | 零位 |p| = 839.4 mm vs 官方 850 mm 臂展(1.3%);与官方 ROS 2 描述 URDF 逐行一致 |
iiwa7 |
demo_iiwa7 |
KUKA LBR iiwa 7 R800 | 改进 DH(原表) | 6 维位姿(冗余) | 零位法兰 (0,0,1266) mm = KUKA 公布尺寸 A,偏差 0;与 iiwa_stack URDF 的限位/偏移一致 |
iiwa3 |
demo_iiwa7 |
同上机型的前 3 关节(真实子链) | 改进 DH | 3 维位置(秩 2) | 末端锁在半径 d3 = 400 mm 球面上 |
puma560 |
demo_puma560 |
Unimation PUMA 560 | 标准 DH → form_sdh |
6 维位姿 | 零位腕心 (452.1, −150.05, 431.8) mm;与 RTB mdl_puma560.m 的 DH 与 qlim 逐项一致 |
puma560_tool |
demo_puma560 |
PUMA 560 + 工具行 + 锁死连杆 | 标准 DH(非平凡工具行) | 6 维位姿 | T(0,7) = T(0,6)·E 误差 0 |
scara4 |
demo_scara |
AdeptOne SCARA(R-R-P-R) | 标准 DH → form_sdh |
3 维位置 | 零位水平半径 800 mm = a1+a2 = 425+375 |
cartesian3 |
demo_cartesian |
三轴直角坐标机器人 | 正交构型(厂商无 DH 表) | 3 维位置 | J 为常数正交置换阵,JᵀJ = I |
参数来源与逐条核验记录在
demo/demo_models.h的注释里,test/real_robot_test.cpp会把这些 外部核验(1266 mm、800 mm、秩、零列等)固化成断言。UR5 与 iiwa 7 R800 的运动学还与公开开源 描述(Universal_Robots_ROS2_Description、IFL-CAMP/iiwa_stack)逐项核对过,PUMA 560 与 RTB 的mdl_puma560.m逐项一致;注意 MuJoCo Menagerie 里只有 UR5e 与 iiwa 14,不是同型号。 两处未确认的外部信息已在代码注释中明确标注为建模选择而非厂商数据:AdeptOne 的关节限位 (原厂手册未能提取)与直角坐标构型的轴顺序。详见 demo/mujoco/README.md 的「开源模型调研」。
flowchart TB
USER(["用户代码"])
subgraph PUB["公开 API | 一级命名空间 robospace"]
TYPE["robotype.h<br/>AngleUnit · ChainTopo · PoseVec / JointVec / VecX / MatX"]
MATH["robomath.hpp · robotensor.hpp<br/>角度 / 姿态 / 齐次变换互转 · rank / pinv · kron / vecmat"]
LINK["robolink.hpp<br/>RoboLink(DH 恒定量 + 关节状态)· StandardDhLink · form_sdh"]
CHAIN["robochain.hpp | 核心<br/>RoboChain / RoboSpace:连杆 · 关节状态 · 限位 · 变换缓存 T_<br/>算法虚接口 tr · fk · jacob · pinvjacob · ik"]
end
subgraph INNER["内部实现层(非公开 API)"]
DETAIL["robospace::detail<br/>默认算法 tr · jacob · ik<br/>工具 task_error · clamp_joints · step_to_joint_unit"]
end
AGG["robospace.h | 聚合头(仅 include,不实现功能)"]
TYPE --> MATH
MATH --> LINK
LINK --> CHAIN
MATH --> CHAIN
CHAIN -->|"默认转发"| DETAIL
AGG -->|"一次性引入全部模块"| CHAIN
USER -->|"调用公开接口"| CHAIN
- 公开接口集中在
robospace一级命名空间:RoboLink/RoboChain/RoboSpace/form_sdh以及robomath/robotensor的工具函数。 - 模型与算法解耦:
RoboChain只持有模型(连杆、关节状态、限位、变换缓存)与算法虚接口; 默认算法实现位于内部命名空间robospace::detail(实现细节,非公开 API),由虚接口转发调用。 - 聚合头不做实现:
robospace.h仅#include全部模块,可整库引入,也可按需只包含单个模块。
flowchart LR
subgraph MODEL["模型 / 状态"]
RLINK["RoboLink<FP, UNIT><br/>DH 恒定量 alpha / a / d / theta(构造后只读)<br/>关节状态 q · 限位 qlim"]
RCHAIN["RoboChain<TASK_DIM, LINK_NUM, FP, UNIT><br/>links_ 连杆列表 · T_ 变换缓存"]
RLINK -->|"DH 恒定量(只读)"| RCHAIN
RCHAIN -->|"关节状态 / 限位"| RLINK
end
subgraph FLOW["运动学数据流"]
Q(["关节值 q"])
Q -->|"前向"| TR["tr(q)"] --> TT["齐次变换 T(0,n)"] --> FK["fk(q)"] --> POSE(["位姿 [x, y, z, rx, ry, rz]"])
Q -->|"微分"| JAC["jacob(q)"] --> J["J (6×n)"] --> PV["pinvjacob(q)"] --> JP["J⁺ (n×6)"] --> IK["ik(pt, q0)"] --> QS(["关节解 q"])
end
RCHAIN -->|"缓存 T(0,n)"| TT
- DH 恒定量与关节状态分离:
RoboLink中alpha/a/d/theta为构造后只读的参数, 关节状态由q()/q(v)读写,RoboChain::joints()/joints(q)是链级封装。 几何参数不会因写关节角而被覆盖。 - 变换缓存:
fk(q, settr=true)、jacob(q, settr=true)会把该位形的T(0,n)缓存到tr(),避免重复前向计算;默认settr=false,不产生副作用。 - 显式关节量优先:
fk/jacob/tr/ik均通过参数接收q,与内部关节状态解耦; 仅不传q0的ik重载使用joints()作为迭代初值。
robochain.hpp 同时承载模型与默认算法:RoboChain 的虚接口 tr/fk/jacob/pinvjacob/ik
只有转发体,默认算法实现在内部命名空间 robospace::detail(属实现细节,非公开 API);
robospace.h 只是聚合 #include。模型与算法彼此解耦,核心算法可由外部整体替换,共有三种方式:
- 派生覆写虚函数:继承
RoboChain覆写tr/fk/jacob/pinvjacob/ik中任意一个, 通过基类指针调用即进入派生实现。test/robochain_test.cpp的RoboChainVirtual.*用例演示了tr、fk、jacob、ik的替换(含 GoogleMock 派发测试)。 - 替换
robospace::detail的默认实现:内部算法是自由函数模板(首参为链对象, 经chain.links()/dh_terms()/dh_matrix()/check_joints()访问模型), 直接改写robochain.hpp中robospace::detail的tr/jacob/ik或新增自己的实现即可(这是内部实现层,不属于公开 API)。 - 自行组合算法:不依赖前两者,直接用
robomath.hpp/robotensor.hpp的工具与RoboChain的公开模型访问器(links()、joints()、dh_terms()、dh_matrix()) 编写求解器,RoboSpace等占位类型可用于承载机型层扩展。
说明:
fk经由虚函数tr()获取变换、ik经由虚函数jacob()获取雅可比并缓存变换, 所以覆写tr/jacob同样会改变fk/ik的行为。只包含
robochain.hpp的编译单元即可定义机器人、读写关节状态与偏移、查看拓扑/连杆与变换缓存, 并直接调用tr/fk/jacob/pinvjacob/ik:算法接口的转发体、默认算法定义与虚表都在robochain.hpp内,无需再额外包含其它算法头文件。
| 功能 | 接口 | 说明 |
|---|---|---|
| 正运动学 | fk(q, settr, order) |
关节量 → 末端位姿 [x,y,z,rx,ry,rz] |
| 变换矩阵 | tr(q, j_start, j_end) |
关节量 → 齐次变换矩阵,可只算子链 |
| 几何雅可比 | jacob(q, settr) |
世界系表达,J = [Jv ; Jω] |
| 伪逆雅可比 | pinvjacob(q, eps, settr) |
基于 SVD 的 Moore-Penrose 伪逆 |
| 逆运动学 | ik(pt, q0, tol, max_iter, order) / ik(pt, tol, max_iter, setj, order) |
阻尼最小二乘 + 回溯线搜索 |
| 关节状态 | joints() / joints(q) |
读写关节状态,写入时校验限位 |
| 关节偏移 | offsets() / offsets(q) |
零位偏移,与关节量叠加 |
| 标准 DH 接入 | form_sdh(sdh, types, qlim, base*, tool*) |
标准 DH 参数表 → 本库改进 DH 模型(含工具补偿) |
| 位姿互转 | eul2rot/rot2eul、tform2trvec/trvec2tform |
6 种欧拉角顺序(ZYX/XYZ/YXZ/YZX/XZY/ZXY) |
| 四元数 | quat2rot/rot2quat、eul2quat/quat2eul |
基于 Eigen::Quaternion,输入自动归一化 |
| 数值线性代数 | rank、pinv |
秩、Moore-Penrose 伪逆(带相对容差) |
| 张量 | kron、vecmat |
Kronecker 积、AX = YB 约束矩阵 |
| 依赖 | 版本要求 | 说明 |
|---|---|---|
| C++ 标准 | C++20 | 顶层 CMakeLists.txt 已设置 CMAKE_CXX_STANDARD 20 |
| CMake | ≥ 3.10(测试需 ≥ 3.14) | 测试使用 FetchContent / gtest_discover_tests |
| Eigen | 3.x | 仓库内置 3rd/Eigen,无需单独安装 |
| GoogleTest | 1.15.2 | 仅测试需要,构建时自动获取(见下) |
# 配置 + 编译(默认构建 demo/ 示例程序与单元测试)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
# 运行测试
ctest --test-dir build --output-on-failure
# 只构建库与测试,不构建 demo/ 示例程序
cmake -S . -B build -DROBOSPACE_BUILD_DEMO=OFF
cmake --build build本项目统一使用单一构建目录
build/,不为测试、覆盖率等场景另建目录:单元测试默认 开启,切换配置只需在同一目录重新给出 CMake 选项(如-DROBOSPACE_ENABLE_COVERAGE=ON) 再cmake --build build,CMake 会自动重编受影响的目标。
主要 CMake 选项:
| 选项 | 默认 | 说明 |
|---|---|---|
ROBOSPACE_BUILD_DEMO |
ON |
是否构建 demo/ 下的独立示例程序(5 个示例 + 可选的 mj_export) |
ROBOSPACE_BUILD_TESTS |
ON |
是否构建单元测试 |
ROBOSPACE_ENABLE_COVERAGE |
OFF |
是否开启 --coverage 插桩并启用 cov-report 目标 |
ROBOSPACE_GTEST_FETCH |
ON |
系统未安装 GoogleTest 时是否自动下载 |
ROBOSPACE_GTEST_URL |
官方 v1.15.2 压缩包 | 可换用内网镜像地址 |
robospace 是 INTERFACE(头文件)库,已导出三组包含目录,两种写法均可:
add_subdirectory(robospace) # 或 add_subdirectory(<path-to-robospace>)
target_link_libraries(my_app PRIVATE robospace)#include "robospace/robochain.hpp" // 建模 + 运动学与逆运动学(推荐入口)
#include "robospace/robospace.h" // 或一次性包含全部模块
// 或
#include "robochain.hpp" // 直接使用 robospace/ 目录FetchContent 首次配置需要联网下载 GoogleTest。离线环境可预先准备源码目录:
cmake -S . -B build \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST=/path/to/googletest-src
# 或直接关闭测试
cmake -S . -B build -DROBOSPACE_BUILD_TESTS=OFF#include "robospace/robospace.h"
namespace rbs = robospace;
// 改进 DH 参数:{ 名称, alpha, a, d, theta, offset, qlim, 关节类型 }
// 角度参数单位与模板参数 UNIT 一致(此处为度),长度单位统一为 mm
std::vector<rbs::RoboLink<double>> links = {
{ "L1", 0.0, 0.0, 100.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L2", 90.0, 0.0, 0.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L3", -90.0, 0.0, 300.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L4", 90.0, 0.0, 0.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L5", -90.0, 0.0, 250.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L6", 90.0, 0.0, 0.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L7", -90.0, 0.0, 150.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
};
// 第一个模板参数 = 任务空间维数(3 = 仅位置 [x,y,z],6 = 完整位姿 [x,y,z,rx,ry,rz])
// 第二个模板参数 = 关节数(此例 7 关节控制 6 维位姿,属冗余机器人)
rbs::RoboChain<6, 7, double, rbs::AngleUnit::DEG> robot("arm7", links);
rbs::VecX<7, double> q = { 15.0, -25.0, 40.0, -10.0, 60.0, 35.0, -5.0 };
rbs::PoseVec<double> pose = robot.fk(q, /*settr=*/true); // [x, y, z, rx, ry, rz]
std::cout << "位姿: " << pose.transpose() << std::endl;
std::cout << "变换矩阵:\n" << robot.tr() << std::endl; // fk(q,true) 缓存的 T(0,n)auto J = robot.jacob(q); // 6 x 7,世界坐标系表达,单位 [mm/rad ; rad/rad]
auto Jp = robot.pinvjacob(q); // 7 x 6,Moore-Penrose 伪逆
// 关节速度 → 末端速度(关节速度对转动关节为 rad/s)
rbs::VecX<7, double> qd = rbs::VecX<7, double>::Zero();
qd(0) = 1.0;
rbs::VecX<6, double> twist = J * qd; // [线速度 mm/s ; 角速度 rad/s]rbs::PoseVec<double> target = robot.fk(q);
rbs::VecX<7, double> q0 = rbs::VecX<7, double>::Zero();
// 收敛容差作用于 [位置(mm); 姿态(rad)] 误差向量的二范数
try {
rbs::VecX<7, double> qi = robot.ik(target, q0, 1e-8, 4000);
std::cout << "关节解: " << qi.transpose() << std::endl;
} catch(const std::runtime_error &e) {
std::cout << "求解失败: " << e.what() << std::endl; // 不可达 / 被限位阻挡
}
// 以当前关节状态为初值,并回写结果
robot.ik(target, 1e-8, 4000, /*setj=*/true);// 欧拉角 <-> 旋转矩阵:向量恒为 [rx, ry, rz],order 只决定施加顺序
auto R = rbs::eul2rot<double, rbs::AngleUnit::DEG>({ 10.0, 20.0, 30.0 }, "ZYX");
auto e = rbs::rot2eul<double, rbs::AngleUnit::DEG>(R, "ZYX"); // 得到 [10, 20, 30]
// 齐次变换 <-> 位姿向量(ZYX 顺序与 IEEE 一致)
rbs::VecX<6, double> p = { 1.0, 2.0, 3.0, 10.0, 20.0, 30.0 };
auto T = rbs::trvec2tform<double, rbs::AngleUnit::DEG>(p);
auto p2 = rbs::tform2trvec<double, rbs::AngleUnit::DEG>(T); // 与 p 一致
// 四元数(Eigen 类型,输入自动归一化)
Eigen::Quaterniond qq = rbs::eul2quat<double, rbs::AngleUnit::DEG>(e);
auto R2 = rbs::quat2rot<double>(qq);using rbs::AngleUnit;
std::vector<rbs::RoboLink<double, AngleUnit::RAD>> links_rad = {
{ "L1", 0.0, 0.0, 100.0, 0.0, 0.0, { -M_PI, M_PI }, rbs::JointType::REVOLUTE },
{ "L2", M_PI/2, 0.0, 0.0, 0.0, 0.0, { -M_PI, M_PI }, rbs::JointType::REVOLUTE },
// ...
};
rbs::RoboChain<6, 2, double, AngleUnit::RAD> robot_rad("rad", links_rad);demo/ 下除文本示例外还提供了一套 MuJoCo 可视化:mj_export 用库导出的 MDH 连杆表、
库 ik() 求出的关节轨迹与库 fk() 算出的位姿生成 JSON,Python 脚本据此自动生成
MJCF 并渲染成动画——MJCF 完全来自库的模型(单一事实来源),且用 MuJoCo 自己的正运动学与
库的 fk() 逐点交叉验证(实测 7 个机型的位置偏差 ≤ 1.3e-12 mm、姿态偏差 ≤ 4.0e-9 deg)。
它不属于库本体,C++ 构建本身不依赖 MuJoCo。
模型与运动定义在 demo/demo_models.h:7 个机型全部是真实公开机型(UR5、KUKA iiwa 7 R800、
PUMA 560、AdeptOne SCARA、三轴直角坐标,以及 iiwa 前 3 关节这条真实子链),示例程序、导出器与
测试共用同一份参数表,因此画面、文本输出与测试断言不可能互相矛盾。
每个示例演示的都是一条工具路径(demo_models.h 里的 PathBuilder):从 初始位姿 fk(q_init)
出发,依次走若干直线段与圆弧回转段(绕基座轴回转,可边转边升降),每段按长度离散成若干路点,
逐点用 ik() 求解并打印残差。路径是大范围的:TCP 行程 1.8 ~ 6.0 m(见下表),而库的
ik() / fk() 逐点跟住它 —— 这既演示了工作空间覆盖,也演示了限位余量与奇异性。每段路径同时给出
末端姿态变化:arc 段的 drx / dry / drz 让工具在回转中偏航、俯仰、滚转(直线段也能带姿态
偏置),因此动画里工具坐标系是转着走完全程的,而不是只平移。
渲染出的动画里 绿方块 = 初始位姿、红方块 = 路径终点、黄点 + 红/绿/蓝三轴 = TCP 工具坐标系、白点 = 路径拐点、淡蓝带 = TCP 轨迹。
机器的连杆画橙色、关节画深蓝色(关节轴处的圆柱壳体),二者一眼可分,不会糊成一种颜色。
TCP 就是库的 T(0, n)(描述文件里的 ee site):渲染器在工具坐标系原点画黄点 + 红/绿/蓝 x/y/z 三轴
—— 球体无法体现旋转,而三轴坐标系能,所以姿态变化一眼可见;沿途再稀疏地摆 6 个同样的坐标系快照,
于是整条路径上工具姿态如何演变也能一眼读出。库 fk() 给出的 TCP 轨迹则连成一条淡蓝带。
| 机型 | 路径段数 | 路点 | TCP 行程 | 最紧关节用量 | 最大位置残差 |
|---|---|---|---|---|---|
ur5 |
96 | 543 | 6005 mm | 65 %(J1) | 9.5e-13 mm |
iiwa7 |
74 | 433 | 3978 mm | 86 %(J6) | 1.3e-12 mm |
puma560 |
34 | 285 | 1804 mm | 88 %(J1) | 1.1e-12 mm |
scara4 |
72 | 471 | 3818 mm | 83 %(J3) | 3.6e-13 mm |
cartesian3 |
9 | 343 | 5243 mm | 90 %(J3) | 6.7e-13 mm |
各 demo 运行效果(GIF):下面每一张都对应 demo/gifs/<id>.gif,由 uv run demo/run_demo.py
渲染(绿方块 = 初始位姿,红方块 = 终点,黄点 + 三轴坐标系 = TCP 工具坐标系,淡蓝带 = TCP 轨迹,白点 = 路径拐点):
| 机型(demo) | 机型(demo) |
|---|---|
ur5(demo_ur5)![]() |
iiwa7(demo_iiwa7)![]() |
puma560(demo_puma560)![]() |
puma560_tool(demo_puma560)![]() |
scara4(demo_scara)![]() |
cartesian3(demo_cartesian)![]() |
iiwa3(demo_iiwa7,关节空间演示)![]() |
GIF 分辨率 480×360、约 9 s 循环(放慢以便观看),单个 1.1 ~ 1.6 MB。
「最紧关节用量」=
max |q_j - 中心| / 半行程,100 % 表示正好顶在限位上。这些数字不是估计值:mj_export每导出一个机型就打一行,test/real_robot_test.cpp把它们固化成断言(路径必须够长、 必须全部收敛、且限位用量不得超过 95 %)。路径设计中被卡住的实例也留在了注释里 —— 例如 PUMA 560 的回转不能超过 ~160°(否则工具会跑出 J1 的可达方位带),iiwa7 / PUMA 560 的工具偏航要跟着 位置方位角一起转,否则腕关节会先顶到限位。SCARA 与直角坐标是位置任务(3 维),路径里根本没有 姿态目标(waypoints()会丢弃姿态偏置),所以它们的工具坐标系在整个动画里是不转的 —— 这也是库的 计算结果,而不是渲染的偷懒。
每个机型的 MuJoCo 描述单独放在 demo/robot/<id>.xml(7 个机型 7 个文件,MuJoCo 原生 MJCF
格式,只用 capsule / sphere / cylinder 等图元,无网格资产,单文件几 KB;连杆橙色、关节深蓝(关节轴处
的圆柱壳体)、底座灰蓝;TCP 由 ee site 标记,移动关节的滑块画成圆柱形轴承而不是方块)。
入口是跨平台的 Python 脚本 demo/run_demo.py,Python 环境由 uv 管理(demo/pyproject.toml,
uv sync 会装好 mujoco / numpy / pillow,不依赖系统 pip):
uv sync # 首次:创建 demo/.venv 并安装依赖(uv 自动准备 Python)
cmake --build build # 构建(含 mj_export 与 5 个示例;默认行为)
uv run demo/run_demo.py # 一键:构建 → 导出 → 渲染;有显示则开交互窗口,否则离屏
uv run demo/run_demo.py --no-gui # 强制离屏,生成 build/mujoco/ 下全部 GIF/海报
uv run demo/run_demo.py --demo ur5 # 先跑该示例(打印初始位姿、工具路径与运动过程),再渲染它覆盖的机型
uv run demo/run_demo.py --list # 列出机型、所属 demo 与各自的描述文件
ls build/mujoco/ # 离屏产物:<robot>.gif 与 <robot>_poster.png系统 Python 也能直接跑:python3 demo/run_demo.py —— 缺少依赖时它会自动改用 uv 环境执行。
在有显示的环境(含 WSL 的 WSLg)里,脚本默认打开 MuJoCo 交互窗口并循环播放轨迹,关闭窗口后
继续下一个机型;--no-gui 强制只生成文件。检测依据在 Linux 上是 DISPLAY / WAYLAND_DISPLAY
非空(macOS / Windows 无需环境变量),并要求标准输出为终端(非终端调用一律离屏渲染,避免无人值守
时被窗口阻塞)。
(--gui 强制开窗、--no-gui 强制离屏、--robot NAME 只渲染单个机型、--jobs N 控制构建并行度、
--help 查看全部参数。)
内存:
demo/run_demo.py默认用-j4构建而不是全部核心。Eigen 的重模板实例化会让单个编译 进程的峰值内存达到 1GB 量级,16GB 机器上 20 核全并行会把内存打满;渲染则按机型串行进行。 本机实测:全量构建 + 首轮渲染峰值常驻内存 1.79 GB,纯渲染(--no-build)553 MB。
详见 doc/07 §3 与 demo/mujoco/README.md。
模板第一个参数是任务空间维数,取 3 表示只约束末端位置 [x, y, z],可用于位置控制的
机械臂(如 3 自由度取放臂、直角坐标龙门)。
// 3 转动关节,只做位置控制
std::vector<rbs::RoboLink<double>> links3 = {
{ "L1", 0.0, 0.0, 100.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L2", 90.0, 0.0, 0.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
{ "L3", -90.0, 0.0, 300.0, 0.0, 0.0, { -360, 360 }, rbs::JointType::REVOLUTE },
};
rbs::RoboChain<3, 3, double, rbs::AngleUnit::DEG> arm3("arm3", links3);
rbs::VecX<3, double> q = { 20.0, -30.0, 45.0 };
rbs::PoseVec<double> pose = arm3.fk(q); // 仍是完整 6 维位姿
auto J3 = arm3.jacob(q); // 3 x 3,只含线速度行(单位 mm/rad)
auto T3 = arm3.tr(q); // 齐次变换矩阵
rbs::VecX<3, double> target = arm3.fk(rbs::VecX<3, double>{ -40.0, 55.0, -70.0 }).head<3>();
rbs::VecX<3, double> qs = arm3.ik(target, rbs::VecX<3, double>::Zero(), 1e-9, 2000);
// 位置误差已收敛;3 自由度位置任务不约束末端姿态,故解不唯一只拿到厂商手册里的标准 DH(T = Rz(θ)Dz(d)Dx(a)Rx(α))参数表时,用 robolink.hpp 的
form_sdh() 直接转换成库内的改进 DH 模型:
// 标准 DH 一行一个关节:{ 常量 theta, d, a, alpha }
std::vector<rbs::StandardDhLink<double>> sdh = {
{ 0.0, 0.0, 0.0, 90.0 }, // theta, d, a, alpha(角度单位随模板参数)
{ 0.0, 0.0, 431.8, 0.0 },
{ 0.0, 150.05, 20.3, -90.0 },
{ 0.0, 431.8, 0.0, 90.0 },
{ 0.0, 0.0, 0.0, -90.0 },
{ 0.0, 0.0, 0.0, 0.0 },
};
std::vector<rbs::JointType> types(sdh.size(), rbs::JointType::REVOLUTE);
std::vector<rbs::VecX<2, double>> qlim(sdh.size(), { -180.0, 180.0 });
rbs::MatX<4, 4, double> base_bias = rbs::MatX<4, 4, double>::Identity();
rbs::MatX<4, 4, double> tool_bias = rbs::MatX<4, 4, double>::Identity();
auto links = rbs::form_sdh<double, rbs::AngleUnit::DEG>(sdh, types, qlim, &base_bias, &tool_bias);
rbs::RoboChain<6, 6, double, rbs::AngleUnit::DEG> robot("puma", links);
// 关系:T_sdh(q) = base_bias * robot.tr(q) * tool_bias
// base_bias 恒为单位阵;tool_bias = Rx(alpha_n)·Dx(a_n),末行 a/alpha 为 0 时亦为单位阵要点(详见 doc/08):
- 转换是参数下标移位:改进 DH 第
i行的(a, alpha)取自标准 DH 第i-1行,首行取零;theta/d保持不变; - 工具补偿
tool_bias不可省略:当标准 DH 末行的a_n或alpha_n非零时必须右乘它, 否则末端位姿会整体偏差约|a_n|(实测漏用导致 29.6 mm 误差,正确使用后残差 1.1e-13); - 精度:
double约1e-13,float约2e-5;完整示例见demo/demo_stdh.cpp。
移动关节(
JointType::PRISMATIC)同样受支持:其关节量、d常量、offset与qlim都是长度量(mm),且不做角度取模。含移动关节的完整示例见demo/demo_prismatic.cpp。
cmake -S . -B build && cmake --build build
ctest --test-dir build --output-on-failure
# 或直接运行测试可执行文件(支持 GoogleTest 过滤)
./build/test/robospace_tests
./build/test/robospace_tests --gtest_filter='InverseKinematics.*'测试组织(共 194 个用例,48 个测试套件):
| 文件 | 覆盖内容 |
|---|---|
robomath_test.cpp |
角度换算、fmod/in_range/clip、6 种欧拉角顺序的互逆性、万向节死锁、位姿与变换互转、刚体逆、四元数归一化、秩、伪逆的 Moore-Penrose 四条件、容差行为、float 实例化 |
robolink_test.cpp |
DH 参数存取、单位换算(角度/弧度、转动/移动关节)、状态与限位校验、非法限位、类型切换、参数重置 |
robochain_test.cpp |
构造校验、未初始化保护、子链区间、位姿与变换一致性、超限异常信息、雅可比形状/伪逆、关节状态与偏移、虚函数派发(GoogleMock 测试替身) |
kinematics_test.cpp |
平面 2R 臂解析解与解析雅可比、奇异位形秩降、移动关节轴向、与独立参考实现比对(随机 8 组位形)、有限差分校验雅可比(线速度+角速度)、角度/弧度链一致性、float 冒烟、变换缓存 |
ik_test.cpp |
可达目标收敛(含 179° 大姿态误差)、随机初值 8 组、弧度链、限位约束、冗余解、6 种欧拉角顺序、不可达/限位阻挡/迭代不足的异常、参数校验、setj 回写、容差语义、近奇异收敛 |
robot_arm6_test.cpp |
6 自由度机械臂:与独立参考实现比对 FK、有限差分校验雅可比、满秩判据、IK 往返与不可达异常、解满足限位 |
robot_arm3_test.cpp |
3 自由度位置控制臂(TASK_DIM == 3):3×3 雅可比只含线速度行、位置 IK 收敛与多解、伪逆性质、不可达异常 |
robot_prismatic_test.cpp |
含移动关节机型:3 轴直角坐标龙门(位置任务)与「龙门 + 3R 手腕」(位姿任务);移动/转动关节的雅可比列结构、移动关节常量与偏移语义、不做角度取模 |
robot_arm7_test.cpp |
7 自由度冗余臂(S-R-S):满秩判据、多初值收敛且关节解互异(冗余性)、零空间维度 = LINK_NUM − rank(J)、容差与近奇异行为 |
sweep_test.cpp |
参数化批量扫描(5 机型 × 50 组随机位形):FK 与独立参考实现、雅可比与中心差分(线/角速度)、子链一致性;IK 批量成功率与最坏残差(含失败样本的异常断言) |
contract_test.cpp |
异常与契约矩阵:逐接口覆盖未初始化 / 连杆数不符 / qlim 反序 / 超限 / 非法欧拉顺序 / 非法 tol 与 max_iter / 不可达 / 卡限位,断言异常类型与消息关键字;并固化 tr() 不校验限位、create() 重置缓存、失败时状态不变等既有契约 |
precision_test.cpp |
单位 × 精度 × 类型交叉(TASK_DIM{3,6} × {DEG,RAD} × {float,double} 全部 8 组合):跨单位一致性、单精度收敛性、状态单位往返 ULP 现象固化 |
algorithm_swap_test.cpp |
算法外部替代增量场景:覆写 tr/fk/jacob/ik 的调用次数、缓存来源与重载独立替换(含 gmock 记录调用) |
robolink_stdh_test.cpp |
标准 DH → MDH 转换:PUMA560 / UR5 / 含移动关节 / float / DEG 与异常校验;外加 34 组随机标准 DH 机型验证 |
robotensor_test.cpp |
Kronecker 积定义与维数、vecmat 方阵/非方阵(含用 SVD 零空间还原 X、Y 的端到端用例)、按列堆叠约定 |
cmake -S . -B build -DROBOSPACE_ENABLE_COVERAGE=ON
cmake --build build
ctest --test-dir build
cmake --build build --target cov-report # 生成 build/coverage.txt最新结果(gcov,同一模板实例化取并集):
| 文件 | 已执行行 | 可执行行 | 行覆盖率 |
|---|---|---|---|
robospace/robochain.hpp(核心:模型 + 内部 detail 默认算法) |
212 | 214 | 99.1% |
robospace/robolink.hpp |
80 | 80 | 100.0% |
robospace/robomath.hpp |
91 | 91 | 100.0% |
robospace/robotensor.hpp |
19 | 20 | 95.0% |
| 合计 | 402 | 405 | 99.3% |
(robospace.h 仅聚合 include,无可执行行;上表为 217 个用例的实测结果。)
未覆盖的 3 行是防御性分支与依赖具体实例化的分支(ik() 中 LDLT 求解出现非有限值的兜底、
阻尼因子超过上限的保护性跳出,以及 robotensor.hpp:85),正常数值范围内不会触发。
函数覆盖率为 541/699 = 77.4%,低于行覆盖率属预期:统计口径按「模板实例化」计数,
而本库为虚函数接口,编译器会为每个被实例化的 TASK_DIM/LINK_NUM 组合都生成虚函数表项与
析构函数(即便从未被调用);若只按「被调用过的逻辑函数」统计则接近 100%。
提示:在同一
build/目录里从普通构建切到覆盖率构建(或反过来)会重编所有目标, 采集前建议先删除旧的*.gcda(find build -name '*.gcda' -delete), 否则 gcov 可能报profiling error: overwriting an existing profile data。
算法推导、接口契约与数值方法细节见 doc/ 目录:
| 文档 | 内容 |
|---|---|
| doc/01-位姿表示与坐标变换.md | 旋转矩阵性质与证明、内外旋等价性、6 种欧拉角顺序的解析求逆完整公式表、万向节死锁解集与本库返回分支、四元数与 Shepperd 分支、刚体逆、误差量级 |
| doc/02-改进DH参数与正运动学.md | 标准/改进 DH 完整对照、单连杆变换逐项推导(与代码逐行对应)、关节变量与常量/零位偏移、「末端原点在末关节轴上」定理与 3 条推论、RRP 规避方案、数值算例 |
| doc/03-雅可比矩阵.md | 两条推导路线与一致性证明、关节轴取法的命题化证明、分块与量纲、几何雅可比 vs 解析雅可比($B(\varphi)$、$\det B=\cos r_y$)、奇异值与条件数实测、工具点偏移雅可比变换、有限差分校验方法 |
| doc/04-逆运动学数值方法.md | 对数映射推导、四种误差表示对比、高斯-牛顿与 Tikhonov、量纲不平衡的三条定量机理与修复前后实测、信赖域+回溯线搜索(下降性证明)、$\lambda$ 自适应、限位策略对比、tol 选型、与 CCD/FABRIK/解析法对比、迭代次数实测 |
| doc/05-线性代数工具.md | SVD 几何意义、伪逆四条件与唯一性证明、四种特例闭式、相对容差公式推导与选型表、LU/SVD 判秩差异、Kronecker 与 vec 恒等式三步推导、$AX=YB$ 维数核对与零空间求解、手眼标定背景 |
| doc/06-数值实现与工程约定.md | 分层架构图与算法替换三途径、逐接口单位表、编译期校验清单、异常逐字消息表、精度与稳定性实测、性能与分配、线程安全、Eigen 协作、20 条陷阱(现象→原因→正确做法)、覆盖率工作流 |
| doc/07-示例程序与Demo使用说明.md | 构建运行、五个示例逐段详解(含真实输出与修改练习)、MuJoCo 可视化 demo、接入自有机器人的可编译走查 + 12 项自检清单、测试与覆盖率流程、FAQ、API 速查表、术语表 |
| doc/08-标准DH到改进DH的转换.md | 两套 DH 约定的坐标系差异、 |
| doc/09-API参考手册.md | 完整 API 参考手册:逐接口的签名/参数/返回值/异常/单位/注意事项,全部示例可编译运行;与 01–08 的分工是「01–08 讲原理推导,09 讲接口契约」 |
- 任务空间为 3 维或 6 维:模板参数
TASK_DIM只允许 3(位置任务[x,y,z])或 6 (位姿任务[x,y,z,rx,ry,rz],类内static_assert显式校验)。fk()始终返回完整 6 维位姿,jacob()/ik()按任务维数工作;关节数LINK_NUM可大于TASK_DIM以支持冗余机器人。 任意任务子空间(如平面位姿[x,y,φ])尚不支持。 - 仅支持串联开链:不包含闭链、并联机构、浮动基座与关节弹性。
闭链是独立的模型族,库中已通过
ChainTopo(SERIAL/ 预留的CLOSED)与robochain.hpp末尾的接口预留做出区分,以保证后续加入闭链类时不会与现有 API 冲突; 闭链的 FK/IK 本身尚未实现。 - 逆运动学为局部数值解法:收敛性依赖初值,可能落入局部极小或在大姿态误差下
需要更多迭代;不可达或被限位阻挡时抛出
std::runtime_error,不做随机重启。 另外,同一物理目标在DEG与RAD两种UNIT下的收敛路径可能不同(步长换算与限位 截断的单位不同引入舍入差异,使回溯线搜索的接受/拒绝翻转),极端目标可能出现其中一个 不收敛;若遇到,请改用另一种UNIT、调整初值或放宽tol。 - 逆运动学忽略零空间优化:冗余机器人返回最小范数解,不支持零空间自定义代价 (如关节行程中心化、避障)。
- 限位检查为严格区间检查:不做角度环绕归一化,
q必须在qlim内; 连续回转关节请把qlim设为足够宽的区间(如±infinity)。 - 无动力学:不含质量、惯量、重力、摩擦与力矩计算。
robodynamic.hpp目前只是占位 头文件(其中写明了规划中的RoboDynamics接口与算法),动力学与逆动力学尚未实现, 因此本库当前只做运动学。 - 关节状态按弧度存储、按
UNIT呈现:deg → rad → deg往返存在约 2 ULP 误差 (例如15°读回14.999999999999998°)。因此比较状态时请用≥1e-12的容差, 不要用逐位相等(isApprox(x, 0.0));ik(..., setj=true)回写的关节量同理。 robotype.h中DYNAMIC为int常量,与Eigen::Dynamic取值一致但不建议混用。vecmat的列数约定:以B.cols()作为观测列数q,非方阵情形要求A(m×n)、B(p×q)满足A X = Y B的一致性维数。
- 闭链/并联机构:实现
RoboClosedChain(多支链 + 闭环约束求解), 接口预留与差异说明见robospace/robochain.hpp末尾。难点在于被动关节求解与 平台雅可比的隐函数推导,计划复用现有的阻尼最小二乘与支链雅可比。 - 任务子空间扩展:支持任意任务矩阵(如平面位姿
[x, y, φ]、仅姿态任务)。 - 逆运动学增强:零空间投影(次级代价优化)、随机重启与多初值求解、 解析解与数值解混合(针对常见 6R 构型)。
- 轨迹规划:关节空间插值(梯形/S 曲线)、笛卡尔直线与圆弧插补, 新增独立模块承载。
- 动力学与逆动力学:在
robodynamic.hpp中实现递归牛顿-欧拉(RNEA)、 复合刚体算法(CRBA)、正动力学与辨识回归矩阵,并补充质量/惯量/摩擦模型。 - 标定能力完善:基于
vecmat的AX = YB手眼标定求解流程封装。 - 工具链:CI 中固化单元测试与覆盖率门限(行覆盖率 ≥ 80%), 增加静态分析与 Sanitizer(ASan/UBSan)流水线。
见 LICENSE。






