python的机器人基础原理工业场景模拟第一百三十七篇:机械臂仿真,记录PD控制下期望角度,实际角度,输出误差随时间变化曲线。
《Python机器人基础原理》第一百三十七篇:机械臂仿真——记录PD控制下期望角度、实际角度与输出误差时程曲线
声明:本篇全部为课程离线仿真代码,不能直接用于真实机器人控制器,规避工程误导风险。
1. 实际工业应用场景
周一早班,3C电子装配线。一台2R竖直平面机械臂要把末端吸嘴从待料位移动到PCB焊盘上方,完成元件贴装。任务要求末端沿平滑轨迹运动,到位后姿态稳定,位置误差不超过±0.5 mm。
"第12个循环开始抖动。"上位机弹出关节1的实际角度曲线,小赵盯着屏幕:"期望角度是一条平滑的三次多项式,起步段速度从零开始、到位时速度也回到零,但实际控制力矩在起步段末端有明显跳变,角度误差曲线在0.5 s处出现一个尖峰。"
他调出PD参数:Kp=[120, 80] N·m/rad,Kd=[12, 8] N·m·s/rad。连杆1质量3.0 kg、长度0.40 m、转动惯量0.16 kg·m²、质心0.20 m;连杆2质量1.5 kg、长度0.35 m、转动惯量0.06 kg·m²、质心0.18 m。重力9.81 m/s²。轨迹时长2.0 s,起步段0.5 s,保持段1.5 s。
"如果控制器里没有重力前馈,到位后比例项要一直扛着自重,稳态误差会很大。现在加上前馈,看看期望、实际、误差三组曲线到底长什么样。"小赵打开仿真脚本,"先跑一个带重力前馈的,再跑一个不带前馈的,对比稳态误差。"
⚠️ 声明:本代码仅用于课程离线仿真,运行在普通PC上。不包含任何真实机器人控制器的实时通信栈或硬件驱动。不可直接用于产线设备。
2. 痛点分析
课程模块 本篇对应的工程痛点
第4章 机器人动力学建模 → 刚体动力学 连杆质量、惯量、重力耦合形成非线性扰动,期望轨迹与实际运动之间存在动态偏差
第5章 机器人关节驱动与控制 → PD伺服 比例增益决定响应速度,微分增益抑制超调,重力前馈决定稳态精度
第6章 轨迹规划与控制 → 轨迹平滑性 起步段和保持段衔接处速度不连续会产生力矩阶跃,必须用高阶多项式保证速度连续
本篇代码解决 L2 离线仿真校核层。输入:2R连杆参数、PD增益、三次多项式轨迹。处理:RK4积分真实动力学,对比有/无重力前馈两种工况。输出:4张分析图表 + 单元测试。
不覆盖:摩擦力矩、关节柔性、编码器量化噪声、通信延迟。
3. 核心逻辑讲解
物理直觉
PD控制就像在两个关节上各装了一根"虚拟弹簧+阻尼器"。期望角度是当前位置,实际角度是弹簧另一端,偏差越大,弹簧拉力(比例项)越强;实际角度运动越快,阻尼力(微分项)越大。控制器每一时刻计算"弹簧+阻尼"的总拉力,再加上可选的重力前馈抵消连杆自重,这个合力矩作用到真实动力学上,推动连杆运动。
起步瞬间,期望角度开始平滑变化,偏差逐渐增大,比例项输出一个逐渐增强的力矩。接近目标时,偏差变小,微分项开始"刹车"。到位后,如果开了重力前馈,前馈力矩刚好平衡自重,PD只需补偿微小残余,稳态误差趋近于零;如果没开前馈,自重完全靠比例项扛,稳态误差等于自重除以Kp,是一个固定的常值。
轨迹平滑性很关键:如果起步段末端速度不为零,进入保持段时速度突然归零,期望速度跳变会让微分项瞬间输出一个巨大的"刹车"力矩,角度误差曲线上会出现一个尖峰。三次多项式同时约束起点和终点的速度都为零,保证速度连续,避免这个问题。
业务逻辑 → 代码映射
输入:2R连杆参数 + PD增益 + 三次多项式轨迹
↓
【步骤1:建立动力学模型】
质量矩阵M(q)、科氏力C(q,qd)、重力G(q)
正向动力学:qdd = M⁻¹·(τ - C - G)
↓
【步骤2:轨迹生成(三次多项式)】
起步段:q(t) = a·t³ + b·t²,满足q(0)=q_start, qd(0)=0, q(T)=q_target, qd(T)=0
保持段:q_des恒等于q_target, qd_des=0
↓
【步骤3:RK4闭环仿真(双工况对比)】
工况A:τ = Kp·e + Kd·ē + G(q_des) (带重力前馈)
工况B:τ = Kp·e + Kd·ē (无重力前馈)
每步记录 q_des, q_actual, e = q_des - q_actual
↓
【步骤4:可视化】
4张图:期望vs实际角度、角度误差、角速度、控制力矩
关键公式文字翻译
三次多项式轨迹:q(t) = a·t³ + b·t² + c·t + d。四个系数由四个边界条件确定:起点位置、起点速度(零)、终点位置、终点速度(零)。这保证位置、速度都连续,加速度在衔接处可能不连续,但力矩不会出现阶跃。
PD控制律:τ = Kp·(q_des - q) + Kd·(qd_des - qd) + G_ff。Kp乘以角度偏差,产生与偏差方向相反的恢复力矩;Kd乘以角速度偏差,提供阻尼;G_ff是重力前馈项,可选。
稳态误差(无前馈):e_ss ≈ G(q_target) / Kp。自重产生的重力矩完全由比例项平衡,偏差量等于重力矩除以比例增益。本篇参数下关节1稳态误差约9.0/120 ≈ 0.075 rad ≈ 4.3°,这是一个可观的偏差。
4. OOP代码实现
技术栈
限定:numpy, matplotlib。不引入pybullet、roboticstoolbox等第三方重型机器人库。
项目结构树
pd_joint_tracking_sim/
├── run_sim.py # 主入口
├── requirements.txt
├── robot/
│ ├── __init__.py
│ ├── models.py # dataclass数据模型
│ ├── dynamics.py # 2R刚体动力学
│ ├── trajectory.py # 三次多项式轨迹
│ ├── controller.py # PD(可选重力前馈)
│ ├── simulator.py # RK4仿真器
│ └── visualize.py # 4张图表
├── tests/
│ ├── __init__.py
│ └── test_integration.py # 3类10个测试
├── results/ # 4张图表
└── README.md
核心源码
"robot/models.py"
"""
Data models for 2R planar manipulator PD tracking simulation.
# OFFLINE SIMULATION ONLY — not for production controllers.
"""
import numpy as np
from dataclasses import dataclass
@dataclass
class Link2R:
"""2R竖直平面臂单连杆参数。"""
name: str
mass: float # kg
length: float # m
inertia: float # kg·m^2
com_offset: float # m(质心距前一关节)
def __post_init__(self):
if self.mass <= 0:
raise ValueError(f"{self.name}: mass must be positive")
if self.length <= 0:
raise ValueError(f"{self.name}: length must be positive")
if self.inertia <= 0:
raise ValueError(f"{self.name}: inertia must be positive")
if not 0 < self.com_offset <= self.length:
raise ValueError(
f"{self.name}: com_offset must be in (0, {self.length}]")
@dataclass
class PDGains2R:
"""2R PD增益。"""
Kp: np.ndarray
Kd: np.ndarray
def __post_init__(self):
self.Kp = np.asarray(self.Kp, dtype=float)
self.Kd = np.asarray(self.Kd, dtype=float)
if self.Kp.shape != (2,):
raise ValueError(f"Kp must be length-2, got {self.Kp.shape}")
if self.Kd.shape != (2,):
raise ValueError(f"Kd must be length-2, got {self.Kd.shape}")
if np.any(self.Kp <= 0) or np.any(self.Kd < 0):
raise ValueError("Kp must be positive, Kd non-negative")
@dataclass
class SimConfig2R:
"""2R仿真全局配置。"""
links: list
gravity: float = 9.81
dt_phys: float = 0.0001
tau_limit: float = 50.0
def __post_init__(self):
if len(self.links) != 2:
raise ValueError(
f"SimConfig2R requires exactly 2 links, got {len(self.links)}")
if self.dt_phys <= 0:
raise ValueError("dt_phys must be positive")
if self.tau_limit <= 0:
raise ValueError("tau_limit must be positive")
"robot/dynamics.py"
"""
2R竖直平面刚体动力学。
# OFFLINE SIMULATION ONLY — not for production controllers.
"""
import warnings
import numpy as np
import math
from robot.models import SimConfig2R
class Dynamics2R:
"""
2R竖直平面臂动力学:M(q)·qdd + C(q,qd) + G(q) = τ
连杆参数全部内置,自包含可复现。
"""
def __init__(self, config: SimConfig2R):
self.cfg = config
self.g = config.gravity
self.l1 = config.links[0].length
self.l2 = config.links[1].length
self.m1 = config.links[0].mass
self.m2 = config.links[1].mass
self.r1 = config.links[0].com_offset
self.r2 = config.links[1].com_offset
self.I1 = config.links[0].inertia
self.I2 = config.links[1].inertia
def mass_matrix(self, q):
"""2×2质量矩阵M(q)。对2R竖直平面臂,M始终正定,无需奇异检测。"""
q = np.asarray(q, dtype=float)
c2 = math.cos(q[1])
M11 = (self.I1 + self.m1 * self.r1**2
+ self.m2 * (self.l1**2 + self.r2**2 + 2 * self.l1 * self.r2 * c2))
M12 = self.m2 * self.r2 * (self.r2 + self.l1 * c2)
M22 = self.I2 + self.m2 * self.r2**2
return np.array([[M11, M12],
[M12, M22]], dtype=float)
def coriolis_vector(self, q, qd):
"""科氏力/离心力向量C(q,qd)。"""
q = np.asarray(q, dtype=float)
qd = np.asarray(qd, dtype=float)
s2 = math.sin(q[1])
# h = m2·l1·r2,来自拉格朗日方程的交叉项系数
h = self.m2 * self.l1 * self.r2
C1 = h * s2 * qd[1] * (2 * qd[0] + qd[1])
C2 = -h * s2 * qd[0]**2
return np.array([C1, C2], dtype=float)
def gravity_vector(self, q):
"""重力向量G(q)。q=0时连杆水平向右,重力矩为零。"""
q = np.asarray(q, dtype=float)
c1 = math.cos(q[0])
c12 = math.cos(q[0] + q[1])
G1 = (self.m1 * self.g * self.r1 * c1
+ self.m2 * self.g * (self.l1 * c1 + self.r2 * c12))
G2 = self.m2 * self.g * self.r2 * c12
return np.array([G1, G2], dtype=float)
def forward_dynamics(self, q, qd, tau):
"""
正向动力学:qdd = M⁻¹·(τ - C - G)
使用np.linalg.solve避免显式求逆,数值更稳定。
"""
M = self.mass_matrix(q)
C = self.coriolis_vector(q, qd)
G = self.gravity_vector(q)
rhs = np.asarray(tau, dtype=float) - C - G
# 2R臂M(q)始终正定,det>0,无需额外奇异检测
qdd = np.linalg.solve(M, rhs)
return qdd
"robot/trajectory.py"
"""
三次多项式轨迹生成器:起步段(位置、速度连续)+ 保持段。
# OFFLINE SIMULATION ONLY — not for production controllers.
"""
import numpy as np
import math
from robot.models import SimConfig2R
class CubicTrajectory2R:
"""
关节空间三次多项式轨迹:
q(t) = a·t³ + b·t² + c·t + d
边界条件:q(0)=q_start, qd(0)=0, q(T_ramp)=q_target, qd(T_ramp)=0
起步段位置、速度均连续;保持段q_des恒等于q_target。
"""
def __init__(self, q_start, q_target, T_ramp, T_total):
self.q_start = np.asarray(q_start, dtype=float)
self.q_target = np.asarray(q_target, dtype=float)
self.T_ramp = float(T_ramp)
self.T_total = float(T_total)
if self.T_ramp <= 0 or self.T_total <= self.T_ramp:
raise ValueError("Require T_ramp > 0 and T_total > T_ramp")
# 求解三次多项式系数
# q(0) = d = q_start
# qd(0) = c = 0
# q(T) = a·T³ + b·T² + q_start = q_target
# qd(T) = 3·a·T² + 2·b·T = 0 → b = -1.5·a·T
T = self.T_ramp
delta = self.q_target - self.q_start
self.a = 2.0 * delta / (T**3) # a = 2·Δ/T³
self.b = -3.0 * delta / (T**2) # b = -3·Δ/T²
self.c = np.zeros(2) # c = 0(起点速度为零)
self.d = self.q_start.copy() # d = q_start
def get_at(self, t):
"""返回 [q_des, qd_des, qdd_des]。"""
if t <= 0:
return self.q_start.copy(), np.zeros(2), np.zeros(2)
if t >= self.T_ramp:
return self.q_target.copy(), np.zeros(2), np.zeros(2)
# 起步段内:三次多项式求值
t2 = t * t
t3 = t2 * t
q_des = self.a * t3 + self.b * t2 + self.d
qd_des = 3.0 * self.a * t2 + 2.0 * self.b * t
qdd_des = 6.0 * self.a * t + 2.0 * self.b
return q_des, qd_des, qdd_des
"robot/controller.py"
"""
PD控制器 + 可选重力前馈。
# OFFLINE SIMULATION ONLY — not for production controllers.
"""
import numpy as np
from robot.models import PDGains2R
from robot.dynamics import Dynamics2R
class PDController2R:
"""
PD控制律:
- 带前馈:τ = Kp·(q_des - q) + Kd·(qd_des - qd) + G(q_des)
- 无前馈:τ = Kp·(q_des - q) + Kd·(qd_des - qd)
重力前馈使用期望位形下的重力向量,抵消大部分自重。
"""
def __init__(self, gains: PDGains2R, dynamics: Dynamics2R,
use_gravity_ff=True):
self.Kp = gains.Kp
self.Kd = gains.Kd
self.dyn = dynamics
self.use_gravity_ff = use_gravity_ff
def compute_torque(self, q, qd, q_des, qd_des):
"""计算控制力矩。"""
e = q_des - q
ed = qd_des - qd
tau_pd = self.Kp * e + self.Kd * ed
if self.use_gravity_ff:
# 重力前馈:用期望关节角计算,避免反馈延迟
grav_ff = self.dyn.gravity_vector(q_des)
tau = tau_pd + grav_ff
else:
tau = tau_pd
return tau
"robot/simulator.py"
"""
RK4仿真器:记录PD控制下期望角度、实际角度、输出误差。
# OFFLINE SIMULATION ONLY — not for production controllers.
"""
import numpy as np
from robot.dynamics import Dynamics2R
from robot.controller import PDController2R
from robot.models import SimConfig2R
class Simulator2R:
def __init__(self, dynamics: Dynamics2R, controller: PDController2R,
trajectory, config: SimConfig2R):
self.dyn = dynamics
self.ctrl = controller
self.traj = trajectory
self.config = config
self.n_phys_steps = int(round(self.traj.T_total / config.dt_phys))
def run(self):
"""
运行仿真,返回完整时程数据。
初始状态从轨迹起点对齐,避免起步跳变。
"""
q_start, qd_start, _ = self.traj.get_at(0.0)
x = np.concatenate([q_start, qd_start])
dt = self.config.dt_phys
n = self.n_phys_steps
t_arr = np.arange(n) * dt
# 预分配历史数组
q_des_hist = np.zeros((n, 2))
q_actual_hist = np.zeros((n, 2))
qd_actual_hist = np.zeros((n, 2))
error_hist = np.zeros((n, 2)) # q_des - q_actual
tau_hist = np.zeros((n, 2))
for i in range(n):
t = t_arr[i]
# 期望轨迹
q_des, qd_des, _ = self.traj.get_at(t)
q_des_hist[i] = q_des
# 当前状态
q = x[:2]
qd = x[2:4]
# 记录
q_actual_hist[i] = q
qd_actual_hist[i] = qd
error_hist[i] = q_des - q
# 控制力矩(RK4各子步内采用零阶保持,dt=0.1ms下精度足够)
tau = self.ctrl.compute_torque(q, qd, q_des, qd_des)
tau = np.clip(tau, -self.config.tau_limit, self.config.tau_limit)
tau_hist[i] = tau
# RK4积分
def deriv(xx, tau_fixed=tau):
qdd = self.dyn.forward_dynamics(xx[:2], xx[2:4], tau_fixed)
return np.concatenate([xx[2:4], qdd])
k1 = deriv(x)
k2 = deriv(x + 0.5 * dt * k1)
k3 = deriv(x + 0.5 * dt * k2)
k4 = deriv(x + dt * k3)
x = x + (dt / 6.0) * (k1 + 2*k2 + 2*k3 + k4)
return {
't': t_arr,
'q_des': q_des_hist,
'q_actual': q_actual_hist,
'qd_actual': qd_actual_hist,
'error': error_hist,
'tau': tau_hist,
}
"robot/visualize.py"
"""
4张PD跟踪仿真可视化图表。
# OFFLINE SIMULATION ONLY — not for production controllers.
"""
import os
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans',
'Noto Sans CJK SC']
plt.rcParams['axes.unicode_minus'] = False
class Visualizer2R:
def __init__(self, results_dir="results"):
self.rd = results_dir
os.makedirs(results_dir, exist_ok=True)
def plot_angle_tracking(self, result_ff, result_noff):
"""图1:期望角度 vs 实际角度时程(双工况对比)。"""
t = result_ff['t']
q_des = result_ff['q_des']
q_act_ff = result_ff['q_actual']
q_act_noff = result_noff['q_actual']
fig, axes = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
# 关节1
axes[0].plot(t, np.degrees(q_des[:, 0]), 'k--', linewidth=1.5,
label='期望角度')
axes[0].plot(t, np.degrees(q_act_ff[:, 0]), 'b-', linewidth=1.0,
label='实际(带前馈)', alpha=0.8)
axes[0].plot(t, np.degrees(q_act_noff[:, 0]), 'r-', linewidth=0.8,
label='实际(无前馈)', alpha=0.6)
axes[0].set_ylabel('关节1角度 (deg)')
axes[0].legend(fontsize=7)
axes[0].grid(True, alpha=0.3)
# 关节2
axes[1].plot(t, np.degrees(q_des[:, 1]), 'k--', linewidth=1.5,
label='期望角度')
axes[1].plot(t, np.degrees(q_act_ff[:, 1]), 'b-', linewidth=1.0,
label='实际(带前馈)', alpha=0.8)
axes[1].plot(t, np.degrees(q_act_noff[:, 1]), 'r-', linewidth=0.8,
label='实际(无前馈)', alpha=0.6)
axes[1].set_xlabel('时间 (s)')
axes[1].set_ylabel('关节2角度 (deg)')
axes[1].legend(fontsize=7)
axes[1].grid(True, alpha=0.3)
fig.suptitle('PD控制下期望角度 vs 实际角度(带/无重力前馈对比)')
plt.tight_layout()
path = os.path.join(self.rd, "01_angle_tracking.png")
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_angle_error(self, result_ff, result_noff):
"""图2:角度误差时程(双工况对比)。"""
t = result_ff['t']
err_ff = np.degrees(result_ff['error'])
err_noff = np.degrees(result_noff['error'])
fig, axes = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
axes[0].plot(t, err_ff[:, 0], 'b-', linewidth=1.0, label='带前馈')
axes[0].plot(t, err_noff[:, 0], 'r-', linewidth=0.8, label='无前馈')
axes[0].axhline(0, color='k', linestyle='-', linewidth=0.5)
axes[0].set_ylabel('关节1角度误差 (deg)')
axes[0].legend(fontsize=7)
axes[0].grid(True, alpha=0.3)
axes[1].plot(t, err_ff[:, 1], 'b-', linewidth=1.0, label='带前馈')
axes[1].plot(t, err_noff[:, 1], 'r-', linewidth=0.8, label='无前馈')
axes[1].axhline(0, color='k', linestyle='-', linewidth=0.5)
axes[1].set_xlabel('时间 (s)')
axes[1].set_ylabel('关节2角度误差 (deg)')
axes[1].legend(fontsize=7)
axes[1].grid(True, alpha=0.3)
fig.suptitle('PD控制角度误差时程(带/无重力前馈对比)')
plt.tight_layout()
path = os.path.join(self.rd, "02_angle_error.png")
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_velocity(self, result):
"""图3:实际角速度时程(带前馈工况)。"""
t = result['t']
qd = np.degrees(result['qd_actual'])
fig, axes = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
axes[0].plot(t, qd[:, 0], 'b-', linewidth=1.0)
axes[0].set_ylabel('关节1角速度 (deg/s)')
axes[0].grid(True, alpha=0.3)
axes[1].plot(t, qd[:, 1], 'r-', linewidth=1.0)
axes[1].set_xlabel('时间 (s)')
axes[1].set_ylabel('关节2角速度 (deg/s)')
axes[1].grid(True, alpha=0.3)
fig.suptitle('实际关节角速度时程(带重力前馈)')
plt.tight_layout()
path = os.path.join(self.rd, "03_velocity.png")
plt.savefig(path, dpi=150)
plt.close()
return path
def plot_torque(self, result_ff, result_noff):
"""图4:控制力矩时程(双工况对比)。"""
t = result_ff['t']
tau_ff = result_ff['tau']
tau_noff = result_noff['tau']
fig, axes = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
axes[0].plot(t, tau_ff[:, 0], 'b-', linewidth=1.0, label='带前馈')
axes[0].plot(t, tau_noff[:, 0], 'r-', linewidth=0.8, label='无前馈')
axes[0].axhline(0, color='k', linestyle='-', linewidth=0.5)
axes[0].set_ylabel('关节1力矩 (N·m)')
axes[0].legend(fontsize=7)
axes[0].grid(True, alpha=0.3)
axes[1].plot(t, tau_ff[:, 1], 'b-', linewidth=1.0, label='带前馈')
axes[1].plot(t, tau_noff[:, 1], 'r-', linewidth=0.8, label='无前馈')
axes[1].axhline(0, color='k', linestyle='-', linewidth=0.5)
axes[1].set_xlabel('时间 (s)')
axes[1].set_ylabel('关节2力矩 (N·m)')
axes[1].legend(fontsize=7)
axes[1].grid(True, alpha=0.3)
fig.suptitle('PD控制力矩时程(带/无重力前馈对比)')
plt.tight_layout()
path = os.path.join(self.rd, "04_torque.png")
plt.savefig(path, dpi=150)
plt.close()
return path
5. 可视化要求
序号 图表文件 服务结论
①
"01_angle_tracking.png" 期望vs实际角度——对比带/无重力前馈两种工况的跟踪效果
②
"02_angle_error.png" 角度误差时程——量化前馈对稳态误差的消除效果
③
"03_velocity.png" 实际角速度——验证三次多项式起步段速度平滑、衔接处无跳变
④
"04_torque.png" 控制力矩——观察前馈如何分担自重、降低PD负担
6. README使用说明 + 核心知识点卡片
快速开始
pip install -r requirements.txt
python -m pytest tests/ -v
python run_sim.py
数据格式说明
参数 修改位置 说明
连杆参数
"run_sim.py" 中
"Link2R(...)" 修改质量/长度/惯量
PD增益
"PDGains2R(Kp=[...], Kd=[...])" 调整各关节增益
轨迹目标
"CubicTrajectory2R(q_start, q_target, ...)" 修改起点/目标角度
起步时间
"T_ramp=0.5" 调整起步段时长
📌 知识点卡片
PD关节伺服跟踪(对应课本第5、6章)
- 三次多项式轨迹:q(t)=a·t³+b·t²,满足起点/终点位置、速度(零)四边界条件,保证速度连续
- PD+重力前馈:τ = Kp·e + Kd·ē + G(q_des),前馈抵消自重,稳态误差趋近于零
- 无前馈稳态误差:e_ss ≈ G(q_target) / Kp,比例项独自平衡自重
- RK4积分:四阶龙格-库塔法,每子步内控制量零阶保持,dt=0.1 ms下精度足够
7. 总结与工程师思考
工业落地难处
一、摩擦力矩未建模。 仿真中没有任何摩擦,实际关节有库仑摩擦和粘性摩擦,起步瞬间需要额外力矩克服静摩擦,到位后摩擦会帮助消除微小振荡。
二、参数辨识误差。 连杆质量、质心位置、惯量都是标称值,实际存在5%~10%的偏差,重力前馈不完全准确,稳态误差会略大于仿真结果。
三、实时控制器积分步长限制。 RK4需要很小的dt(本篇0.1 ms)才能保证数值稳定性,实时控制器受计算周期限制,通常用更高阶或隐式积分方法。
工程师心得
小赵看着图2的曲线:"带重力前馈时,到位后关节1的稳态误差几乎压到零附近;关掉前馈,同样的Kp=120,稳态误差直接跳到约4°。三次多项式起步段速度连续,0.5 s衔接处没有任何尖峰——这跟之前用二阶多项式时那个突刺完全不同。"
老周点头:"重力前馈就是干这个的。你看图4,带前馈时到位后关节1力矩稳定在约9 N·m,正好是平衡自重需要的数值;无前馈时这个9 N·m全靠比例项扛,误差自然就大了。调试PD的第一步是确认前馈是否正确,前馈对了,PD只需要处理动态跟踪,增益可以设得比较保守。"
"而且轨迹本身也得平滑。速度不连续的轨迹会在衔接处制造一个假的'冲击',让你误以为是增益没调好,其实根子在轨迹规划。"
8. 写作与风格限制
本篇全部代码为课程离线仿真,运行在普通PC上,用于PD伺服跟踪的离线校核。代码中不包含任何真实机器人控制器的实时通信栈或硬件驱动逻辑。 完全去营销,中立客观。全部物理量标注完整单位。
9. 文末附录
环境配置
pip install numpy>=1.24.0 matplotlib>=3.7.0 pytest>=7.0.0
"requirements.txt":
numpy>=1.24.0
matplotlib>=3.7.0
pytest>=7.0.0
运行说明
cd pd_joint_tracking_sim
pip install -r requirements.txt
python -m pytest tests/ -v
python run_sim.py
预期运行时间:纯Python实现,约10~30秒(取决于CPU),请耐心等待。
参数修改指南
想改什么 改哪里 示例
连杆质量
"Link2R(..., mass=3.0, ...)" 改为5.0
PD增益
"PDGains2R(Kp=[120,80], ...)" 调整为[200,150]
目标角度
"q_target=[np.pi/4, np.pi/6]" 改为[np.pi/3, 0]
起步时间
"T_ramp=0.5" 改为0.8
输出文件清单
文件 含义
"results/01_angle_tracking.png" 期望角度 vs 实际角度时程(双工况)
"results/02_angle_error.png" 角度误差时程(双工况)
"results/03_velocity.png" 实际角速度时程(带前馈)
"results/04_torque.png" 控制力矩时程(双工况)
正文完。以下为
"run_sim.py" 完整主入口 +
"tests/test_integration.py" 完整测试文件。
"run_sim.py"
"""
Main entry: 2R PD joint tracking simulation.
记录PD控制下期望角度、实际角度、输出误差随时间变化曲线。
对比带重力前馈与无重力前馈两种工况。
Run: python run_sim.py
# OFFLINE SIMULATION ONLY — not for production controllers.
"""
import os
import sys
import math
import numpy as np
sys.path.insert(0, os.path.dirname(__file__))
from robot.models import SimConfig2R, PDGains2R, Link2R
from robot.dynamics import Dynamics2R
from robot.trajectory import CubicTrajectory2R
from robot.controller import PDController2R
from robot.simulator import Simulator2R
def run_scenario(name, links, gains, traj, use_gravity_ff):
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)