🎮 简单控制器 — PD 控制¶
前面我们用手动设置 qpos 或恒定力矩来驱动机器人。这一节,你将学会写一个PD 控制器——这是机器人控制中最基础也最常用的控制器。
什么是 PD 控制器?¶
PD = Proportional(比例)+ Derivative(微分)
直觉理解:
- P 项(比例):离目标越远,力气越大 —— 像一根弹簧把你拉向目标
- D 项(微分):速度越快,阻力越大 —— 像阻尼器防止你冲过头
从零实现一个 PD 控制器¶
"""
simple_pd_controller.py — 从零实现的 PD 控制器
"""
import numpy as np
class SimplePDController:
"""
多关节 PD 控制器。
对每个关节独立计算力矩:
torque = kp * (target - current_pos) + kd * (0 - current_vel)
参数:
kp: 比例增益 — 越大跟踪越快,但可能振荡
kd: 微分增益 — 越大越稳定,但可能迟钝
"""
def __init__(self, nu: int, kp: float = 100.0, kd: float = 10.0):
"""
Args:
nu: 执行器数量(关节数)
kp: 比例增益,典型值 50~500
kd: 微分增益,典型值 5~50
"""
self.nu = nu
self.kp = np.full(nu, kp, dtype=np.float64) # 每个关节可以有不同的增益
self.kd = np.full(nu, kd, dtype=np.float64)
def compute(self, target_qpos, current_qpos, current_qvel):
"""
计算控制力矩。
Args:
target_qpos: 目标关节位置 (nu,) —— "想去哪"
current_qpos: 当前关节位置 (nq,) —— "现在在哪"
current_qvel: 当前关节速度 (nv,) —— "现在多快"
Returns:
torque: 控制力矩 (nu,) —— "该用多大劲"
"""
pos_error = target_qpos - current_qpos[:self.nu]
vel_error = np.zeros(self.nu) - current_qvel[:self.nu]
torque = self.kp * pos_error + self.kd * vel_error
return torque.astype(np.float64)
# ============================================================
# 在环境中使用 PD 控制器
# ============================================================
def demo_pd_control(env, target_angle: float = 0.8, steps: int = 150):
"""
演示:用 PD 控制器驱动关节 0 到目标角度。
观察关节如何平滑地到达目标(而不是瞬移)。
"""
nu = env.model.nu
pd = SimplePDController(nu=nu, kp=150.0, kd=12.0)
# 目标关节位置 —— 只有关节 0 动,其他保持原位
target_qpos = env.data.qpos[:nu].copy()
target_qpos[0] = target_angle # 关节 0 转到 target_angle 弧度
print(f"目标角度: {target_qpos[0]:.3f} rad")
print(f"初始角度: {env.data.qpos[0]:.3f} rad")
for i in range(steps):
# PD 计算力矩
ctrl = pd.compute(
target_qpos=target_qpos,
current_qpos=env.data.qpos,
current_qvel=env.data.qvel,
)
# 执行仿真
env.do_simulation(ctrl, env.frame_skip)
env.render()
# 打印进度
pos_error = abs(target_qpos[0] - env.data.qpos[0])
if i % 15 == 0:
print(f" Step {i:3d}: pos={env.data.qpos[0]:+.4f}, "
f"error={pos_error:.4f}, torque={ctrl[0]:+.2f}")
# 到达目标后提前退出
if pos_error < 0.001:
print(f" ✅ 到达目标!耗时 {i} 步")
break
print(f"最终角度: {env.data.qpos[0]:.3f} rad")
PD 参数调优¶
调 PD 参数就像调热水龙头——太热加点冷水,太冷加点热水:
| 现象 | 原因 | 调整 |
|---|---|---|
| 关节振荡、来回抖动 | Kp 太大 | ⬇️ 减小 Kp,⬆️ 增大 Kd |
| 响应慢、跟不上目标 | Kp 太小 | ⬆️ 增大 Kp |
| 到达后停不住、微微振 | Kd 太小 | ⬆️ 增大 Kd |
| 像在蜂蜜里动、太迟钝 | Kd 太大 | ⬇️ 减小 Kd |
推荐的调优流程:
# 第 1 步:Kd=0,逐渐增大 Kp 直到关节开始轻微振荡
pd = SimplePDController(nu=nu, kp=50, kd=0) # 试
pd = SimplePDController(nu=nu, kp=100, kd=0) # 再试
pd = SimplePDController(nu=nu, kp=200, kd=0) # 振荡了!回退到 150
# 第 2 步:固定 Kp=150,逐渐增大 Kd 直到振荡消失
pd = SimplePDController(nu=nu, kp=150, kd=5) # 试
pd = SimplePDController(nu=nu, kp=150, kd=10) # 好多了
pd = SimplePDController(nu=nu, kp=150, kd=15) # 有点迟钝,回退到 12
不同场景的参考值¶
| 场景 | Kp | Kd | 说明 |
|---|---|---|---|
| 轻量机械臂 (≤2kg) | 80~150 | 8~15 | 惯性小,低增益即可 |
| 重型机械臂 (≥10kg) | 200~500 | 20~50 | 需要更大的力 |
| 高精度任务 | 300~500 | 30~50 | 需要快速响应 |
| 人机协作 | 50~100 | 15~30 | 安全优先,不能太"硬" |
控制器在环境中的位置¶
你的 step() 方法
│
├── action (策略/程序的输出)
│ │
│ ├── 位置控制: action = 目标关节角度
│ │ └── PD.compute(target=action, ...) → 力矩
│ │
│ ├── 增量控制: action = 角度增量
│ │ └── target = current + action
│ │ └── PD.compute(target, ...) → 力矩
│ │
│ └── 力矩控制: action 就是力矩
│ └── 直接 do_simulation(action, ...)
│
└── do_simulation(ctrl, frame_skip)
完整示例:位置控制环境¶
class PositionControlEnv(OrcaGymEulerEnv):
"""动作 = 目标关节角度,内部用 PD 转化为力矩"""
def __init__(self, frame_skip, orcagym_addr, agent_names, time_step, **kwargs):
super().__init__(
frame_skip=frame_skip,
orcagym_addr=orcagym_addr,
agent_names=agent_names,
time_step=time_step,
**kwargs,
)
# 创建 PD 控制器
self._pd = SimplePDController(nu=self.model.nu, kp=150.0, kd=12.0)
# 动作空间 = 关节限位范围
ranges = []
for i in range(self.model.nu):
joint_name = self.model.joint_id2name(i)
info = self.model.get_joint_byname(joint_name)
if info.get("Limited", False):
ranges.append(info["Range"])
else:
ranges.append([-3.14, 3.14])
self.action_space = spaces.Box(
low=np.array([r[0] for r in ranges], dtype=np.float32),
high=np.array([r[1] for r in ranges], dtype=np.float32),
)
obs = self._get_obs()
self.observation_space = spaces.Dict({
"joint_pos": spaces.Box(-np.inf, np.inf, shape=(self.model.nq,)),
"joint_vel": spaces.Box(-np.inf, np.inf, shape=(self.model.nv,)),
})
def _get_obs(self):
return {
"joint_pos": self.data.qpos.copy().astype(np.float32),
"joint_vel": self.data.qvel.copy().astype(np.float32),
}
def step(self, action):
# action = 目标关节角度
ctrl = self._pd.compute(
target_qpos=action,
current_qpos=self.data.qpos,
current_qvel=self.data.qvel,
)
self.do_simulation(ctrl, self.frame_skip)
obs = self._get_obs()
tracking_error = np.mean(np.abs(action - self.data.qpos[:self.model.nu]))
reward = -tracking_error # 追踪越准,奖励越高
return obs, reward, False, False, {}
def reset_model(self):
self.set_joint_qpos(self.init_qpos)
self.set_joint_qvel(self.init_qvel)
self.mj_forward()
self._sync_view()
return self._get_obs(), {}
下一步¶
你学会了如何精确控制关节。现在把前面学的所有知识组合起来,搭建一个完整的任务:🏆 搭建一个任务。