1⃣ 背景:URDF 与代码生成

URDF (Unified Robot Description Format) 是一种 XML 格式,用于描述机器人的结构信息,包括:

  • 关节(Joint):旋转或平移类型、自由度等
  • 连杆(Link):物理尺寸、质量、几何形状等
  • 父子关系(Parent-Child):机器人链结构
    在机器人运动学计算中,我们希望从 URDF 自动生成 高效的 C++/Python/Matlab 代码,用来计算:
  1. 正向运动学(Forward Kinematics, FK)
  2. 雅可比矩阵(Jacobian, J)
  3. 甚至动力学(可扩展)

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) Tlili1(qi)
其中:

  • T l i l i − 1 T_{l_i}^{l_{i-1}} Tlili1 表示第 i i i 个连杆 l i l_i li 相对于其父连杆 l i − 1 l_{i-1} li1 的位姿
  • 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)=Tl1bTl2l1Tl3l2Tlnln1

解释
这条公式表示我们将每个关节的相对变换矩阵依次相乘,就得到末端在基座坐标系下的整体位姿。
对应编程实现时,可以直接按链式循环生成矩阵乘法代码。

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 vR6:末端线速度和角速度(通常前 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)= q1xq1yq1zq2xq2yq2zq3xq3yq3z
  • 每一列对应一个关节对末端位置的贡献
  • 对旋转关节: ∂ 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}) qip=zi1×(peeoi1)
  • 对平移关节: ∂ p ∂ q i = z i − 1 \frac{\partial p}{\partial q_i} = z_{i-1} qip=zi1

解释
雅可比矩阵可以理解为“关节速度如何映射到末端速度”。
自动生成代码时,我们只需要对每个关节计算其对末端位置的偏导数,然后按列组装矩阵即可。

4⃣ 从 URDF 到高效代码

  1. 解析 URDF
    • 读取每个 link 的父子关系
    • 读取每个 joint 的类型、轴向、初始值
  2. 生成 FK 矩阵代码
    • 对每个关节生成 T l i l i − 1 ( q i ) T_{l_i}^{l_{i-1}}(q_i) Tlili1(qi)
    • 按链乘法组合
  3. 生成雅可比矩阵代码
    • 对每个关节生成偏导数公式
    • 对旋转关节用叉乘公式
    • 对平移关节直接使用关节轴
  4. 优化与加速
    • 尽量展开矩阵乘法,减少循环
    • 使用 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= cosq1sinq100sinq1cosq1000010L1cosq1L1sinq101
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= cosq2sinq200sinq2cosq2000010L2cosq2L2sinq201
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= cosq3sinq300sinq3cosq3000010L3cosq3L3sinq301
于是末端位姿:
F K ( q ) = T 1 ⋅ T 2 ⋅ T 3 FK(q) = T_1 \cdot T_2 \cdot T_3 FK(q)=T1T2T3
展开末端坐标 ( 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)=[q1xq1yq2xq2yq3xq3y]
计算偏导数:
∂ 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) q1x=L1sinq1L2sin(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) q2x=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) q3x=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) q1y=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) q2y=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) q3y=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 到代码生成

  1. 解析 URDF:得到关节数量 3,类型旋转,长度 L 1 , L 2 , L 3 L_1, L_2, L_3 L1,L2,L3
  2. 生成 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);
  1. 生成雅可比矩阵代码
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)可以分两步:
      1. 求腕部角度,使末端达到期望姿态
      2. 求位置值,使末端达到期望笛卡尔位置 [ x , y , z ] [x, y, z] [x,y,z]
    • 一般的通用 IK 求解器无法利用这种特定知识
      局限性
  • 硬件特定实现通常可扩展性有限
  • 硬件更新不频繁(除了附加设备,如相机和末端执行器)
4⃣ 编译代码的优势

通过 代码生成(Compiler)生成的代码有以下优点:

  1. 可测试性
    • 可以测试内存分配情况
    • 可验证实时性(Real-time Compatibility)
    • 对安全关键应用(如外科手术机器人)至关重要
  2. 硬件特定优化
    • 因为代码在每台机器上源代码编译,可以启用特定 CPU 优化
  3. 相比模板化机器人构建器(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

总结

最可能的情况是你需要:

  1. 安装 KDL 相关库
  2. 创建或获取 forwardKinematicsTest.cpp 文件
  3. 准备 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} 通常为 includeinclude/<project>
${CMAKE_INSTALL_LIBDIR} liblib64
${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.cmake
  • fast_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 包配置的核心部分。

工作流程

  1. 读取 .in 模板
  2. 替换模板变量
  3. 生成 .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)。
  • 默认构建类型:
    if(NOT DEFINED CMAKE_BUILD_TYPE OR "${CMAKE_BUILD_TYPE}" STREQUAL "")
      set(CMAKE_BUILD_TYPE Release)
    endif()
    
    说明:在 Makefile generator 下,若不显式指定 CMAKE_BUILD_TYPE,编译器不会得到优化选项。强制默认 Release 是为了保证 SIMD/CUDA/性能测试使用优化编译器选项。

2. 测试目标与机器人配置常量

  • 两个测试可执行:
    • forward_kinematics_test(正运动学测试)
    • inverse_kinematics_test(逆运动学测试)
  • URDF 与链端点设置:
    set(URDF_FILE ${CMAKE_SOURCE_DIR}/test/urdf/robot.urdf)
    set(ROOT base_link)
    set(TIP grasp_link)
    
    说明:这些常量会在后续的代码生成或 KDL 实现中被传递/引用(例如 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 代码生成)分支详解

主要动作:

  1. 为测试目标定义宏 USE_FAST_KINEMATICS,代码中会以 #ifdef USE_FAST_KINEMATICS 分支来使用生成的实现。
  2. 为测试程序加入 include 目录,确保能找到 public header。
  3. 调用自定义 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 并编译成静/共享库。
  4. 若为 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=1nAi(θ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=TiAi+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)详解

这是“降级到成熟库”的路径,用来做正确性验证或在没有高性能实现时使用。
主要步骤:

  1. 使用 ExternalProject_Add(kdl-parser, ...) 下载并构建 kdl_parser(来自 ROS 的 repo),目标生成 libkdl_parser.soBUILD_BYPRODUCTS 告诉 CMake 哪个文件是构建产物以便依赖管理。
  2. add_library(... IMPORTED) + set_target_properties(... IMPORTED_LOCATION ...) 将 ExternalProject 的生成产物呈现为 CMake 的 IMPORTED 目标,便于主项目链接。
  3. 同理对 orocos_kinematics_dynamics(Orocos KDL)进行 ExternalProject_Add 并包装为 kdl_lib
  4. 创建 kdl_implementation(项目内的 KDL 适配层)并将 URDF_FILEROOTTIPNUMBER_OF_JOINTS 通过 target_compile_definitions 传递给源码(这样源码可以在编译期读取宏来定位 URDF、设置数组大小等)。
  5. 链接 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. 调试建议(遇到生成/构建错误时)

  1. 确认 URDF 路径与 ROOT/TIP 是否正确get_num_joints.py 会从 tip 逆向到 root,若 link 名不匹配会崩溃或返回错误。
  2. 手动运行代码生成脚本
    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_link
    
    检查输出是否合理、格式是否包含 FAST_FK_NUMBER_OF_JOINTS= 等。
  3. 若使用 KDL 分支,先单独构建 ExternalProject:网络不好或 repo tag 改变会造成下载失败,查看 ExternalProject 的日志目录(CMake 输出)定位错误。
  4. 检查编译器/CPU 指令集兼容性:若出现非法指令(SIGILL),可能是 -march=native 启用了主机不支持的指令(例如在交叉/兼容机器上)。
  5. 比较实现的正确性:先用小 MULTIPLIER 与 KDL 比较结果(位置/姿态误差),再放大测试量级。

10. 进阶建议(改善可用性/可维护性)

  • CMake 中把 FK_IMPL 变为 optionCACHE 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,iRjoint(θ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} ROOTL1L2TIP
但 parent_map 是从 child 指向 parent:
TIP ← L n ← ⋯ ← ROOT \text{TIP} \leftarrow L_n \leftarrow \cdots \leftarrow \text{ROOT} TIPLnROOT
因此需要从 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。

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=1n1jointi=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。

最终总结(含数学原理)

这段脚本做了以下工作:

  1. 解析 URDF
  2. 从末端 TIP 反向遍历 parent_map,直到 ROOT
  3. 找到所有参与 FK 计算的可动关节(非 fixed)
  4. 计数并输出:
    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=jj.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. 脚本功能概述

该脚本做的事情是:

  1. 解析 URDF 文件
  2. 从 base(root link)一路找到 tip(末端 link)
  3. 计算每个关节的固定变换(不含 DOF)
  4. 将可动关节(Revolute/Prismatic)的固定参数提取出来:
    • 固定旋转矩阵 R i R_i Ri
    • 固定位移向量 p i \mathbf{p}_i pi
  5. 将结果传给 Jinja2 模板,生成对应的 C++ forward kinematics(快速前向运动学) 代码
  6. 输出关节数量供 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=TjointTfixed
    然后把 R R R p \mathbf{p} p 分别保存。
    你保存的是:
  • R i R_i Ri:固定旋转
  • p i p_i pi:固定平移
  • type:关节类型
    用于 C++ 模板里的 FK。

6. 关节类型处理

你处理的类型包括:

  • fixed
  • revolute
  • continuous(视为 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>105x105

8. 使用 Jinja2 模板生成 C++ FK 代码

你读取 .j2 模板并替换其中的:

  • rotations
  • offsets
  • types
    这些数据构成 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=1NTi(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;

用途:

  1. 每个关节计算自己的局部旋转矩阵 R_tmp = R_fixed * R_actuation
  2. 再乘上前一个关节在 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=Ri1RfixediRθ
    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=pi1+Ri1(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=RfixedRθ
因为 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θ0sinθ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+Roldoffset

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+Roldoffset+Roldjointaxislinear

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=xtxc

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=δx2+δy2+δz2

梯度公式(最重要)

对于任意一个 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} qif=2,δxqi(xc)2,δyqi(yc)2,δzqi(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) qipe=ωi×(pepi)
旋转对轴方向影响:
∂ 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} qixaxis=ω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=L0L1Ln=TIP
并对路径上的关节执行:
0 T n = ∏ i = 1 n i − 1 T i {}^0T_n = \prod_{i=1}^{n} {^{i-1}T_i} 0Tn=i=1ni1Ti

小结


含义 来源
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 个关节都配置了 positionvelocity command_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. 经典坑 & 小贴士

  1. PR2 的 base_link 的 X 轴是朝后的!(和 REP-103 相反)
    • 正前方其实是 -X 方向
    • 所以很多教程里会看到 origin rpy="0 0 3.14159" 来翻转坐标系
  2. 夹爪开口 0.09m ≈ 90mm,实际能抓的最大物体约 85mm
  3. 手臂有大量 turnGravityOff>true,仿真时不会下垂,真实机器人会!
  4. 所有 roll 关节都是 continuous(无限转),规划时要注意避开奇异位形
  5. 头部 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_linkee_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_linkee_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 写法口诀(背下来就再也不怕了)

  1. 先写 link(刚体)
  2. 再写 joint 连两个 link
  3. 每个 link 都要有 visual + collision + inertial(仿真必须)
  4. joint 必须写 parent/child + origin + axis + type
  5. 需要颜色就加 material
  6. 需要限位就加 limit
  7. 需要镜像就加 <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) RdSO(3)
target_pose_ 目标位置 p d ∈ R 3 p_d \in \mathbb{R}^3 pdR3
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)pd2+wRLog(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} ϵ=103
    • 或函数变化比率小于
      ϵ rel = 1 0 − 3 \epsilon_{\text{rel}}=10^{-3} ϵrel=103
  • 最多迭代 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} qJ
    若优化失败(如出现 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)pd2+β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)) qJ=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) RSO(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_posetarget_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() 解引用两层:

  1. joint_data.data() → 指向第 0 个关节(一个 array)
  2. .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=wpppd2+wRRRd2
返回:

  • 优化次数
  • 成功与否
  • 最终误差
  • 梯度范数
    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:qbaseTi
其中 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=f1(T)
  • chainjnttojacsolver.hpp
    生成雅可比矩阵 J ( q ) J(q) J(q)
  • Eigen/CoreEigen/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_FILE
  • ROOT
  • TIP
  • NUMBER_OF_JOINTS
    转换成字符串。例如:
STRINGIZE_VALUE_OF(URDF_FILE)

变成实际的 "path/to/urdf.urdf"

## 3. JointData 结构体的作用

JointData 是一个 使用 KDL 的运动学封装类,主要提供:

  1. 读取 URDF → 构建 KDL::Tree
  2. 从 Tree 得到 KDL::Chain(root → tip)
  3. 创建正解(FK)和逆解(IK)求解器
  4. 为 fast-FK 预留的 sin/cos 表示法接口
  5. 提供变换矩阵、关节数组、链结构等数据

## 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} qRN,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 qminf(q)Td2
通过 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)= cosqsinq0sinqcosq0001
    因此若你缓存:
  • 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=T1T2Tk
输出 4×4 变换矩阵:
T k ∈ S E ( 3 ) T_k \in SE(3) TkSE(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} baseTeeR4×4
也就是机器人手在三维空间中的位置 + 姿态
这个程序做的就是:

  1. 生成大量随机关节角度
  2. 调用不同的运动学实现
  3. 统计运行时间
  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=16384
multiplier 进一步放大规模,它依赖于 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= q1q2q6

随机初始化

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) i1Ti=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=1ni1Ti

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=调用次数总时间

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐