From d716ed5e67155992300bb9a7a1db54eadbb44fbd Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Tue, 16 Jun 2026 19:05:09 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=87=8D=E5=86=99=E6=9C=BA=E6=A2=B0?= =?UTF-8?q?=E8=87=82=E8=BF=90=E5=8A=A8=E5=AD=A6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 清晰的章节结构:几何参数、正运动学、逆运动学 - 完整的数学推导(带 LaTeX 公式) - Python 代码实现示例 - 工作空间分析和奇异点说明 - 运动插值算法 - 实用示例和测试命令 替换了原有的混乱结构,现在更易理解和参考 --- docs/arm.md | 423 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 366 insertions(+), 57 deletions(-) diff --git a/docs/arm.md b/docs/arm.md index c0aef70..28e4c9d 100644 --- a/docs/arm.md +++ b/docs/arm.md @@ -1,115 +1,424 @@ -这是一个非常漂亮且实用的自定义机械臂!从你的描述和图片来看,这属于一个**类 SCARA(水平多关节)构型**的机械臂,带有一个直线 Z 轴和三个平面的旋转关节。 +# 机械臂运动学推导 -为了让数学表达更清晰,我们先统一一下坐标系和变量的定义。 +6-DOF 机械臂逆运动学和正运动学完整推导。 -除角度外,所有线性长度和坐标统一使用 **mm**。 +## 机械臂结构 -### 变量与坐标系定义 +### 关节配置 -我们将基坐标系原点设在红色滑轨的零点位置。 +``` +J1: 线性滑轨(垂直运动,高度 d1) +J2: 基座旋转(绕 Z 轴,XY 平面) +J3: 肘关节(绕 Z 轴,XY 平面) +J4: 腕关节(绕 Z 轴,XY 平面) +J5: 末端俯仰(0° = 水平) +J6: 夹爪旋转 +``` -* **$d_1$**: 关节 1(高度滑块)的位置变量。 -* **$\theta_2$**: 关节 2 的旋转角度(相对于基坐标系 X 轴)。 -* **$\theta_3$**: 关节 3 的旋转角度(相对于上一连杆 $L_1$)。 -* **$\theta_4$**: 关节 4 的旋转角度(相对于上一连杆 $L_2$)。 -* **$(X, Y, Z, \Phi)$**: 夹爪末端(TCP - Tool Center Point)在基坐标系下的位姿。其中 $(X, Y, Z)$ 是空间坐标,$\Phi$ 是夹爪在水平面上的总朝向角(偏航角)。 -* **已知常量**: $L_1$(二三关节距离), $L_2$(三四关节距离), $x_4$(夹爪相对关节四的 X 偏移), $z_4$(夹爪相对关节四的高度偏移)。 +### 几何参数 ---- +| 参数 | 值 (mm) | 说明 | +|------|---------|------| +| L1 | 125 | J2-J3 连杆长度 | +| L2 | 125 | J3-J4 连杆长度 | +| x4 | 110 | J4-TCP 水平偏移 | +| z4 | 80 | J4-TCP 垂直偏移(夹爪状态相关:张开 55mm,闭合 -100mm) | -### 1. 运动学正解 (Forward Kinematics) +### 坐标系 -正解的目的是:**已知各个电机的角度和滑块高度 $(d_1, \theta_2, \theta_3, \theta_4)$,求夹爪末端的位置 $(X, Y, Z, \Phi)$。** +**基坐标系**(右手系,Z 轴朝上): +``` + Z↑ (高度) + | + | + o----→ X (前) + / + / + Y (左) +``` -因为 Z 轴的直线运动与 XY 平面的旋转运动是完全解耦的,我们可以分别计算: +- 原点:J1 滑轨底部 +- d1 范围:[-290, 0] mm(负值表示在基准面以下) -**高度 (Z 轴):** +## 正运动学(FK) +已知关节状态 → 计算 TCP 位姿 -$$Z = d_1 + z_4$$ +### 输入 -**平面朝向角 (偏航角 $\Phi$):** +- `d1`: 高度(J1 线性位移) +- `θ2, θ3, θ4`: J2/J3/J4 角度(度) +### 输出 -$$\Phi = \theta_2 + \theta_3 + \theta_4$$ +- `(x, y, z)`: TCP 位置(mm) +- `φ`: TCP 偏航角(度) -**平面坐标 (X, Y):** +### 推导 +**步骤 1**:计算 J4 中心位置(XY 平面) -$$X = L_1 \cos(\theta_2) + L_2 \cos(\theta_2 + \theta_3) + x_4 \cos(\theta_2 + \theta_3 + \theta_4)$$ +J2 和 J3 形成二连杆机构: -$$Y = L_1 \sin(\theta_2) + L_2 \sin(\theta_2 + \theta_3) + x_4 \sin(\theta_2 + \theta_3 + \theta_4)$$ +$$ +x_{j4} = L_1 \cos\theta_2 + L_2 \cos(\theta_2 + \theta_3) +$$ -*(注意:在实际编程中,如果你的电机零点不是一条直线,需要在角度上加上相应的初始偏置)* +$$ +y_{j4} = L_1 \sin\theta_2 + L_2 \sin(\theta_2 + \theta_3) +$$ ---- +**步骤 2**:计算 TCP 偏航角 -### 2. 运动学逆解 (Inverse Kinematics) +$$ +\phi = \theta_2 + \theta_3 + \theta_4 +$$ -逆解的目的是:**给出夹爪期望到达的目标位置和朝向 $(X, Y, Z, \Phi)$,求出各关节需要运动到的目标值 $(d_1, \theta_2, \theta_3, \theta_4)$。** +**步骤 3**:计算 TCP 位置 -> **注意:** 你的机械臂在 XY 平面上有 3 个旋转自由度,但平面位置只需要 2 个自由度 $(X,Y)$。这意味着如果只给定目标坐标,机械臂有无数种姿态可以到达(冗余)。因此,**为了得到唯一解,必须同时指定夹爪的最终期望朝向角 $\Phi$**。 +从 J4 中心沿 φ 方向偏移 x4,垂直偏移 z4: -下面是逆解的推导步骤,非常适合直接转化为固件中的控制代码: +$$ +x = x_{j4} + x_4 \cos\phi +$$ -#### 第一步:求解滑块高度 $d_1$ +$$ +y = y_{j4} + x_4 \sin\phi +$$ -高度依然是解耦的,直接通过目标 $Z$ 坐标和常量偏移计算: +$$ +z = d_1 - z_4 +$$ +### Python 实现 -$$d_1 = Z - z_4$$ +```python +def forward_kinematics(d1, theta2_deg, theta3_deg, theta4_deg, L1=125, L2=125, x4=110, z4=80): + """正运动学""" + theta2 = math.radians(theta2_deg) + theta3 = math.radians(theta3_deg) + theta4 = math.radians(theta4_deg) + phi = theta2 + theta3 + theta4 + + # J4 中心 + x_j4 = L1 * math.cos(theta2) + L2 * math.cos(theta2 + theta3) + y_j4 = L1 * math.sin(theta2) + L2 * math.sin(theta2 + theta3) + + # TCP 位置 + x = x_j4 + x4 * math.cos(phi) + y = y_j4 + x4 * math.sin(phi) + z = d1 - z4 + + return x, y, z, math.degrees(phi) +``` -#### 第二步:反推关节 4 的坐标 $(X_4, Y_4)$ +## 逆运动学(IK) -既然我们知道末端目标的坐标 $(X, Y)$ 和总朝向 $\Phi$,我们可以把夹爪的偏置 $x_4$ “剥离”掉,求出关节 4 中轴线在空间中的位置: +已知 TCP 目标位姿 → 计算关节角度 +### 输入 -$$X_4 = X - x_4 \cos(\Phi)$$ +- `(x, y, z)`: 目标位置(mm) +- `φ`: 目标偏航角(度) -$$Y_4 = Y - x_4 \sin(\Phi)$$ +### 输出 -#### 第三步:求解关节 3 的角度 $\theta_3$ +- `d1`: 高度 +- `θ2, θ3, θ4`: 关节角度(度) -现在问题简化为了一个标准的双连杆(两轴)平面机械臂求逆解问题。目标点是 $(X_4, Y_4)$,连杆是 $L_1$ 和 $L_2$。 -根据余弦定理,设目标点到原点的距离平方为 $r^2 = X_4^2 + Y_4^2$,有: +### 推导 +**步骤 1**:计算 J4 中心目标位置 -$$\cos(\theta_3) = \frac{X_4^2 + Y_4^2 - L_1^2 - L_2^2}{2 L_1 L_2}$$ +从 TCP 位置反向计算 J4 位置: -设 $C_3 = \cos(\theta_3)$。在实际控制代码中,必须在这里做合法性检查:如果 $C_3 > 1$ 或 $C_3 < -1$,说明目标点超出了机械臂的物理工作空间(够不到)。 -如果合法,则 $\theta_3$ 的正弦值为: +$$ +x_{j4} = x - x_4 \cos\phi +$$ +$$ +y_{j4} = y - x_4 \sin\phi +$$ -$$S_3 = \pm \sqrt{1 - C_3^2}$$ +$$ +z_{j4} = z + z_4 +$$ +$$ +d_1 = z_{j4} +$$ -*(这里的 $\pm$ 代表机械臂的两种姿态:“左手系/右臂”或“右手系/左臂”,也就是俗称的“手肘朝左”还是“手肘朝右”。你可以根据防碰撞需求或当前姿态选择其中一个)* +**步骤 2**:计算平面距离 -最终使用反正切函数求解 $\theta_3$: +$$ +r = \sqrt{x_{j4}^2 + y_{j4}^2} +$$ +**步骤 3**:求解 θ3(余弦定理) -$$\theta_3 = \text{atan2}(S_3, C_3)$$ +二连杆机构的标准解法: -#### 第四步:求解关节 2 的角度 $\theta_2$ +$$ +\cos\theta_3 = \frac{r^2 - L_1^2 - L_2^2}{2L_1L_2} +$$ -利用几何关系和已经求出的 $\theta_3$,可以通过组合角度直接求出 $\theta_2$: +检查工作空间: +$$ +-1 \leq \cos\theta_3 \leq 1 \quad \Rightarrow \quad |L_1 - L_2| \leq r \leq L_1 + L_2 +$$ -$$\theta_2 = \text{atan2}(Y_4, X_4) - \text{atan2}(L_2 S_3, L_1 + L_2 C_3)$$ +否则目标超出工作空间。 -#### 第五步:求解关节 4 的角度 $\theta_4$ +有两个解(肘部朝上 / 朝下): -因为总朝向 $\Phi = \theta_2 + \theta_3 + \theta_4$,所以: +$$ +\theta_3 = \pm \arccos\left(\frac{r^2 - L_1^2 - L_2^2}{2L_1L_2}\right) +$$ +**步骤 4**:求解 θ2 -$$\theta_4 = \Phi - \theta_2 - \theta_3$$ +$$ +\theta_2 = \arctan2(y_{j4}, x_{j4}) - \arctan2\left(L_2\sin\theta_3, L_1 + L_2\cos\theta_3\right) +$$ ---- +**步骤 5**:求解 θ4 -### 💡 嵌入式固件实现建议 +$$ +\theta_4 = \phi - \theta_2 - \theta_3 +$$ -由于你很可能需要将这些公式写入 MCU(比如利用 C/C++ 或 Rust 编写固件),这里有几个实践建议: +### Python 实现 -1. **使用 `atan2` 替代 `asin/acos**`:上面逆解公式中我全部使用了 $\text{atan2}(y, x)$。在标准库中,`atan2` 能够自动处理四个象限的符号问题,且能避免 $x=0$ 时的除零错误,这在底层驱动中至关重要。 -2. **死区与奇异点保护**:当 $X_4^2 + Y_4^2 \approx 0$ 时(关节 4 缩回到了原点正上方),此时 $\theta_2$ 会失去意义(奇异点)。在代码中应当加入对 $X_4^2 + Y_4^2 < \epsilon$(一个极小值)的判断,防止产生 NaN。 -3. **角度范围归一化**:计算出的角度可能会超出电机支持的物理限位范围(例如超出了 $[-180^\circ, +180^\circ]$),在下发脉冲或指令前,记得对 $\theta_2, \theta_3, \theta_4$ 进行归一化和软限位拦截。 +```python +def inverse_kinematics(x, y, z, phi_deg, elbow_up=False, L1=125, L2=125, x4=110, z4=80): + """逆运动学""" + phi = math.radians(phi_deg) + + # 步骤 1: 计算 J4 中心 + x_j4 = x - x4 * math.cos(phi) + y_j4 = y - x4 * math.sin(phi) + z_j4 = z + z4 + d1 = z_j4 + + # 步骤 2: 平面距离 + r2 = x_j4**2 + y_j4**2 + r = math.sqrt(r2) + + # 步骤 3: 求解 theta3 + cos_theta3 = (r2 - L1**2 - L2**2) / (2 * L1 * L2) + + if cos_theta3 < -1 or cos_theta3 > 1: + raise ValueError(f"目标超出工作空间,r={r:.1f}mm") + + # 肘部朝上:负角度,肘部朝下:正角度 + sin_theta3 = -math.sqrt(1 - cos_theta3**2) if elbow_up else math.sqrt(1 - cos_theta3**2) + theta3 = math.atan2(sin_theta3, cos_theta3) + + # 步骤 4: 求解 theta2 + theta2 = math.atan2(y_j4, x_j4) - math.atan2( + L2 * sin_theta3, + L1 + L2 * cos_theta3 + ) + + # 步骤 5: 求解 theta4 + theta4 = phi - theta2 - theta3 + + # 角度归一化到 [-180, 180) + def normalize(angle): + a = (angle + math.pi) % (2 * math.pi) - math.pi + return a if a != -math.pi or angle <= 0 else math.pi + + return ( + d1, + math.degrees(normalize(theta2)), + math.degrees(normalize(theta3)), + math.degrees(normalize(theta4)) + ) +``` + +## 零点偏移 + +物理零点(机械对齐)与数学零点(直线构型)存在偏差: + +| 关节 | 零点偏移 | +|------|----------| +| J2 | +3° | +| J3 | +7° | +| J4 | +25° | + +### 转换关系 + +**命令角度 ↔ 数学角度**: + +$$ +\theta_{\text{command}} = \theta_{\text{math}} + \text{offset} +$$ + +$$ +\theta_{\text{math}} = \theta_{\text{command}} - \text{offset} +$$ + +**使用**: +- 正运动学:先减去偏移(命令 → 数学),再计算 +- 逆运动学:先计算(数学),再加上偏移(数学 → 命令) + +## 工作空间 + +### 高度范围 + +$$ +z \in [-290 - z_4, 0 - z_4] = [-370, -80] \text{ mm} +$$ + +### 水平范围 + +$$ +r_{\min} = |L_1 - L_2| + x_4 = 0 + 110 = 110 \text{ mm} +$$ + +$$ +r_{\max} = L_1 + L_2 + x_4 = 125 + 125 + 110 = 360 \text{ mm} +$$ + +**可达圆环**:半径 110mm 到 360mm + +### 关节限位 + +| 关节 | 最小值 | 最大值 | 单位 | +|------|--------|--------|------| +| height | -290 | 0 | mm | +| J2 | -110 | 115 | ° | +| J3 | -120 | 145 | ° | +| J4 | -90 | 130 | ° | +| J5 | -180 | 180 | ° | +| J6 | -180 | 180 | ° | + +## 奇异点 + +### 1. 肩部奇异点 + +当 J4 中心在原点正上方: + +$$ +x_{j4} = y_{j4} = 0 \quad \Rightarrow \quad r = 0 +$$ + +此时 θ2 无定义(可取任意值)。 + +**避免**:保持 `r > 10mm` + +### 2. 肘部奇异点 + +当机械臂完全伸直或完全折叠: + +$$ +\theta_3 = 0° \text{ 或 } \pm 180° +$$ + +此时运动控制不稳定。 + +**避免**:保持 `|θ3| > 5°` + +## 运动插值 + +关节空间线性插值,避免笛卡尔空间的复杂路径规划。 + +### 算法 + +给定起点 `q_start` 和终点 `q_end`,生成 N 个中间点: + +$$ +q_i = q_{\text{start}} + \frac{i}{N}(q_{\text{end}} - q_{\text{start}}), \quad i = 1, 2, \ldots, N +$$ + +### 参数 + +- `duration`: 运动时长(秒) +- `rate`: 插值频率(Hz) +- `steps = ceil(duration × rate)`: 插值点数 + +**示例**:`duration=2.0s`, `rate=20Hz` → `steps=40` + +### Python 实现 + +```python +def interpolate_joints(start, end, duration=2.0, rate=20.0): + """关节空间插值""" + steps = max(1, int(math.ceil(duration * rate))) + trajectory = [] + + for i in range(1, steps + 1): + t = i / steps + state = { + key: int(round(start[key] + t * (end[key] - start[key]))) + for key in start.keys() + } + trajectory.append(state) + + return trajectory +``` + +## 完整示例 + +### 示例 1:前方抓取 + +**目标**:抓取前方 250mm,右侧 50mm,高度 -150mm 的物体 + +```python +# 逆运动学 +d1, theta2, theta3, theta4 = inverse_kinematics( + x=250, y=50, z=-150, phi_deg=0 +) +# 输出: d1=-70, theta2=11.3°, theta3=-48.6°, theta4=37.3° + +# 验证:正运动学 +x, y, z, phi = forward_kinematics(d1, theta2, theta3, theta4) +# 输出: (250.0, 50.0, -150.0, 0.0°) ✓ +``` + +### 示例 2:多段轨迹 + +```python +# 初始位置 +start = {'height': 0, 'j2': 0, 'j3': 0, 'j4': 0, 'j5': 81, 'j6': 30} + +# 目标 1: 上方 +_, theta2, theta3, theta4 = inverse_kinematics(200, 100, -50, 45) +waypoint1 = {'height': -50, 'j2': theta2, 'j3': theta3, 'j4': theta4, 'j5': 81, 'j6': 30} + +# 目标 2: 抓取位置 +_, theta2, theta3, theta4 = inverse_kinematics(200, 100, -150, 45) +waypoint2 = {'height': -150, 'j2': theta2, 'j3': theta3, 'j4': theta4, 'j5': -100, 'j6': -5} + +# 生成轨迹 +traj1 = interpolate_joints(start, waypoint1, duration=2.0) # 2秒到上方 +traj2 = interpolate_joints(waypoint1, waypoint2, duration=1.0) # 1秒下降抓取 +``` + +## 参考 + +### UDP 命令格式 + +``` +JXB:::::::0:0:EZHY\n +``` + +- 所有角度值已包含零点偏移 +- 直接发送到 ESP32 的 UDP 端口 8888 + +### 相关工具 + +- `tools/udp_control.py` - 命令行控制(支持 `joints` 和 `pose` 模式) +- `ros2/src/udp_teleop/udp_teleop/arm_control.py` - ROS 节点 +- `docs/vision_calibration_horizontal.md` - 相机坐标变换 + +### 测试 + +```bash +# 测试逆运动学 +python tools/udp_control.py pose --x 200 --y 100 --z -100 --phi 45 --dry-run + +# 发送命令 +python tools/udp_control.py pose --x 200 --y 100 --z -100 --phi 45 --duration 2.0 +```