CppCon 2024 学习:Code Generation from Unified Robot Description Format (URDF) for Accelerated Roboti
URDF (Unified Robot Description Format) 是一种 XML 格式,用于描述机器人的结构信息,包括:机器人正向运动学的目标是给定关节向量 q=[q1,q2,...,qn]Tq = [q_1, q_2, ..., q_n]^Tq=[q1,q2,...,qn]T,计算末端执行器(End-Effector)在基座坐标系下的位置和姿态。如果机器人由 nnn 个关节和
1⃣ 背景:URDF 与代码生成
URDF (Unified Robot Description Format) 是一种 XML 格式,用于描述机器人的结构信息,包括:
- 关节(Joint):旋转或平移类型、自由度等
- 连杆(Link):物理尺寸、质量、几何形状等
- 父子关系(Parent-Child):机器人链结构
在机器人运动学计算中,我们希望从 URDF 自动生成 高效的 C++/Python/Matlab 代码,用来计算:
- 正向运动学(Forward Kinematics, FK)
- 雅可比矩阵(Jacobian, J)
- 甚至动力学(可扩展)
2⃣ 正向运动学公式
机器人正向运动学的目标是给定关节向量 q = [ q 1 , q 2 , . . . , q n ] T q = [q_1, q_2, ..., q_n]^T q=[q1,q2,...,qn]T,计算末端执行器(End-Effector)在基座坐标系下的位置和姿态。
如果机器人由 n n n 个关节和 n n n 个连杆组成,可以用齐次变换矩阵描述每个连杆相对上一连杆的位姿:
T l i l i − 1 ( q i ) T_{l_i}^{l_{i-1}}(q_i) Tlili−1(qi)
其中:
- T l i l i − 1 T_{l_i}^{l_{i-1}} Tlili−1 表示第 i i i 个连杆 l i l_i li 相对于其父连杆 l i − 1 l_{i-1} li−1 的位姿
- q i q_i qi 是关节 i i i 的角度(旋转关节)或位移(平移关节)
于是,整个末端执行器相对于机器人基座 b b b 的齐次变换矩阵为:
F K ( q ) = T l 1 b T l 2 l 1 T l 3 l 2 ⋯ T l n l n − 1 FK(q) = T_{l_1}^{b} T_{l_2}^{l_1} T_{l_3}^{l_2} \cdots T_{l_n}^{l_{n-1}} FK(q)=Tl1bTl2l1Tl3l2⋯Tlnln−1
解释:
这条公式表示我们将每个关节的相对变换矩阵依次相乘,就得到末端在基座坐标系下的整体位姿。
对应编程实现时,可以直接按链式循环生成矩阵乘法代码。
3⃣ 雅可比矩阵公式
雅可比矩阵 J ( q ) J(q) J(q) 描述了 关节速度向量 q ˙ \dot{q} q˙ 与末端速度向量 v v v 之间的线性关系:
v = J ( q ) q ˙ v = J(q) \dot{q} v=J(q)q˙
- v ∈ R 6 v \in \mathbb{R}^6 v∈R6:末端线速度和角速度(通常前 3 行是平移,后 3 行是旋转)
- q ˙ ∈ R n \dot{q} \in \mathbb{R}^n q˙∈Rn:关节速度
如果只考虑末端位置(忽略姿态),位置雅可比为:
J ( q ) = [ ∂ x ∂ q 1 ∂ x ∂ q 2 ∂ x ∂ q 3 ⋯ ∂ y ∂ q 1 ∂ y ∂ q 2 ∂ y ∂ q 3 ⋯ ∂ z ∂ q 1 ∂ z ∂ q 2 ∂ z ∂ q 3 ⋯ ] J(q) = \begin{bmatrix} \frac{\partial x}{\partial q_1} & \frac{\partial x}{\partial q_2} & \frac{\partial x}{\partial q_3} & \cdots \\ \frac{\partial y}{\partial q_1} & \frac{\partial y}{\partial q_2} & \frac{\partial y}{\partial q_3} & \cdots \\ \frac{\partial z}{\partial q_1} & \frac{\partial z}{\partial q_2} & \frac{\partial z}{\partial q_3} & \cdots \\ \end{bmatrix} J(q)= ∂q1∂x∂q1∂y∂q1∂z∂q2∂x∂q2∂y∂q2∂z∂q3∂x∂q3∂y∂q3∂z⋯⋯⋯ - 每一列对应一个关节对末端位置的贡献
- 对旋转关节: ∂ p ∂ q i = z i − 1 × ( p e e − o i − 1 ) \frac{\partial p}{\partial q_i} = z_{i-1} \times (p_{ee} - o_{i-1}) ∂qi∂p=zi−1×(pee−oi−1)
- 对平移关节: ∂ p ∂ q i = z i − 1 \frac{\partial p}{\partial q_i} = z_{i-1} ∂qi∂p=zi−1
解释:
雅可比矩阵可以理解为“关节速度如何映射到末端速度”。
自动生成代码时,我们只需要对每个关节计算其对末端位置的偏导数,然后按列组装矩阵即可。
4⃣ 从 URDF 到高效代码
- 解析 URDF
- 读取每个 link 的父子关系
- 读取每个 joint 的类型、轴向、初始值
- 生成 FK 矩阵代码
- 对每个关节生成 T l i l i − 1 ( q i ) T_{l_i}^{l_{i-1}}(q_i) Tlili−1(qi)
- 按链乘法组合
- 生成雅可比矩阵代码
- 对每个关节生成偏导数公式
- 对旋转关节用叉乘公式
- 对平移关节直接使用关节轴
- 优化与加速
- 尽量展开矩阵乘法,减少循环
- 使用 SIMD 或 Eigen 库矩阵运算加速
5⃣ 总结
- FK:依次相乘每个关节的齐次变换矩阵即可
- J:列对应每个关节的速度对末端的贡献
- URDF → 代码生成:通过解析 URDF 自动生成 FK/J 公式,实现快速计算
如果你愿意,我可以帮你画一张 示意图,把: - 连杆 l 1 , l 2 , . . . l_1, l_2, ... l1,l2,...
- 关节 q 1 , q 2 , . . . q_1, q_2, ... q1,q2,...
- FK 矩阵相乘
- 雅可比矩阵列的含义
1⃣ 机械臂假设
- 三个旋转关节 q 1 , q 2 , q 3 q_1, q_2, q_3 q1,q2,q3
- 连杆长度分别为 L 1 , L 2 , L 3 L_1, L_2, L_3 L1,L2,L3
- 末端执行器在 z z z 方向延伸
机器人结构如下(简化平面示意):
Base ---[q1]--- L1 ---[q2]--- L2 ---[q3]--- L3 --- End-Effector
2⃣ 正向运动学 (FK)
每个关节的变换矩阵 T i T_i Ti(齐次矩阵)为:
T 1 = [ cos q 1 − sin q 1 0 L 1 cos q 1 sin q 1 cos q 1 0 L 1 sin q 1 0 0 1 0 0 0 0 1 ] T_1 = \begin{bmatrix} \cos q_1 & -\sin q_1 & 0 & L_1 \cos q_1 \\ \sin q_1 & \cos q_1 & 0 & L_1 \sin q_1 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T1=
cosq1sinq100−sinq1cosq1000010L1cosq1L1sinq101
T 2 = [ cos q 2 − sin q 2 0 L 2 cos q 2 sin q 2 cos q 2 0 L 2 sin q 2 0 0 1 0 0 0 0 1 ] T_2 = \begin{bmatrix} \cos q_2 & -\sin q_2 & 0 & L_2 \cos q_2 \\ \sin q_2 & \cos q_2 & 0 & L_2 \sin q_2 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T2=
cosq2sinq200−sinq2cosq2000010L2cosq2L2sinq201
T 3 = [ cos q 3 − sin q 3 0 L 3 cos q 3 sin q 3 cos q 3 0 L 3 sin q 3 0 0 1 0 0 0 0 1 ] T_3 = \begin{bmatrix} \cos q_3 & -\sin q_3 & 0 & L_3 \cos q_3 \\ \sin q_3 & \cos q_3 & 0 & L_3 \sin q_3 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T3=
cosq3sinq300−sinq3cosq3000010L3cosq3L3sinq301
于是末端位姿:
F K ( q ) = T 1 ⋅ T 2 ⋅ T 3 FK(q) = T_1 \cdot T_2 \cdot T_3 FK(q)=T1⋅T2⋅T3
展开末端坐标 ( x , y ) (x, y) (x,y):
x = L 1 cos q 1 + L 2 cos ( q 1 + q 2 ) + L 3 cos ( q 1 + q 2 + q 3 ) x = L_1 \cos q_1 + L_2 \cos(q_1 + q_2) + L_3 \cos(q_1 + q_2 + q_3) x=L1cosq1+L2cos(q1+q2)+L3cos(q1+q2+q3)
y = L 1 sin q 1 + L 2 sin ( q 1 + q 2 ) + L 3 sin ( q 1 + q 2 + q 3 ) y = L_1 \sin q_1 + L_2 \sin(q_1 + q_2) + L_3 \sin(q_1 + q_2 + q_3) y=L1sinq1+L2sin(q1+q2)+L3sin(q1+q2+q3)
3⃣ 雅可比矩阵 (J)
雅可比矩阵公式(位置部分):
J ( q ) = [ ∂ x ∂ q 1 ∂ x ∂ q 2 ∂ x ∂ q 3 ∂ y ∂ q 1 ∂ y ∂ q 2 ∂ y ∂ q 3 ] J(q) = \begin{bmatrix} \frac{\partial x}{\partial q_1} & \frac{\partial x}{\partial q_2} & \frac{\partial x}{\partial q_3} \\ \frac{\partial y}{\partial q_1} & \frac{\partial y}{\partial q_2} & \frac{\partial y}{\partial q_3} \end{bmatrix} J(q)=[∂q1∂x∂q1∂y∂q2∂x∂q2∂y∂q3∂x∂q3∂y]
计算偏导数:
∂ x ∂ q 1 = − L 1 sin q 1 − L 2 sin ( q 1 + q 2 ) − L 3 sin ( q 1 + q 2 + q 3 ) \frac{\partial x}{\partial q_1} = -L_1 \sin q_1 - L_2 \sin(q_1 + q_2) - L_3 \sin(q_1 + q_2 + q_3) ∂q1∂x=−L1sinq1−L2sin(q1+q2)−L3sin(q1+q2+q3)
∂ x ∂ q 2 = − L 2 sin ( q 1 + q 2 ) − L 3 sin ( q 1 + q 2 + q 3 ) \frac{\partial x}{\partial q_2} = -L_2 \sin(q_1 + q_2) - L_3 \sin(q_1 + q_2 + q_3) ∂q2∂x=−L2sin(q1+q2)−L3sin(q1+q2+q3)
∂ x ∂ q 3 = − L 3 sin ( q 1 + q 2 + q 3 ) \frac{\partial x}{\partial q_3} = -L_3 \sin(q_1 + q_2 + q_3) ∂q3∂x=−L3sin(q1+q2+q3)
∂ y ∂ q 1 = L 1 cos q 1 + L 2 cos ( q 1 + q 2 ) + L 3 cos ( q 1 + q 2 + q 3 ) \frac{\partial y}{\partial q_1} = L_1 \cos q_1 + L_2 \cos(q_1 + q_2) + L_3 \cos(q_1 + q_2 + q_3) ∂q1∂y=L1cosq1+L2cos(q1+q2)+L3cos(q1+q2+q3)
∂ y ∂ q 2 = L 2 cos ( q 1 + q 2 ) + L 3 cos ( q 1 + q 2 + q 3 ) \frac{\partial y}{\partial q_2} = L_2 \cos(q_1 + q_2) + L_3 \cos(q_1 + q_2 + q_3) ∂q2∂y=L2cos(q1+q2)+L3cos(q1+q2+q3)
∂ y ∂ q 3 = L 3 cos ( q 1 + q 2 + q 3 ) \frac{\partial y}{\partial q_3} = L_3 \cos(q_1 + q_2 + q_3) ∂q3∂y=L3cos(q1+q2+q3)
所以雅可比矩阵为:
$$
J(q) =
\begin{bmatrix}
- L_1 \sin q_1 - L_2 \sin(q_1+q_2) - L_3 \sin(q_1+q_2+q_3) & - L_2 \sin(q_1+q_2) - L_3 \sin(q_1+q_2+q_3) & - L_3 \sin(q_1+q_2+q_3) \
L_1 \cos q_1 + L_2 \cos(q_1+q_2) + L_3 \cos(q_1+q_2+q_3) & L_2 \cos(q_1+q_2) + L_3 \cos(q_1+q_2+q_3) & L_3 \cos(q_1+q_2+q_3)
\end{bmatrix}
$$
4⃣ 从 URDF 到代码生成
- 解析 URDF:得到关节数量 3,类型旋转,长度 L 1 , L 2 , L 3 L_1, L_2, L_3 L1,L2,L3
- 生成 FK 代码:
double x = L1*cos(q1) + L2*cos(q1+q2) + L3*cos(q1+q2+q3);
double y = L1*sin(q1) + L2*sin(q1+q2) + L3*sin(q1+q2+q3);
- 生成雅可比矩阵代码:
J[0][0] = -L1*sin(q1) - L2*sin(q1+q2) - L3*sin(q1+q2+q3);
J[0][1] = -L2*sin(q1+q2) - L3*sin(q1+q2+q3);
J[0][2] = -L3*sin(q1+q2+q3);
J[1][0] = L1*cos(q1) + L2*cos(q1+q2) + L3*cos(q1+q2+q3);
J[1][1] = L2*cos(q1+q2) + L3*cos(q1+q2+q3);
J[1][2] = L3*cos(q1+q2+q3);
这段代码就是从 URDF 自动生成的 FK/J 代码示例,完全展开避免了循环计算,效率更高。
动机(Motivation)
1⃣ 高速运行的必要性
一些算法需要极高的运行速度才能达到最佳性能。
尤其是对于被高成本算法反复调用的公共子例程,速度至关重要。
举例:
- 机器人路径规划算法 RRT(Rapidly-exploring Random Tree)依赖于:
- 最近邻查找(Nearest Neighbor Lookup)
- 正向运动学(Forward Kinematics, FK)
- 碰撞检测(Collision Detection)
这些子例程如果运行缓慢,会直接拖慢整个算法的效率。
2⃣ 基于向量化的采样规划(Vectorized Sampling-Based Planning)
论文《Motions in Microseconds via Vectorized Sampling-Based Planning》(Wil Thomason, Zachary Kingston, Lydia E. Kavraki)提出:
- 相比最先进方法,性能提升超过 500 × 500\times 500×
- 主要思路:从 URDF 文件生成优化代码
- 优化方法包括:
- 构建数据结构以优化 SIMD(单指令多数据)执行
- 跳过不必要的计算,例如:
- 合并固定关节
- 展开循环(Loop Unrolling)
理解:
URDF 描述了机器人结构,编译器解析后生成专门针对该机器人的高效 SIMD 代码,使常用子例程运行速度极快。
3⃣ 硬件特定优化的优势与局限
硬件厂商编写的软件有天然优势:
- 可针对特定平台进行优化,充分利用硬件知识
- 举例:带球形腕(spherical wrist)的机器人
- 逆向运动学(IK)可以分两步:
- 求腕部角度,使末端达到期望姿态
- 求位置值,使末端达到期望笛卡尔位置 [ x , y , z ] [x, y, z] [x,y,z]
- 一般的通用 IK 求解器无法利用这种特定知识
局限性:
- 逆向运动学(IK)可以分两步:
- 硬件特定实现通常可扩展性有限
- 硬件更新不频繁(除了附加设备,如相机和末端执行器)
4⃣ 编译代码的优势
通过 代码生成(Compiler)生成的代码有以下优点:
- 可测试性
- 可以测试内存分配情况
- 可验证实时性(Real-time Compatibility)
- 对安全关键应用(如外科手术机器人)至关重要
- 硬件特定优化
- 因为代码在每台机器上源代码编译,可以启用特定 CPU 优化
- 相比模板化机器人构建器(Templated Robot Builder)的优势:
- 不易出错(Less likely to make mistakes)
- 不必为每台机器人单独维护代码
总结:
通过 URDF 文件生成专用的高效代码,不仅加速了常用子例程,还保证了安全性、可测试性和硬件优化能力,是通用模板方法无法比拟的。
手动实现正向运动学
xiaqiu@xz:~/test/CppCon/day513/code$ tree
.
├── CMakeLists.txt
├── data
│ └── robot.urdf
└── forwardKinematicsTest.cpp
2 directories, 3 files
1.安装必要的依赖
sudo apt update
sudo apt install -y \
liborocos-kdl-dev \
liburdfdom-dev \
liburdfdom-headers-dev \
libkdl-parser-dev \
libconsole-bridge-dev \
libtinyxml2-dev \
libeigen3-dev \
libboost-all-dev
2.创建 forwardKinematicsTest.cpp:
#include <iostream>
#include <kdl/chain.hpp>
#include <kdl/chainfksolverpos_recursive.hpp>
#include <kdl/frames.hpp>
#include <kdl_parser/kdl_parser.hpp>
#include <urdf/model.h>
int main() {
// 加载 URDF 文件
urdf::Model model;
if (!model.initFile("data/robot.urdf")) {
std::cerr << "Failed to parse URDF file" << std::endl;
return -1;
}
// 创建 KDL 树
KDL::Tree kdl_tree;
if (!kdl_parser::treeFromUrdfModel(model, kdl_tree)) {
std::cerr << "Failed to construct KDL tree" << std::endl;
return -1;
}
// 提取运动链
KDL::Chain chain;
if (!kdl_tree.getChain("base_link", "tool0", chain)) {
std::cerr << "Failed to get chain from tree" << std::endl;
return -1;
}
// 创建正向运动学求解器
KDL::ChainFkSolverPos_recursive fk_solver(chain);
// 设置关节角度
KDL::JntArray joint_positions(chain.getNrOfJoints());
for (unsigned int i = 0; i < chain.getNrOfJoints(); i++) {
joint_positions(i) = 0.0; // 全部设为 0
}
// 计算正向运动学
KDL::Frame end_effector_pose;
int result = fk_solver.JntToCart(joint_positions, end_effector_pose);
if (result >= 0) {
std::cout << "End-effector position: " << std::endl;
std::cout << "x: " << end_effector_pose.p.x() << std::endl;
std::cout << "y: " << end_effector_pose.p.y() << std::endl;
std::cout << "z: " << end_effector_pose.p.z() << std::endl;
} else {
std::cerr << "FK computation failed" << std::endl;
return -1;
}
return 0;
}
2. 对应的简化 CMakeLists.txt:
cmake_minimum_required(VERSION 3.22)
project(fast_forward_kinematics)
# ========================================================
# 设置 C++ 标准
# ========================================================
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ========================================================
# Boost 警告修复
#
# 新版 Boost 会因为绑定占位符(_1,_2...) 导致旧代码警告:
# warning: use of deprecated header ‘boost/bind.hpp’
# 解决方法:加入 BOOST_BIND_GLOBAL_PLACEHOLDERS
# ========================================================
add_definitions(-DBOOST_BIND_GLOBAL_PLACEHOLDERS)
# ========================================================
# 查找第三方依赖库
# ========================================================
find_package(Eigen3 REQUIRED)
find_package(Boost REQUIRED COMPONENTS system filesystem)
# ========================================================
# 通过 pkg-config 查找机器人学库 KDL, URDFDOM, KDL_PARSER
# ========================================================
find_package(PkgConfig REQUIRED)
pkg_check_modules(OROCOS_KDL REQUIRED orocos-kdl)
pkg_check_modules(URDFDOM REQUIRED urdfdom)
pkg_check_modules(KDL_PARSER REQUIRED kdl_parser)
# ========================================================
# 创建可执行文件
# ========================================================
add_executable(forward_kinematics_test
forwardKinematicsTest.cpp
)
# ========================================================
# 设置 include 路径
#
# PRIVATE 表示仅对当前 target 可见
# ========================================================
target_include_directories(forward_kinematics_test
PRIVATE
${EIGEN3_INCLUDE_DIR}
${OROCOS_KDL_INCLUDE_DIRS}
${URDFDOM_INCLUDE_DIRS}
${KDL_PARSER_INCLUDE_DIRS}
/usr/include/eigen3 # 某些系统 Eigen 安装在这里
)
# ========================================================
# 链接库文件
#
# KDL, URDFDOM, KDL_PARSER 都是依赖 URDF 与运动学解析的重要库
# Boost 用于文件系统/系统功能
# ========================================================
target_link_libraries(forward_kinematics_test
PRIVATE
${OROCOS_KDL_LIBRARIES}
${URDFDOM_LIBRARIES}
${KDL_PARSER_LIBRARIES}
${Boost_LIBRARIES}
)
# ========================================================
# 链接目录(.so 库所在位置)
# ========================================================
target_link_directories(forward_kinematics_test
PRIVATE
${OROCOS_KDL_LIBRARY_DIRS}
${URDFDOM_LIBRARY_DIRS}
${KDL_PARSER_LIBRARY_DIRS}
)
# ========================================================
# 资源文件自动复制:robot.urdf
#
# 场景:
# 你的项目包含资源文件 data/robot.urdf(机器人结构)
# 你希望构建之后可执行文件能够直接找到这个文件
#
# 目标:
# 自动将 source/data/robot.urdf 拷贝到 build/data/robot.urdf
# ========================================================
# 资源目录(源码目录)
set(DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data")
# 具体文件路径
set(DATA_FILE "${DATA_DIR}/robot.urdf")
# build 时输出的目标目录,例如 build/data/
set(OUTPUT_DATA_DIR "${CMAKE_CURRENT_BINARY_DIR}/data")
# ========================================================
# 添加自定义构建步骤:拷贝 URDF 文件
#
# POST_BUILD:
# 表示在 target 编译完成后执行
#
# -E make_directory:
# 如果目录不存在则创建 build/data/
#
# -E copy_if_different:
# 如果文件内容不同则复制;如果相同则跳过
#
# COMMENT:
# 构建时在终端输出提示
# ========================================================
add_custom_command(
TARGET forward_kinematics_test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${OUTPUT_DATA_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${DATA_FILE}"
"${OUTPUT_DATA_DIR}/robot.urdf"
COMMENT "Copying robot.urdf to build directory"
)
方案 4:创建简单的 URDF 测试文件
如果你没有 data/robot.urdf,创建一个简单的:
mkdir -p data
data/robot.urdf:
<?xml version="1.0"?>
<robot name="simple_robot">
<link name="base_link">
<visual>
<geometry>
<box size="0.1 0.1 0.1"/>
</geometry>
</visual>
</link>
<link name="link1">
<visual>
<geometry>
<cylinder length="0.5" radius="0.05"/>
</geometry>
</visual>
</link>
<joint name="joint1" type="revolute">
<parent link="base_link"/>
<child link="link1"/>
<origin xyz="0 0 0.1" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit lower="-3.14" upper="3.14" effort="100" velocity="1.0"/>
</joint>
<link name="tool0">
<visual>
<geometry>
<sphere radius="0.05"/>
</geometry>
</visual>
</link>
<joint name="joint2" type="revolute">
<parent link="link1"/>
<child link="tool0"/>
<origin xyz="0 0 0.5" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<limit lower="-3.14" upper="3.14" effort="100" velocity="1.0"/>
</joint>
</robot>
编译和运行
mkdir -p build && cd build
cmake ..
make
./forward_kinematics_test
总结
最可能的情况是你需要:
- 安装 KDL 相关库
- 创建或获取
forwardKinematicsTest.cpp文件 - 准备 URDF 文件
xiaqiu@xz:~/test/CppCon/day513/code/build$ ./forward_kinematics_test
End-effector position:
x: 0
y: 0
z: 0.6
xiaqiu@xz:~/test/CppCon/day513/code/build$
机器人运动学库 - 项目重构
用户级安装(不需要 sudo)
pip3 install --user urdf-parser-py
或者使用虚拟环境
python3 -m venv ~/venv
source ~/venv/bin/activate
pip install urdf-parser-py
pip install jinja2
pip install numpy
然后使用虚拟环境的 Python
cmake … -DBUILD_TESTING=ON
熟悉一下ppt的代码
https://github.com/pac48/fast_robot_kinematics
项目结构
xiaqiu@xz:~/fast_robot_kinematics-main$ tree -L 4
├── CMakeLists.txt # 顶层 CMake 构建文件,配置整个工程
├── README.md # 项目说明文档(使用方法/构建示例)
├── build # 构建输出目录(通常由 CMake 生成)
│ └── test # 单元测试构建输出
│ ├── CMakeFiles # CMake 自动生成的内部文件
│ ├── cmake_install.cmake # 安装脚本(由 CMake 生成)
│ ├── forward_kinematics_lib.cpp # 模板展开后的 FK 内核实际生成代码(CPU)
│ ├── forward_kinematics_test # 正向运动学测试可执行文件
│ ├── inverse_kinematics_test # 逆向运动学测试可执行文件
│ └── libfast_forward_kinematics_library.so # 生成的共享库(主要产物)
├── code_generation # 代码生成相关脚本与模板
│ ├── get_num_joints.py # 从 URDF 解析并返回机器人关节数的脚本
│ ├── robot_config.cpp.template # 生成 CPU 版机器人 FK/IK 内核代码模板
│ ├── robot_config.cu.template # 生成 GPU (CUDA) 版 FK/IK 内核代码模板
│ └── robot_gen.py # 核心生成器:解析 URDF→填充模板→生成 C++ 代码
├── fast_forward_kinematics-config.cmake.in # CMake package 配置输入模板(用于 find_package)
├── fast_forward_kinematics.cmake # 安装/导出包的配置文件
├── include # 项目对外暴露的头文件
│ └── fast_forward_kinematics
│ ├── fast_inverse_kinematics.hpp # 快速 IK(基于解析/梯度)C++ 接口
│ ├── fast_kinematics.hpp # 快速 FK(模板展开)C++ API
│ └── kinematics_interface.hpp # FK/IK 通用接口定义(抽象层)
├── perf_stat # 性能测试/benchmark 工具目录(可选)
└── test # 测试目录(包含 C++ 参考实现和单测)
├── CMakeLists.txt # 测试项目 CMake 配置文件
├── build # 测试的独立构建目录
│ ├── CMakeCache.txt # 测试项目的 CMake cache
│ └── CMakeFiles # CMake 生成的内部文件
│ ├── 3.28.3 # CMake 内部版本目录
│ ├── CMakeConfigureLog.yaml # 配置日志
│ ├── CMakeScratch # CMake 的 scratch 工作区
│ ├── cmake.check_cache # Cache 检查文件
│ └── pkgRedirects # 包重定向(由 CMake 生成)
├── fetch_content_test # 测试 find_package / FetchContent 示例
│ ├── CMakeLists.txt # 测试项目 CMake 配置
│ └── urdf
│ └── robot.urdf # 简单示例用 URDF(测试 FetchContent)
├── forward_kinematics_test.cpp # FK 功能测试(对比 KDL 与生成版)
├── include
│ └── kdl_kinematics.hpp # KDL 参考实现,用于对比验证
├── inverse_kinematics_test.cpp # IK 单元测试
├── kdl
│ └── kdl_implementation.cpp # KDL 库运动学的对照实现
└── urdf
├── pr2.urdf # PR2 机器人 URDF
├── robot.urdf # 简单机器人 URDF(测试用)
└── ur5.urdf # UR5 机械臂 URDF(主测试模型)
60 directories, 226 files
xiaqiu@xz:~/fast_robot_kinematics-main$
├── CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
# 指定最低 CMake 版本。项目使用了较新的功能,因此要求 3.22 或以上。
project(fast_forward_kinematics)
# 定义项目名称 fast_forward_kinematics
set(FFK_VERSION_MAJOR 0)
set(FFK_VERSION_MINOR 1)
set(FFK_VERSION ${FFK_VERSION_MAJOR}.${FFK_VERSION_MINOR})
# 定义项目版本号(major.minor),用于安装与包管理。
option(BUILD_TESTING "Build fast_forward_kinematics tests" OFF)
# 定义可选项:是否构建测试。默认 OFF。
# 用户可通过 -DBUILD_TESTING=ON 来启用。
# Include GNUInstallDirs to get canonical paths
include(GNUInstallDirs)
# 引入 GNUInstallDirs,以支持标准安装路径变量,例如:
# CMAKE_INSTALL_INCLUDEDIR → include
# CMAKE_INSTALL_LIBDIR → lib 或 lib64
# CMAKE_INSTALL_DATADIR → share
include(CTest)
# 引入 CTest,用于启用 testing 框架(但是否添加测试取决于 BUILD_TESTING)
# Add cmake macro to project
include(fast_forward_kinematics.cmake)
# 引入本项目提供的 CMake 宏模块(用于生成 FK/IK 代码、检查依赖等)
add_library(fast_forward_kinematics_header INTERFACE)
# 定义一个 INTERFACE 库:
# - 不编译源文件
# - 用于向外暴露 include 路径与依赖关系
target_include_directories(
fast_forward_kinematics_header
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
# 构建阶段的 include 路径(源代码目录下的 include)
$<INSTALL_INTERFACE:include/fast_forward_kinematics>
# 安装后使用时的 include 路径(安装到前缀下)
)
# INTERFACE 关键字表示:任何 link 到此库的目标都会继承其 include 路径
if(BUILD_TESTING)
add_subdirectory(test)
# 若开启测试选项,则进入 test/ 目录构建单元测试
endif()
include(CMakePackageConfigHelpers)
# 引入 CMake 工具包,提供生成 package config 文件的辅助工具
configure_package_config_file(
fast_forward_kinematics-config.cmake.in
fast_forward_kinematics-config.cmake
# 使用 .in 模板生成包配置文件 fast_forward_kinematics-config.cmake
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/fast_forward_kinematics"
# 指定配置文件安装位置(通常是 lib/cmake/...)
PATH_VARS CMAKE_INSTALL_INCLUDEDIR CMAKE_INSTALL_LIBDIR
# 使模板可引用路径变量
NO_CHECK_REQUIRED_COMPONENTS_MACRO
# 不自动生成 REQUIRED COMPONENTS 检查宏
)
write_basic_package_version_file(
fast_forward_kinematics-config-version.cmake
VERSION ${FFK_VERSION}
COMPATIBILITY AnyNewerVersion)
# 生成版本检查文件,确保 find_package 时遵守版本兼容要求
install(
FILES
"${CMAKE_BINARY_DIR}/fast_forward_kinematics-config.cmake"
"${CMAKE_BINARY_DIR}/fast_forward_kinematics-config-version.cmake"
"${CMAKE_SOURCE_DIR}/fast_forward_kinematics.cmake"
DESTINATION "${CMAKE_INSTALL_DATADIR}/fast_forward_kinematics/cmake")
# 安装 package config 文件和本项目的 cmake 模块。
# DATADIR 通常是 share/fast_forward_kinematics/cmake
install(DIRECTORY "${CMAKE_SOURCE_DIR}/code_generation"
DESTINATION "${CMAKE_INSTALL_DATADIR}/fast_forward_kinematics/cmake")
# 安装 code_generation 目录(包括 Python 模板和生成脚本)
install(DIRECTORY include/
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/fast_forward_kinematics")
# 安装 include/ 下的所有头文件到 include/fast_forward_kinematics
# Create an export set
install(TARGETS fast_forward_kinematics_header
EXPORT fast_forward_kinematicsTargets)
# 将 INTERFACE 库导出为 fast_forward_kinematicsTargets 集合,
# 供 find_package 使用
# Targets files
export(
EXPORT fast_forward_kinematicsTargets
FILE ${CMAKE_CURRENT_BINARY_DIR}/fast_forward_kinematics-targets.cmake)
# 在 build 目录生成 targets 文件(供 build-tree 使用)
install(
EXPORT fast_forward_kinematicsTargets
FILE fast_forward_kinematics-targets.cmake
DESTINATION "${CMAKE_INSTALL_DATADIR}/fast_forward_kinematics/cmake")
# 安装 targets 文件,使安装后的包可被其他项目 find_package 使用
CMakeLists.txt:逐段详细理解(含解释原理)
1. CMake 版本要求
cmake_minimum_required(VERSION 3.22)
理解
指定项目所需的 最低 CMake 版本。
为什么需要 ≥ 3.22?
因为:
- 本项目使用了
$<BUILD_INTERFACE:...>$和$<INSTALL_INTERFACE:...>$generator expressions - 使用
configure_package_config_file()(需要 CMake 3.14+) - 使用更规范的
GNUInstallDirs(推荐 3.16+) - 为确保包导出 (
export,install(EXPORT)) 兼容现代 CMake
因此 CMake 3.22 可视为一个安全的兼容下限。
2. 项目定义
project(fast_forward_kinematics)
定义项目名,在 CMake 内部作为命名空间使用,如:
${PROJECT_SOURCE_DIR}${PROJECT_NAME}${PROJECT_BINARY_DIR}
3. 定义版本号变量
set(FFK_VERSION_MAJOR 0)
set(FFK_VERSION_MINOR 1)
set(FFK_VERSION ${FFK_VERSION_MAJOR}.${FFK_VERSION_MINOR})
版本号为 0.1 0.1 0.1。
作用
- 用于生成
fast_forward_kinematics-config-version.cmake - 用户执行
find_package(fast_forward_kinematics 0.1 REQUIRED)时会进行版本匹配 - 版本号后续可能随代码生成模板升级而更新
4. 可选项:是否构建测试
option(BUILD_TESTING "Build fast_forward_kinematics tests" OFF)
理解
定义一个布尔开关,供用户指定是否构建测试。
默认:OFF
用户可以通过命令启用:
cmake -DBUILD_TESTING=ON ..
背后原理
CMake 内部 OPTION 就相当于:
if user sets it → use that
else → default value
5. GNUInstallDirs
include(GNUInstallDirs)
理解
这个模块用于提供一组 跨平台、遵从 GNU/Linux 目录规范的路径变量:
常见变量与含义:
| 变量名 | 含义 |
|---|---|
${CMAKE_INSTALL_INCLUDEDIR} |
通常为 include 或 include/<project> |
${CMAKE_INSTALL_LIBDIR} |
lib 或 lib64 |
${CMAKE_INSTALL_DATADIR} |
share |
为什么要用它?
因为不同操作系统的 lib 目录可能不一致,如:
- Ubuntu:
lib - Fedora:
lib64 - macOS:
lib
使用 GNUInstallDirs 可以自动适配。
6. 引入 CTest(可选测试)
include(CTest)
让 CMake 支持:
enable_testing()add_test()
但实际是否执行测试仍由BUILD_TESTING控制。
7. 引入自定义的 CMake 宏文件
include(fast_forward_kinematics.cmake)
这个文件通常包含以下功能:
- FK/IK 代码生成宏
- URDF 解析支持
- Python 脚本调用
- 生成机器人专属 C++ 代码
即:
它定义了整个库最重要的“代码生成流水线”。
8. 定义 INTERFACE 头文件库
add_library(fast_forward_kinematics_header INTERFACE)
INTERFACE 库:
- 没有源文件
- 不编译
- 只携带:
- include path
- 编译选项
- 链接依赖
这是 header-only库的标准写法。
9. 为 header-only 库定义 include 路径
target_include_directories(
fast_forward_kinematics_header
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/fast_forward_kinematics>
)
理解
| 场景 | 功能 |
|---|---|
$<BUILD_INTERFACE:...> |
构建阶段包含源代码目录 |
$<INSTALL_INTERFACE:...> |
安装后包含最终安装路径 |
为什么 INSTALL_INTERFACE 不是 include/ 而是 include/fast_forward_kinematics?
因为我们在 install() 中这样安装:
install(DIRECTORY include/
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/fast_forward_kinematics")
所以安装后的路径是:
<prefix>/include/fast_forward_kinematics/fast_kinematics.hpp
10. 构建测试(如果开启)
if(BUILD_TESTING)
add_subdirectory(test)
endif()
进入 test/ 目录构建 FK/IK 测试程序。
11. 引入 PackageConfig 辅助工具
include(CMakePackageConfigHelpers)
用于生成:
fast_forward_kinematics-config.cmakefast_forward_kinematics-config-version.cmake
让其他项目可以:
find_package(fast_forward_kinematics)
12. 生成 fast_forward_kinematics-config.cmake
configure_package_config_file(
fast_forward_kinematics-config.cmake.in
fast_forward_kinematics-config.cmake
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/fast_forward_kinematics"
PATH_VARS CMAKE_INSTALL_INCLUDEDIR CMAKE_INSTALL_LIBDIR
NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
这是 CMake 包配置的核心部分。
工作流程
- 读取
.in模板 - 替换模板变量
- 生成
.cmake配置文件
13. 生成版本检查文件
write_basic_package_version_file(
fast_forward_kinematics-config-version.cmake
VERSION ${FFK_VERSION}
COMPATIBILITY AnyNewerVersion)
COMPATIBILITY=AnyNewerVersion 的含义:
允许用户请求的版本 ≤ 当前版本 \text{允许用户请求的版本} \le \text{当前版本} 允许用户请求的版本≤当前版本
也就是说只要安装的版本更新或相等即可。
14. 安装 CMake 配置文件
install(
FILES
"${CMAKE_BINARY_DIR}/fast_forward_kinematics-config.cmake"
"${CMAKE_BINARY_DIR}/fast_forward_kinematics-config-version.cmake"
"${CMAKE_SOURCE_DIR}/fast_forward_kinematics.cmake"
DESTINATION "${CMAKE_INSTALL_DATADIR}/fast_forward_kinematics/cmake")
这让其他项目可以:
find_package(fast_forward_kinematics)
15. 安装 code_generation(代码生成脚本)
install(DIRECTORY "${CMAKE_SOURCE_DIR}/code_generation"
DESTINATION "${CMAKE_INSTALL_DATADIR}/fast_forward_kinematics/cmake")
这意味着安装后即可使用生成逻辑:
robot_gen.py
robot_config.cpp.template
robot_config.cu.template
get_num_joints.py
这是 FK/IK 自动生成的核心。
16. 安装头文件
install(DIRECTORY include/
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/fast_forward_kinematics")
安装后结构示例:
prefix/include/fast_forward_kinematics/fast_kinematics.hpp
prefix/include/fast_forward_kinematics/fast_inverse_kinematics.hpp
17. 安装和导出 INTERFACE 目标
install(TARGETS fast_forward_kinematics_header
EXPORT fast_forward_kinematicsTargets)
创建一个 target 集合,让 find_package() 能正确导出目标。
18. 在 build 目录生成 targets.cmake
export(
EXPORT fast_forward_kinematicsTargets
FILE ${CMAKE_CURRENT_BINARY_DIR}/fast_forward_kinematics-targets.cmake)
build-tree 可直接使用(无需安装)。
19. 安装 target 导出文件
install(
EXPORT fast_forward_kinematicsTargets
FILE fast_forward_kinematics-targets.cmake
DESTINATION "${CMAKE_INSTALL_DATADIR}/fast_forward_kinematics/cmake")
完成 CMake 包结构:
fast_forward_kinematics-config.cmake
fast_forward_kinematics-config-version.cmake
fast_forward_kinematics-targets.cmake
完整理解总结
这份 CMakeLists.txt 的目的可以总结为:
1. 定义一个 header-only 的高性能 FK/IK 运动学库
2. 提供完整的 CMake 包导出(供 find_package 使用)
3. 安装必要的代码生成脚本
4. 可选构建测试
5. 在 build-tree 与 install-tree 中均可正确使用
└── test
├── CMakeLists.txt
set(CMAKE_CXX_STANDARD 20)
# 使用 C++20 标准进行编译。
if(NOT DEFINED CMAKE_BUILD_TYPE OR "${CMAKE_BUILD_TYPE}" STREQUAL "")
set(CMAKE_BUILD_TYPE Release)
endif()
# 若用户未指定构建类型,则默认为 Release。
# CMake 默认是空构建类型,会导致编译器不启用优化,因此这里强制指定 Release。
add_executable(forward_kinematics_test forward_kinematics_test.cpp)
add_executable(inverse_kinematics_test inverse_kinematics_test.cpp)
# 定义两个测试可执行程序:正向 / 逆向运动学。
set(URDF_FILE ${CMAKE_SOURCE_DIR}/test/urdf/robot.urdf)
# 机器人 URDF 文件路径
set(ROOT base_link)
set(TIP grasp_link)
# 运动学链的起点和终点(基坐标系 → 末端执行器)
# generate FK/IK implementation
set(FK_IMPL "FFK")
# 设置运动学实现方式:
# - FFK:快速 CPU SIMD 实现(默认)
# - FFK_CUDA:CUDA GPU 实现
# - KDL:使用 Orocos KDL 库实现
# set(FK_IMPL "FFK_CUDA")
# set(FK_IMPL "KDL") # 注:可以手动选择实现方式。
############################################################
# 分支:FFK(快速 CPU SIMD)实现
############################################################
if(${FK_IMPL} STREQUAL "FFK")
target_compile_definitions(forward_kinematics_test PUBLIC USE_FAST_KINEMATICS)
target_compile_definitions(inverse_kinematics_test PUBLIC USE_FAST_KINEMATICS)
# 定义宏 USE_FAST_KINEMATICS,通知代码使用 FFK 实现
target_include_directories(forward_kinematics_test PUBLIC include)
target_include_directories(inverse_kinematics_test PUBLIC include)
# 测试程序加入 include 目录,以便包含 fast_kinematics.hpp
generate_fast_forward_kinematics_library(
fast_forward_kinematics_library
URDF_FILE ${URDF_FILE}
ROOT_LINK ${ROOT}
TIP_LINK ${TIP}
)
# 使用自定义 CMake 宏生成 FK 库:
# - 自动生成 FK 源代码
# - 自动编译为 fast_forward_kinematics_library
if(${CMAKE_BUILD_TYPE} STREQUAL "Release")
set_target_properties(fast_forward_kinematics_library
PROPERTIES CMAKE_BUILD_TYPE Release)
target_compile_options(fast_forward_kinematics_library PUBLIC -Ofast -march=native)
endif()
# Release 模式启用最高级别优化:
# - -Ofast:比 O3 更激进
# - -march=native:使用本机 CPU 指令集(如 AVX2、AVX512)
target_link_libraries(forward_kinematics_test PUBLIC fast_forward_kinematics_library)
target_compile_definitions(forward_kinematics_test PUBLIC MULTIPLIER=64)
# 定义 MULTIPLIER,用于压力测试循环次数
target_link_libraries(inverse_kinematics_test PRIVATE fast_forward_kinematics_library)
target_compile_definitions(inverse_kinematics_test PUBLIC MULTIPLIER=32)
############################################################
# 分支:CUDA 实现
############################################################
elseif(${FK_IMPL} STREQUAL "FFK_CUDA")
set(CMAKE_CUDA_ARCHITECTURES "native")
# 自动使用本机 GPU 架构,例如 sm_86(3080)、sm_89(4090)
target_compile_definitions(forward_kinematics_test PUBLIC USE_FAST_KINEMATICS)
target_compile_definitions(inverse_kinematics_test PUBLIC USE_FAST_KINEMATICS)
generate_fast_forward_kinematics_library_cuda(
fast_forward_kinematics_library
URDF_FILE ${URDF_FILE}
ROOT_LINK ${ROOT}
TIP_LINK ${TIP}
)
# CUDA 版本生成与编译
if(${CMAKE_BUILD_TYPE} STREQUAL "Release")
set_target_properties(fast_forward_kinematics_library
PROPERTIES CMAKE_BUILD_TYPE Release)
target_compile_options(fast_forward_kinematics_library PUBLIC -O3)
endif()
target_link_libraries(forward_kinematics_test PUBLIC fast_forward_kinematics_library)
target_compile_definitions(forward_kinematics_test PUBLIC MULTIPLIER=64)
target_link_libraries(inverse_kinematics_test PRIVATE fast_forward_kinematics_library)
target_compile_definitions(inverse_kinematics_test PUBLIC MULTIPLIER=32)
############################################################
# 分支:KDL(Orocos KDL)实现
############################################################
else()
set(CXX_FLAGS "-Ofast")
# KDL 构建使用最高编译器优化等级
set(LIB_KDL_PARSER
${CMAKE_CURRENT_BINARY_DIR}/kdl_parser/src/kdl-parser-build/libkdl_parser.so)
# KDL parser 的输出库文件路径
include(ExternalProject)
# 引入 ExternalProject,用于自动下载、构建第三方项目
###########################################################
# 下载并构建 kdl_parser
###########################################################
ExternalProject_Add(
kdl-parser
PREFIX ${CMAKE_CURRENT_BINARY_DIR}/kdl_parser
SOURCE_SUBDIR kdl_parser
GIT_REPOSITORY https://github.com/ros/kdl_parser.git
GIT_TAG 2.12.0
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}
-DCMAKE_CXX_FLAGS=${CXX_FLAGS}
BUILD_BYPRODUCTS ${LIB_KDL_PARSER}
)
add_library(kdl_parser_lib SHARED IMPORTED)
add_custom_target(kdl_parser_lib-target DEPENDS kdl-parser)
add_dependencies(kdl_parser_lib kdl_parser_lib-target)
set_target_properties(kdl_parser_lib PROPERTIES IMPORTED_LOCATION ${LIB_KDL_PARSER})
# IMPORTED 库用于包装 ExternalProject 构建生成的文件
###########################################################
# 下载并编译 Orocos-KDL(主 KDL 运动学动力学库)
###########################################################
set(LIB_KDL
${CMAKE_CURRENT_BINARY_DIR}/kdl/src/kdl-content-build/src/liborocos-kdl.so)
ExternalProject_Add(
kdl-content
PREFIX ${CMAKE_CURRENT_BINARY_DIR}/kdl
SOURCE_SUBDIR orocos_kdl
GIT_REPOSITORY https://github.com/orocos/orocos_kinematics_dynamics.git
GIT_TAG master # 或 v1.5.1
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}
-DCMAKE_CXX_FLAGS=${CXX_FLAGS}
BUILD_BYPRODUCTS ${LIB_KDL}
)
add_library(kdl_lib SHARED IMPORTED)
add_custom_target(kdl_lib-target DEPENDS kdl-content)
add_dependencies(kdl_lib kdl_lib-target)
set_target_properties(kdl_lib PROPERTIES IMPORTED_LOCATION ${LIB_KDL})
###########################################################
# 构建本项目的 KDL 包装层
###########################################################
find_package(Eigen3 3.3 NO_MODULE)
add_library(kdl_implementation SHARED kdl/kdl_implementation.cpp)
target_include_directories(kdl_implementation PUBLIC ../include)
target_include_directories(kdl_implementation PUBLIC include)
# 本地 KDL 封装层的头文件路径
# 传递 URDF、joint 数、根、tip 名称给 KDL 实现
target_compile_definitions(kdl_implementation PUBLIC NUMBER_OF_JOINTS=6)
target_compile_definitions(kdl_implementation PUBLIC URDF_FILE=${URDF_FILE})
target_compile_definitions(kdl_implementation PUBLIC ROOT=${ROOT})
target_compile_definitions(kdl_implementation PUBLIC TIP=${TIP})
if(${CMAKE_BUILD_TYPE} STREQUAL "Release")
set_target_properties(kdl_implementation PROPERTIES CMAKE_BUILD_TYPE Release)
target_compile_options(kdl_implementation PUBLIC ${CXX_FLAGS_ARGS})
endif()
target_link_libraries(kdl_implementation PUBLIC kdl_lib kdl_parser_lib Eigen3::Eigen)
# KDL 动力学库
# KDL URDF parser
# Eigen 数学库
# KDL 生成的头文件(由 ExternalProject 安装)路径
target_include_directories(kdl_implementation PUBLIC ${CMAKE_BINARY_DIR}/include/kdl)
target_include_directories(kdl_implementation PUBLIC ${CMAKE_BINARY_DIR}/include/kdl_parser)
target_link_libraries(forward_kinematics_test PUBLIC kdl_implementation)
target_link_libraries(inverse_kinematics_test PUBLIC kdl_implementation)
target_compile_definitions(forward_kinematics_test PUBLIC MULTIPLIER=2)
target_compile_definitions(inverse_kinematics_test PUBLIC MULTIPLIER=1)
endif()
概览
这个 CMake 脚本负责在 test/ 子目录中为三种不同的运动学实现方式生成/构建测试程序:
- FFK(Fast Forward Kinematics)—— 高性能、通过代码生成展开矩阵运算并用 SIMD 优化的 CPU 实现(默认)。
- FFK_CUDA —— 将生成的计算部署到 CUDA 核心在 GPU 上并行执行。
- KDL —— 使用 Orocos KDL(成熟的运动学动力学库)作为参考实现(通过
ExternalProject下载并构建第三方库)。
脚本还设置 C++ 标准、默认构建类型、测试可执行目标、URDF 路径、运动链的 root 与 tip 等工程级常量,并根据所选实现配置编译选项、宏、链接关系与生成步骤。
1. 全局构建配置(C++ 标准与默认构建类型)
set(CMAKE_CXX_STANDARD 20):强制使用 C++20。CMake 会在生成器下添加相应编译选项(例如-std=gnu++20)。- 默认构建类型:
说明:在 Makefile generator 下,若不显式指定if(NOT DEFINED CMAKE_BUILD_TYPE OR "${CMAKE_BUILD_TYPE}" STREQUAL "") set(CMAKE_BUILD_TYPE Release) endif()CMAKE_BUILD_TYPE,编译器不会得到优化选项。强制默认Release是为了保证 SIMD/CUDA/性能测试使用优化编译器选项。
2. 测试目标与机器人配置常量
- 两个测试可执行:
forward_kinematics_test(正运动学测试)inverse_kinematics_test(逆运动学测试)
- URDF 与链端点设置:
说明:这些常量会在后续的代码生成或 KDL 实现中被传递/引用(例如set(URDF_FILE ${CMAKE_SOURCE_DIR}/test/urdf/robot.urdf) set(ROOT base_link) set(TIP grasp_link)generate_fast_forward_kinematics_library或通过target_compile_definitions编译为宏)。
3. 三种实现的选择与整体策略
- 通过
set(FK_IMPL "FFK")选择实现。可能值:"FFK"(默认):生成展开的、高度优化的 CPU 代码。"FFK_CUDA":生成 CUDA 版本并编译 GPU kernel。"KDL":使用外部库 Orocos KDL(更通用、可靠,但通常不如手工展开的 SIMD 代码快)。
设计意图:用同一套测试驱动(同样的 URDF、同样的接口)比较不同实现的性能与正确性。
4. FFK(CPU 代码生成)分支详解
主要动作:
- 为测试目标定义宏
USE_FAST_KINEMATICS,代码中会以#ifdef USE_FAST_KINEMATICS分支来使用生成的实现。 - 为测试程序加入
include目录,确保能找到 public header。 - 调用自定义 CMake 宏
generate_fast_forward_kinematics_library(...),此宏负责:- 调用
get_num_joints.py得到关节数(与 CMake 正则提取匹配配合), - 用
robot_gen.py与模板(robot_config.cpp.template)渲染生成展开的 C++ 源(包含每个关节的矩阵乘法展开、sin/cos 展开等), - 将生成的源添加到
fast_forward_kinematics_library并编译成静/共享库。
- 调用
- 若为
Release,为目标添加-Ofast -march=native,以获得最高性能(注意-Ofast会放宽 IEEE 浮点语义)。
实现意图与数学背景(FK 的基本公式):
机器人多关节的正运动学可用链式齐次变换表示为:
T 0 n ( θ ) = ∏ i = 1 n A i ( θ i ) T_{0}^{n}(\theta) = \prod_{i=1}^{n} A_i(\theta_i) T0n(θ)=i=1∏nAi(θi)
其中每个关节的变换矩阵 A i ( θ i ) A_i(\theta_i) Ai(θi)( 4 × 4 4\times4 4×4)包含旋转和平移。展开计算时,常将每步写成:
T i + 1 = T i ⋅ A i + 1 ( θ i + 1 ) T_{i+1} = T_i \cdot A_{i+1}(\theta_{i+1}) Ti+1=Ti⋅Ai+1(θi+1)
为了性能,代码生成器通常会将 A i A_i Ai 展开为常数子表达(例如固定旋转 R fixed R_{\text{fixed}} Rfixed 与偏移 p offset p_{\text{offset}} poffset)与可变项(如 sin ( θ ) , cos ( θ ) \sin(\theta), \cos(\theta) sin(θ),cos(θ)),并把矩阵乘法按标量算子展开以利于编译器做向量化或手工插入 SIMD 指令。
5. FFK_CUDA(CUDA)分支详解
- 设置
CMAKE_CUDA_ARCHITECTURES "native"让 CUDA 生成对本机 GPU 架构优化的 PTX/SASS。 - 调用
generate_fast_forward_kinematics_library_cuda(...):渲染 CUDA 模板、生成 kernel、构建 CU 文件并链接到fast_forward_kinematics_library。 - Release 模式下使用
-O3。
要点: - GPU 版适合批量大量 FK/IK 求解(例如大量初值的并行 IK、或并行正向仿真)。
- 记得 GPU 实现可能需要处理数据传输(host↔device)带来的开销 — 只有在大量并行工作时才划算。
6. KDL 分支(ExternalProject)详解
这是“降级到成熟库”的路径,用来做正确性验证或在没有高性能实现时使用。
主要步骤:
- 使用
ExternalProject_Add(kdl-parser, ...)下载并构建kdl_parser(来自 ROS 的 repo),目标生成libkdl_parser.so。BUILD_BYPRODUCTS告诉 CMake 哪个文件是构建产物以便依赖管理。 - 用
add_library(... IMPORTED)+set_target_properties(... IMPORTED_LOCATION ...)将 ExternalProject 的生成产物呈现为 CMake 的 IMPORTED 目标,便于主项目链接。 - 同理对
orocos_kinematics_dynamics(Orocos KDL)进行ExternalProject_Add并包装为kdl_lib。 - 创建
kdl_implementation(项目内的 KDL 适配层)并将URDF_FILE、ROOT、TIP、NUMBER_OF_JOINTS通过target_compile_definitions传递给源码(这样源码可以在编译期读取宏来定位 URDF、设置数组大小等)。 - 链接
kdl_implementation到测试可执行文件并设置较小的MULTIPLIER(表明 KDL 用作正确性验证而非高强度性能压力测试)。
ExternalProject 行为要点:
ExternalProject_Add会在 configure/build/install 阶段做外部项目的完整构建流程。主 CMake 项目会在需要这些产物作为依赖时确保 ExternalProject 完成。- 使用 IMPORTED + custom target 的组合来确保链接目标在需要时依赖 ExternalProject 的构建(避免 race condition)。
- 缺点是:外部项目下载/编译会增加构建时间与复杂度,并且可能受网络或外部 git tag 的影响。
7. 宏、编译选项、以及测试参数传播模型
- 宏
USE_FAST_KINEMATICS:在源码中用于条件编译,启用/禁用不同实现路径。 MULTIPLIER:用作压力测试循环计数参数。不同实现设定不同默认值(FFK 比 KDL 的默认 MULTIPLIER 更高)。target_compile_options:对fast_forward_kinematics_library(生成库)设置优化标志,目的是只影响生成库而非全局。target_include_directories:确保生成的目标能找到头文件。对于 KDL 链接,额外把ExternalProject安装出的 include 路径添加到kdl_implementation。
8. 关键性能与数值精度考量(要点)
-Ofast:可带来更高性能,但可能禁用严格 IEEE754 行为(如舍入/严格别名/浮点精度),需要在对数值稳定性敏感的算法上谨慎使用。-march=native:会打开本机特有的指令集(AVX2 / AVX512)。若在发布二进制到不同 CPU 时使用,可能导致不兼容。- 对于 IK(优化、梯度)算法,必须保证浮点稳定性;在一些情况下建议用
-O3而不是-Ofast做对比测试。 - CUDA 实现需要考虑 host↔device 数据传输开销 —— 当单次问题规模(或并行个数)较小,GPU 可能不划算。
9. 调试建议(遇到生成/构建错误时)
- 确认 URDF 路径与 ROOT/TIP 是否正确:
get_num_joints.py会从 tip 逆向到 root,若 link 名不匹配会崩溃或返回错误。 - 手动运行代码生成脚本:
检查输出是否合理、格式是否包含python3 code_generation/get_num_joints.py test/urdf/robot.urdf base_link grasp_link python3 code_generation/robot_gen.py test/urdf/robot.urdf robot_config.cpp.template out.cpp base_link grasp_linkFAST_FK_NUMBER_OF_JOINTS=等。 - 若使用 KDL 分支,先单独构建 ExternalProject:网络不好或 repo tag 改变会造成下载失败,查看 ExternalProject 的日志目录(CMake 输出)定位错误。
- 检查编译器/CPU 指令集兼容性:若出现非法指令(SIGILL),可能是
-march=native启用了主机不支持的指令(例如在交叉/兼容机器上)。 - 比较实现的正确性:先用小
MULTIPLIER与 KDL 比较结果(位置/姿态误差),再放大测试量级。
10. 进阶建议(改善可用性/可维护性)
- 在
CMake中把FK_IMPL变为option或CACHE STRING,这让用户在cmake命令行更方便切换:set(FK_IMPL "FFK" CACHE STRING "FK implementation: FFK|FFK_CUDA|KDL") - 在生成步骤中对
get_num_joints.py的 Python 可执行路径做健壮检测(find_package(Python3 COMPONENTS Interpreter)),并在失败时给出明确错误提示。 - 对
-march=native做 guard,提供FFK_CPU_ARCH缓存选项以便在 CI/发行包中控制目标 ISA。 - 增加单元测试脚本用来比较三个实现的输出(例如阈值内断言)并把它们纳入
ctest。
11. 关键公式与表达(总结)
- 正向运动学(链式相乘):
T 0 n ( θ ) = A 1 ( θ 1 ) A 2 ( θ 2 ) ⋯ A n ( θ n ) T_0^n(\theta)=A_1(\theta_1) A_2(\theta_2) \cdots A_n(\theta_n) T0n(θ)=A1(θ1)A2(θ2)⋯An(θn) - 每步变换展开形式(简化):
A i ( θ i ) = [ R i ( θ i ) p i 0 1 ] , R i ( θ i ) = R fixed , i ⋅ R joint ( θ i ) A_i(\theta_i)= \begin{bmatrix} R_i(\theta_i) & p_i \\ 0 & 1 \end{bmatrix},\quad R_i(\theta_i)=R_{\text{fixed},i}\cdot R_{\text{joint}}(\theta_i) Ai(θi)=[Ri(θi)0pi1],Ri(θi)=Rfixed,i⋅Rjoint(θi)
其中 R joint ( θ i ) R_{\text{joint}}(\theta_i) Rjoint(θi) 可能只依赖于 sin θ i , cos θ i \sin\theta_i,\cos\theta_i sinθi,cosθi,这正是代码生成器将 sin / cos \sin/\cos sin/cos 提前计算并展开的理由。
code_generation
get_num_joints.py
from urdf_parser_py import urdf # 引入 URDF 解析库,用于解析机器人 URDF 结构
import argparse # 用于解析命令行参数
def run():
parser = argparse.ArgumentParser() # 创建命令行解析器
parser.add_argument("urdf_file") # 第一个参数:URDF 文件路径
parser.add_argument("root_link_name") # 第二个参数:根节点(基座 link)名称
parser.add_argument("tip_link_name") # 第三个参数:末端执行器 link 名称
args = parser.parse_args() # 解析用户输入的参数
root_link_name = args.root_link_name # 记录根节点 link 名称
tip_link_name = args.tip_link_name # 记录目标末端 link 名称
# 读取 URDF 文件并解析成 Robot 对象(包含 link/joint 树)
with open(args.urdf_file) as f:
robot = urdf.Robot.from_xml_string(f.read())
joint_names = [] # 用于存储 ROOT→TIP 路径上的所有“非固定”关节名称
# 从 tip_link 开始沿着 URDF 的 parent_map 逐步向上追溯
# parent_map[child] = (joint_name, parent_link_name)
while tip_link_name != root_link_name:
tip_joint_name, tip_link_name = robot.parent_map[tip_link_name]
# tip_joint_name : 把当前 link 连接到父 link 的 joint 名称
# tip_link_name : 更新为父 link,继续向上遍历
joint = robot.joint_map[tip_joint_name] # 从 joint_map 获取关节对象
# URDF 中 type == "fixed" 表示它是固定关节(不产生 DOF)
# Forward Kinematics 只关心可动关节(revolute、prismatic 等)
if not joint.type == "fixed":
joint_names.append(tip_joint_name)
# 输出关节数量作为 CMake 编译时宏,例如:
# FAST_FK_NUMBER_OF_JOINTS=6
# end="" 保证不换行,方便 CMake 捕获结果。
print(f"FAST_FK_NUMBER_OF_JOINTS={len(joint_names)}", end="")
if __name__ == "__main__":
run() # 入口函数
详细理解(含数学解释)
1. 导入模块
from urdf_parser_py import urdf # 引入 URDF 解析库,用于解析机器人 URDF 结构
import argparse # 用于解析命令行参数
urdf_parser_py能将 URDF XML 文档解析成一个Robot对象。- 这个对象包含:
- link 树结构
- joint 列表
- parent_map
- joint_map
URDF(Unified Robot Description Format)描述机械臂的结构:
- 每个 link 是刚体
- 每个 joint 连接两个 link,并可能提供自由度(例如 revolute)
2. 构建 CLI 参数解析器
parser = argparse.ArgumentParser() # 创建命令行解析器
parser.add_argument("urdf_file") # 第一个参数:URDF 文件路径
parser.add_argument("root_link_name") # 第二个参数:根节点(基座 link)名称
parser.add_argument("tip_link_name") # 第三个参数:末端执行器 link 名称
args = parser.parse_args() # 解析用户输入的参数
这三个参数允许脚本在 CMake 中被调用:
示例:
python get_joint_count.py robot.urdf base_link tool0
3. 加载 URDF 文件
with open(args.urdf_file) as f:
robot = urdf.Robot.from_xml_string(f.read())
URDF 会被解析成一个结构体形式的 Robot,内部包含重要映射表:
核心:URDF 的 parent_map / joint_map
URDF parser 会自动生成:
parent_map
parent_map[child_link] = (joint_name, parent_link)
比如:
parent_map["link2"] = ("joint2", "link1")
joint_map
joint_map[joint_name] = Joint(...) # Joint object
Joint 对象包含:
- joint.type(revolute / prismatic / fixed)
- joint.origin(变换矩阵)
- joint.axis
4. 反向追溯运动学链
joint_names = [] # 用于存储 ROOT→TIP 之间所有可动关节
基本思想
机械臂运动学链可以表示为:
ROOT → L 1 → L 2 → ⋯ → TIP \text{ROOT} \rightarrow L_1 \rightarrow L_2 \rightarrow \cdots \rightarrow \text{TIP} ROOT→L1→L2→⋯→TIP
但 parent_map 是从 child 指向 parent:
TIP ← L n ← ⋯ ← ROOT \text{TIP} \leftarrow L_n \leftarrow \cdots \leftarrow \text{ROOT} TIP←Ln←⋯←ROOT
因此需要从 tip 反向追溯到 root。
5. 反向遍历并记录关节
while tip_link_name != root_link_name:
tip_joint_name, tip_link_name = robot.parent_map[tip_link_name]
解释:
- 当前 link 是 tip
- 根据 parent_map 查到:
- 连接它的 joint 名称 →
tip_joint_name - 它的父 link → 更新
tip_link_name
如此反复直到到达 ROOT。
- 连接它的 joint 名称 →
6. 判断关节是否为可动关节
joint = robot.joint_map[tip_joint_name] # 获取 joint 对象
URDF 中 joint 可能是:
revolute(旋转关节,1 自由度)prismatic(滑动关节,1 自由度)continuous(无限旋转)fixed(固定,没有 DOF)
你只需要带自由度的关节,因此过滤掉 fixed:
if not joint.type == "fixed":
joint_names.append(tip_joint_name)
数学上我们只计入 DOF:
DOF = ∑ i = 1 n 1 joint i ≠ fixed \text{DOF} = \sum_{i=1}^{n} \mathbf{1}_{\text{joint}_i \neq \text{fixed}} DOF=i=1∑n1jointi=fixed
7. 输出 CMake 用到的宏
print(f"FAST_FK_NUMBER_OF_JOINTS={len(joint_names)}", end="")
例如:
FAST_FK_NUMBER_OF_JOINTS=6
CMake 可使用这个输出作为编译时常量:
N = 关节数量 N = \text{关节数量} N=关节数量
并在 C++ 中变成:
constexpr int FAST_FK_NUMBER_OF_JOINTS = 6;
用于生成 SIMD 优化代码或 CUDA kernel。
最终总结(含数学原理)
这段脚本做了以下工作:
- 解析 URDF
- 从末端 TIP 反向遍历 parent_map,直到 ROOT
- 找到所有参与 FK 计算的可动关节(非 fixed)
- 计数并输出:
FAST_FK_NUMBER_OF_JOINTS = ∣ j ∣ j . t y p e ≠ " f i x e d " ∣ \text{FAST\_FK\_NUMBER\_OF\_JOINTS} = |{ j \mid j.type \neq "fixed" }| FAST_FK_NUMBER_OF_JOINTS=∣j∣j.type="fixed"∣
这是用于静态代码生成的关键步骤,使你的 FFK(Fast Forward Kinematics)能够根据机械臂结构自动生成最优化的 SIMD 或 CUDA 运算。
robot_gen.py
from jinja2 import Template # Jinja2 模板引擎,用于生成 C++ FK 源代码
from urdf_parser_py import urdf # URDF 解析库,将 XML 转换成 Robot 对象
import argparse # 命令行参数解析
import numpy as np # 用于处理旋转矩阵与平移
def run():
# --------------------------
# 解析命令行参数
# --------------------------
parser = argparse.ArgumentParser()
parser.add_argument("urdf_file") # 输入 URDF 文件路径
parser.add_argument("fk_template") # Jinja2 模板文件(.j2)
parser.add_argument("fk_output_file") # 输出的 C++ 文件路径
parser.add_argument("root_link_name") # 运动链起点(base link)
parser.add_argument("tip_link_name") # 运动链终点(末端执行器)
args = parser.parse_args()
root_link_name = args.root_link_name # 根 link 名称
tip_link_name = args.tip_link_name # 末端 link 名称
# --------------------------
# 加载并解析 URDF
# --------------------------
with open(args.urdf_file) as f:
robot = urdf.Robot.from_xml_string(f.read()) # Robot 对象包含 parent_map / joint_map
# -------------------------------------------------------------
# 函数:将 URDF 的 joint.origin 转换成 4×4 齐次变换矩阵
# -------------------------------------------------------------
def get_transform(joint):
rpy = joint.origin.rpy # 关节的固定旋转(roll/pitch/yaw)
xyz = joint.origin.xyz # 关节的固定平移 (x,y,z)
T = np.eye(4) # 初始化为单位矩阵
# URDF RPY 顺序为:roll → pitch → yaw
# 以下分别为绕 Z/Y/X 轴的旋转矩阵
# yaw (绕 z 轴旋转)
yaw = np.array([
[ np.cos(rpy[2]), np.sin(rpy[2]), 0],
[-np.sin(rpy[2]), np.cos(rpy[2]), 0],
[ 0, 0, 1],
])
# pitch (绕 y 轴旋转)
pitch = np.array([
[ np.cos(rpy[1]), 0, np.sin(rpy[1])],
[ 0, 1, 0 ],
[-np.sin(rpy[1]), 0, np.cos(rpy[1])],
])
# roll (绕 x 轴旋转)
roll = np.array([
[1, 0, 0 ],
[0, np.cos(rpy[0]), np.sin(rpy[0])],
[0,-np.sin(rpy[0]), np.cos(rpy[0])],
])
# --------------------------
# 合成旋转矩阵
# --------------------------
# 注意:URDF 的 RPY 的变换顺序是:Rz(yaw) * Ry(pitch) * Rx(roll)
T[:3, :3] = yaw @ pitch @ roll
# 平移部分
T[:3, 3] = xyz
return T
# 存储结果的列表(最终传给 jinja2 模板)
rotations = [] # 每个关节的旋转矩阵 R_i
offsets = [] # 每个关节的常量位移向量 p_i
types = [] # 关节类型(revolute / prismatic)
joint_names = [] # ROOT → TIP 的关节顺序
# -------------------------------------------------------------
# 反向追溯运动链:根据 parent_map 从 TIP 回溯到 ROOT
# -------------------------------------------------------------
while tip_link_name != root_link_name:
tip_joint_name, tip_link_name = robot.parent_map[tip_link_name]
# parent_map[child] = (joint_name, parent_link)
# 如:parent_map["link2"] = ("joint2", "link1")
joint_names.append(tip_joint_name)
# 将关节顺序反转,从 ROOT → TIP
joint_names.reverse()
# -------------------------------------------------------------
# 处理固定变换(fixed joint 折叠)
# -------------------------------------------------------------
T_fixed = np.eye(4) # 累积连续 fixed joint 的变换
for joint_name in joint_names:
joint = robot.joint_map[joint_name]
# -------- fixed joint:累积其变换,不作为 DOF --------
if joint.type == "fixed":
T_fixed = T_fixed * get_transform(joint)
# -------- revolute 关节:有 1 DOF --------
elif joint.type == "revolute":
# 公式:T_total = T_joint * T_fixed
T = get_transform(joint) @ T_fixed
T_fixed = np.eye(4) # 重置 fixed 变换累积器
rotations.append(T[:3, :3]) # 固定旋转 R
offsets.append(T[:3, 3]) # 固定偏移 p
types.append("revolute") # 旋转关节
# -------- continuous(无限转动,等同于 revolute) --------
elif joint.type == "continuous":
T = get_transform(joint) @ T_fixed
T_fixed = np.eye(4)
rotations.append(T[:3, :3])
offsets.append(T[:3, 3])
types.append("revolute")
# -------- prismatic 关节:平移 DOF --------
elif joint.type == "prismatic":
T = get_transform(joint) @ T_fixed
T_fixed = np.eye(4)
rotations.append(T[:3, :3])
offsets.append(T[:3, 3])
types.append("prismatic")
else:
raise Exception(f"joint type {joint.type} in URDF not supported")
# -------------------------------------------------------------
# 清零非常小的浮点数(避免 C++ 输出 1e-18 之类的垃圾值)
# -------------------------------------------------------------
offsets = [val * (np.abs(val) > 1e-5) for val in offsets]
rotations = [val * (np.abs(val) > 1e-5) for val in rotations]
# -------------------------------------------------------------
# 使用 Jinja2 模板生成 C++ FK 源代码
# -------------------------------------------------------------
with open(args.fk_template, "r") as f:
j2_template = Template(f.read())
code = j2_template.render(
{"rotations": rotations, "offsets": offsets, "types": types},
trim_blocks=True,
)
# 输出到文件
with open(args.fk_output_file, "w") as f:
f.write(code)
# 输出关节数,让 CMake 捕获(用于定义 FAST_FK_NUMBER_OF_JOINTS)
print(f"FAST_FK_NUMBER_OF_JOINTS={len(types)}", end="")
# 程序入口
if __name__ == "__main__":
run()
详细理解(带数学公式)
1. 脚本功能概述
该脚本做的事情是:
- 解析 URDF 文件
- 从 base(root link)一路找到 tip(末端 link)
- 计算每个关节的固定变换(不含 DOF)
- 将可动关节(Revolute/Prismatic)的固定参数提取出来:
- 固定旋转矩阵 R i R_i Ri
- 固定位移向量 p i \mathbf{p}_i pi
- 将结果传给 Jinja2 模板,生成对应的 C++ forward kinematics(快速前向运动学) 代码
- 输出关节数量供 CMake 使用
2. 解析 URDF 文件
urdf.Robot.from_xml_string(...) 会生成一个 Robot 对象,内部包含两个重要结构:
parent_map[child_link] = (joint_name, parent_link)joint_map[joint_name] = Joint 对象
这样你可以 从末端往上追溯整个运动链。
3. get_transform(joint) —— 关节的固定空间变换矩阵
URDF 中每个关节都有 origin:
- 平移:
xyz = (x, y, z) - 固定旋转:
rpy = (roll, pitch, yaw)
你将其转换成 4×4 齐次变换矩阵
T = [ R p 0 1 ] T = \begin{bmatrix} R & \mathbf{p} \\ 0 & 1 \end{bmatrix} T=[R0p1]
其中 R R R 是旋转矩阵, p \mathbf{p} p 是平移向量。
URDF 的 RPY 顺序
URDF 定义旋转为(右乘顺序):
R = R z ( yaw ) ⋅ R y ( pitch ) ⋅ R x ( roll ) R = R_z(\text{yaw}) \cdot R_y(\text{pitch}) \cdot R_x(\text{roll}) R=Rz(yaw)⋅Ry(pitch)⋅Rx(roll)
你的代码正是这么实现的:
T[:3, :3] = yaw @ pitch @ roll
这是正确的。
4. 提取 ROOT → TIP 的运动链
你从 tip 反向使用:
tip_joint_name, tip_link_name = robot.parent_map[tip_link_name]
直到追溯到 root link。
得到关节序列后:
joint_names.reverse()
这样就得到从 root → tip 的运动链关节顺序。
5. 处理固定变换(fixed joint 折叠)
URDF 有大量 fixed joint,比如机械臂手腕相机等固接结构,这些不需要出现在 FK 计算里,所以要把它们折叠进前一个可动关节里。
你使用了累积矩阵:
T fixed_acc = T 1 T 2 T 3 ⋯ T_\text{fixed\_acc} = T_1 T_2 T_3 \cdots Tfixed_acc=T1T2T3⋯
直到遇到可动关节:
- 对 revolute / prismatic 关节:
T = T joint ⋅ T fixed T = T_{\text{joint}} \cdot T_{\text{fixed}} T=Tjoint⋅Tfixed
然后把 R R R 和 p \mathbf{p} p 分别保存。
你保存的是: - R i R_i Ri:固定旋转
- p i p_i pi:固定平移
- type:关节类型
用于 C++ 模板里的 FK。
6. 关节类型处理
你处理的类型包括:
fixedrevolutecontinuous(视为revolute)prismatic
其他类型将报 error。
7. 清理小浮点误差
避免在 C++ 中输出像 1e-17 这样的垃圾值:
offsets = [val * (np.abs(val) > 1e-5) for val in offsets]
rotations = [val * (np.abs(val) > 1e-5) for val in rotations]
数学含义是:
x = { x , ∣ x ∣ > 1 0 − 5 0 , ∣ x ∣ ≤ 1 0 − 5 x = \begin{cases} x, & |x| > 10^{-5} \\ 0, & |x| \le 10^{-5} \end{cases} x={x,0,∣x∣>10−5∣x∣≤10−5
8. 使用 Jinja2 模板生成 C++ FK 代码
你读取 .j2 模板并替换其中的:
rotationsoffsetstypes
这些数据构成 C++ 中的:
p i , R i p_i, \quad R_i pi,Ri
用于你自己的快速 FK 引擎。
9. 输出关节数供 CMake 使用
末尾的:
print(f"FAST_FK_NUMBER_OF_JOINTS={len(types)}", end="")
给 CMake 用,例如:
FAST_FK_NUMBER_OF_JOINTS=6
总结数学模型
你在做的事情是将 URDF 关节链转换为:
- 固定旋转矩阵 R i R_i Ri
- 固定偏移向量 p i \mathbf{p}_i pi
它们分别代表:
有 DOF 的关节变换: T i ( q i ) = [ R i p i 0 1 ] ⋅ [ R ( q i ) d ( q i ) 0 1 ] \text{有 DOF 的关节变换: } T_i(q_i) = \begin{bmatrix} R_i & p_i \\ 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} R(q_i) & d(q_i) \\ 0 & 1 \end{bmatrix} 有 DOF 的关节变换: Ti(qi)=[Ri0pi1]⋅[R(qi)0d(qi)1]
这是 可动关节 的「标准形」。
最终 FK 是:
T = ∏ i = 1 N T i ( q i ) T = \prod_{i=1}^{N} T_i(q_i) T=i=1∏NTi(qi)
robot_config.cpp.template
总览:代码到底在做什么?
代码是 Fast Forward Kinematics(快速正运动学)+ Inverse Kinematics(反向运动学梯度) 的核心计算模块。
该代码来自自动生成(Jinja2),输入 URDF 中关节的旋转矩阵、偏移、关节类型(revolute/prismatic),生成对特定机器人完全专用、无分支、无虚函数、最优化的 FK/IK 计算内核。
核心思想是:
1. input_data 的结构(最关键的基础)
每一个关节有 data_size = 16 个 float:
| index | 含义 |
|---|---|
| 0 | sin ( θ ) \sin(\theta) sin(θ) 或 prismatic 的 d |
| 1 | cos ( θ ) \cos(\theta) cos(θ)(只有 revolute 才有) |
| 2–4 | 在 base frame 中 joint_i 的位置 ( p x , p y , p z ) (p_x, p_y, p_z) (px,py,pz) |
| 5–13 | 在 base frame 中 joint_i 的旋转矩阵(9 个数,行主序) |
| 14–15 | 备用 |
| 代码经常写: |
float &R11 = input_data[ind * data_size + 5];
表示取第 ind 个关节的旋转矩阵。
2. rotations & offsets(URDF 静态信息)
URDF 每个 joint 都有:
- 固定旋转 R_fixed(由 axis + origin.rpy 构成)
- 固定偏移 offset(由 origin.xyz)
代码把它们展平成:
constexpr std::array<float, N*9> R_all;
constexpr std::array<float, N*3> offset_all;
用途:
- 每个关节计算自己的局部旋转矩阵 R_tmp = R_fixed * R_actuation
- 再乘上前一个关节在 base 中的旋转矩阵 R_old 得到 R_base
3. 主函数:forward_kinematics_internal
这个函数按顺序更新每个关节的:
- 旋转矩阵 R i R_i Ri
- 位置向量 p i p_i pi
整个算法基本就是:
R i = R i − 1 ⋅ R fixed ∗ i ⋅ R ∗ θ R_i = R_{i-1} \cdot R_{\text{fixed}*i} \cdot R*{\theta} Ri=Ri−1⋅Rfixed∗i⋅R∗θ
p i = p i − 1 + R i − 1 ⋅ ( o f f s e t i + a c t u a t i o n i ) p_i = p_{i-1} + R_{i-1} \cdot (offset_i + actuation_i) pi=pi−1+Ri−1⋅(offseti+actuationi)
其中: - R θ R_{\theta} Rθ 是旋转关节生成的旋转矩阵
- a c t u a t i o n i actuation_i actuationi 是 prismatic 关节的线性伸长
4. 具体解释每一个计算步骤(含数学公式)
以下按你的代码结构逐行深入解析。
第 0 个关节的特殊处理
模板中判断:
{% if loop.index0 == 0 %}
表示第一个关节没有父节点,所以:
1⃣ 对于 revolute(旋转关节)
R = R fixed ⋅ R θ R = R_{\text{fixed}} \cdot R_{\theta} R=Rfixed⋅Rθ
因为 base frame 就是 joint0 的父坐标系。
偏移量无需旋转(因为 R_old = I):
p = offset
2⃣ 对于 prismatic(线性关节)
R = R fixed R = R_{\text{fixed}} R=Rfixed
p = o f f s e t + R fixed ⋅ [ 0 0 l i n e a r ] p = offset + R_{\text{fixed}} \cdot \begin{bmatrix}0\\0\\linear\end{bmatrix} p=offset+Rfixed⋅
00linear
第 i (>0) 个关节的通用公式
Revolute:
旋转轴通常是 Z 轴:
R θ = [ cos θ − sin θ 0 sin θ cos θ 0 0 0 1 ] R_{\theta} = \begin{bmatrix} \cos\theta & -\sin\theta & 0\\ \sin\theta & \cos\theta & 0\\ 0 & 0 & 1 \end{bmatrix} Rθ=
cosθsinθ0−sinθcosθ0001
你的代码将:
- R fixed R_{\text{fixed}} Rfixed 先和 R θ R_{\theta} Rθ 合成 R t m p R_{tmp} Rtmp
- 再乘 R o l d R_{old} Rold 得到 base frame 中的旋转矩阵
即:
R = R o l d , R t m p R = R_{old}, R_{tmp} R=Rold,Rtmp
位置:
p = p o l d + R o l d ⋅ o f f s e t p = p_{old} + R_{old} \cdot offset p=pold+Rold⋅offset
Prismatic:
无旋转,所以:
R = R o l d , R f i x e d R = R_{old}, R_{fixed} R=Rold,Rfixed
位置:
p = p o l d + R o l d ⋅ o f f s e t + R o l d ⋅ j o i n t a x i s ⋅ l i n e a r p = p_{old} + R_{old}\cdot offset + R_{old}\cdot joint_axis \cdot linear p=pold+Rold⋅offset+Rold⋅jointaxis⋅linear
5. InverseKinematics::operator() — 梯度计算核心
IK 目标函数为:
$$
J = |x_{\text{target}} - x_{\text{current}}|^2
- |y_{\text{target}} - y_{\text{current}}|^2
- |z_{\text{target}} - z_{\text{current}}|^2
$$
也就是保持三个坐标轴的方向同时对齐。
Target frame 的三个点
定义:
- 终端执行器位置:
p e p_e pe - 当前末端坐标系的三根轴:
x a x i s , , y a x i s , , z a x i s x_{axis},, y_{axis},, z_{axis} xaxis,,yaxis,,zaxis - 对应的 target 点:
x t = p t + x a x i s t a r g e t x_t = p_t + x_{axis_target} xt=pt+xaxistarget
y t = p t + y a x i s t a r g e t y_t = p_t + y_{axis_target} yt=pt+yaxistarget
z t = p t + z a x i s t a r g e t z_t = p_t + z_{axis_target} zt=pt+zaxistarget
当前点:
x c = p e + x a x i s x_c = p_e + x_{axis} xc=pe+xaxis
误差:
δ x = x t − x c \delta_x = x_t - x_c δx=xt−xc
Eigen::Vector3<float> delta_x = target_x_ - current_x;
代价函数:
f = ∣ δ x ∣ 2 + ∣ δ y ∣ 2 + ∣ δ z ∣ 2 f = |\delta_x|^2 + |\delta_y|^2 + |\delta_z|^2 f=∣δx∣2+∣δy∣2+∣δz∣2
梯度公式(最重要)
对于任意一个 revolute 关节 i:
∂ f ∂ q i = − 2 , δ x ⊤ ∂ ( x c ) ∂ q i − 2 , δ y ⊤ ∂ ( y c ) ∂ q i − 2 , δ z ⊤ ∂ ( z c ) ∂ q i \frac{\partial f}{\partial q_i} = -2, \delta_x^\top \frac{\partial (x_c)}{\partial q_i} -2, \delta_y^\top \frac{\partial (y_c)}{\partial q_i} -2, \delta_z^\top \frac{\partial (z_c)}{\partial q_i} ∂qi∂f=−2,δx⊤∂qi∂(xc)−2,δy⊤∂qi∂(yc)−2,δz⊤∂qi∂(zc)
而:
p e ( q ) = p i + R i ⋅ δ p_e(q) = p_i + R_i \cdot \delta \ pe(q)=pi+Ri⋅δ
旋转关节的雅可比:
∂ p e ∂ q i = ω i × ( p e − p i ) \frac{\partial p_e}{\partial q_i} = \omega_i \times (p_e - p_i) ∂qi∂pe=ωi×(pe−pi)
旋转对轴方向影响:
∂ x a x i s ∂ q i = ω i × x a x i s \frac{\partial x_{axis}}{\partial q_i} = \omega_i \times x_{axis} ∂qi∂xaxis=ωi×xaxis
你代码中对应:
jac_linear = joint_axis.cross(delta);
jac_angular = joint_axis;
最终:
δ x ′ = j a c l i n e a r + j a c a n g u l a r × x a x i s \delta_x' = jac_linear + jac_angular \times x_{axis} δx′=jaclinear+jacangular×xaxis
ddelta_x_dq_ind = jac_linear + jac_angular.cross(axis_scale * x_axis);
梯度:
grad[ind] -= dJ_ddelta_x.dot(ddelta_x_dq_ind);
总结(如果你要写论文/报告可以直接用)
你的 FK/IK 系统:
使用 逐关节展开的硬编码 FK
没有矩阵库开销
每个关节的计算都展开成标量运算
IK 的目标函数包含三个轴的方向一致性
梯度完全手工推导,并且自动展开
对每一个 URDF 都能生成专用高性能代码
这是最强的 FK/IK 优化方式(类似 MoveIt 的 Tesseract / Drake 的模板化优化做法)。
robot_config.hpp.in
文件内容解析(详细理解)
#pragma once
// Robot configuration
#define FAST_FK_NUMBER_OF_JOINTS @FAST_FK_NUMBER_OF_JOINTS@
#define NUMBER_OF_JOINTS @NUMBER_OF_JOINTS@
#define MULTIPLIER @MULTIPLIER@
#define URDF_FILE @URDF_FILE@
#define ROOT @ROOT@
#define TIP @TIP@
#pragma once
#pragma once 用于防止头文件被多次包含。
与 include guard(#ifndef xxx)作用相同,但更简洁、无命名冲突。
宏定义部分(被 CMake configure_file 自动替换)
这些宏都不是代码里直接写死的,而是通过 configure_file() 根据 CMake 配置动态替换成具体值。
1⃣ #define FAST_FK_NUMBER_OF_JOINTS @FAST_FK_NUMBER_OF_JOINTS@
这是 快速前向运动学(FAST FK) 使用的关节数量。
由 Python 脚本解析 URDF 时打印出来,例如:
FAST_FK_NUMBER_OF_JOINTS=6
最终生成的头文件中会变成:
#define FAST_FK_NUMBER_OF_JOINTS 6
即:
系统自动检测机器人从 ROOT → TIP 之间有几个 非固定(非 fixed)关节。
2⃣ #define NUMBER_OF_JOINTS @NUMBER_OF_JOINTS@
通常这是用户配置的关节数。
可能与 FAST_FK_NUMBER_OF_JOINTS 相同,也可能不同(例如预留空间等)。
你的代码中可能需要:
FAST_FK_NUMBER_OF_JOINTS:前向运动学实际计算用NUMBER_OF_JOINTS:数据结构大小、buffer 预分配等
3⃣ #define MULTIPLIER @MULTIPLIER@
这是一个优化相关的倍数因子,通常用于控制:
- SIMD 批处理个数
- 循环展开优化
- 并行任务大小
例如:
constexpr int multiplier = 128 * MULTIPLIER;
如果用户设置:
-DMULTIPLIER=8
那 multiplier 就是 1024。
4⃣ #define URDF_FILE @URDF_FILE@
这是 URDF 文件路径。例如:
data/ur5.urdf
被 configure_file 替换后:
#define URDF_FILE "data/ur5.urdf"
你的 C++ 程序就能直接使用这个路径:
load_urdf(URDF_FILE);
5⃣ #define ROOT @ROOT@
6⃣ #define TIP @TIP@
这是前向运动学链的起点和终点,例如:
# ROOT=base_link
# TIP=tool0
替换后:
#define ROOT "base_link"
#define TIP "tool0"
Python 脚本正是用它们从 parent_map 中爬链,找 joint path。
数学上,FK 处理的链实际就是:
ROOT = L 0 → L 1 → ⋯ → L n = TIP \text{ROOT} = L_0 \rightarrow L_1 \rightarrow \cdots \rightarrow L_n = \text{TIP} ROOT=L0→L1→⋯→Ln=TIP
并对路径上的关节执行:
0 T n = ∏ i = 1 n i − 1 T i {}^0T_n = \prod_{i=1}^{n} {^{i-1}T_i} 0Tn=i=1∏ni−1Ti
小结
| 宏 | 含义 | 来源 |
|---|---|---|
FAST_FK_NUMBER_OF_JOINTS |
真实参与 FK 的关节数 | Python 脚本解析 URDF |
NUMBER_OF_JOINTS |
用户设定的关节数(可能不同) | CMake 变量 |
MULTIPLIER |
计算优化倍数 | CMake 变量 |
URDF_FILE |
URDF 文件路径 | CMake 变量 |
ROOT |
FK 链起点 | CMake 变量 |
TIP |
FK 链终点 | CMake 变量 |
robot.urdf
这是一份完整的 UR5e 机械臂 + Robotiq 85 二指夹爪 + 腕部 RealSense D415 + 场景外置 RealSense D435 + 工作台 的 ROS2(ROS 2 Humble/Iron/Jazzy)完整机器人描述文件(URDF + ros2_control + Gazebo 配置),由 xacro 自动生成,文件名大概是 picknik_ur5e_charmander.urdf.xacro 加工后的最终 URDF。
下面给你最详细、最清晰的逐部分解读,帮助你彻底理解这份文件里每一块在干什么。
1. 机器人整体概况
- 机器人名字:
<robot name="ur5e"> - 控制器类型:ROS2 Control(ros2_control),使用 Universal Robots 官方驱动
ur_robot_driver - 真实机器人 IP:
192.10.0.11(你自己的机器人要改成实际 IP) - 末端执行器:Robotiq 2F-85 二指平行夹爪
- 腕部相机:Intel RealSense D415(朝前看)
- 场景相机:Intel RealSense D435(放在外部,俯视工作区)
- 还有一个大工作台(2m×2m×0.25m)和一个工具快换装置(MTC UR Tool Changer)
2. 关键坐标系(Frame)一览(非常重要!)
| 坐标系名称 | 含义说明 | 常用场景 |
|---|---|---|
world |
绝对世界坐标系 | Gazebo 世界原点 |
base_link |
REP-103 标准坐标系(X向前,Y向左,Z向上) | MoveIt / RViz 中常用的机器人基坐标系 |
base |
UR 控制器内部坐标系(X向后,Y向右),比 base_link 绕 Z 转了 180°(π) |
UR 示教器、PolyScope 里显示的 Base 坐标系 |
tool0 |
UR 官方标准工具坐标系(TCP),位于法兰盘中心,方向:X向左,Y向上,Z向前 | UR 脚本、MoveIt 规划时的默认 TCP |
flange |
机械法兰盘坐标系 | 安装工具的机械接口 |
wrist_3_link |
UR5e 第6轴(wrist_3)输出法兰坐标系 | 实际机械末端 |
realsense_camera_adapter_tool0 |
腕部相机适配适器上的 tool0(和 robot tool0 不重合) | 腕部相机的 TCP |
wrist_mounted_camera_link |
D415 相机本体坐标系 | 发布图像的 link |
wrist_mounted_camera_color_optical_frame / depth_optical_frame` |
相机光心坐标系(遵循 ROS 光学坐标系惯例:Z向前,X向右,Y向下) | 图像、点云的 frame_id |
grasp_link |
夹爪最适合抓取的虚拟 TCP(往前伸了 13.4cm(绿色小球可视化位置) | MoveIt Studio 快速抓取任务默认使用的 TCP |
manual_grasp_link |
MoveIt Studio 要求的“手动快速抓取”坐标系(比 grasp_link 稍微偏一点) | Studio 里的 Inspect Surface 等快速任务使用 |
scene_camera_color_optical_frame |
外部 D435 相机的光学坐标系 | 场景点云、图像 |
3. UR5e 本体关节限位(实际物理限制)
| 关节名 | 位置限制(rad) | 速度限制(rad/s) | 备注 |
|---|---|---|---|
| shoulder_pan_joint | -2π ~ 2π(全周) | ±3.15 (~180°/s) | 肩部回转 |
| shoulder_lift_joint | -2π ~ 2π(实际会被软限位限制) | ±3.15 | 但实际软限位约 -270°~+90°(见注释) |
| elbow_joint | -π ~ π | ±3.15 | |
| wrist_1/2/3_joint | -2π ~ 2π | ±3.2 | 手腕三轴全周 |
| 真实使用时,MoveIt 会再加一层软限位(safety limits),防止撞到自己。 |
4. Robotiq 2F-85 夹爪关键点
- 实际只有 1 个主动自由度:
robotiq_85_left_knuckle_joint(0 ~ 0.804 rad) - 其他关节全部用
<mimic>标签做被动跟随(镜像、反向) - 夹爪开合范围对应大约 0~85mm(0.804 rad ≈ 85mm 开口)
- 绿色小球在
robotiq_85_base_link前 16cm 处,用于可视化最佳抓取点 grasp_link才是真正推荐的抓取 TCP(往前伸 13.4cm)
5. 腕部 RealSense D415 安装姿态
从文件可以看到相机安装方式:
<!-- 从 tool_changer_tool0 到相机本体的大概姿态 -->
<origin rpy="0 -1.4835298641951802 1.5707963267948966" xyz="..." />
换算成人话就是:
- 先绕 Y 轴转约 -85°(低头)
- 再绕 Z 轴转 90°
- 最终相机镜头朝前、稍微向下俯视,适合看工作台
6. 场景外部相机 D435 位置
<joint name="external_camera_joint" type="fixed">
<origin rpy="0.0 0.4 0" xyz="-0.3 0.3 1.0"/>
</joint>
即:放在机器人前方左上方(X=-0.3m, Y=0.3m, Z=1.0m),俯角约 23°(0.4 rad),俯视整个工作台。
7. ros2_control 配置要点
- 使用
URPositionHardwareInterface(官方驱动) - 已经把 6 个关节都配置了
position和velocitycommand_interface - 夹爪目前 没有 配置 ros2_control 接口(所以只能用 Robotiq 官方的 Action 或 Modbus 方式控制)
- 还有一个 F/T 传感器接口
tcp_fts_sensor(6轴力矩),但本体没接传感器,只是预留接口
8. Gazebo 仿真相关
- 相机插件用了
libgazebo_ros_camera.so,发布以下话题(已重映射):/wrist_mounted_camera/color/image_raw/wrist_mounted_camera/depth/image_rect_raw/scene_camera/color/image_raw/scene_camera/depth/image_rect_raw
- 夹爪使用了
roboticsgroup_gazebo_mimic_joint_plugin来实现多关节被动跟随
9. 常用 TCP 推荐(实际项目中你会用到的)
| 用途 | 推荐使用的 link / frame | 说明 |
|---|---|---|
| MoveIt 规划(最常用) | tool0 |
UR 官方标准 TCP |
| 夹爪抓取(推荐) | grasp_link |
已经往前伸到夹爪中间,绿色小球位置 |
| MoveIt Studio 快速任务 | manual_grasp_link |
Studio 强制要求这个名字 |
| 腕部相机视觉伺服 | wrist_mounted_camera_color_optical_frame |
相机光心坐标系 |
| 外部场景相机点云对齐 | scene_camera_depth_optical_frame |
pr2.urdf
这是 经典 Willow Garage PR2 机器人 的完整 URDF(ROS 1 时代,Gazebo Classic 7/8/9 用的版本),来自 moveit_resources_pr2_description 包,是目前 MoveIt 官方教程里仍在使用的标准 PR2 模型。
下面给你最详细、最清晰的逐层解析,帮你彻底看懂这份超长 URDF 的每一部分在干什么。
1. 整体概况(一眼看懂 PR2 有什么)
| 部件 | 数量 | 关键传感器/执行器 | 备注 |
|---|---|---|---|
| 移动底盘 | 1 | 4 个独立转向万向轮(caster)+ 8 个驱动轮 | 四轮全向移动 |
| 升降躯干 | 1 | torso_lift_joint 升降 0~0.33m |
丝杠驱动 |
| 双机械臂 | 2 | 各 7 自由度(含连续旋转 roll 关节) | 完全对称,左臂是右臂的镜像 |
| 二指平行夹爪 | 2 | 单电机丝杠驱动,开口 0~90mm | 带接触传感器 + 力反馈仿真 |
| 头部云台 | 1 | head_pan (±172°) + head_tilt (-27°~80°) |
|
| 头部传感器 | 多 | - 高清 Prosilica(单目) - 双目宽角立体相机(wide stereo) - 双目窄角立体相机(narrow stereo) - Kinect(RGB-D) - 投影仪(结构光) |
非常豪华的感知套件 |
| 前额倾斜激光 | 1 | Hokuyo UTM-30LX,安装在可倾斜云台上 | laser_tilt_mount_joint |
| 底盘激光 | 1 | Hokuyo,固定在前方 | base_laser_link |
| 前臂相机 | 2 | 每个前臂各一个宽角相机(用于抓取对准) |
2. 最重要的坐标系(Frame)一览表(实际用的时候必看!)
| frame 名称 | 含义 | 常用场景 |
|---|---|---|
base_footprint |
地面投影点(REP-103 标准) | MoveIt 规划、导航的根坐标系 |
base_link |
底盘中心(比 footprint 高 5.1cm) | 机器人本体坐标系 |
torso_lift_link |
升降躯干顶部 | 两个肩膀的共同父坐标系 |
l/r_gripper_palm_link |
夹爪掌心 | 抓取规划常用 |
l/r_gripper_tool_frame |
推荐的抓取 TCP(掌心往前伸 18cm) | MoveIt 抓取规划默认用这个 |
l/r_gripper_l_finger_tip_frame |
左手指尖(夹爪实际接触点) | 精细抓取、接触检测 |
l/r_forearm_cam_optical_frame |
左前臂相机光学坐标系(Z向前,X向右,Y向下) | 手眼标定、手爪相机视觉伺服 |
head_plate_frame |
头部安装板 | 所有头部传感器的共同安装基准 |
wide_stereo_optical_frame |
宽角立体相机光心(双目中心) | 场景大范围感知 |
narrow_stereo_optical_frame |
窄角立体相机光心(远距离高精度) | |
high_def_optical_frame |
Prosilica 高清相机光心 | 高分辨率纹理获取 |
head_mount_kinect_rgb_optical_frame |
头部 Kinect 彩色相机光心 | |
laser_tilt_link |
倾斜激光雷达坐标系 | 3D 扫描、建图 |
3. 移动底盘结构(最经典的四轮万向轮设计)
PR2 采用 4 个 caster(每个 caster 两个轮子,共 8 个轮子):
fl_caster_rotation_joint → fl_caster_l/r_wheel_joint
fr_caster_rotation_joint → fr_caster_l/r_wheel_joint
bl_caster_rotation_joint → bl_caster_l/r_wheel_joint
br_caster_rotation_joint → br_caster_l/r_wheel_joint
- 每个 caster 的旋转是 continuous 关节(可无限转)
- 每个轮子也是 continuous 关节
- 实际控制时只发底盘速度命令(
/cmd_vel),由pr2_base_controller完成差速转向解算
4. 躯干升降机构(torso_lift_joint)
- 类型:prismatic(直线关节)
- 行程:0 ~ 0.33 m
- 驱动方式:丝杠(Gazebo 中用
<joint:screw>模拟) - 实际机器人用的是滚珠丝杠 + 伺服电机
5. 双臂关节顺序(以右臂为例,左臂完全镜像)
torso_lift_link
└─ r_shoulder_pan_joint (revolute, ±164°左右)
└─ r_shoulder_lift_joint (revolute, -30°~80°)
└─ r_upper_arm_roll_joint (continuous, 无限转)
└─ r_upper_arm_link
└─ r_elbow_flex_joint (revolute, -133°~0°)
└─ r_forearm_roll_joint (continuous)
└─ r_wrist_flex_joint (revolute, -125°~0°)
└─ r_wrist_roll_joint (continuous)
└─ r_gripper_palm_link
6. 夹爪(Gripper)设计亮点(ROS 1 时代的经典丝杠夹爪)
- 实际只有 1 个主动自由度:
r/l_gripper_joint(prismatic,0~0.09m) - 其他所有指节都是 mimic 关节(被动跟随)
- 丝杠螺母机构用 Gazebo 的
<joint:screw>+<joint:slider>完美仿真 - 指尖有接触传感器(
gazebo_ros_bumper) - 提供真实力反馈(
gazebo_ros_f3d) - 提供“抓取黑魔法”插件(
gripper标签,自动把物体吸住用于演示)
7. 头部传感器布局(极其豪华)
| 传感器 | 安装位置 | 分辨率 / FOV | 用途 |
|---|---|---|---|
| Prosilica 高清相机 | 额头正中 | 2448×2050, 45° | 高清纹理、条码识别 |
| 宽角立体相机 | 额头左右 | 640×480, 90° | 近距离大视场感知 |
| 窄角立体相机 | 额头左右(更靠下) | 640×480, 45° | 远距离高精度立体视觉 |
| Kinect (OpenNI) | 额头下方 | RGB-D | 人体检测、点云 |
| 结构光投影仪 | 额头正中 | 投射红外网格图案 | 增强立体匹配(纹理投影) |
| 倾斜 Hokuyo 激光 | 额头可倾斜云台 | 270°扫描 | 3D 环境扫描 |
8. Gazebo 仿真关键点(这份 URDF 是为仿真而生的)
- 大量使用
turnGravityOff>true关闭重力(手臂、夹爪等) - 使用
gazebo_ros_bumper做接触检测 - 使用
gazebo_ros_f3d提供夹爪力反馈 - 使用
gazebo_ros_p3d发布真实位姿(ground truth) - 所有 roll 关节用
fudgeFactor解决连续关节积分漂移问题 - 夹爪用复杂的
<joint:screw>+<joint:hinge>+<joint:slider>组合精确仿真丝杠平行机构
9. 常用 TCP 推荐(实际写 MoveIt 程序时用哪个?)
| 用途 | 推荐 frame | 说明 |
|---|---|---|
| 通用抓取规划 | r/l_gripper_tool_frame |
掌心往前伸 18cm,最稳 |
| 精细抓取 / 接触检测 | r/l_gripper_l_finger_tip_frame |
真实接触点 |
| 手眼标定(前臂相机) | l_forearm_cam_optical_frame |
相机光心坐标系 |
| 头部视觉感知 | wide_stereo_optical_frame |
大视场 |
| 高精度远距离视觉 | narrow_stereo_optical_frame |
窄视场高精度 |
| MoveIt 规划根坐标系 | base_footprint |
必须用这个 |
10. 经典坑 & 小贴士
- PR2 的
base_link的 X 轴是朝后的!(和 REP-103 相反)- 正前方其实是
-X方向 - 所以很多教程里会看到
origin rpy="0 0 3.14159"来翻转坐标系
- 正前方其实是
- 夹爪开口 0.09m ≈ 90mm,实际能抓的最大物体约 85mm
- 手臂有大量
turnGravityOff>true,仿真时不会下垂,真实机器人会! - 所有 roll 关节都是 continuous(无限转),规划时要注意避开奇异位形
- 头部 Kinect 的 frame 是
head_mount_kinect_rgb_optical_frame,不是head_plate_frame
ur5.urdf
这是一份非常典型的“现代 ROS 2 + Ignition Gazebo(现已改名 Gazebo)时代” 的 UR5(非 e 系列)机械臂 URDF,专门用于 Gazebo Sim(原 Ignition Gazebo) 仿真。
它来自 universal_robot_ign 这个新社区包(2023–2025 年活跃),是目前 ROS 2 Humble / Iron / Jazzy 下最推荐的 UR5/UR5e 纯仿真模型。
下面给你最详细、最清晰的逐行解读 + 核心设计思路说明。
1. 整体概况(一眼看懂这是什么)
| 项目 | 内容 |
|---|---|
| 机器人型号 | Universal Robots UR5(非 e 系列,负载 5kg) |
| 适用环境 | ROS 2 + Gazebo Sim(Ignition/Harmonic) |
| 所属包 | universal_robot_ign(社区新版,替代旧的 ur_description) |
| 控制器插件 | libgazebo_ros_control.so(ros2_control 标准接口) |
| 末端执行器 | 只有一个虚拟的 ee_link(带一个小盒子碰撞体 + 0.25kg 负载) |
| 特别设计 | 在 wrist_3_link 和 ee_link 之间插入了 6 个虚拟自由度 用来模拟六轴力/力矩传感器 |
2. 标准 UR5 机械结构(完全符合官方 CAD)
| 关节名称 | 轴向 | 位置偏移(关键尺寸) | 限位(rad) | 最大速度(rad/s) |
|---|---|---|---|---|
| shoulder_pan_joint | Z | +0.089159 m(底座到肩部高度) | ±2π(全周) | 3.15 |
| shoulder_lift_joint | Y | +0.13585 m(肩部偏置) | ±2π | 3.15 |
| elbow_joint | Y | 上臂长 0.425 m,肘部偏置 -0.1197 m | ±π | 3.15 |
| wrist_1_joint | Y | 前臂长 0.39225 m | ±2π | 3.2 |
| wrist_2_joint | Z | +0.093 m | ±2π | 3.2 |
| wrist_3_joint | Y | +0.09465 m | ±2π | 3.2 |
| 所有尺寸与 Universal Robots 官方 CAD 完全一致,可以直接用于真实机器人控制(只要加上校准文件)。 |
3. 最特殊的设计:虚拟六轴力/力矩传感器(FT Sensor)
从 wrist_3_link 到 ee_link 之间,作者插入了整整 6 个零质量的虚拟关节:
wrist_3_link
└─ FT_x_joint (prismatic along X) → FT_y
└─ FT_y_joint (prismatic along -Y) → FT_z
└─ FT_z_joint (prismatic along -Z) → FT_theta_x
└─ FT_theta_x_joint (revolute around X) → FT_theta_y
└─ FT_theta_y_joint (revolute around -Y) → FT_theta_z
└─ FT_theta_z_joint (revolute around -Z) → ee_link
为什么这么做?
这是目前(2025年)Gazebo Sim 官方还不支持 <sensor type="force_torque"> 直接读取关节力矩(ROS 1 的 gazebo_ros_ft_sensor 不能用),所以社区最流行的“土办法”就是:
用 6 个单自由度关节(3平动+3转动 串联起来,质量设为接近 0,然后在 ros2_control 里写一个 ForceTorqueSensorHW,读取这 6 个关节的 effort(力/力矩),就实现了完美的六轴力传感器仿真!
这套写法在 2024–2025 年的所有新 UR 仿真项目里都成了标配。
4. 末端执行器 ee_link 的设计意图
<link name="ee_link">
<collision>
<box size="0.01 0.01 0.01"/> <!-- 一个小方块,防止穿模 -->
</collision>
<inertial>
<mass value="0.25"/> <!-- 模拟 250g 的末端负载 -->
<origin xyz="0.06 -0.02 -0.03"/> <!-- 重心偏一点,更真实 -->
</inertial>
</link>
这个 ee_link 就是你最终要用的 TCP(工具中心点),相当于真实机器人装了夹爪或吸盘后的最末端 link。
5. 关键坐标系(Frame)一览表(实际编程必看)
| frame 名称 | 含义 | 推荐用途 |
|---|---|---|
base_link |
底座中心(Z向上,X向前) | 机器人根坐标系、MoveIt 规划根坐标系 |
wrist_3_link |
第6轴输出法兰(真实机械末端) | 安装工具的机械接口 |
ee_link |
最终工具中心点(TCP) | MoveIt 规划、视觉伺服、抓取点全部用这个 |
FT_theta_z |
虚拟力矩传感器的最末端坐标系 | 读取力矩时用这个 frame |
6. 配套的 ros2_control 配置(你还需要自己补)
这份 URDF 本身只提供了模型部分,要跑起来还需要一个 ros2_control 的 yaml 文件,大概长这样(供参考):
controller_manager:
ros__parameters:
update_rate: 1000
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
ur_arm_controller:
type: joint_trajectory_controller/JointTrajectoryController
joints:
- shoulder_pan_joint
- shoulder_lift_joint
- elbow_joint
- wrist_1_joint
- wrist_2_joint
- wrist_3_joint
command_interfaces:
- position
state_interfaces:
- position
- velocity
ft_sensor_broadcaster:
type: force_torque_sensor_broadcaster/ForceTorqueSensorBroadcaster
sensor_name: ft_sensor
frame_id: FT_theta_z
interface_types:
- force/x
- force/y
- force/z
- torque/x
- torque/y
- torque/z
这样你就能在 /ft_sensor 话题上收到真实的六轴力/力矩数据了。
7. 总结:这份 URDF 的定位
| 优点 | 说明 |
|---|---|
| 完全符合官方尺寸 | 可直接用于真实 UR5 控制(加校准文件即可) |
| 现代 ROS 2 + Gazebo Sim 原生支持 | 不再依赖旧的 ur_description + gazebo_ros_pkgs |
| 自带虚拟六轴力传感器 | 2025 年最流行的仿真力控方案 |
| 干净、模块化 | 没有 Robotiq 夹爪、没有 RealSense,纯粹的 UR5 本体,方便你自己加末端执行器 |
下面给一个 最简单、最标准、最好懂的 URDF 示例,从零开始,一步步解释 URDF 的核心格式和必备元素。
适合初学者 5 分钟就能完全看懂,然后你就可以自己随便加关节、加夹爪、加相机了。
1. 最简 URDF(只有一个底座 + 一个转动的杆子)
文件名:my_first_robot.urdf
<?xml version="1.0"?>
<robot name="my_first_robot">
<!-- 1. 第一个 link:底座,固定在世界 -->
<link name="base_link">
<visual>
<geometry>
<cylinder radius="0.2" length="0.1"/>
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<collision>
<geometry>
<cylinder radius="0.2" length="0.1"/>
</geometry>
</collision>
<inertial>
<mass value="5"/>
<inertia ixx="0.1" ixy="0" ixz="0" iyy="0.1" iyz="0" izz="0.05"/>
</inertial>
</link>
<!-- 2. 第二个 link:一根会转的杆子 -->
<link name="arm_link">
<visual>
<origin xyz="0 0 0.3" rpy="0 0 0"/> <!-- 视觉模型往上挪 30cm -->
<geometry>
<box size="0.05 0.05 0.6"/>
</geometry>
<material name="red">
<color rgba="1 0 0 1"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0.3"/>
<geometry>
<box size="0.05 0.05 0.6"/>
</geometry>
</collision>
<inertial>
<origin xyz="0 0 0.3"/>
<mass value="2"/>
<inertia ixx="0.02" ixy="0" ixz="0" iyy="0.02" iyz="0" izz="0.005"/>
</inertial>
</link>
<!-- 3. 关节:把杆子连到底座上,可以绕 Z 轴转 -->
<joint name="base_to_arm_joint" type="revolute">
<parent link="base_link"/>
<child link="arm_link"/>
<origin xyz="0 0 0.05" rpy="0 0 0"/> <!-- 关节原点在底座顶面往上 5cm -->
<axis xyz="0 0 1"/> <!-- 绕 Z 轴转 -->
<limit lower="-3.14" upper="3.14" effort="100" velocity="2"/>
<dynamics damping="0.5"/>
</joint>
</robot>
保存为 my_first_robot.urdf,然后在终端运行:
ros2 launch urdf_tutorial display.launch.py model:=my_first_robot.urdf
就能在 RViz 里看到一个蓝色圆柱底座 + 红色杆子,还能用 Joint State Publisher 拖动它转圈。
2. URDF 核心标签速查表(99% 的模型都只用这些)
| 标签 | 作用 | 必填项 / 常用子标签 |
|---|---|---|
<robot name=""> |
根标签,机器人名字 | name 属性必填 |
<link name=""> |
一个刚体(可以有形状、质量、碰撞体) | name 必填 |
<joint name=""> |
连接两个 link 的关节 | name、type、parent、child 必填 |
<visual> |
外观(RViz 显示用) | <geometry>, <origin>, <material> |
<collision> |
碰撞体(Gazebo/物理引擎用) | <geometry>, <origin> |
<inertial> |
质量和惯性(物理仿真必备) | <mass>, <origin>, <inertia> |
<geometry> |
形状 | <box>, <cylinder>, <sphere>, <mesh> |
<material name=""> |
颜色/材质 | <color rgba=""> 或 <texture> |
<origin xyz="" rpy=""> |
坐标变换(米 + 弧度) | 常用! |
<axis xyz=""> |
关节旋转轴(revolute/continuous 用) | 必须归一化 |
<limit> |
关节限位 | lower, upper, effort, velocity |
3. 关节类型(type)速记
| type | 含义 | 是否有限位 |
|---|---|---|
| fixed | 完全固定(焊死) | 无需 limit |
| revolute | 单向转动,有角度限位 | 必须写 limit |
| continuous | 可以无限转(如轮子、手腕) | 不需要 lower/upper |
| prismatic | 直线滑动(如气缸、升降台) | 必须写 limit |
| floating | 6自由度浮动(很少用) | 不常用 |
| planar | 平面内移动+转动(很少用) | 不常用 |
4. 再来一个稍微复杂一点的例子(UR5e 风格 + 夹爪)
<?xml version="1.0"?>
<robot name="ur5_with_gripper">
<!-- 省略 UR5 的 6 个关节,这里直接写末端法兰 -->
<link name="wrist_3_link"/>
<!-- 夹爪底座 -->
<link name="gripper_base_link">
<visual>
<origin xyz="0 0 0.05"/>
<geometry><box size="0.06 0.12 0.03"/></geometry>
<material name="silver"><color rgba="0.8 .8 .8 1"/></material>
</visual>
</link>
<!-- 夹爪底座和机械臂连接 -->
<joint name="wrist_3_to_gripper" type="fixed">
<parent link="wrist_3_link"/>
<child link="gripper_base_link"/>
<origin xyz="0 0 0.0823" rpy="0 0 0"/> <!-- UR 官方法兰到工具面距离 -->
</joint>
<!-- 左手指 -->
<link name="left_finger"/>
<joint name="left_finger_joint" type="prismatic">
<parent link="gripper_base_link"/>
<child link="left_finger"/>
<origin xyz="0 0.03 0.05"/>
<axis xyz="0 1 0"/>
<limit lower="0" upper="0.08" effort="100" velocity="0.2"/>
</joint>
<!-- 右手指(镜像) -->
<link name="right_finger"/>
<joint name="right_finger_joint" type="prismatic">
<parent link="gripper_base_link"/>
<child link="right_finger"/>
<origin xyz="0 -0.03 0.05"/>
<axis xyz="0 1 0"/>
<mimic joint="left_finger_joint" multiplier="-1"/> <!-- 关键:镜像运动 -->
</joint>
</robot>
这样就实现了“一个电机驱动,两指对称张合”的最常见平行夹爪。
5. 小结:URDF 写法口诀(背下来就再也不怕了)
- 先写 link(刚体)
- 再写 joint 连两个 link
- 每个 link 都要有 visual + collision + inertial(仿真必须)
- joint 必须写 parent/child + origin + axis + type
- 需要颜色就加 material
- 需要限位就加 limit
- 需要镜像就加
<mimic joint="xxx" multiplier="-1"/>
fast_inverse_kinematics.hpp
#pragma once
#include <Eigen/Core>
#include <Eigen/Dense>
#include "memory"
#ifdef FAST_FK_USE_IK
#include "LBFGS.h"
#include "kinematics_interface.hpp"
namespace fast_fk::internal
{
// ================================================================
// InverseKinematics
// 使用 LBFGS 算法求解机器人逆运动学(IK)问题。
// ================================================================
class InverseKinematics
{
public:
// 目标末端旋转矩阵(3x3)
Eigen::Matrix<float, 3, 3> target_rot_;
// 目标末端位置向量(3x1)
Eigen::Vector<float, 3> target_pose_;
// 存放机器人每个关节的 4x4 齐次变换矩阵(用 16 个 float 表示)
// joint_data[j] 存的是第 j 个关节的变换矩阵。
std::array<std::array<float, 16>, FAST_FK_NUMBER_OF_JOINTS> joint_data = { 0 };
// LBFGS 求解器(智能指针)
std::unique_ptr<LBFGSpp::LBFGSSolver<float>> solver;
// LBFGS 求解参数结构体
LBFGSpp::LBFGSParam<float> param;
// ------------------------------------------------------------
// 构造函数:初始化目标旋转、位置,并设置优化器参数
// ------------------------------------------------------------
InverseKinematics(const Eigen::Matrix<float, 3, 3>& target_rot,
const Eigen::Vector<float, 3>& target_pose)
: target_rot_{ target_rot }, target_pose_{ target_pose }
{
// LBFGS 参数设置
param.epsilon = 1E-3; // 绝对误差停止条件
param.epsilon_rel = 1E-3; // 相对误差停止条件
param.max_iterations = 30; // 最大迭代次数
// 创建求解器实例
solver = std::make_unique<LBFGSpp::LBFGSSolver<float>>(param);
}
// ===============================================================
// inverse_kinematics()
//
// 输入:
// transform 目标末端位姿(4x4 齐次矩阵)
// q_guess 关节角初值(由外部提供)
//
// 输出:
// IKSolverStats:包含最终代价、迭代次数、梯度范数、是否成功等信息。
//
// 算法:
// 1. 从 transform 提取旋转 + 平移 → 更新 target_rot_ 和 target_pose_
// 2. 调用 LBFGS solver->minimize()
// 3. 异常处理:当优化失败时(奇异、梯度异常等)
// ===============================================================
fk_interface::IKSolverStats inverse_kinematics(Eigen::Matrix<float, 4, 4>& transform,
Eigen::VectorX<float>& q_guess)
{
// 从 4x4 齐次矩阵提取旋转部分(左上 3x3)
target_rot_ = transform.block<3, 3>(0, 0);
// 提取平移部分
target_pose_(0) = transform(0, 3);
target_pose_(1) = transform(1, 3);
target_pose_(2) = transform(2, 3);
float fx = 1E10; // 初始代价函数值(会在最小化中更新)
int niter; // 记录最终迭代次数
try
{
// minimize() 调用 operator() 计算 cost 和 grad
niter = solver->minimize(*this, q_guess, fx);
}
catch(const std::runtime_error& e)
{
// 若优化过程中出现异常(如 NaN、梯度发散),返回失败状态
return { fx, niter, solver->final_grad_norm(), false, e.what() };
}
// 求解成功
return { fx, niter, solver->final_grad_norm(), true, "" };
}
// ==========================================================
// operator()
//
// LBFGS 的目标函数回调:
// 输入:q(当前关节角)
// 输出:
// 返回 → 标量代价值(末端误差)
// grad → 代价函数对 q 的梯度
//
// 备注:
// 在 cpp 文件中实现,因为需要 FK 和雅可比计算。
// ==========================================================
float operator()(const Eigen::VectorX<float>& q, Eigen::VectorX<float>& grad);
};
} // namespace fast_fk::internal
#else
// 若未启用 FAST_FK_USE_IK,则提供 dummy 实现
namespace fast_fk::internal
{
class InverseKinematics
{
public:
InverseKinematics(const Eigen::Matrix<float, 3, 3>& target_rot,
const Eigen::Vector<float, 3>& target_pose)
{}
fk_interface::IKSolverStats inverse_kinematics(Eigen::Matrix<float, 4, 4>& transform,
Eigen::VectorX<float>& q_guess)
{
// 如果未启用 IK,则抛异常
throw std::logic_error("Function not implemented.");
return {};
}
float operator()(const Eigen::VectorX<float>& q, Eigen::VectorX<float>& grad);
};
} // namespace fast_fk::internal
#endif
fast_inverse_kinematics.hpp 详细理解(含数学公式)
本文件实现一个基于 LBFGS 优化算法的 逆运动学(IK)求解器。
目标:给定末端执行器的目标位姿(旋转 + 平移),求解机器人关节角 q q q。
1⃣ 类结构概览
class InverseKinematics
此类包含:
| 成员 | 意义 |
|---|---|
target_rot_ |
目标旋转矩阵 R d ∈ S O ( 3 ) R_d \in SO(3) Rd∈SO(3) |
target_pose_ |
目标位置 p d ∈ R 3 p_d \in \mathbb{R}^3 pd∈R3 |
joint_data |
用于存储各关节齐次矩阵(由 FK 生成) |
solver |
LBFGS 求解器 |
param |
LBFGS 参数结构体 |
2⃣ IK 的数学问题
逆运动学目标:
求解关节角 q q q,使机器人末端位姿逼近目标:
f ( q ) = [ R ( q ) p ( q ) 0 1 ] ≈ [ R d p d 0 1 ] f(q) = \begin{bmatrix} R(q) & p(q) \\ 0 & 1 \end{bmatrix} \approx \begin{bmatrix} R_d & p_d \\ 0 & 1 \end{bmatrix} f(q)=[R(q)0p(q)1]≈[Rd0pd1]
构造代价函数:
J ( q ) = ∣ p ( q ) − p d ∣ 2 + w R ⋅ ∣ Log ( R d T R ( q ) ) ∣ 2 J(q) = | p(q) - p_d |^2 + w_R \cdot | \text{Log}(R_d^T R(q)) |^2 J(q)=∣p(q)−pd∣2+wR⋅∣Log(RdTR(q))∣2
其中:
- p ( q ) p(q) p(q):末端位置
- R ( q ) R(q) R(q):末端朝向
- Log ( ⋅ ) \text{Log}(\cdot) Log(⋅):李群 S O ( 3 ) SO(3) SO(3) 的向量化对数映射,得到旋转误差向量
IK 的目标是:
q ∗ = arg min q J ( q ) q^* = \arg \min_q J(q) q∗=argqminJ(q)
本文件就是对这个目标函数 J ( q ) J(q) J(q) 使用 LBFGS 求最小值。
3⃣ 构造函数解析
InverseKinematics(const Eigen::Matrix<float, 3, 3>& target_rot,
const Eigen::Vector<float, 3>& target_pose)
: target_rot_{ target_rot }, target_pose_{ target_pose }
{
param.epsilon = 1E-3;
param.epsilon_rel = 1E-3;
param.max_iterations = 30;
solver = std::make_unique<LBFGSpp::LBFGSSolver<float>>(param);
}
说明:
- 保存目标旋转 R d R_d Rd 和目标位置 p d p_d pd
- 配置 LBFGS 停止条件:
- 当梯度范数小于
ϵ = 1 0 − 3 \epsilon=10^{-3} ϵ=10−3 - 或函数变化比率小于
ϵ rel = 1 0 − 3 \epsilon_{\text{rel}}=10^{-3} ϵrel=10−3
- 当梯度范数小于
- 最多迭代 30 30 30 次
4⃣ inverse_kinematics() 功能解析
fk_interface::IKSolverStats inverse_kinematics(Eigen::Matrix<float, 4, 4>& transform,
Eigen::VectorX<float>& q_guess)
功能:
- 从 4×4 齐次矩阵中提取:
R d = T 0 e [ 0 ! : ! 3 , 0 ! : ! 3 ] , p d = T 0 e [ 0 ! : ! 3 , 3 ] R_d = T_{0e}[0!:!3,0!:!3],\quad p_d = T_{0e}[0!:!3,3] Rd=T0e[0!:!3,0!:!3],pd=T0e[0!:!3,3] - 调用 LBFGS:
niter = solver->minimize(*this, q_guess, fx);
即:让 LBFGS 重复调用 class 的 operator() 计算:
- 代价函数 J ( q ) J(q) J(q)
- 梯度 ∂ J ∂ q \frac{\partial J}{\partial q} ∂q∂J
若优化失败(如出现 N a N NaN NaN),会抛异常并被捕获。
5⃣ operator() —— 代价函数回调
float operator()(const Eigen::VectorX<float>& q, Eigen::VectorX<float>& grad);
这是 LBFGS 的核心接口:
LBFGS 每次迭代都会执行:
cost = ik(q, grad)
其中:
- 输入:当前关节角 q q q
- 输出:
- 返回值:代价 J ( q ) J(q) J(q)
grad:梯度向量
IK 中常用的代价函数:
J ( q ) = α ∣ p ( q ) − p d ∣ 2 + β ∣ Log ( R d T R ( q ) ) ∣ 2 J(q) = \alpha|p(q) - p_d|^2 + \beta |\text{Log}(R_d^T R(q))|^2 J(q)=α∣p(q)−pd∣2+β∣Log(RdTR(q))∣2
梯度通常通过雅可比矩阵计算:
∂ J ∂ q = J p ⊤ ( p ( q ) − p d ) + J R ⊤ , Log ( R d T R ( q ) ) \frac{\partial J}{\partial q} = J_p^\top (p(q)-p_d) + J_R^\top , \text{Log}(R_d^T R(q)) ∂q∂J=Jp⊤(p(q)−pd)+JR⊤,Log(RdTR(q))
其中:
- J p J_p Jp:位置雅可比
- J R J_R JR:旋转雅可比
** 本文件只声明 operator(),真正实现要放在 cpp 文件中(否则依赖 FK 和雅可比)。**
6⃣ 条件编译的作用
#ifdef FAST_FK_USE_IK
若启用了 IK,则使用完整实现;
否则提供 dummy:
throw std::logic_error("Function not implemented.");
用于编译不需要 IK 的版本(例如性能测试或仅使用 FK)。
🟦 整体流程图(清晰化)
输入目标变换 T
│
▼
inverse_kinematics()
│ 更新 target_rot_, target_pose_
│ 调用 LBFGS
│
▼
LBFGS 循环迭代:
1. operator(q, grad)
│
├─ forward kinematics → 得到 R(q), p(q)
├─ 计算位置误差: p(q)-p_d
├─ 计算旋转误差: Log(R_dᵀ R(q))
├─ 计算总代价 J(q)
└─ 计算梯度 grad
│
▼
LBFGS 收敛?
│
├─ 是 → 输出 q*
└─ 否 → 达到迭代上限
fast_kinematics.hpp
#pragma once // 防止头文件被重复 include
#include <memory> // 用于 std::unique_ptr
#include <Eigen/Core> // Eigen 基础矩阵/向量类型
#include <Eigen/Dense> // Eigen 扩展数学运算
#include "fast_forward_kinematics/fast_inverse_kinematics.hpp" // 引入 IK 求解器定义
namespace fast_fk
{
namespace internal
{
// --------------------------------------------------------------------------
// forward_kinematics_internal
//
// 内部实现的快速前向运动学(FK)计算函数。
// input_data 输入数组中包含:每个关节的 sin(t), cos(t) 以及输出位置 px, py, pz 和旋转矩阵 R11..R33。
// size = FAST_FK_NUMBER_OF_JOINTS * 16,表示每个关节占用 16 个 float。
// --------------------------------------------------------------------------
void forward_kinematics_internal(float* input_data, size_t size);
} // namespace internal
// ==========================================================================
// JointData:存储机器人所有关节的输入/输出数据,并封装 FK / IK 接口。
// ==========================================================================
struct JointData
{
static constexpr size_t get_num_joints() // 返回机器人关节数(由 CMake 脚本自动生成 FAST_FK_NUMBER_OF_JOINTS)
{
return FAST_FK_NUMBER_OF_JOINTS;
}
JointData()
: target_pose{ Eigen::Vector<float, 3>::Zero() } // 目标位置向量初始化为 0
, target_rot{ Eigen::Matrix<float, 3, 3>::Zero() } // 目标旋转矩阵初始化为 0(IK 会更新)
, fun{ std::make_unique<internal::InverseKinematics>(target_rot, target_pose) }
// 创建 IK 求解器对象,传入目标姿态的引用
{}
// ------------------------------------------------------------------------
// set_joint(ind, value)
//
// 设置第 ind 个关节的值。为了提升 FK 性能,内部只存储 sin(t) 和 cos(t),
// 在 FK 计算中不再调用 std::sin / std::cos。
// joint_data[ind][0] = sin(t)
// joint_data[ind][1] = cos(t)
// ------------------------------------------------------------------------
void set_joint(size_t ind, float value)
{
joint_data[ind][0] = std::sin(value);
joint_data[ind][1] = std::cos(value);
}
// 一次性设置所有关节角(Eigen 向量版本)
void set_joints(const Eigen::Vector<float, FAST_FK_NUMBER_OF_JOINTS>& values)
{
#pragma unroll // 编译器优化:展开循环
for(auto ind = 0; ind < FAST_FK_NUMBER_OF_JOINTS; ++ind)
{
joint_data[ind][0] = std::sin(values[ind]); // 预存 sin
joint_data[ind][1] = std::cos(values[ind]); // 预存 cos
}
}
// set_joint:直接传入 sin 和 cos(避免重复计算)
void set_joint(size_t ind, float sin_t, float cos_t)
{
joint_data[ind][0] = sin_t;
joint_data[ind][1] = cos_t;
}
// 一次性设置所有关节 sin/cos(指针版)
void set_joints(const float* sin_values, const float* cos_values)
{
#pragma unroll
for(auto ind = 0; ind < FAST_FK_NUMBER_OF_JOINTS; ++ind)
{
joint_data[ind][0] = sin_values[ind];
joint_data[ind][1] = cos_values[ind];
}
}
// 一次性设置所有关节 sin/cos(Eigen 向量版)
void set_joints(const Eigen::Vector<float, FAST_FK_NUMBER_OF_JOINTS>& sin_values,
const Eigen::Vector<float, FAST_FK_NUMBER_OF_JOINTS>& cos_values)
{
#pragma unroll
for(auto ind = 0; ind < FAST_FK_NUMBER_OF_JOINTS; ++ind)
{
joint_data[ind][0] = sin_values[ind];
joint_data[ind][1] = cos_values[ind];
}
}
// ------------------------------------------------------------------------
// get_joint()
//
// 从 sin(t), cos(t) 还原关节角:
// t = atan2( sin(t), cos(t) )
//
// 数学上满足:
// $$ \theta = \operatorname{atan2}(\sin\theta,\cos\theta) $$
//
// ------------------------------------------------------------------------
[[nodiscard]] float get_joint(size_t ind) const
{
return atan2f(joint_data[ind][0], joint_data[ind][1]);
}
// 获取所有关节角(Eigen 向量)
void get_joints(Eigen::Vector<float, FAST_FK_NUMBER_OF_JOINTS>& values) const
{
#pragma unroll
for(auto ind = 0; ind < FAST_FK_NUMBER_OF_JOINTS; ++ind)
{
values[ind] = atan2f(joint_data[ind][0], joint_data[ind][1]);
}
}
// ------------------------------------------------------------------------
// get_frame(index, transform)
//
// 返回第 index 个关节/连杆的 4x4 齐次变换矩阵:
//
// $$
// T = \begin{bmatrix}
// R_{3×3} & p_{3×1} \\
// 0 & 1
// \end{bmatrix}
// $$
//
// 数据来自 joint_data[index][2..13]
// ------------------------------------------------------------------------
void get_frame(size_t index, Eigen::Matrix<float, 4, 4>& transform) const
{
transform(0, 3) = joint_data[index][2];
transform(1, 3) = joint_data[index][3];
transform(2, 3) = joint_data[index][4];
transform(0, 0) = joint_data[index][5];
transform(0, 1) = joint_data[index][6];
transform(0, 2) = joint_data[index][7];
transform(1, 0) = joint_data[index][8];
transform(1, 1) = joint_data[index][9];
transform(1, 2) = joint_data[index][10];
transform(2, 0) = joint_data[index][11];
transform(2, 1) = joint_data[index][12];
transform(2, 2) = joint_data[index][13];
transform(3, 0) = 0.0;
transform(3, 1) = 0.0;
transform(3, 2) = 0.0;
transform(3, 3) = 1.0; // 齐次矩阵固定为 1
}
// ------------------------------------------------------------------------
// forward_kinematics()
//
// 使用 AVX 或 SSE 优化过的底层函数进行前向运动学。
// internal::forward_kinematics_internal 会更新 joint_data 中的
// - 位置 px,py,pz
// - 旋转矩阵 R
// 从而加速 FK 计算。
// ------------------------------------------------------------------------
void forward_kinematics()
{
internal::forward_kinematics_internal(joint_data.data()->data(),
joint_data.size() * 16);
}
// ------------------------------------------------------------------------
// inverse_kinematics()
//
// 调用 fast_inverse_kinematics.hpp 中的 LBFGS IK 求解器。
// 输入:
// - transform: 目标末端位姿 $T_{d}$
// - q_guess: IK 初值
//
// 输出:
// IKSolverStats:最终代价值、迭代次数、梯度范数、成功标志等
// ------------------------------------------------------------------------
fk_interface::IKSolverStats inverse_kinematics(Eigen::Matrix<float, 4, 4>& transform,
Eigen::VectorX<float>& q_guess)
{
return fun->inverse_kinematics(transform, q_guess);
}
// ----------------------------------------------------------------------
// 成员变量说明
// ----------------------------------------------------------------------
Eigen::Matrix<float, 3, 3> target_rot; // IK 目标旋转矩阵 $R_d$
Eigen::Vector<float, 3> target_pose; // IK 目标位置 $p_d$
// 存储每个关节的 16 个 float:
// [0] sin(t)
// [1] cos(t)
// [2..4] 位置 px, py, pz
// [5..13] 旋转矩阵 R11..R33
std::array<std::array<float, 16>, FAST_FK_NUMBER_OF_JOINTS> joint_data = { 0 };
// IK 求解器
std::unique_ptr<internal::InverseKinematics> fun;
};
} // namespace fast_fk
详细理解(逐段解释 + 数学公式规范)
下面是对整个文件的逐段深度解析,让你能完全理解:
文件开头与头文件包含
#pragma once // 防止头文件被重复 include
相当于传统头文件宏:
#ifndef XXX_H
#define XXX_H
...
#endif
防止多重包含。
#include <memory> // 用于 std::unique_ptr
#include <Eigen/Core> // Eigen 基础矩阵/向量类型
#include <Eigen/Dense> // Eigen 扩展数学运算
说明:
std::unique_ptr用于管理 IK 求解器对象,避免手动管理内存。Eigen提供矩阵、向量、旋转等数学工具。
#include "fast_forward_kinematics/fast_inverse_kinematics.hpp" // 引入 IK 求解器定义
这使得 JointData 既能做 FK,也能做 IK。
SIMD 前向运动学内部函数
void forward_kinematics_internal(float* input_data, size_t size);
这是 SIMD 加速的核心 FK 函数。
数据布局固定为每关节 16 个 float:
| 索引范围 | 内容 |
|---|---|
| 0 | sin ( θ ) \sin(\theta) sin(θ) |
| 1 | cos ( θ ) \cos(\theta) cos(θ) |
| 2…4 | 位置 p x , p y , p z p_x,p_y,p_z px,py,pz |
| 5…13 | 旋转矩阵 R ∈ S O ( 3 ) R \in SO(3) R∈SO(3) |
| 这是为 SIMD 批量处理设计的结构。 |
JointData:机器人运动学核心数据结构
get_num_joints()
static constexpr size_t get_num_joints()
{
return FAST_FK_NUMBER_OF_JOINTS;
}
关节数量来自 CMake(自动生成)。
构造函数
JointData()
: target_pose{ Eigen::Vector<float, 3>::Zero() }
, target_rot{ Eigen::Matrix<float, 3, 3>::Zero() }
, fun{ std::make_unique<internal::InverseKinematics>(target_rot, target_pose) }
{}
解释:
target_pose = 0:IK 默认末端目标位置为 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)。target_rot = 0:旋转矩阵先设为零(IK 会修改)。- 将
target_pose与target_rot传给 IK 求解器,使 IK 直接操作它们。
设置关节角(支持多种输入形式)
单个关节的角度
void set_joint(size_t ind, float value)
{
joint_data[ind][0] = std::sin(value);
joint_data[ind][1] = std::cos(value);
}
说明:
将角度 θ \theta θ 转换为:
- sin ( θ ) \sin(\theta) sin(θ)
- cos ( θ ) \cos(\theta) cos(θ)
FK 计算时不再调用三角函数,从而: - 避免函数开销
- 避免精度损失
- SIMD 可以直接使用
所有关节角(Eigen 向量版本)
void set_joints(const Eigen::Vector<float, FAST_FK_NUMBER_OF_JOINTS>& values)
使用 #pragma unroll 对循环进行展开,提高性能。
直接传入 sin/cos
void set_joint(size_t ind, float sin_t, float cos_t)
适用于:
- 已经批量计算过 sin/cos
- 外部控制器直接提供 sin/cos
批量设置 sin/cos(指针版 + Eigen 版)
两种版本目的相同,只是输入类型不同。
获取关节角
float get_joint(size_t ind) const
{
return atan2f(joint_data[ind][0], joint_data[ind][1]);
}
恢复角度:
θ = atan2 ( sin θ , cos θ ) \theta = \operatorname{atan2}(\sin\theta, \cos\theta) θ=atan2(sinθ,cosθ)
使用 atan2 是为了避免:
- 四象限丢失
- 三角函数反解不唯一的问题
这是最稳定的反向恢复角度方式。
获取所有关节角
values[ind] = atan2f(...)
也是直接批量恢复。
获取单个关节的齐次变换矩阵
void get_frame(size_t index, Eigen::Matrix<float, 4, 4>& transform) const
矩阵形式:
T = [ R p 0 1 ] T = \begin{bmatrix} R & p \\ 0 & 1 \end{bmatrix} T=[R0p1]
其中:
$R$为 3 × 3 3 \times 3 3×3 旋转矩阵$p$为位置向量
代码里直接从joint_data数组读取:[2..4]→p[5..13]→R
前向运动学(FK)
void forward_kinematics()
{
internal::forward_kinematics_internal(joint_data.data()->data(),
joint_data.size() * 16);
}
重点讲解:
joint_data 是:
array< array<float, 16>, N >
joint_data.data()->data() 解引用两层:
joint_data.data()→ 指向第 0 个关节(一个 array).data()→ 指向第 0 个关节的 float[16]
这样得到了 连续内存块首地址(SIMD 所需)。
传入:
N * 16 个 float
底层 SIMD 代码会:
- 更新每个关节的
$p_x,p_y,p_z$ - 更新
$R$(旋转矩阵)
速度极快(AVX2/AVX512)。
逆向运动学(IK)
fk_interface::IKSolverStats inverse_kinematics(...)
IK 使用 LBFGS 优化法:
目标误差为:
E = w p ∣ p − p d ∣ 2 + w R ∣ R − R d ∣ 2 E = w_p | p - p_d |^2 + w_R | R - R_d |^2 E=wp∣p−pd∣2+wR∣R−Rd∣2
返回:
- 优化次数
- 成功与否
- 最终误差
- 梯度范数
q_guess为初始关节角。
成员变量
Eigen::Matrix<float, 3, 3> target_rot; // IK 目标旋转矩阵 R_d
Eigen::Vector<float, 3> target_pose; // IK 目标位置 p_d
std::array<std::array<float, 16>, FAST_FK_NUMBER_OF_JOINTS> joint_data;
每个关节 16 个 float,SIMD-friendly。
std::unique_ptr<internal::InverseKinematics> fun;
LBFGS IK 求解器。
kinematics_interface.hpp
#pragma once
#include <Eigen/Core>
#include <concepts>
namespace fk_interface
{
// ========================================================
// IKSolverStats:逆向运动学(IK)求解器执行后的统计数据
// ========================================================
// fx → 最终目标函数值(越小越好)
// niter → 优化迭代次数
// grad_norm → 梯度范数,用于评估是否接近最优点
// success → 是否成功收敛
// what → 错误信息或提示文本
// ========================================================
struct IKSolverStats
{
float fx = 0;
int niter = 0;
float grad_norm = 0;
bool success = false;
std::string what;
};
// ========================================================
// Concept 1:KinematicInterfaceConstraint
// ----------------------------------------
// 约束 T 必须满足:
// - 提供关节数量接口 T::get_num_joints()
// - 能够设置单个关节角度、所有关节角度
// - 支持设置关节角的两种形式:角度值 或 sin/cos
// - 支持获取单个关节角度、全部关节角度
//
// 换句话说:
// KinematicInterfaceConstraint 描述“可设置与获取关节角”的机器人模型。
// ========================================================
template <typename T>
concept KinematicInterfaceConstraint =
requires(T obj, size_t ind, float value,
const Eigen::Vector<float, T::get_num_joints()>& values,
Eigen::Vector<float, T::get_num_joints()>& values_non_const,
Eigen::VectorX<float>& q_guess)
{
// 必须有静态函数 get_num_joints(),用于编译期确定关节数量
{ T::get_num_joints() };
// 设置单个关节角度:set_joint(index, angle)
{ obj.set_joint(ind, value) };
// 设置全部关节角(以 Eigen::Vector 输入)
{ obj.set_joints(values) };
// 设置单个关节:指定 sin(angle) 和 cos(angle)
{ obj.set_joint(ind, value, value) };
// 批量设置 sin(angle) 与 cos(angle)
{ obj.set_joints(values, values) };
// 获取单个关节角
{ obj.get_joint(ind) };
// 获取所有关节角
{ obj.get_joints(values_non_const) };
};
// ========================================================
// Concept 2:ForwardKinematicInterfaceConstraint
// -----------------------------------------------
// 约束 T 必须满足:
// - 能根据关节角生成指定关节的齐次变换矩阵 get_frame()
// - 能执行前向运动学计算 forward_kinematics()
//
// 也就是说:
// T 必须是某种“支持 FK 的机器人”。
// ========================================================
template <typename T>
concept ForwardKinematicInterfaceConstraint =
requires(T obj, size_t ind, Eigen::Matrix<float, 4, 4>& transform)
{
// 获得第 ind 个关节的变换矩阵
{ obj.get_frame(ind, transform) };
// 执行前向运动学
{ obj.forward_kinematics() };
};
// ========================================================
// Concept 3:InverseInterfaceConstraint
// --------------------------------------
// 约束 T 必须满足:
// - 提供 inverse_kinematics(transform, guess)
// - 返回类型必须为 IKSolverStats
//
// 也就是说:
// T 必须是“支持 IK(逆运动学)的机器人模型”。
// ========================================================
template <typename T>
concept InverseInterfaceConstraint =
requires(T obj, Eigen::VectorX<float>& q_guess,
Eigen::Matrix<float, 4, 4>& transform)
{
// inverse_kinematics() 必须返回 IKSolverStats
{
obj.inverse_kinematics(transform, q_guess)
} -> std::same_as<IKSolverStats>;
};
// ========================================================
// ForwardKinematicsInterface:
// 给任意满足 FK 与基本关节接口的类,封装为“前向运动学接口”
//
// 继承自 KI(即用户提供的类型)
// 好处:
// - 调用者可以统一使用 ForwardKinematicsInterface<KI>
// - 保证 KI 满足 FK 所需的所有接口
//
// 使用方式:
// using MyRobotFK = fk_interface::ForwardKinematicsInterface<JointData>;
// ========================================================
template <typename KI>
requires KinematicInterfaceConstraint<KI> &&
ForwardKinematicInterfaceConstraint<KI>
struct ForwardKinematicsInterface : KI
{
// 没有添加任何新成员,只是“概念包装器”
};
// ========================================================
// InverseKinematicsInterface:
// 给任何同时满足 Kinematics + FK + IK 的类型,构造一个统一接口
//
// 继承原始类型 IK
//
// 使用方式:
// using MyRobotIK = fk_interface::InverseKinematicsInterface<JointData>;
// ========================================================
template <typename IK>
requires KinematicInterfaceConstraint<IK> &&
ForwardKinematicInterfaceConstraint<IK> &&
InverseInterfaceConstraint<IK>
struct InverseKinematicsInterface : IK
{
// 与 ForwardKinematicsInterface 类似,这里只是概念包装
};
} // namespace fk_interface
完整理解文档(含数学公式排版)
下面内容不是代码注释,而是对代码的深入解释,帮助你彻底理解整个接口体系设计。
1. IKSolverStats —— IK 求解的收敛信息
在逆向运动学(Inverse Kinematics,IK)中,我们通常要求解关节向量
q = ( q 1 , q 2 , … , q n ) \mathbf{q} = (q_1, q_2, \dots, q_n) q=(q1,q2,…,qn)
使得末端执行器的变换矩阵满足目标位姿。
IK 求解器通常是一个数值优化算法(例如 L-BFGS),每次求解过程都会产生如下统计信息:
struct IKSolverStats {
float fx;
int niter;
float grad_norm;
bool success;
std::string what;
};
其含义为:
- fx:最终目标函数值,即 f ( q ) f(\mathbf{q}) f(q)
- niter:优化的迭代次数
- grad_norm:最终梯度范数 ∣ ∇ f ( q ) ∣ |\nabla f(\mathbf{q})| ∣∇f(q)∣
- success:是否成功收敛
- what:错误信息
可视为 IK 求解过程的一个总结报告。
2. Concept: KinematicInterfaceConstraint
这是机器人运动学接口体系的 基础约束。
它规定:
“一个类型 T 若要用于运动学计算,它必须支持对关节变量的设置与读出。”
用数学公式表示,机器人关节角为
q = ( q 1 , q 2 , … , q n ) \mathbf{q} = (q_1, q_2, \dots, q_n) q=(q1,q2,…,qn)
该 Concept 要求类型 T 必须:
(1)提供静态关节数量:
T::get_num_joints()
这让 C++ 能用模板参数Eigen::Vector<float, T::get_num_joints()>
来定义 定长向量。
(2)支持设置关节角:
- 设置一个关节:
- 用角度值 q i q_i qi
- 或用 ( sin q i , cos q i ) (\sin q_i, \cos q_i) (sinqi,cosqi)
- 设置全部关节:
- 一次性设置 q 1 , q 2 , … q_1, q_2, \dots q1,q2,…
- 或一次性设置全部 sin q i \sin q_i sinqi 和 cos q i \cos q_i cosqi
之所以支持 sin \sin sin 和 cos \cos cos,是因为 前向运动学中旋转矩阵通常会反复用到正弦与余弦,直接存储它们可以加速计算。
(3)支持读取关节角:
- 获取单个: q i = a t a n 2 ( sin q i , cos q i ) q_i = \mathrm{atan2}(\sin q_i, \cos q_i) qi=atan2(sinqi,cosqi)
- 获取全部:返回向量 ( q 1 , … , q n ) (q_1, \dots, q_n) (q1,…,qn)
3. Concept: ForwardKinematicInterfaceConstraint
此 Concept 表示:
“类型 T 必须是一个支持前向运动学 (FK) 的机器人模型。”
数学上:
FK : q ↦ b a s e T i \text{FK}: \mathbf{q} \mapsto {}^{base}T_{i} FK:q↦baseTi
其中 b a s e T i {}^{base}T_{i} baseTi 是第 i i i 个连杆的齐次变换矩阵。
因此 T 必须提供两个接口:
(1)获取某连杆的变换矩阵
obj.get_frame(ind, transform)
其中 transform 是 4 × 4 4\times 4 4×4 齐次矩阵:
T = [ R p 0 1 ] T = \begin{bmatrix} R & \mathbf{p} \\ 0 & 1 \end{bmatrix} T=[R0p1]
(2)执行前向运动学:
obj.forward_kinematics();
这一步通常会计算所有关节空间坐标。
4. Concept: InverseInterfaceConstraint
此 Concept 定义:
“一个类型若要支持逆向运动学,它必须提供 inverse_kinematics() 接口。”
数学上描述:
求 q = IK ( T target ) \text{求 } \mathbf{q} = \text{IK}(T_\text{target}) 求 q=IK(Ttarget)
T 必须提供:
obj.inverse_kinematics(transform, q_guess)
并且返回值类型必须为:
std::same_as<IKSolverStats>
确保 IK 接口统一规范。
5. ForwardKinematicsInterface —— FK 接口包装器
这是一个 类型适配器(type wrapper):
template <typename KI>
requires KinematicInterfaceConstraint<KI> &&
ForwardKinematicInterfaceConstraint<KI>
struct ForwardKinematicsInterface : KI
{
};
它的作用不是添加成员,而是:
编译期强制检查
类型 KI 必须满足:
- 能读写关节角(KinematicInterfaceConstraint)
- 能做前向运动学(ForwardKinematicInterfaceConstraint)
然后才允许用于 FK 系统。
用法示例:
using MyFK = fk_interface::ForwardKinematicsInterface<JointData>;
这让调用方可以统一使用:
MyFK robot;
robot.forward_kinematics();
6. InverseKinematicsInterface —— FK + IK 包装器
类似地,这个接口要求:
- 关节接口(读写关节)
- 前向运动学
- 逆向运动学
全部都必须满足。
template <typename IK>
requires KinematicInterfaceConstraint<IK> &&
ForwardKinematicInterfaceConstraint<IK> &&
InverseInterfaceConstraint<IK>
struct InverseKinematicsInterface : IK
{
};
数学意义:
要做 IK,就必须先能做 FK。
数值 IK 算法通常需要在迭代中多次调用 FK:
f ( q ) = pose_error ( FK ( q ) , T target ) f(\mathbf{q}) = \text{pose\_error}(\text{FK}(\mathbf{q}), T_{\text{target}}) f(q)=pose_error(FK(q),Ttarget)
所以概念设计上要求 IK 类型必须包含 FK。
总结(核心思想)
整个文件构建了一个完整的运动学接口体系:
- 基础概念:关节角设置 / 获取
- 功能概念:前向运动学(FK)
- 功能概念:逆向运动学(IK)
- 包装器:将用户的类型变为可统一使用的接口类型
test
kdl_kinematics.hpp
#pragma once
#include "chainfksolverpos_recursive.hpp" // KDL 链式正向运动学(位置)求解器
#include "chainfksolvervel_recursive.hpp" // KDL 链式正向运动学(速度)求解器
#include "chainiksolverpos_lma.hpp" // KDL LMA 数值逆向运动学求解器
#include "chainjnttojacsolver.hpp" // KDL 雅可比矩阵求解器
#include "eigen3/Eigen/Core" // Eigen 基础矩阵功能
#include "eigen3/Eigen/LU" // Eigen LU 分解
#include "fstream" // 读取 URDF 文件
#include "kdl_parser/kdl_parser.hpp" // 将 URDF 转换为 KDL 数据结构
#include "kinematics_interface.hpp" // 自定义运动学接口
#include "memory" // std::shared_ptr
#include "treejnttojacsolver.hpp" // KDL 树型雅可比求解器(用于 Tree)
// 将宏值转为字符串字面量
#define STRINGIZE(x) #x
#define STRINGIZE_VALUE_OF(x) STRINGIZE(x)
namespace kdl_impl
{
// ------------------------------------------------------------
// JointData:使用 KDL 实现的关节数据及运动学封装
// ------------------------------------------------------------
struct JointData
{
// 静态函数:返回关节数量(由 CMake 定义的 NUMBER_OF_JOINTS 宏提供)
static constexpr size_t get_num_joints()
{
return NUMBER_OF_JOINTS;
}
// ------------------------------------------------------------
// 构造函数:加载 URDF,构建 KDL Tree 和 KDL Chain,并初始化求解器
// ------------------------------------------------------------
JointData()
{
// -----------------------------
// 读取 URDF 文件(从宏获取路径)
// -----------------------------
std::string urdf_file = STRINGIZE_VALUE_OF(URDF_FILE);
std::string robot_description;
{
std::fstream f;
f.open(urdf_file);
std::stringstream ss;
ss << f.rdbuf(); // 将文件内容读入字符串
robot_description = ss.str();
}
// -----------------------------
// 将 URDF 转换为 KDL::Tree
// -----------------------------
kdl_parser::treeFromString(robot_description, robot_tree);
std::string root_name = STRINGIZE_VALUE_OF(ROOT);
std::string tip_name = STRINGIZE_VALUE_OF(TIP);
// -----------------------------
// 从 Tree 中生成 KDL::Chain
// -----------------------------
if(!robot_tree.getChain(root_name, tip_name, chain))
{
throw std::runtime_error("failed to load robot from URDF");
}
// 检查 URDF 中的关节数是否匹配宏定义
auto num_joints_ = chain.getNrOfJoints();
assert(num_joints_ == NUMBER_OF_JOINTS);
// 初始化关节数组
q = KDL::JntArray(num_joints_);
q_out = q;
// -----------------------------
// 创建 KDL 正运动学与逆运动学求解器
// -----------------------------
fk_pos_solver = std::make_shared<KDL::ChainFkSolverPos_recursive>(chain);
ik_solver_lma = std::make_shared<KDL::ChainIkSolverPos_LMA>(chain);
// -----------------------------
// 在 Chain 中找到末端执行器 segment 的 index
// ee_ind 表示用于 forward_kinematics 时的段索引
// -----------------------------
for(auto i = 0; i < chain.getNrOfSegments(); ++i)
{
if(chain.getSegment(i).getName() == tip_name)
{
ee_ind = i + 1; // +1 因为 KDL 的 segment index 是从 1 开始
break;
}
}
if(ee_ind == -1)
{
throw std::runtime_error("The tip `" + tip_name + " is missing from the chain.");
}
}
// ------------------------------------------------------------
// 设置单个关节(以角度值输入)
// 但当前 fast FK 中使用的是 sin/cos 表示法,因此这里被注释掉
// ------------------------------------------------------------
void set_joint(size_t ind, float value)
{
// joint_data[ind][0] = std::sin(value);
// joint_data[ind][1] = std::cos(value);
}
// 设置多个关节角度(Eigen Vector 输入)
void set_joints(const Eigen::Vector<float, NUMBER_OF_JOINTS>& values)
{
#pragma unroll
for(auto ind = 0; ind < NUMBER_OF_JOINTS; ++ind)
{
// joint_data[ind][0] = std::sin(values[ind]);
// joint_data[ind][1] = std::cos(values[ind]);
}
}
// 设置一个关节(直接提供 sin/cos)
void set_joint(size_t ind, float sin_t, float cos_t)
{
// joint_data[ind][0] = sin_t;
// joint_data[ind][1] = cos_t;
}
// 设置多个关节(sin/cos 数组输入)
void set_joints(const float* sin_values, const float* cos_values)
{
#pragma unroll
for(auto ind = 0; ind < NUMBER_OF_JOINTS; ++ind)
{
// joint_data[ind][0] = sin_values[ind];
// joint_data[ind][1] = cos_values[ind];
}
}
// 设置多个关节(Eigen Vector 输入)
void set_joints(const Eigen::Vector<float, NUMBER_OF_JOINTS>& sin_values,
const Eigen::Vector<float, NUMBER_OF_JOINTS>& cos_values)
{
#pragma unroll
for(auto ind = 0; ind < NUMBER_OF_JOINTS; ++ind)
{
// joint_data[ind][0] = sin_values[ind];
// joint_data[ind][1] = cos_values[ind];
}
}
// ------------------------------------------------------------
// 获取单个关节角度(返回 arctan(sin/cos))
// 当前实现未启用 fast 表示法,因此返回值为 0
// ------------------------------------------------------------
[[nodiscard]] float get_joint(size_t ind) const
{
return 0;
// return atan2f(joint_data[ind][0], joint_data[ind][1]);
}
// 获取全部关节角度
void get_joints(Eigen::Vector<float, NUMBER_OF_JOINTS>& values) const
{
#pragma unroll
for(auto ind = 0; ind < NUMBER_OF_JOINTS; ++ind)
{
// values[ind] = atan2f(joint_data[ind][0], joint_data[ind][1]);
}
}
// 获取某一段的变换矩阵(实现见 .cpp)
void get_frame(size_t index, Eigen::Matrix<float, 4, 4>& tf) const;
// KDL 正向运动学
void forward_kinematics();
// KDL 逆向运动学
fk_interface::IKSolverStats inverse_kinematics(Eigen::Matrix<float, 4, 4>& transform,
Eigen::VectorX<float>& q_guess);
// ------------------------------------------------------------
// 成员变量
// ------------------------------------------------------------
Eigen::Matrix<float, 4, 4> transform; // 末端位姿矩阵
KDL::JntArray q; // 输入关节数组
KDL::JntArray q_out; // FK/IK 输出关节数组
int ee_ind = -1; // KDL 中末端 segment index
KDL::Tree robot_tree; // 从 URDF 解析的 KDL Tree
KDL::Chain chain; // KDL Chain(root→tip)
KDL::Frame frame; // KDL Frame(一次 FK 的输出)
std::shared_ptr<KDL::ChainFkSolverPos_recursive> fk_pos_solver; // 正向运动学求解器
std::shared_ptr<KDL::ChainIkSolverPos_LMA> ik_solver_lma; // LMA 逆向运动学求解器
};
} // namespace kdl_impl
JointData(详细理解 + 数学公式)
下面按模块分析。
## 1. 文件包含(include)部分
这些包含提供了 KDL 运动学、雅可比、逆解等关键功能:
chainfksolverpos_recursive.hpp
用于链结构的**正向运动学(位置)**求解器,计算末端位姿 T = f ( q ) T = f(q) T=f(q)。chainfksolvervel_recursive.hpp
链结构速度正向运动学,计算末端 twist(速度)。chainiksolverpos_lma.hpp
基于 Levenberg–Marquardt (LMA) 数值求解法的逆向运动学求解器,用来求
q ∗ = f − 1 ( T ) q^* = f^{-1}(T) q∗=f−1(T)chainjnttojacsolver.hpp
生成雅可比矩阵 J ( q ) J(q) J(q)。Eigen/Core与Eigen/LU
提供矩阵存储与基本线性代数(如 LU 分解)。kdl_parser/kdl_parser.hpp
将 URDF 文本解析为 KDL 树结构(KDL::Tree)。treejnttojacsolver.hpp
用于树结构机器人(非串联)的雅可比计算。
## 2. 宏展开工具(STRINGIZE / STRINGIZE_VALUE_OF)
#define STRINGIZE(x) #x
#define STRINGIZE_VALUE_OF(x) STRINGIZE(x)
用于把 CMake 传进来的宏:
URDF_FILEROOTTIPNUMBER_OF_JOINTS
转换成字符串。例如:
STRINGIZE_VALUE_OF(URDF_FILE)
变成实际的 "path/to/urdf.urdf"。
## 3. JointData 结构体的作用
JointData 是一个 使用 KDL 的运动学封装类,主要提供:
- 读取 URDF → 构建 KDL::Tree
- 从 Tree 得到 KDL::Chain(root → tip)
- 创建正解(FK)和逆解(IK)求解器
- 为 fast-FK 预留的 sin/cos 表示法接口
- 提供变换矩阵、关节数组、链结构等数据
## 4. get_num_joints()
static constexpr size_t get_num_joints()
{
return NUMBER_OF_JOINTS;
}
返回关节数量。
对应的数学意义:
q ∈ R N , N = NUMBER_OF_JOINTS q \in \mathbb{R}^{N},\quad N = \text{NUMBER\_OF\_JOINTS} q∈RN,N=NUMBER_OF_JOINTS
## 5. JointData 构造函数(重点)
构造函数完整流程:
### (1) 从 URDF 文件读取机器人描述
std::fstream f;
f.open(urdf_file);
std::stringstream ss;
ss << f.rdbuf();
robot_description = ss.str();
这段读取的是 URDF 的 pure text(XML),随后传给解析器。
### (2) URDF → KDL Tree
kdl_parser::treeFromString(robot_description, robot_tree);
KDL 把 URDF 中的 link/joint 结构解析成一棵树:
Tree = node: link , edge: joint \text{Tree} = { \text{node: link},\\ \text{edge: joint} } Tree=node: link,edge: joint
### (3) KDL::Tree → KDL::Chain(root → tip)
robot_tree.getChain(root_name, tip_name, chain)
Chain 是一条从 root 到 tip 的 有序串联链:
S 1 , S 2 , . . . , S n { S_1, S_2, ..., S_n } S1,S2,...,Sn
包含:
- link 的惯性参数
- joint 轴
- joint transform
- 固定连接等
### (4) 验证关节数量
assert(num_joints_ == NUMBER_OF_JOINTS);
因为 fast-fk 版本通常要求关节数量在编译期确定,用于生成 SIMD 优化代码。
### (5) 初始化关节数组
q = KDL::JntArray(num_joints_);
q_out = q;
数学含义:
q = [ q 1 q 2 ⋮ q N ] q = \begin{bmatrix} q_1 \ q_2 \ \vdots \ q_N \end{bmatrix} q=[q1 q2 ⋮ qN]
### (6) 创建 FK / IK 求解器
fk_pos_solver = std::make_shared<KDL::ChainFkSolverPos_recursive>(chain);
ik_solver_lma = std::make_shared<KDL::ChainIkSolverPos_LMA>(chain);
● FK 计算:
T = f ( q ) ∈ S E ( 3 ) T = f(q) \in SE(3) T=f(q)∈SE(3)
计算变换矩阵:
T = [ R p 0 1 ] T = \begin{bmatrix} R & p \\ 0 & 1 \end{bmatrix} T=[R0p1]
● IK(LMA)求解:
求解方程:
f ( q ) = T d e s i r e d f(q) = T_{desired} f(q)=Tdesired
转换为非线性最小二乘:
min q ∣ f ( q ) − T d ∣ 2 \min_q | f(q) - T_d |^2 qmin∣f(q)−Td∣2
通过 LMA 进行迭代。
### (7) 找到末端执行器 segment index
这是 KDL 的实现细节:
getNrOfSegments()返回 所有 segment(包括 fixed joint 的 link)- 其中只有部分是 joint
- 计算 FK 时 fksolver 要指定 segment index
代码:
if(chain.getSegment(i).getName() == tip_name)
{
ee_ind = i + 1;
}
为什么 +1?
因为 KDL 的 segment 编号是:
1 → root fixed frame
2 → first link
3 → second link
…
不是从 0 开始。
## 6. set_joint / set_joints(sin/cos 表示法)
你当前把 fast-fk 版本注释掉了。
fast-fk 常见的优化:
- 对 revolute joint(旋转关节),其位置完全由 sin ( q ) \sin(q) sin(q) 和 cos ( q ) \cos(q) cos(q) 决定
- 大量 trigonometry 可以提前算好,避免重复调用
sin/cos
经典 trick:
R z ( q ) = [ cos q − sin q 0 sin q cos q 0 0 0 1 ] R_z(q) = \begin{bmatrix} \cos q & -\sin q & 0 \\ \sin q & \cos q & 0 \\ 0 & 0 & 1 \end{bmatrix} Rz(q)= cosqsinq0−sinqcosq0001
因此若你缓存: - s i = sin ( q i ) s_i = \sin(q_i) si=sin(qi)
- c i = cos ( q i ) c_i = \cos(q_i) ci=cos(qi)
可以减少大量计算。
## 7. get_joint()
return 0;
当前 fast FK 未启用,保留接口。
原本逻辑应该是:
q i = atan2 ( s i , c i ) q_i = \text{atan2}(s_i, c_i) qi=atan2(si,ci)
## 8. get_frame()(获取链上任意一段的变换)
数学意义:
若 segment index = k k k,则:
T k = T 1 T 2 ⋯ T k T_k = T_1 T_2 \cdots T_k Tk=T1T2⋯Tk
输出 4×4 变换矩阵:
T k ∈ S E ( 3 ) T_k \in SE(3) Tk∈SE(3)
## 9. forward_kinematics()
本函数最终调用 KDL:
T = f ( q ) T = f(q) T=f(q)
等价于:
T = T 1 ( q 1 ) T 2 ( q 2 ) ⋯ T N ( q N ) T = T_1(q_1) T_2(q_2) \cdots T_N(q_N) T=T1(q1)T2(q2)⋯TN(qN)
## 10. inverse_kinematics()
调用 LMA IK 求解器:
迭代更新公式为:
q k + 1 = q k + Δ q q_{k+1} = q_k + \Delta q qk+1=qk+Δq
其中 Δ q \Delta q Δq 求解:
Δ q = ( J T J + λ I ) − 1 J T e \Delta q = (J^T J + \lambda I)^{-1} J^T e Δq=(JTJ+λI)−1JTe
## 11. 成员变量意义总结
| 变量名 | 类型 | 含义 |
|---|---|---|
transform |
4 × 4 4\times4 4×4 矩阵 | 末端位姿 T T T |
q |
KDL::JntArray | 输入关节角度 q q q |
q_out |
KDL::JntArray | 输出(FK 或 IK 的结果) |
ee_ind |
int | KDL 末端 segment index |
robot_tree |
KDL::Tree | 解析后的 URDF 树 |
chain |
KDL::Chain | root→tip 串联链 |
frame |
KDL::Frame | FK 求解的内部输出 |
fk_pos_solver |
shared_ptr | 正向运动学求解器 |
ik_solver_lma |
shared_ptr | LMA 逆运动学求解器 |
forward_kinematics_test.cpp
#include "chrono"
#include "iostream"
// ========================================================
// 根据是否定义 USE_FAST_KINEMATICS 来切换不同的运动学实现
// - 若 USE_FAST_KINEMATICS=ON,则使用你自定义的 fast_kinematics
// - 否则使用 KDL 版本的运动学求解器
// 这使得代码可以通过 CMake 选项灵活切换实现,以便做性能比较。
// ========================================================
#ifdef USE_FAST_KINEMATICS
#include "fast_forward_kinematics/fast_kinematics.hpp"
using KI = fast_fk::JointData; // 快速 FK 的关节数据结构
#else
#include "kdl_kinematics.hpp"
using KI = kdl_impl::JointData; // KDL 的关节数据结构
#endif
int main(int arc, char** argv)
{
// ========================================================
// 配置性能测试参数
// iterations = 128*128(即16384)
// multiplier = 128 * MULTIPLIER (MULTIPLIER 由 CMake 注入)
//
// 总调用次数 = iterations * multiplier
// 用 multiplier 放大计算量,用于让计时更稳定。
// ========================================================
constexpr int iterations = 128 * 128;
constexpr int multiplier = 128 * MULTIPLIER;
// ========================================================
// 创建随机输入:
// rand_values 为长度 iterations 的数组,
// 每个元素是一个 Eigen::Vector<float, num_joints>
//
// 用随机关节角度做测试可以避免优化器将某些路径优化掉。
// Eigen::Vector<...>::Random() 会生成 [-1,1] 范围的随机浮点数。
// ========================================================
std::array<Eigen::Vector<float, KI::get_num_joints()>, iterations> rand_values;
for(auto& rand_val : rand_values)
{
rand_val = Eigen::Vector<float, KI::get_num_joints()>::Random();
}
// ========================================================
// 创建通用前向运动学接口对象
// ForwardKinematicsInterface<KI> 是一个模板类
// 它会根据 KI 的类型选择不同的 FK 实现
// ========================================================
fk_interface::ForwardKinematicsInterface<KI> fk_interface;
// 用于保存计算出的 4x4 齐次变换矩阵(尽管这里未实际使用)
Eigen::Matrix<float, 4, 4> tf;
// ========================================================
// 开始计时
// high_resolution_clock 提供最高精度
// ========================================================
auto start = std::chrono::high_resolution_clock::now();
// ========================================================
// 主性能测试循环
// multiplier 次大循环(增加总计算量)
// 每次遍历 iterations 个随机关节角度
//
// 对于每次迭代:
// 1. set_joints() — 设置关节变量
// 2. forward_kinematics() — 执行前向运动学计算
// ========================================================
for(int k = 0; k < multiplier; k++)
{
for(int i = 0; i < iterations; i++)
{
fk_interface.set_joints(rand_values[i]); // 设置输入关节值
fk_interface.forward_kinematics(); // 执行 FK(输出内部保存或优化掉)
}
}
// ========================================================
// 停止计时并计算时间差
// duration_cast<nanoseconds> → 将时间转换为纳秒
// ========================================================
auto stop = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(stop - start);
// ========================================================
// 打印总耗时(单位:纳秒)
// ========================================================
std::cout << "Time taken by function: " << (float)duration.count() << " nanoseconds"
<< std::endl;
// ========================================================
// 打印单次 FK 平均耗时(纳秒级)
// ========================================================
std::cout << "Average: " << ((float)duration.count()) / (iterations * multiplier)
<< " nanoseconds" << std::endl;
return 0;
}
程序整体目的
这个程序的作用是 测量前向运动学(Forward Kinematics, FK)算法的性能。
所谓前向运动学,就是给定一个机器人关节向量
q = [ q 1 , q 2 , . . . , q n ] T \mathbf{q} = [q_1, q_2, ..., q_n]^T q=[q1,q2,...,qn]T
计算末端执行器(End Effector)的齐次变换矩阵:
b a s e T e e ∈ R 4 × 4 {}^{base}T_{ee} \in \mathbb{R}^{4 \times 4} baseTee∈R4×4
也就是机器人手在三维空间中的位置 + 姿态。
这个程序做的就是:
- 生成大量随机关节角度
- 调用不同的运动学实现
- 统计运行时间
- 输出总耗时与平均耗时(单位:纳秒)
1. 选择不同的运动学实现
#ifdef USE_FAST_KINEMATICS
#include "fast_forward_kinematics/fast_kinematics.hpp"
using KI = fast_fk::JointData;
#else
#include "kdl_kinematics.hpp"
using KI = kdl_impl::JointData;
#endif
这里通过 #ifdef 根据 CMake 的开关 USE_FAST_KINEMATICS 做编译时选择。
目的
让你可以:
- 对比 自己实现的 fast_kinematics
- 与 KDL 的标准运动学实现
之间的性能差异。
2. 配置性能测试参数
constexpr int iterations = 128 * 128;
constexpr int multiplier = 128 * MULTIPLIER;
计算:
i t e r a t i o n s = 128 × 128 = 16384 iterations = 128 \times 128 = 16384 iterations=128×128=16384multiplier 进一步放大规模,它依赖于 CMake 中的:
set(MULTIPLIER 4)
或其他设定。
总调用次数
TotalCalls = i t e r a t i o n s × m u l t i p l i e r \text{TotalCalls} = iterations \times multiplier TotalCalls=iterations×multiplier
例如 MULTIPLIER=4 时:
TotalCalls = 16384 × 512 = 8 , 388 , 608 \text{TotalCalls} = 16384 \times 512 = 8,388,608 TotalCalls=16384×512=8,388,608
数量级足够大,确保计时稳定。
3. 生成随机关节角度
std::array<Eigen::Vector<float, KI::get_num_joints()>, iterations> rand_values;
这一行的含义:
- 数组长度 =
iterations = 16384 - 每个元素类型是一个 Eigen 列向量,长度等于机器人关节数
例如 UR5 是 6 关节,则:
q = [ q 1 q 2 ⋮ q 6 ] \mathbf{q} = \begin{bmatrix} q_1 \\ q_2 \\ \vdots \\ q_6 \end{bmatrix} q= q1q2⋮q6
随机初始化
rand_val = Eigen::Vector<float, KI::get_num_joints()>::Random();
Random() 会生成每个分量在 [ − 1 , 1 ] [-1, 1] [−1,1] 的随机数。
为什么要随机?
为了防止编译器优化掉 FK 的结果,例如:
- 如果对同一输入反复计算,可能被优化成常量表达式
- 随机输入能模拟真实机器人运动数据
- 能让缓存行为更真实,从而体现实际性能
4. 创建前向运动学接口对象
fk_interface::ForwardKinematicsInterface<KI> fk_interface;
这是你设计的模板化运动学接口:
- 若
KI = fast_fk::JointData→ 使用 fast_kinematics - 若
KI = kdl_impl::JointData→ 使用 KDL
好处: - 测试代码 不用改
- 切换运动学后端只需 CMake 参数
5. 计时开始与主循环
计时开始:
auto start = std::chrono::high_resolution_clock::now();
主循环:
for(int k = 0; k < multiplier; k++)
for(int i = 0; i < iterations; i++)
{
fk_interface.set_joints(rand_values[i]);
fk_interface.forward_kinematics();
}
两层循环,总次数:
N calls = i t e r a t i o n s × m u l t i p l i e r N_\text{calls} = iterations \times multiplier Ncalls=iterations×multiplier
每一轮计算的数学步骤包含:
- 设置关节角:
q ( i ) = r a n d v a l u e s [ i ] \mathbf{q}^{(i)} = rand_values[i] q(i)=randvalues[i] - 调用 FK:
b a s e T e e = F K ( q ( i ) ) {}^{base}T_{ee} = FK(\mathbf{q}^{(i)}) baseTee=FK(q(i))
机器人每个关节的 DH 变换:
i − 1 T i = R o t z ( q i ) ⋅ T r a n s z ( d i ) ⋅ T r a n s x ( a i ) ⋅ R o t x ( α i ) {}^{i-1}T_i = Rot_z(q_i) \cdot Trans_z(d_i) \cdot Trans_x(a_i) \cdot Rot_x(\alpha_i) i−1Ti=Rotz(qi)⋅Transz(di)⋅Transx(ai)⋅Rotx(αi)
前向运动学最终乘积:
b a s e T e e = ∏ i = 1 n i − 1 T i {}^{base}T_{ee} = \prod_{i=1}^{n} {}^{i-1}T_i baseTee=i=1∏ni−1Ti
6. 计时结束
auto stop = std::chrono::high_resolution_clock::now();
auto duration =
std::chrono::duration_cast<std::chrono::nanoseconds>(stop - start);
duration 的单位是纳秒(ns)。
7. 打印结果
总时间(纳秒):
Time taken by function: X nanoseconds
平均每次 FK 用时:
t a v g = duration i t e r a t i o n s × m u l t i p l i e r t_{avg} = \frac{\text{duration}}{iterations \times multiplier} tavg=iterations×multiplierduration
Average: Y nanoseconds
如果你看到:
- average ≈ 50ns → 属于 SIMD 优化后的巅峰性能
- average ≈ 300ns → 常规 FK(如 KDL)属正常
- average > 1000ns → 有可能是缓存瓶颈或未优化的路径
总结(最简化理解)
| 部分 | 作用 |
|---|---|
| 选择 fast / KDL | 性能对比 |
| 随机关节角度 | 防优化、真实模拟 |
| 计时 | 测性能 |
| multiplier & iterations | 让计时更稳定 |
| 输出平均值 | 评估 FK 每次耗时 |
最终要测的就是:
t F K = 总时间 调用次数 t_{FK} = \frac{\text{总时间}}{\text{调用次数}} tFK=调用次数总时间
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)