跳转至

四自由度机械臂:运动学与轨迹控制

概述

本项目实现一个四自由度(4-DOF)串联机械臂的完整控制系统,从理论建模到实际抓取任务。读者将学习:

学习目标 难度 预计时间
DH参数建模与齐次变换矩阵 ★★★★☆ 60分钟
几何法正逆运动学推导 ★★★★☆ 60分钟
工作空间分析与奇异性处理 ★★★★★ 45分钟
关节空间与笛卡尔空间轨迹规划 ★★★★☆ 45分钟
视觉引导抓取系统集成 ★★★★★ 90分钟

前置知识:三角函数、矩阵运算、STM32 PWM输出、基础C语言

硬件清单(BOM)

器件 型号 数量 参考价格 购买渠道
主控 STM32F103C8T6 1 ¥15 立创商城
底座舵机 MG996R (11kg·cm) 1 ¥18 淘宝
大臂舵机 MG996R (11kg·cm) 1 ¥18 淘宝
小臂舵机 MG996R (11kg·cm) 1 ¥18 淘宝
末端舵机 SG90 (1.8kg·cm) 1 ¥5 淘宝
夹爪舵机 SG90 (1.8kg·cm) 1 ¥5 淘宝
舵机电源 5V/5A开关电源 1 ¥25 淘宝
机械臂套件 铝合金4DOF(LeArm兼容) 1 ¥120 淘宝
摄像头(可选) OV7670 或 ESP32-CAM 1 ¥15 淘宝
逻辑电平转换 TXS0108E 1 ¥3 立创商城

连杆参数(需根据实际机械臂测量):

L0 = 80mm(底座高度,J1轴到地面)
L1 = 105mm(大臂长度,J2轴到J3轴)
L2 = 100mm(小臂长度,J3轴到J4轴)
L3 = 65mm(末端长度,J4轴到夹爪中心)

背景知识

机器人运动学基础

机械臂运动学(Kinematics)研究关节角度与末端位置/姿态之间的映射关系,不涉及力和质量。分为两类:

  • 正运动学(Forward Kinematics, FK):已知关节角度 → 求末端位姿
  • 逆运动学(Inverse Kinematics, IK):已知末端位姿 → 求关节角度
正运动学(唯一解):
  [θ1, θ2, θ3, θ4] ──FK──→ [x, y, z, φ]

逆运动学(可能多解或无解):
  [x, y, z, φ] ──IK──→ [θ1, θ2, θ3, θ4](肘上/肘下解)

DH参数(Denavit-Hartenberg Parameters)

DH参数是描述串联机器人连杆关系的标准方法,由 Denavit 和 Hartenberg 于1955年提出。每个连杆用4个参数描述:

参数 符号 含义
连杆长度 aᵢ 沿 xᵢ 轴,从 zᵢ₋₁ 到 zᵢ 的距离
连杆扭角 αᵢ 绕 xᵢ 轴,从 zᵢ₋₁ 到 zᵢ 的转角
关节偏距 dᵢ 沿 zᵢ₋₁ 轴,从 xᵢ₋₁ 到 xᵢ 的距离
关节角度 θᵢ 绕 zᵢ₋₁ 轴,从 xᵢ₋₁ 到 xᵢ 的转角(变量

4-DOF机械臂DH参数表(标准DH约定):

关节 i aᵢ (mm) αᵢ (°) dᵢ (mm) θᵢ (变量范围)
1 0 90 L0=80 θ1 (−90°~+90°)
2 L1=105 0 0 θ2 (0°~135°)
3 L2=100 0 0 θ3 (−135°~0°)
4 L3=65 0 0 θ4 (−90°~+90°)

DH坐标系建立规则

步骤1:确定各关节旋转轴 z₀, z₁, z₂, z₃
步骤2:确定公垂线方向作为 x 轴
步骤3:按右手定则确定 y 轴
步骤4:测量 a, α, d, θ 四个参数

齐次变换矩阵(Homogeneous Transformation Matrix)

每个连杆的变换矩阵 Tᵢ₋₁ᵢ 由DH参数构造:

        ┌ cos θᵢ   -sin θᵢ cos αᵢ    sin θᵢ sin αᵢ   aᵢ cos θᵢ ┐
Tᵢ₋₁ᵢ = │ sin θᵢ    cos θᵢ cos αᵢ   -cos θᵢ sin αᵢ   aᵢ sin θᵢ │
        │    0          sin αᵢ            cos αᵢ          dᵢ    │
        └    0             0                 0              1    ┘

末端相对于基座的变换矩阵:

T₀₄ = T₀₁ · T₁₂ · T₂₃ · T₃₄

T₀₄ = ┌ R₃ₓ₃  p₃ₓ₁ ┐
      └  0₁ₓ₃   1   ┘

其中 R 是3×3旋转矩阵,p = [x, y, z]ᵀ 是末端位置向量

C语言实现4×4矩阵乘法

// 4x4齐次变换矩阵(行主序存储)
typedef float Mat4[4][4];

// 矩阵乘法:C = A * B
void mat4_mul(const Mat4 A, const Mat4 B, Mat4 C) {
    Mat4 tmp = {0};
    for (int i = 0; i < 4; i++)
        for (int j = 0; j < 4; j++)
            for (int k = 0; k < 4; k++)
                tmp[i][j] += A[i][k] * B[k][j];
    // 避免原地修改时的别名问题
    for (int i = 0; i < 4; i++)
        for (int j = 0; j < 4; j++)
            C[i][j] = tmp[i][j];
}

// 根据DH参数构造单个连杆变换矩阵
// theta_deg: 关节角(度), d: 偏距(mm), a: 连杆长(mm), alpha_deg: 扭角(度)
void dh_matrix(float theta_deg, float d, float a, float alpha_deg, Mat4 T) {
    float th = theta_deg * M_PI / 180.0f;
    float al = alpha_deg * M_PI / 180.0f;
    float ct = cosf(th), st = sinf(th);
    float ca = cosf(al), sa = sinf(al);

    T[0][0] = ct;    T[0][1] = -st*ca;  T[0][2] = st*sa;   T[0][3] = a*ct;
    T[1][0] = st;    T[1][1] =  ct*ca;  T[1][2] = -ct*sa;  T[1][3] = a*st;
    T[2][0] = 0.0f;  T[2][1] = sa;      T[2][2] = ca;      T[2][3] = d;
    T[3][0] = 0.0f;  T[3][1] = 0.0f;   T[3][2] = 0.0f;    T[3][3] = 1.0f;
}

工作空间分析(Workspace Analysis)

机械臂的工作空间分为两类:

可达工作空间(Reachable Workspace):末端能到达的所有点的集合(至少一种姿态可达)

灵巧工作空间(Dexterous Workspace):末端能以任意姿态到达的点的集合(通常是可达空间的子集)

4-DOF机械臂工作空间示意(侧视图,J1=0°平面):

        z
        │    ┌──────────────────────────────┐
   L0+  │    │  可达工作空间(环形区域)      │
        │    │  ┌──────────────────────┐    │
        │    │  │  灵巧工作空间(较小)  │    │
        │    │  └──────────────────────┘    │
   L0   ├────┼──────────────────────────────┼──── r
        │    │                              │
        │    └──────────────────────────────┘
        0   |L1-L2-L3|        L1+L2+L3

最大伸展半径 = L1 + L2 + L3 = 105 + 100 + 65 = 270mm
最小收缩半径 = |L1 - L2 - L3| ≈ 0mm(可折叠)

工作空间数值计算(Python验证脚本)

import numpy as np
import matplotlib.pyplot as plt

L1, L2, L3 = 105, 100, 65  # mm

# 采样关节角度空间
theta2 = np.linspace(0, 135, 50)    # 大臂
theta3 = np.linspace(-135, 0, 50)   # 小臂
theta4 = np.linspace(-90, 90, 20)   # 末端

points = []
for t2 in theta2:
    for t3 in theta3:
        for t4 in theta4:
            a1 = np.radians(t2)
            a2 = np.radians(t3)
            a3 = np.radians(t4)
            r = (L1*np.cos(a1) + L2*np.cos(a1+a2)
                 + L3*np.cos(a1+a2+a3))
            z = (L1*np.sin(a1) + L2*np.sin(a1+a2)
                 + L3*np.sin(a1+a2+a3))
            points.append((r, z))

r_vals, z_vals = zip(*points)
plt.scatter(r_vals, z_vals, s=0.1, alpha=0.3)
plt.xlabel('水平距离 r (mm)')
plt.ylabel('高度 z (mm)')
plt.title('4-DOF机械臂可达工作空间(侧视图)')
plt.axis('equal')
plt.savefig('workspace.png', dpi=150)

机械臂与其他运动系统对比

特性 4-DOF串联臂 6-DOF串联臂 Delta并联臂 SCARA
自由度 4 6 3 4
末端姿态控制 部分(1轴) 完整(3轴) 无旋转 平面+旋转
工作空间 中等 小(圆柱) 大(平面)
奇异性
控制复杂度
典型应用 教学/小型抓取 工业焊接/装配 高速分拣 PCB贴片
成本 ¥200~500 ¥5000+ ¥1000+ ¥2000+

核心内容

正运动学(Forward Kinematics)

基于DH矩阵的完整正运动学实现:

#include <math.h>
#include <string.h>

#define L0  80.0f   // 底座高度(mm)
#define L1  105.0f  // 大臂长度(mm)
#define L2  100.0f  // 小臂长度(mm)
#define L3  65.0f   // 末端长度(mm)
#define DEG2RAD  (3.14159265358979f / 180.0f)
#define RAD2DEG  (180.0f / 3.14159265358979f)

typedef float Mat4[4][4];

typedef struct {
    float x, y, z;      // 末端位置(mm)
    float roll;          // 绕x轴旋转(度),本臂固定为0
    float pitch;         // 绕y轴旋转(度)
    float yaw;           // 绕z轴旋转(度)
} EndEffectorPose;

typedef struct {
    float j1;  // 底座旋转(度),范围 -90~+90
    float j2;  // 大臂俯仰(度),范围 0~135
    float j3;  // 小臂俯仰(度),范围 -135~0
    float j4;  // 末端俯仰(度),范围 -90~+90
} JointAngles;

// DH矩阵构造(见背景知识章节)
static void dh_matrix(float theta_deg, float d,
                      float a, float alpha_deg, Mat4 T) {
    float th = theta_deg * DEG2RAD;
    float al = alpha_deg * DEG2RAD;
    float ct = cosf(th), st = sinf(th);
    float ca = cosf(al), sa = sinf(al);
    T[0][0]=ct;    T[0][1]=-st*ca; T[0][2]=st*sa;  T[0][3]=a*ct;
    T[1][0]=st;    T[1][1]=ct*ca;  T[1][2]=-ct*sa; T[1][3]=a*st;
    T[2][0]=0.0f;  T[2][1]=sa;     T[2][2]=ca;     T[2][3]=d;
    T[3][0]=0.0f;  T[3][1]=0.0f;   T[3][2]=0.0f;   T[3][3]=1.0f;
}

static void mat4_mul(const Mat4 A, const Mat4 B, Mat4 C) {
    Mat4 tmp;
    memset(tmp, 0, sizeof(tmp));
    for (int i = 0; i < 4; i++)
        for (int j = 0; j < 4; j++)
            for (int k = 0; k < 4; k++)
                tmp[i][j] += A[i][k] * B[k][j];
    memcpy(C, tmp, sizeof(Mat4));
}

// 完整正运动学:输入关节角度,输出末端位姿
EndEffectorPose forward_kinematics(const JointAngles *q) {
    Mat4 T01, T12, T23, T34, T02, T03, T04;

    // 按DH参数表构造各连杆矩阵
    // 关节1:θ1变量,d=L0,a=0,α=90°
    dh_matrix(q->j1, L0, 0.0f, 90.0f, T01);
    // 关节2:θ2变量,d=0,a=L1,α=0°
    dh_matrix(q->j2, 0.0f, L1, 0.0f, T12);
    // 关节3:θ3变量,d=0,a=L2,α=0°
    dh_matrix(q->j3, 0.0f, L2, 0.0f, T23);
    // 关节4:θ4变量,d=0,a=L3,α=0°
    dh_matrix(q->j4, 0.0f, L3, 0.0f, T34);

    mat4_mul(T01, T12, T02);
    mat4_mul(T02, T23, T03);
    mat4_mul(T03, T34, T04);

    EndEffectorPose pose;
    pose.x = T04[0][3];
    pose.y = T04[1][3];
    pose.z = T04[2][3];
    // 从旋转矩阵提取欧拉角(ZYX约定)
    pose.yaw   = atan2f(T04[1][0], T04[0][0]) * RAD2DEG;
    pose.pitch = atan2f(-T04[2][0],
                  sqrtf(T04[2][1]*T04[2][1] + T04[2][2]*T04[2][2])) * RAD2DEG;
    pose.roll  = atan2f(T04[2][1], T04[2][2]) * RAD2DEG;
    return pose;
}

几何法逆运动学(Geometric IK)

几何法利用三角形余弦定理直接求解,计算量小,适合嵌入式实时控制:

// IK求解结果
typedef struct {
    JointAngles q;      // 关节角度解
    int valid;          // 1=有效解,0=无解
    int elbow_up;       // 1=肘部向上解,0=肘部向下解
} IKResult;

// 关节限位检查
static int check_limits(const JointAngles *q) {
    if (q->j1 < -90.0f || q->j1 > 90.0f)  return 0;
    if (q->j2 <   0.0f || q->j2 > 135.0f) return 0;
    if (q->j3 < -135.0f|| q->j3 > 0.0f)   return 0;
    if (q->j4 < -90.0f || q->j4 > 90.0f)  return 0;
    return 1;
}

// 几何法IK:给定末端位置(x,y,z)和末端姿态角end_pitch(度)
// elbow_up=1求肘部向上解,=0求肘部向下解
IKResult inverse_kinematics(float x, float y, float z,
                             float end_pitch, int elbow_up) {
    IKResult result = {.valid = 0};

    // ── 步骤1:求J1(底座旋转角)──────────────────────────
    result.q.j1 = atan2f(y, x) * RAD2DEG;

    // ── 步骤2:计算腕部位置(去掉末端连杆L3的贡献)──────────
    float r_total = sqrtf(x*x + y*y);  // 水平投影距离
    float ep = end_pitch * DEG2RAD;
    float wx = r_total - L3 * cosf(ep);  // 腕部水平距离
    float wz = z - L0 - L3 * sinf(ep);  // 腕部高度(相对J2轴)

    // ── 步骤3:用余弦定理求J3 ─────────────────────────────
    float d2 = wx*wx + wz*wz;  // 腕部到J2轴距离的平方
    float d  = sqrtf(d2);

    // 可达性检查
    float reach_max = L1 + L2;
    float reach_min = fabsf(L1 - L2);
    if (d > reach_max + 1e-3f || d < reach_min - 1e-3f) {
        // 目标不可达
        return result;
    }
    // 夹紧到合法范围(浮点误差保护)
    float cos_j3 = (d2 - L1*L1 - L2*L2) / (2.0f * L1 * L2);
    cos_j3 = fmaxf(-1.0f, fminf(1.0f, cos_j3));

    float j3_abs = acosf(cos_j3) * RAD2DEG;  // 0~180°
    // 肘部向上:J3为负(小臂向后折)
    result.q.j3 = elbow_up ? -j3_abs : j3_abs;

    // ── 步骤4:求J2 ──────────────────────────────────────
    float alpha = atan2f(wz, wx);  // 腕部方向角
    float sin_j3 = sinf(result.q.j3 * DEG2RAD);
    float cos_j3v = cosf(result.q.j3 * DEG2RAD);
    // 用正弦定理辅助
    float beta = atan2f(L2 * sin_j3,
                        L1 + L2 * cos_j3v);
    result.q.j2 = (alpha - beta) * RAD2DEG;

    // ── 步骤5:求J4(末端姿态补偿)──────────────────────────
    result.q.j4 = end_pitch - result.q.j2 - result.q.j3;

    // 关节限位检查
    result.valid = check_limits(&result.q);
    result.elbow_up = elbow_up;
    return result;
}

// 自动选择最优解(优先肘部向上,关节角度变化最小)
IKResult ik_best_solution(float x, float y, float z,
                           float end_pitch,
                           const JointAngles *current) {
    IKResult up   = inverse_kinematics(x, y, z, end_pitch, 1);
    IKResult down = inverse_kinematics(x, y, z, end_pitch, 0);

    if (!up.valid && !down.valid) {
        IKResult none = {.valid = 0};
        return none;
    }
    if (!up.valid)   return down;
    if (!down.valid) return up;

    // 计算关节角度变化量(选变化最小的解)
    if (current == NULL) return up;  // 默认肘部向上

    float cost_up = fabsf(up.q.j1 - current->j1)
                  + fabsf(up.q.j2 - current->j2)
                  + fabsf(up.q.j3 - current->j3)
                  + fabsf(up.q.j4 - current->j4);
    float cost_down = fabsf(down.q.j1 - current->j1)
                    + fabsf(down.q.j2 - current->j2)
                    + fabsf(down.q.j3 - current->j3)
                    + fabsf(down.q.j4 - current->j4);
    return (cost_up <= cost_down) ? up : down;
}

舵机标定与驱动

// 舵机标定参数(根据实际安装方向调整)
typedef struct {
    uint16_t pulse_min;   // 最小脉宽(us),对应min_angle
    uint16_t pulse_max;   // 最大脉宽(us),对应max_angle
    float    angle_min;   // 最小角度(度)
    float    angle_max;   // 最大角度(度)
    float    zero_offset; // 机械零位偏移(度)
    int8_t   direction;   // 方向:+1或-1
} ServoCalib;

// 5个舵机的标定参数(J1~J4 + 夹爪)
ServoCalib calib[5] = {
    {500, 2500, -90.0f,  90.0f,   0.0f, +1},  // J1 底座
    {500, 2500,   0.0f, 135.0f,   5.0f, +1},  // J2 大臂(+5°零位偏移)
    {500, 2500, -135.0f, 0.0f,   -3.0f, -1},  // J3 小臂(反向安装)
    {500, 2500, -90.0f,  90.0f,   0.0f, +1},  // J4 末端
    {500, 2500,   0.0f,  60.0f,   0.0f, +1},  // 夹爪
};

// 将关节角度转换为PWM脉宽(us)
uint16_t angle_to_pulse(uint8_t joint, float angle) {
    ServoCalib *c = &calib[joint];
    float adj = (angle + c->zero_offset) * c->direction;
    adj = fmaxf(c->angle_min, fminf(c->angle_max, adj));
    float ratio = (adj - c->angle_min) / (c->angle_max - c->angle_min);
    return (uint16_t)(c->pulse_min + ratio * (c->pulse_max - c->pulse_min));
}

// STM32 TIM2/TIM3 PWM输出(50Hz,20ms周期)
// CubeMX配置:TIM2, PSC=71, ARR=19999 → 50Hz @ 72MHz
void set_servo_pulse(uint8_t ch, uint16_t pulse_us) {
    // pulse_us范围500~2500,对应CCR值500~2500(1us/count)
    switch (ch) {
        case 0: TIM2->CCR1 = pulse_us; break;
        case 1: TIM2->CCR2 = pulse_us; break;
        case 2: TIM2->CCR3 = pulse_us; break;
        case 3: TIM2->CCR4 = pulse_us; break;
        case 4: TIM3->CCR1 = pulse_us; break;
    }
}

void arm_set_joints(const JointAngles *q) {
    set_servo_pulse(0, angle_to_pulse(0, q->j1));
    set_servo_pulse(1, angle_to_pulse(1, q->j2));
    set_servo_pulse(2, angle_to_pulse(2, q->j3));
    set_servo_pulse(3, angle_to_pulse(3, q->j4));
}

舵机扭矩计算与选型指南

选择舵机时需要计算各关节的负载扭矩:

扭矩计算公式:
  τ = F × d = m × g × L_arm

其中:
  m    = 负载质量 + 连杆质量 (kg)
  g    = 9.81 m/s²
  L_arm = 力臂长度 (m)(最坏情况:连杆水平伸展)

安全系数:选择额定扭矩 ≥ 计算值 × 2.0(动态载荷)

各关节扭矩估算(4-DOF臂,负载200g)

关节 负载质量 力臂 计算扭矩 推荐舵机 额定扭矩
J4末端 200g 65mm 0.13N·m SG90 0.18N·m
J3小臂 350g 100mm 0.34N·m MG996R 1.08N·m
J2大臂 600g 105mm 0.62N·m MG996R 1.08N·m
J1底座 1200g 旋转惯量 0.30N·m MG996R 1.08N·m

舵机选型对比

型号 扭矩 速度 电压 重量 价格 适用关节
SG90 1.8kg·cm 0.1s/60° 4.8V 9g ¥5 末端/夹爪
MG996R 11kg·cm 0.17s/60° 6V 55g ¥18 主要关节
DS3218 20kg·cm 0.16s/60° 6V 60g ¥35 重载关节
Dynamixel XL430 15kg·cm 0.19s/60° 12V 82g ¥350 精密控制

轨迹规划

关节空间插补(Joint Space Interpolation)

在关节空间中插补,末端轨迹不是直线,但计算简单,不会经过奇异点:

// 全局当前关节角度
static JointAngles g_current_joints = {0, 45, -45, 0};

void arm_get_current_joints(JointAngles *q) {
    *q = g_current_joints;
}

// 关节空间S曲线插补(正弦平滑)
void arm_move_joints(const JointAngles *target, uint32_t duration_ms) {
    JointAngles start = g_current_joints;
    uint32_t steps = duration_ms / 10;  // 10ms控制周期
    if (steps == 0) steps = 1;

    for (uint32_t i = 1; i <= steps; i++) {
        float t = (float)i / (float)steps;
        // S曲线:正弦平滑,加速度连续
        float s = (1.0f - cosf(t * 3.14159265f)) * 0.5f;

        JointAngles q = {
            .j1 = start.j1 + (target->j1 - start.j1) * s,
            .j2 = start.j2 + (target->j2 - start.j2) * s,
            .j3 = start.j3 + (target->j3 - start.j3) * s,
            .j4 = start.j4 + (target->j4 - start.j4) * s,
        };
        arm_set_joints(&q);
        g_current_joints = q;
        HAL_Delay(10);
    }
    g_current_joints = *target;
    arm_set_joints(target);
}

笛卡尔空间直线插补(Cartesian Linear Interpolation)

末端沿直线运动,每步调用逆运动学,适合精确路径控制:

// 末端从当前位置直线运动到目标位置
// 返回0成功,-1路径中有不可达点
int arm_move_linear(float tx, float ty, float tz,
                    float end_pitch, uint32_t duration_ms) {
    EndEffectorPose start_pose = forward_kinematics(&g_current_joints);
    float start_pitch = g_current_joints.j2
                      + g_current_joints.j3
                      + g_current_joints.j4;

    uint32_t steps = duration_ms / 10;
    if (steps == 0) steps = 1;

    for (uint32_t i = 1; i <= steps; i++) {
        float t = (float)i / (float)steps;
        float s = (1.0f - cosf(t * 3.14159265f)) * 0.5f;

        float cx = start_pose.x + (tx - start_pose.x) * s;
        float cy = start_pose.y + (ty - start_pose.y) * s;
        float cz = start_pose.z + (tz - start_pose.z) * s;
        float cp = start_pitch + (end_pitch - start_pitch) * s;

        IKResult ik = ik_best_solution(cx, cy, cz, cp, &g_current_joints);
        if (!ik.valid) {
            return -1;  // 路径中有不可达点
        }
        arm_set_joints(&ik.q);
        g_current_joints = ik.q;
        HAL_Delay(10);
    }
    return 0;
}

关节空间 vs 笛卡尔空间对比

关节空间插补:
  优点:计算简单,不经过奇异点,速度均匀
  缺点:末端轨迹弯曲,不适合需要直线路径的任务
  适用:点到点运动,初始化,回零

笛卡尔空间插补:
  优点:末端走直线,路径可预测
  缺点:可能经过奇异点,计算量大(每步需IK)
  适用:抓取、焊接、涂胶等需要直线路径的任务

时序对比(100步插补,STM32F103 @ 72MHz):
  关节空间:每步 ~5μs(纯数学运算)
  笛卡尔空间:每步 ~45μs(含IK求解)
  两者均满足10ms控制周期要求

深入原理

奇异性分析(Singularity Analysis)

奇异点(Singular Configuration)是机械臂失去一个或多个自由度的构型,此时雅可比矩阵(Jacobian)行列式为零,IK无唯一解。

4-DOF臂的奇异构型

奇异类型1:肩部奇异(Shoulder Singularity)
  发生条件:末端目标在J1轴线上(x=0, y=0)
  现象:J1角度无法确定(atan2(0,0)未定义)
  检测:r = sqrt(x²+y²) < ε(ε≈5mm)

奇异类型2:肘部奇异(Elbow Singularity)
  发生条件:大臂和小臂完全伸展或完全折叠
  现象:J3=0°或J3=±180°,J2和J4有无穷多组合
  检测:|d - (L1+L2)| < ε 或 d < ε

奇异类型3:腕部奇异(Wrist Singularity)
  4-DOF臂无独立腕部,此类奇异不适用

奇异性检测与处理

#define SINGULARITY_THRESHOLD  5.0f  // mm

typedef enum {
    SING_NONE     = 0,
    SING_SHOULDER = 1,
    SING_ELBOW    = 2,
} SingularityType;

SingularityType check_singularity(float x, float y, float z,
                                   float end_pitch) {
    // 检查肩部奇异
    float r = sqrtf(x*x + y*y);
    if (r < SINGULARITY_THRESHOLD) {
        return SING_SHOULDER;
    }

    // 检查肘部奇异
    float ep = end_pitch * DEG2RAD;
    float wx = r - L3 * cosf(ep);
    float wz = z - L0 - L3 * sinf(ep);
    float d  = sqrtf(wx*wx + wz*wz);

    if (fabsf(d - (L1 + L2)) < SINGULARITY_THRESHOLD ||
        d < SINGULARITY_THRESHOLD) {
        return SING_ELBOW;
    }
    return SING_NONE;
}

// 奇异点附近的平滑处理:微小偏移避开奇异点
int arm_move_linear_safe(float tx, float ty, float tz,
                          float end_pitch, uint32_t duration_ms) {
    SingularityType sing = check_singularity(tx, ty, tz, end_pitch);
    if (sing == SING_SHOULDER) {
        // 肩部奇异:在目标点附近加微小偏移
        tx += (tx >= 0) ? SINGULARITY_THRESHOLD : -SINGULARITY_THRESHOLD;
    }
    return arm_move_linear(tx, ty, tz, end_pitch, duration_ms);
}

关节限位处理(Joint Limit Handling)

// 关节软限位(比硬件限位留5°余量)
typedef struct {
    float min_soft;  // 软限位最小值
    float max_soft;  // 软限位最大值
    float min_hard;  // 硬件限位最小值
    float max_hard;  // 硬件限位最大值
} JointLimit;

const JointLimit JOINT_LIMITS[4] = {
    {-85.0f,  85.0f, -90.0f,  90.0f},  // J1
    {  5.0f, 130.0f,   0.0f, 135.0f},  // J2
    {-130.0f, -5.0f,-135.0f,   0.0f},  // J3
    {-85.0f,  85.0f, -90.0f,  90.0f},  // J4
};

// 将关节角度夹紧到软限位范围
float clamp_joint(uint8_t joint, float angle) {
    const JointLimit *lim = &JOINT_LIMITS[joint];
    return fmaxf(lim->min_soft, fminf(lim->max_soft, angle));
}

// 检查关节角度是否在软限位内
int joint_in_soft_limit(uint8_t joint, float angle) {
    const JointLimit *lim = &JOINT_LIMITS[joint];
    return (angle >= lim->min_soft && angle <= lim->max_soft);
}

// 带限位保护的关节运动
void arm_set_joints_safe(const JointAngles *q) {
    JointAngles safe = {
        .j1 = clamp_joint(0, q->j1),
        .j2 = clamp_joint(1, q->j2),
        .j3 = clamp_joint(2, q->j3),
        .j4 = clamp_joint(3, q->j4),
    };
    arm_set_joints(&safe);
    g_current_joints = safe;
}

简单碰撞检测(Collision Detection)

对于教学级机械臂,实现基于包围球(Bounding Sphere)的简化碰撞检测:

// 连杆包围球(球心在连杆中点,半径为连杆长度/2 + 安全余量)
typedef struct {
    float cx, cy, cz;  // 球心坐标
    float radius;       // 半径
} BoundingSphere;

// 计算各连杆包围球(基于正运动学中间结果)
void compute_link_spheres(const JointAngles *q,
                           BoundingSphere spheres[4]) {
    // 计算各关节位置
    Mat4 T01, T12, T23, T34, T02, T03, T04;
    dh_matrix(q->j1, L0, 0.0f, 90.0f, T01);
    dh_matrix(q->j2, 0.0f, L1, 0.0f, T12);
    dh_matrix(q->j3, 0.0f, L2, 0.0f, T23);
    dh_matrix(q->j4, 0.0f, L3, 0.0f, T34);

    Mat4 T02_, T03_, T04_;
    mat4_mul(T01, T12, T02_);
    mat4_mul(T02_, T23, T03_);
    mat4_mul(T03_, T34, T04_);

    // 连杆1包围球(J1轴到J2轴中点)
    spheres[0].cx = T01[0][3] * 0.5f;
    spheres[0].cy = T01[1][3] * 0.5f;
    spheres[0].cz = (0 + T01[2][3]) * 0.5f;
    spheres[0].radius = L0 * 0.5f + 20.0f;

    // 连杆2包围球(J2轴到J3轴中点)
    spheres[1].cx = (T01[0][3] + T02_[0][3]) * 0.5f;
    spheres[1].cy = (T01[1][3] + T02_[1][3]) * 0.5f;
    spheres[1].cz = (T01[2][3] + T02_[2][3]) * 0.5f;
    spheres[1].radius = L1 * 0.5f + 15.0f;

    // 连杆3包围球
    spheres[2].cx = (T02_[0][3] + T03_[0][3]) * 0.5f;
    spheres[2].cy = (T02_[1][3] + T03_[1][3]) * 0.5f;
    spheres[2].cz = (T02_[2][3] + T03_[2][3]) * 0.5f;
    spheres[2].radius = L2 * 0.5f + 15.0f;

    // 连杆4包围球
    spheres[3].cx = (T03_[0][3] + T04_[0][3]) * 0.5f;
    spheres[3].cy = (T03_[1][3] + T04_[1][3]) * 0.5f;
    spheres[3].cz = (T03_[2][3] + T04_[2][3]) * 0.5f;
    spheres[3].radius = L3 * 0.5f + 10.0f;
}

// 检查两球是否相交
static int spheres_intersect(const BoundingSphere *a,
                              const BoundingSphere *b) {
    float dx = a->cx - b->cx;
    float dy = a->cy - b->cy;
    float dz = a->cz - b->cz;
    float dist2 = dx*dx + dy*dy + dz*dz;
    float r_sum = a->radius + b->radius;
    return dist2 < r_sum * r_sum;
}

// 自碰撞检测(检查非相邻连杆)
int check_self_collision(const JointAngles *q) {
    BoundingSphere spheres[4];
    compute_link_spheres(q, spheres);

    // 检查连杆1和连杆3(非相邻)
    if (spheres_intersect(&spheres[0], &spheres[2])) return 1;
    // 检查连杆1和连杆4
    if (spheres_intersect(&spheres[0], &spheres[3])) return 1;
    // 检查连杆2和连杆4
    if (spheres_intersect(&spheres[1], &spheres[3])) return 1;

    // 检查末端是否低于地面(z < 0)
    EndEffectorPose pose = forward_kinematics(q);
    if (pose.z < 0.0f) return 1;

    return 0;  // 无碰撞
}

完整项目实战:视觉引导抓取系统

系统架构

┌─────────────────────────────────────────────────────────────┐
│                    视觉引导抓取系统                           │
│                                                             │
│  ┌──────────────┐    UART/SPI    ┌──────────────────────┐  │
│  │  ESP32-CAM   │ ─────────────→ │   STM32F103C8T6      │  │
│  │  OV2640摄像头 │  目标坐标(x,y) │   主控制器           │  │
│  │  颜色识别     │               │                      │  │
│  │  质心计算     │               │  ┌────────────────┐  │  │
│  └──────────────┘               │  │  运动学引擎     │  │  │
│                                 │  │  FK / IK       │  │  │
│  ┌──────────────┐               │  │  轨迹规划      │  │  │
│  │  上位机(可选) │ ←── UART ───  │  └────────────────┘  │  │
│  │  Python调试  │               │                      │  │
│  └──────────────┘               │  TIM2/TIM3 PWM×5    │  │
│                                 └──────────┬───────────┘  │
│                                            │               │
│                              ┌─────────────▼────────────┐  │
│                              │  5V/5A 舵机电源           │  │
│                              │  J1(MG996R) J2(MG996R)   │  │
│                              │  J3(MG996R) J4(SG90)     │  │
│                              │  夹爪(SG90)               │  │
│                              └──────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

ESP32-CAM 颜色识别模块

// ESP32-CAM 颜色识别代码(Arduino框架)
// 识别红色物体质心,通过UART发送坐标给STM32
// 依赖:esp32-camera库(ESP-IDF内置)

#include "esp_camera.h"
#include "Arduino.h"

// AI-Thinker ESP32-CAM引脚定义
#define PWDN_GPIO_NUM  32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM   0
#define SIOD_GPIO_NUM  26
#define SIOC_GPIO_NUM  27
#define Y9_GPIO_NUM    35
#define Y8_GPIO_NUM    34
#define Y7_GPIO_NUM    39
#define Y6_GPIO_NUM    36
#define Y5_GPIO_NUM    21
#define Y4_GPIO_NUM    19
#define Y3_GPIO_NUM    18
#define Y2_GPIO_NUM     5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  23
#define PCLK_GPIO_NUM  22

// 颜色阈值(HSV空间,红色)
#define H_MIN  0
#define H_MAX  15
#define S_MIN  100
#define V_MIN  80

// RGB转HSV(简化版)
void rgb_to_hsv(uint8_t r, uint8_t g, uint8_t b,
                float *h, float *s, float *v) {
    float rf = r / 255.0f, gf = g / 255.0f, bf = b / 255.0f;
    float cmax = fmaxf(rf, fmaxf(gf, bf));
    float cmin = fminf(rf, fminf(gf, bf));
    float delta = cmax - cmin;

    *v = cmax;
    *s = (cmax > 0.001f) ? (delta / cmax) : 0.0f;

    if (delta < 0.001f) { *h = 0; return; }
    if (cmax == rf)      *h = 60.0f * fmodf((gf-bf)/delta, 6.0f);
    else if (cmax == gf) *h = 60.0f * ((bf-rf)/delta + 2.0f);
    else                 *h = 60.0f * ((rf-gf)/delta + 4.0f);
    if (*h < 0) *h += 360.0f;
}

// 在RGB565帧中查找红色物体质心
// 返回1找到目标,cx/cy为归一化坐标[-1,1]
int find_red_object(camera_fb_t *fb, float *cx, float *cy) {
    uint32_t sum_x = 0, sum_y = 0, count = 0;
    uint16_t w = fb->width, h = fb->height;
    uint16_t *pixels = (uint16_t *)fb->buf;

    for (uint16_t y = 0; y < h; y++) {
        for (uint16_t x = 0; x < w; x++) {
            uint16_t px = pixels[y * w + x];
            // RGB565解包
            uint8_t r = (px >> 11) << 3;
            uint8_t g = ((px >> 5) & 0x3F) << 2;
            uint8_t b = (px & 0x1F) << 3;

            float hh, ss, vv;
            rgb_to_hsv(r, g, b, &hh, &ss, &vv);

            if ((hh <= H_MAX || hh >= 345) &&  // 红色(含环绕)
                ss * 255 >= S_MIN &&
                vv * 255 >= V_MIN) {
                sum_x += x;
                sum_y += y;
                count++;
            }
        }
    }

    if (count < 100) return 0;  // 目标太小,忽略

    // 归一化到[-1, 1]
    *cx = ((float)sum_x / count - w * 0.5f) / (w * 0.5f);
    *cy = ((float)sum_y / count - h * 0.5f) / (h * 0.5f);
    return 1;
}

void setup() {
    Serial.begin(115200);  // 与STM32通信

    camera_config_t config = {};
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer   = LEDC_TIMER_0;
    config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM;
    config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM;
    config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM;
    config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM;
    config.pin_xclk  = XCLK_GPIO_NUM;
    config.pin_pclk  = PCLK_GPIO_NUM;
    config.pin_vsync = VSYNC_GPIO_NUM;
    config.pin_href  = HREF_GPIO_NUM;
    config.pin_sscb_sda = SIOD_GPIO_NUM;
    config.pin_sscb_scl = SIOC_GPIO_NUM;
    config.pin_pwdn  = PWDN_GPIO_NUM;
    config.pin_reset = RESET_GPIO_NUM;
    config.xclk_freq_hz = 20000000;
    config.pixel_format = PIXFORMAT_RGB565;
    config.frame_size   = FRAMESIZE_QVGA;  // 320x240
    config.fb_count     = 1;

    esp_camera_init(&config);
}

void loop() {
    camera_fb_t *fb = esp_camera_fb_get();
    if (!fb) return;

    float cx, cy;
    if (find_red_object(fb, &cx, &cy)) {
        // 发送格式:"V cx cy\n"(V=Vision)
        Serial.printf("V %.3f %.3f\n", cx, cy);
    }
    esp_camera_fb_return(fb);
    delay(50);  // 20fps
}

STM32主控:视觉引导抓取状态机

// STM32F103 主控代码
// 接收ESP32-CAM视觉坐标,执行视觉引导抓取

#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdio.h>
#include <math.h>

// ── 视觉坐标到世界坐标的映射 ─────────────────────────────────
// 相机安装在机械臂正上方,俯视工作台
// 工作台范围:x: 100~200mm, y: -80~80mm, z: 0mm(桌面)
#define TABLE_Z       5.0f   // 桌面高度(mm),略高于0避免碰撞
#define CAMERA_X_MIN  100.0f
#define CAMERA_X_MAX  200.0f
#define CAMERA_Y_MIN  -80.0f
#define CAMERA_Y_MAX   80.0f

void vision_to_world(float cx, float cy,
                     float *wx, float *wy) {
    // cx, cy 范围 [-1, 1],映射到工作台坐标
    *wx = CAMERA_X_MIN + (cx + 1.0f) * 0.5f
          * (CAMERA_X_MAX - CAMERA_X_MIN);
    *wy = CAMERA_Y_MIN + (cy + 1.0f) * 0.5f
          * (CAMERA_Y_MAX - CAMERA_Y_MIN);
}

// ── 抓取状态机 ────────────────────────────────────────────────
typedef enum {
    STATE_IDLE = 0,
    STATE_HOME,
    STATE_SEARCH,       // 等待视觉目标
    STATE_APPROACH,     // 移动到目标上方
    STATE_DESCEND,      // 下降到抓取高度
    STATE_GRASP,        // 闭合夹爪
    STATE_LIFT,         // 提起物体
    STATE_PLACE,        // 移动到放置位置
    STATE_RELEASE,      // 张开夹爪
    STATE_DONE,
} ArmState;

static ArmState g_state = STATE_IDLE;
static float g_target_x, g_target_y;  // 视觉检测到的目标坐标
static int   g_target_valid = 0;

// 放置位置(固定)
#define PLACE_X  -120.0f
#define PLACE_Y   80.0f
#define PLACE_Z   30.0f

// 夹爪控制
void gripper_close(void) {
    set_servo_pulse(4, angle_to_pulse(4, 50));  // 50°=闭合
    HAL_Delay(600);
}
void gripper_open(void) {
    set_servo_pulse(4, angle_to_pulse(4, 0));   // 0°=张开
    HAL_Delay(400);
}

// 主状态机(在主循环中调用)
void arm_state_machine(void) {
    static JointAngles home = {0, 60, -60, 0};

    switch (g_state) {
    case STATE_IDLE:
        g_state = STATE_HOME;
        break;

    case STATE_HOME:
        gripper_open();
        arm_move_joints(&home, 1200);
        g_state = STATE_SEARCH;
        break;

    case STATE_SEARCH:
        // 等待视觉模块检测到目标
        if (g_target_valid) {
            g_target_valid = 0;
            g_state = STATE_APPROACH;
        }
        break;

    case STATE_APPROACH: {
        // 移动到目标正上方80mm处
        float wx, wy;
        vision_to_world(g_target_x, g_target_y, &wx, &wy);
        int ret = arm_move_linear(wx, wy, TABLE_Z + 80.0f, -70.0f, 1000);
        if (ret == 0) {
            g_state = STATE_DESCEND;
        } else {
            // IK失败,回到搜索状态
            g_state = STATE_HOME;
        }
        break;
    }

    case STATE_DESCEND: {
        // 下降到抓取高度(桌面+15mm)
        EndEffectorPose cur = forward_kinematics(&g_current_joints);
        int ret = arm_move_linear(cur.x, cur.y,
                                   TABLE_Z + 15.0f, -80.0f, 600);
        g_state = (ret == 0) ? STATE_GRASP : STATE_HOME;
        break;
    }

    case STATE_GRASP:
        gripper_close();
        g_state = STATE_LIFT;
        break;

    case STATE_LIFT: {
        EndEffectorPose cur = forward_kinematics(&g_current_joints);
        arm_move_linear(cur.x, cur.y, TABLE_Z + 80.0f, -70.0f, 600);
        g_state = STATE_PLACE;
        break;
    }

    case STATE_PLACE:
        arm_move_linear(PLACE_X, PLACE_Y, PLACE_Z + 60.0f, -70.0f, 1200);
        arm_move_linear(PLACE_X, PLACE_Y, PLACE_Z, -80.0f, 600);
        g_state = STATE_RELEASE;
        break;

    case STATE_RELEASE:
        gripper_open();
        g_state = STATE_DONE;
        break;

    case STATE_DONE:
        arm_move_joints(&home, 1000);
        HAL_Delay(500);
        g_state = STATE_SEARCH;  // 继续下一次抓取
        break;
    }
}

// UART接收回调(解析ESP32-CAM发来的视觉坐标)
// 格式:"V cx cy\n"
void parse_vision_uart(const char *line) {
    float cx, cy;
    if (sscanf(line, "V %f %f", &cx, &cy) == 2) {
        g_target_x = cx;
        g_target_y = cy;
        g_target_valid = 1;
    }
}

// 主循环
int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_TIM2_Init();   // PWM输出
    MX_TIM3_Init();
    MX_USART1_UART_Init();  // 与ESP32-CAM通信

    // 启动PWM
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

    g_state = STATE_IDLE;

    while (1) {
        // 处理UART接收(非阻塞,使用DMA+空闲中断)
        process_uart_rx();
        // 执行状态机
        arm_state_machine();
    }
}

性能调优

控制周期优化

// 使用TIM6定时器中断实现精确10ms控制周期
// 避免HAL_Delay阻塞导致的UART数据丢失

volatile uint8_t g_control_tick = 0;

void TIM6_IRQHandler(void) {
    if (__HAL_TIM_GET_FLAG(&htim6, TIM_FLAG_UPDATE)) {
        __HAL_TIM_CLEAR_FLAG(&htim6, TIM_FLAG_UPDATE);
        g_control_tick = 1;
    }
}

// 主循环改为事件驱动
while (1) {
    process_uart_rx();  // 始终处理UART
    if (g_control_tick) {
        g_control_tick = 0;
        arm_state_machine();  // 每10ms执行一次
    }
}

舵机平滑度优化

问题:舵机在目标位置附近抖动(死区效应)
原因:MG996R死区约5μs,对应约0.45°
解决:
  1. 到达目标后停止发送PWM(舵机自然保持)
  2. 或使用数字舵机(Dynamixel)替代模拟舵机
  3. 增加到位判断:|current - target| < 1°时停止插补

// 到位检测
int joints_reached(const JointAngles *target, float threshold_deg) {
    return (fabsf(g_current_joints.j1 - target->j1) < threshold_deg &&
            fabsf(g_current_joints.j2 - target->j2) < threshold_deg &&
            fabsf(g_current_joints.j3 - target->j3) < threshold_deg &&
            fabsf(g_current_joints.j4 - target->j4) < threshold_deg);
}

常见问题与调试

问题1:舵机抖动(Servo Jitter)

现象:舵机在目标位置附近持续小幅振动,无法稳定停止。

原因分析

原因1:PWM信号干扰
  - 舵机电源与MCU共地,大电流引起地线噪声
  - PWM信号线过长,感应干扰
  诊断:用示波器测量PWM信号,观察是否有毛刺

原因2:控制算法问题
  - 插补步长过小,每步变化量小于舵机死区
  - 目标位置在两个PWM值之间反复切换
  诊断:打印每步的pulse_us值,观察是否在两值间跳变

原因3:机械问题
  - 舵机齿轮磨损,间隙过大
  - 连杆安装不牢固,产生共振
  诊断:手动转动舵机输出轴,感受间隙大小

解决方案

// 方案1:PWM滤波(低通滤波器)
static uint16_t pulse_filtered[5] = {1500, 1500, 1500, 1500, 1500};

void set_servo_pulse_filtered(uint8_t ch, uint16_t target) {
    // 一阶低通滤波:α=0.3(较慢但平滑)
    pulse_filtered[ch] = (uint16_t)(
        0.7f * pulse_filtered[ch] + 0.3f * target);
    set_servo_pulse(ch, pulse_filtered[ch]);
}

// 方案2:死区补偿(到位后停止更新PWM)
#define PULSE_DEADBAND  3  // us,小于此值不更新

void set_servo_pulse_deadband(uint8_t ch, uint16_t target) {
    static uint16_t last_pulse[5] = {1500,1500,1500,1500,1500};
    if (abs((int)target - (int)last_pulse[ch]) > PULSE_DEADBAND) {
        set_servo_pulse(ch, target);
        last_pulse[ch] = target;
    }
}

// 方案3:硬件滤波
// 在PWM信号线上串联100Ω电阻 + 并联100nF电容到地
// 截止频率 fc = 1/(2π×100×100e-9) ≈ 16kHz
// 可滤除高频干扰,不影响50Hz PWM信号

问题2:逆运动学无解(IK No-Solution)

现象:调用IK函数返回-1(无效解),机械臂无法到达目标位置。

原因分析与处理

// 完整的IK失败处理策略
typedef enum {
    IK_OK = 0,
    IK_OUT_OF_REACH,    // 目标超出工作空间
    IK_JOINT_LIMIT,     // 解存在但超出关节限位
    IK_SINGULARITY,     // 目标在奇异点附近
    IK_COLLISION,       // 解会导致碰撞
} IKStatus;

IKStatus ik_with_diagnosis(float x, float y, float z,
                            float end_pitch,
                            JointAngles *result) {
    // 1. 检查奇异性
    SingularityType sing = check_singularity(x, y, z, end_pitch);
    if (sing != SING_NONE) {
        // 尝试微小偏移
        float dx = (sing == SING_SHOULDER) ? 5.0f : 0.0f;
        IKResult ik = ik_best_solution(x + dx, y, z,
                                        end_pitch, &g_current_joints);
        if (ik.valid) {
            *result = ik.q;
            return IK_OK;
        }
        return IK_SINGULARITY;
    }

    // 2. 尝试求解
    IKResult ik = ik_best_solution(x, y, z, end_pitch,
                                    &g_current_joints);
    if (!ik.valid) {
        // 判断是超出范围还是关节限位
        float r = sqrtf(x*x + y*y);
        float ep = end_pitch * DEG2RAD;
        float wx = r - L3 * cosf(ep);
        float wz = z - L0 - L3 * sinf(ep);
        float d  = sqrtf(wx*wx + wz*wz);
        if (d > L1 + L2 + 5.0f) return IK_OUT_OF_REACH;
        return IK_JOINT_LIMIT;
    }

    // 3. 碰撞检测
    if (check_self_collision(&ik.q)) {
        return IK_COLLISION;
    }

    *result = ik.q;
    return IK_OK;
}

// 上层调用示例
void safe_move_to(float x, float y, float z, float pitch) {
    JointAngles target;
    IKStatus status = ik_with_diagnosis(x, y, z, pitch, &target);

    switch (status) {
    case IK_OK:
        arm_move_joints(&target, 1000);
        break;
    case IK_OUT_OF_REACH:
        // 记录日志,通知上位机
        uart_send("ERR: target out of reach\n");
        break;
    case IK_JOINT_LIMIT:
        // 尝试调整末端姿态角
        for (float dp = -10.0f; dp <= 10.0f; dp += 2.0f) {
            if (ik_with_diagnosis(x, y, z, pitch+dp,
                                   &target) == IK_OK) {
                arm_move_joints(&target, 1000);
                return;
            }
        }
        uart_send("ERR: joint limit, no solution\n");
        break;
    case IK_SINGULARITY:
        uart_send("WARN: near singularity, offset applied\n");
        break;
    case IK_COLLISION:
        uart_send("ERR: collision detected\n");
        break;
    }
}

问题3:机械间隙(Mechanical Backlash)

现象:机械臂在改变运动方向时,末端位置有明显偏差(通常2~5mm)。

原因:舵机齿轮组存在间隙,换向时需要先消除间隙才能传递力矩。

间隙影响分析:
  MG996R典型间隙:约1°
  J2关节(大臂):1°间隙 → 末端位置误差 ≈ L1×sin(1°) ≈ 1.8mm
  J3关节(小臂):1°间隙 → 末端位置误差 ≈ L2×sin(1°) ≈ 1.7mm
  累积误差:可达3~5mm

补偿方法:
  1. 预加载(Preload):始终从同一方向接近目标
  2. 超调补偿:超过目标一定量后反向回来
  3. 使用无间隙舵机(Dynamixel系列)
// 间隙补偿:始终从"正方向"接近目标
// 如果需要减小关节角度,先减小超过backlash量,再增加到目标
#define BACKLASH_DEG  1.5f  // 估计间隙量(度)

void arm_move_joints_backlash_comp(const JointAngles *target,
                                    uint32_t duration_ms) {
    JointAngles pre_target = *target;
    int need_preload = 0;

    // 检查哪些关节需要反向运动
    if (target->j2 < g_current_joints.j2 - 0.5f) {
        pre_target.j2 = target->j2 - BACKLASH_DEG;
        need_preload = 1;
    }
    if (target->j3 < g_current_joints.j3 - 0.5f) {
        pre_target.j3 = target->j3 - BACKLASH_DEG;
        need_preload = 1;
    }

    if (need_preload) {
        // 先运动到预加载位置
        arm_move_joints(&pre_target, duration_ms * 3 / 4);
        HAL_Delay(50);
    }
    // 再运动到目标位置
    arm_move_joints(target, duration_ms / 4);
}

问题4:舵机过热保护

// 舵机温度监控(通过电流间接估算)
// MG996R额定电流500mA,堵转电流2.5A
// 使用ACS712-5A电流传感器监测总电流

#define CURRENT_OVERLOAD_MA  3000  // 3A触发保护
#define OVERLOAD_DURATION_MS  500  // 持续500ms才保护

static uint32_t overload_start = 0;

void check_servo_overload(void) {
    // ACS712输出:2.5V=0A,灵敏度185mV/A(5A版本)
    uint16_t adc = HAL_ADC_GetValue(&hadc1);
    float voltage = adc * 3.3f / 4096.0f;
    float current_ma = (voltage - 2.5f) / 0.185f * 1000.0f;

    if (current_ma > CURRENT_OVERLOAD_MA) {
        if (overload_start == 0) {
            overload_start = HAL_GetTick();
        } else if (HAL_GetTick() - overload_start > OVERLOAD_DURATION_MS) {
            // 触发保护:停止所有舵机
            for (int i = 0; i < 5; i++) {
                set_servo_pulse(i, 0);  // 0=关闭PWM
            }
            uart_send("ERR: servo overload protection triggered\n");
            g_state = STATE_IDLE;
        }
    } else {
        overload_start = 0;
    }
}

问题5:视觉坐标抖动

现象:ESP32-CAM检测到的目标坐标在每帧之间跳动,导致机械臂频繁微调。

// 视觉坐标滤波(卡尔曼滤波简化版)
typedef struct {
    float x, y;      // 状态估计
    float p;         // 误差协方差
    float q;         // 过程噪声
    float r;         // 测量噪声
} KalmanFilter1D;

void kalman_init(KalmanFilter1D *kf, float q, float r) {
    kf->x = 0; kf->y = 0;
    kf->p = 1.0f;
    kf->q = q;  // 典型值:0.01(目标运动慢)
    kf->r = r;  // 典型值:0.1(摄像头噪声)
}

float kalman_update(KalmanFilter1D *kf, float measurement) {
    // 预测
    kf->p += kf->q;
    // 更新
    float k = kf->p / (kf->p + kf->r);  // 卡尔曼增益
    kf->x += k * (measurement - kf->x);
    kf->p *= (1.0f - k);
    return kf->x;
}

// 使用示例
static KalmanFilter1D kf_x, kf_y;

void init_vision_filter(void) {
    kalman_init(&kf_x, 0.01f, 0.1f);
    kalman_init(&kf_y, 0.01f, 0.1f);
}

void parse_vision_uart_filtered(const char *line) {
    float cx, cy;
    if (sscanf(line, "V %f %f", &cx, &cy) == 2) {
        g_target_x = kalman_update(&kf_x, cx);
        g_target_y = kalman_update(&kf_y, cy);
        g_target_valid = 1;
    }
}

测试与验证

运动学正确性验证

// 验证FK和IK互为逆运算(round-trip测试)
// 在STM32上运行,通过UART输出结果
void test_kinematics_roundtrip(void) {
    // 测试用例:已知关节角度
    JointAngles test_cases[] = {
        {0,   60, -60,  0},   // 正前方伸展
        {45,  90, -90,  0},   // 右侧45°
        {-30, 45, -45, 10},   // 左侧30°,末端上翘
        {0,   30, -30, -20},  // 低位抓取
    };

    int n = sizeof(test_cases) / sizeof(test_cases[0]);
    int pass = 0;

    for (int i = 0; i < n; i++) {
        JointAngles *q_orig = &test_cases[i];

        // FK:关节角 → 末端位置
        EndEffectorPose pose = forward_kinematics(q_orig);

        // IK:末端位置 → 关节角
        float end_pitch = q_orig->j2 + q_orig->j3 + q_orig->j4;
        IKResult ik = ik_best_solution(pose.x, pose.y, pose.z,
                                        end_pitch, q_orig);

        if (!ik.valid) {
            uart_printf("FAIL case %d: IK returned invalid\n", i);
            continue;
        }

        // 检查误差(允许0.5°误差)
        float err = fabsf(ik.q.j1 - q_orig->j1)
                  + fabsf(ik.q.j2 - q_orig->j2)
                  + fabsf(ik.q.j3 - q_orig->j3)
                  + fabsf(ik.q.j4 - q_orig->j4);

        if (err < 2.0f) {  // 总误差 < 2°
            uart_printf("PASS case %d: err=%.3f deg\n", i, err);
            pass++;
        } else {
            uart_printf("FAIL case %d: err=%.3f deg\n", i, err);
            uart_printf("  orig: j1=%.1f j2=%.1f j3=%.1f j4=%.1f\n",
                q_orig->j1, q_orig->j2, q_orig->j3, q_orig->j4);
            uart_printf("  ik:   j1=%.1f j2=%.1f j3=%.1f j4=%.1f\n",
                ik.q.j1, ik.q.j2, ik.q.j3, ik.q.j4);
        }
    }
    uart_printf("Kinematics test: %d/%d passed\n", pass, n);
}

工作空间边界测试

// 测试工作空间边界附近的IK行为
void test_workspace_boundary(void) {
    struct { float x, y, z, pitch; const char *desc; } cases[] = {
        {270, 0, 80, -30, "最大伸展"},
        {50,  0, 80, -30, "最小收缩"},
        {150, 0, 200, 0,  "最高点"},
        {150, 0, -10, -80,"低于桌面(应失败)"},
        {300, 0, 80, -30, "超出范围(应失败)"},
    };

    for (int i = 0; i < 5; i++) {
        JointAngles q;
        IKStatus s = ik_with_diagnosis(cases[i].x, cases[i].y,
                                        cases[i].z, cases[i].pitch, &q);
        uart_printf("[%s] status=%d\n", cases[i].desc, s);
    }
}

舵机标定验证程序

// 交互式标定程序(通过UART命令控制)
// 命令格式:
//   "J1 45\n"  → 将J1移动到45°
//   "CAL 1\n"  → 进入J1标定模式
//   "ZERO\n"   → 设置当前位置为零位

void calibration_mode(void) {
    char cmd[32];
    uart_send("进入标定模式。命令:J<n> <angle> | ZERO | EXIT\n");

    while (1) {
        if (uart_readline(cmd, sizeof(cmd))) {
            int joint; float angle;
            if (sscanf(cmd, "J%d %f", &joint, &angle) == 2) {
                if (joint >= 1 && joint <= 4) {
                    uint16_t pulse = angle_to_pulse(joint-1, angle);
                    set_servo_pulse(joint-1, pulse);
                    uart_printf("J%d → %.1f° (pulse=%d)\n",
                                joint, angle, pulse);
                }
            } else if (strncmp(cmd, "ZERO", 4) == 0) {
                // 将当前PWM值记录为零位
                uart_send("零位已记录\n");
            } else if (strncmp(cmd, "EXIT", 4) == 0) {
                break;
            }
        }
    }
}

延伸阅读

外部参考资源

参考资料

  1. Craig, J.J. (2005). Introduction to Robotics: Mechanics and Control (3rd ed.). Pearson.
  2. Lynch, K.M. & Park, F.C. (2017). Modern Robotics: Mechanics, Planning, and Control. Cambridge University Press.
  3. 熊有伦 (2011). 《机器人技术基础》. 华中科技大学出版社.
  4. Spong, M.W., Hutchinson, S. & Vidyasagar, M. (2006). Robot Modeling and Control. Wiley.
  5. MG996R舵机数据手册 — Tower Pro
  6. STM32F103参考手册 (RM0008) — STMicroelectronics
  7. ESP32-CAM技术规格书 — AI-Thinker
  8. OV2640摄像头数据手册 — OmniVision Technologies