ROS 2从入门到精通系列(十七):机械臂控制 - 正逆运动学与轨迹规划

从运动学理论到实际控制,完整的机械臂控制系统实现。


引言

机械臂(robotic arm)是工业和服务机器人的重要组成部分。控制机械臂需要:

  • 正运动学:已知关节角度,求末端位置
  • 逆运动学:已知目标位置,求关节角度
  • 轨迹规划:生成平滑的运动轨迹
  • 控制执行:驱动关节到目标位置

一、机械臂建模和几何关系

1.1 D-H参数表示

D-H参数
Denavit-Hartenberg

连杆长度
a_i

连杆偏移
d_i

连杆扭角
alpha_i

关节角
theta_i

关节参数
可变

连杆参数
固定

1.2 正运动学方程

关节变换矩阵:

T i i − 1 = [ cos ⁡ θ i − sin ⁡ θ i cos ⁡ α i sin ⁡ θ i sin ⁡ α i a i cos ⁡ θ i sin ⁡ θ i cos ⁡ θ i cos ⁡ α i − cos ⁡ θ i sin ⁡ α i a i sin ⁡ θ i 0 sin ⁡ α i cos ⁡ α i d i 0 0 0 1 ] T_i^{i-1} = \begin{bmatrix} \cos\theta_i & -\sin\theta_i\cos\alpha_i & \sin\theta_i\sin\alpha_i & a_i\cos\theta_i \\ \sin\theta_i & \cos\theta_i\cos\alpha_i & -\cos\theta_i\sin\alpha_i & a_i\sin\theta_i \\ 0 & \sin\alpha_i & \cos\alpha_i & d_i \\ 0 & 0 & 0 & 1 \end{bmatrix} Tii1= cosθisinθi00sinθicosαicosθicosαisinαi0sinθisinαicosθisinαicosαi0aicosθiaisinθidi1

末端变换:

T n 0 = T 1 0 ⋅ T 2 1 ⋅ . . . ⋅ T n n − 1 T_n^0 = T_1^0 \cdot T_2^1 \cdot ... \cdot T_n^{n-1} Tn0=T10T21...Tnn1

1.3 2自由度平面臂的逆运动学

2-DOF 机械臂

基座 Base

Link1 L1

Joint1

Link2 L2

Joint2

末端 x,y

已知目标位置(x, y),求θ1, θ2:

θ 2 = arccos ⁡ ( x 2 + y 2 − L 1 2 − L 2 2 2 L 1 L 2 ) \theta_2 = \arccos\left(\frac{x^2 + y^2 - L_1^2 - L_2^2}{2 L_1 L_2}\right) θ2=arccos(2L1L2x2+y2L12L22)
θ 1 = arctan ⁡ 2 ( y , x ) − arctan ⁡ 2 ( L 2 sin ⁡ θ 2 , L 1 + L 2 cos ⁡ θ 2 ) \theta_1 = \arctan2(y, x) - \arctan2(L_2 \sin\theta_2, L_1 + L_2 \cos\theta_2) θ1=arctan2(y,x)arctan2(L2sinθ2,L1+L2cosθ2)


二、使用MoveIt进行运动规划

2.1 安装MoveIt

# 安装MoveIt2
sudo apt install ros-humble-moveit

# 安装运动规划库
sudo apt install ros-humble-moveit-planners

2.2 URDF机械臂描述

创建 robot.urdf

<?xml version="1.0"?>
<robot name="simple_arm">
  <!-- 基座 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.1 0.1 0.1"/>
      </geometry>
      <material name="blue">
        <color rgba="0 0 1 1"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.1 0.1 0.1"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="1"/>
      <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/>
    </inertial>
  </link>

  <!-- 第一个关节和连杆 -->
  <joint name="joint1" type="revolute">
    <parent link="base_link"/>
    <child link="link1"/>
    <origin xyz="0 0 0.05" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
    <limit lower="0" upper="3.14" effort="10" velocity="1"/>
  </joint>

  <link name="link1">
    <visual>
      <geometry>
        <cylinder length="0.3" radius="0.02"/>
      </geometry>
      <material name="green">
        <color rgba="0 1 0 1"/>
      </material>
      <origin xyz="0.15 0 0" rpy="0 1.57 0"/>
    </visual>
    <collision>
      <geometry>
        <cylinder length="0.3" radius="0.02"/>
      </geometry>
      <origin xyz="0.15 0 0" rpy="0 1.57 0"/>
    </collision>
    <inertial>
      <mass value="1"/>
      <origin xyz="0.15 0 0"/>
      <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/>
    </inertial>
  </link>

  <!-- 第二个关节和连杆 -->
  <joint name="joint2" type="revolute">
    <parent link="link1"/>
    <child link="link2"/>
    <origin xyz="0.3 0 0" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
    <limit lower="0" upper="3.14" effort="10" velocity="1"/>
  </joint>

  <link name="link2">
    <visual>
      <geometry>
        <cylinder length="0.3" radius="0.02"/>
      </geometry>
      <material name="red">
        <color rgba="1 0 0 1"/>
      </material>
      <origin xyz="0.15 0 0" rpy="0 1.57 0"/>
    </visual>
    <collision>
      <geometry>
        <cylinder length="0.3" radius="0.02"/>
      </geometry>
      <origin xyz="0.15 0 0" rpy="0 1.57 0"/>
    </collision>
    <inertial>
      <mass value="1"/>
      <origin xyz="0.15 0 0"/>
      <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/>
    </inertial>
  </link>

  <!-- 末端执行器 -->
  <joint name="ee_joint" type="fixed">
    <parent link="link2"/>
    <child link="ee_link"/>
    <origin xyz="0.3 0 0" rpy="0 0 0"/>
  </joint>

  <link name="ee_link"/>
</robot>

三、正运动学计算

3.1 实现正运动学

#!/usr/bin/env python3
"""
机械臂正运动学计算
"""

import numpy as np
import math

class ArmKinematics:
    """机械臂运动学求解器"""

    def __init__(self, link_lengths):
        """
        初始化
        link_lengths: 各连杆长度列表
        """
        self.link_lengths = link_lengths
        self.n_joints = len(link_lengths)

    def forward_kinematics_2dof(self, theta1, theta2):
        """
        2自由度平面臂的正运动学

        theta1: 第一关节角(弧度)
        theta2: 第二关节角(弧度)

        返回: (x, y) 末端位置
        """
        L1, L2 = self.link_lengths

        # 末端位置
        x = L1 * math.cos(theta1) + L2 * math.cos(theta1 + theta2)
        y = L1 * math.sin(theta1) + L2 * math.sin(theta1 + theta2)

        # 末端角度
        phi = theta1 + theta2

        return x, y, phi

    def forward_kinematics_3dof(self, theta1, theta2, theta3):
        """3自由度臂的正运动学"""
        L1, L2, L3 = self.link_lengths

        # 在xy平面上的投影
        xy_dist = L1 * math.cos(theta1) + \
                  L2 * math.cos(theta1 + theta2) + \
                  L3 * math.cos(theta1 + theta2 + theta3)

        x = xy_dist
        y = L1 * math.sin(theta1) + \
            L2 * math.sin(theta1 + theta2) + \
            L3 * math.sin(theta1 + theta2 + theta3)

        return x, y

    def jacobian_2dof(self, theta1, theta2):
        """
        2自由度臂的雅可比矩阵
        用于速度变换和逆运动学
        """
        L1, L2 = self.link_lengths

        J = np.array([
            [-L1 * math.sin(theta1) - L2 * math.sin(theta1 + theta2),
             -L2 * math.sin(theta1 + theta2)],
            [L1 * math.cos(theta1) + L2 * math.cos(theta1 + theta2),
             L2 * math.cos(theta1 + theta2)]
        ])

        return J

    def velocity_kinematics(self, theta_dot, jacobian):
        """
        速度运动学
        dX = J * dθ
        """
        x_dot = np.dot(jacobian, theta_dot)
        return x_dot


# 使用示例
if __name__ == '__main__':
    # 创建2自由度臂
    arm = ArmKinematics([0.3, 0.3])

    # 计算末端位置
    theta1, theta2 = math.pi / 4, math.pi / 4
    x, y, phi = arm.forward_kinematics_2dof(theta1, theta2)

    print(f'关节角: θ1={theta1:.2f}rad, θ2={theta2:.2f}rad')
    print(f'末端位置: x={x:.3f}m, y={y:.3f}m, φ={phi:.2f}rad')

    # 计算雅可比矩阵
    J = arm.jacobian_2dof(theta1, theta2)
    print(f'雅可比矩阵:\n{J}')

四、逆运动学求解

4.1 解析逆运动学(2自由度)

class ArmKinematics:
    """...(前面的代码)..."""

    def inverse_kinematics_2dof(self, x, y, elbow_up=True):
        """
        2自由度臂的解析逆运动学

        x, y: 目标末端位置
        elbow_up: 肘部朝向(True=上, False=下)

        返回: (theta1, theta2)
        """
        L1, L2 = self.link_lengths

        # 计算目标距离
        d = math.sqrt(x**2 + y**2)

        # 检查可达性
        if d > L1 + L2 or d < abs(L1 - L2):
            return None  # 目标不可达

        # 使用余弦定理计算theta2
        cos_theta2 = (d**2 - L1**2 - L2**2) / (2 * L1 * L2)
        cos_theta2 = np.clip(cos_theta2, -1, 1)  # 防止数值误差

        if elbow_up:
            theta2 = math.acos(cos_theta2)
        else:
            theta2 = -math.acos(cos_theta2)

        # 计算theta1
        alpha = math.atan2(y, x)
        beta = math.atan2(L2 * math.sin(theta2),
                         L1 + L2 * math.cos(theta2))

        theta1 = alpha - beta

        return theta1, theta2

    def inverse_kinematics_numerical(self, target_x, target_y,
                                     initial_guess=None,
                                     max_iterations=100,
                                     tolerance=1e-4):
        """
        数值逆运动学(牛顿-拉夫逊法)

        适用于复杂的多自由度机械臂
        """
        if initial_guess is None:
            theta = np.zeros(self.n_joints)
        else:
            theta = np.array(initial_guess)

        target = np.array([target_x, target_y])

        for i in range(max_iterations):
            # 当前正运动学结果
            x, y = self.forward_kinematics_2dof(theta[0], theta[1])[:2]
            current = np.array([x, y])

            # 误差
            error = target - current
            error_norm = np.linalg.norm(error)

            if error_norm < tolerance:
                return theta

            # 计算雅可比矩阵
            J = self.jacobian_2dof(theta[0], theta[1])

            # 伪逆
            J_pinv = np.linalg.pinv(J)

            # 更新关节角
            delta_theta = np.dot(J_pinv, error)
            theta = theta + delta_theta * 0.5

        if error_norm < tolerance:
            return theta
        else:
            return None  # 未能收敛

五、轨迹规划

5.1 使用MoveIt进行轨迹规划

#!/usr/bin/env python3
"""
使用MoveIt进行轨迹规划和控制
"""

import rclpy
from rclpy.node import Node
from moveit_msgs.action import MoveGroup
from moveit_msgs.srv import GetPositionIK
from geometry_msgs.msg import PoseStamped
from rclpy.action import ActionClient
import math

class ArmController(Node):
    def __init__(self):
        super().__init__('arm_controller')

        # 创建MoveGroup动作客户端
        self.move_group_client = ActionClient(self, MoveGroup, '/move_action_server')

        # 创建IK服务客户端
        self.ik_client = self.create_client(
            GetPositionIK, '/compute_ik')

        self.get_logger().info('机械臂控制器已启动')

    def move_to_pose(self, x, y, z):
        """
        移动机械臂到目标位置
        """
        # 创建目标位姿
        target_pose = PoseStamped()
        target_pose.header.frame_id = 'base_link'
        target_pose.pose.position.x = x
        target_pose.pose.position.y = y
        target_pose.pose.position.z = z
        target_pose.pose.orientation.w = 1.0

        # 使用逆运动学求解
        joint_values = self.solve_ik(target_pose)

        if joint_values is None:
            self.get_logger().error('逆运动学无解')
            return False

        # 规划轨迹
        return self.plan_and_execute(joint_values)

    def solve_ik(self, target_pose):
        """求逆运动学"""
        try:
            # 创建IK请求
            request = GetPositionIK.Request()
            request.ik_request.group_name = 'manipulator'
            request.ik_request.pose_stamped = target_pose
            request.ik_request.timeout = rclpy.duration.Duration(seconds=5)

            # 发送请求
            future = self.ik_client.call_async(request)
            rclpy.spin_until_future_complete(self, future, timeout_sec=5)

            if future.result() is not None:
                return future.result().solution.joint_state.position
            else:
                return None
        except Exception as e:
            self.get_logger().error(f'IK求解失败: {str(e)}')
            return None

    def plan_and_execute(self, joint_values):
        """规划并执行轨迹"""
        # 创建目标
        goal = MoveGroup.Goal()
        goal.request.group_name = 'manipulator'
        goal.request.goal_constraints[0].joint_constraints[0].joint_name = 'joint1'
        goal.request.goal_constraints[0].joint_constraints[0].position = joint_values[0]

        # 发送目标
        send_goal_future = self.move_group_client.send_goal_async(goal)
        rclpy.spin_until_future_complete(self, send_goal_future)

        goal_handle = send_goal_future.result()
        if not goal_handle.accepted:
            self.get_logger().error('目标被拒绝')
            return False

        # 等待执行完成
        result_future = goal_handle.get_result_async()
        rclpy.spin_until_future_complete(self, result_future)

        result = result_future.result().result
        return result.error_code.val == 1  # MoveItErrorCodes::SUCCESS = 1

    def plan_cartesian_path(self, waypoints):
        """
        笛卡尔路径规划(直线轨迹)
        """
        # 创建动作请求
        goal = MoveGroup.Goal()
        goal.request.group_name = 'manipulator'
        goal.request.workspace_parameters.header.frame_id = 'base_link'
        goal.request.workspace_parameters.min_corner.x = -1.0
        goal.request.workspace_parameters.max_corner.x = 1.0

        # 发送目标
        send_goal_future = self.move_group_client.send_goal_async(goal)
        rclpy.spin_until_future_complete(self, send_goal_future)

        return send_goal_future.result()

def main(args=None):
    rclpy.init(args=args)
    node = ArmController()
    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

六、关节控制和力反馈

6.1 直接关节控制

#!/usr/bin/env python3
"""
直接关节控制 - 关节级命令
"""

import rclpy
from rclpy.node import Node
from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint
import math

class JointController(Node):
    def __init__(self):
        super().__init__('joint_controller')

        # 发布关节轨迹
        self.traj_pub = self.create_publisher(
            JointTrajectory, '/joint_trajectory_controller/command', 10)

    def move_joint_trajectory(self, joint_names, positions, duration):
        """
        执行关节轨迹

        joint_names: 关节名称列表
        positions: 目标位置列表
        duration: 执行时间(秒)
        """
        trajectory = JointTrajectory()
        trajectory.joint_names = joint_names

        # 创建轨迹点
        point = JointTrajectoryPoint()
        point.positions = positions
        point.time_from_start.sec = int(duration)
        point.time_from_start.nanosec = int((duration % 1) * 1e9)

        trajectory.points.append(point)

        # 发布轨迹
        self.traj_pub.publish(trajectory)

        self.get_logger().info(f'执行轨迹: {duration}秒')

    def interpolate_trajectory(self, start_pos, end_pos, num_points, duration):
        """
        生成插值轨迹
        """
        trajectory = JointTrajectory()
        trajectory.joint_names = ['joint1', 'joint2']

        for i in range(num_points):
            # 线性插值
            t = i / (num_points - 1)
            positions = [
                start_pos[0] + t * (end_pos[0] - start_pos[0]),
                start_pos[1] + t * (end_pos[1] - start_pos[1])
            ]

            point = JointTrajectoryPoint()
            point.positions = positions
            point.time_from_start.sec = int(t * duration)
            point.time_from_start.nanosec = int(
                ((t * duration) % 1) * 1e9)

            trajectory.points.append(point)

        self.traj_pub.publish(trajectory)

def main(args=None):
    rclpy.init(args=args)
    node = JointController()

    # 示例:移动关节
    start = [0.0, 0.0]
    end = [1.57, 1.57]  # π/2 rad
    node.interpolate_trajectory(start, end, 10, 5.0)

    rclpy.spin(node)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

七、碰撞检测和避障

class ArmPlanner:
    """包含碰撞检测的规划器"""

    def __init__(self, robot_model):
        self.robot_model = robot_model
        self.collision_checker = CollisionChecker(robot_model)

    def plan_with_collision_avoidance(self, start, goal):
        """
        带碰撞检测的路径规划
        """
        # RRT*算法
        path = self.rrt_star(start, goal)

        # 检查碰撞
        if self.collision_checker.is_path_valid(path):
            return path
        else:
            # 尝试重新规划
            return self.plan_with_collision_avoidance(start, goal)

    def rrt_star(self, start, goal, max_iterations=1000):
        """RRT*运动规划算法"""
        # 初始化树
        nodes = [start]
        edges = []

        for _ in range(max_iterations):
            # 随机采样
            random_node = self.random_sample()

            # 找最近邻点
            nearest_idx = self.nearest_neighbor(nodes, random_node)
            nearest_node = nodes[nearest_idx]

            # 向目标扩展
            new_node = self.steer(nearest_node, random_node)

            # 检查碰撞
            if self.collision_checker.is_edge_valid(nearest_node, new_node):
                nodes.append(new_node)
                edges.append((nearest_idx, len(nodes) - 1))

                # 检查是否到达目标
                if self.distance(new_node, goal) < 0.1:
                    return self.extract_path(edges, len(nodes) - 1)

        return None

八、本项目要点总结

正运动学

  • D-H参数表示
  • 变换矩阵
  • 末端位置计算

逆运动学

  • 解析求解(2-DOF)
  • 数值求解(牛顿-拉夫逊)
  • 可达性检查

轨迹规划

  • MoveIt框架
  • 关节空间规划
  • 笛卡尔空间规划

控制执行

  • 关节轨迹控制
  • 插值运动
  • 力/力矩反馈

安全性

  • 碰撞检测
  • 避障规划
  • 速度限制

下一篇预告《ROS2从入门到精通系列(十八):视觉识别系统 - 目标检测与跟踪》

料 机械臂是精密控制的典范。掌握这套系统,你就掌握了工业机器人的核心!

Logo

立足具身智能前沿赛道,致力于搭建全球化、开源化、全栈式技术交流与实践共创平台。

更多推荐