2. PID 工程
PID 公式很短,但工程里真正决定稳定性的,往往是采样周期、单位、限幅、滤波和异常处理。很多“控制器不好用”的问题,不是理论错了,而是这些细节没有处理。
2.1 离散实现
真实控制器按固定频率运行。例如:
- 机械臂关节控制:100Hz 到 1kHz 常见。
- 四足底层电机控制:500Hz 到数 kHz 常见。
- 上层策略或 VLA 推理:几 Hz 到几十 Hz 常见。
连续 PID:
离散实现通常写成:
这里的 dt,换频率后参数很容易失效。
2.2 输出限幅
电机、舵机、液压执行器都有上限:
- 最大力矩
- 最大速度
- 最大电流
- 最大位置范围
- 最大温度或功率
所以控制器输出必须限幅:
def clamp(value, lower, upper):
return max(lower, min(upper, value))
torque_cmd = clamp(torque_cmd, -max_torque, max_torque)
限幅不是“保守”,而是把控制器接入真实硬件的必要接口。没有限幅,仿真里也许只是动作夸张,真机上可能触发驱动器保护甚至损坏结构。
2.3 抗积分饱和
积分项最常见的问题是:输出已经到达上限,积分还在继续累积。
例如目标很远,控制器想输出 100 N·m,但电机最多只能输出 20 N·m。如果积分项继续累积,等关节接近目标时,积分项仍然很大,系统会继续往前冲,产生巨大超调。
常见抗积分饱和方法有三种。
第一种是积分限幅:
integral += error * dt
integral = clamp(integral, -integral_limit, integral_limit)
第二种是输出饱和时暂停积分:
raw_output = kp * error + ki * integral + kd * derivative
output = clamp(raw_output, lower, upper)
if raw_output == output:
integral += error * dt
第三种是反算抗饱和,把饱和前后的差值反馈给积分项。它更平滑,但实现也更复杂。
入门阶段优先掌握前两种就够了。
2.4 微分滤波
微分项对噪声敏感。传感器读数如果有抖动,差分后会被放大:
当
工程里常见做法:
- 对测量速度做低通滤波。
- 使用编码器 / 驱动器估计的速度,而不是自己从位置差分。
- 做 derivative on measurement:对测量值求微分,而不是对误差求微分,避免目标突变导致 D 项尖峰。
关节定点控制常写成:
这等价于目标速度为 0 时的 PD 控制,也避免了目标位置阶跃变化时 D 项产生过大冲击。
2.5 单位检查
机器人控制里,单位错会直接毁掉调参结果。常见坑包括:
| 变量 | 推荐单位 | 常见错误 |
|---|---|---|
| 角度 | rad | 用 degree 调参 |
| 角速度 | rad/s | 把 rpm 当 rad/s |
| 力矩 | N·m | 忽略减速比 |
| 位置 | m | mm 和 m 混用 |
| 时间 | s | ms 没除以 1000 |
如果你发现参数看起来离谱,比如 Kp 要调到几十万才有反应,优先检查单位,而不是继续调参。
2.6 PID 代码
下面是一个可读性优先的 PID 骨架:
class PIDController:
def __init__(self, kp, ki, kd, output_limit, integral_limit):
self.kp = kp
self.ki = ki
self.kd = kd
self.output_limit = output_limit
self.integral_limit = integral_limit
self.integral = 0.0
self.prev_error = 0.0
def reset(self):
self.integral = 0.0
self.prev_error = 0.0
def step(self, target, measurement, dt):
error = target - measurement
self.integral += error * dt
self.integral = clamp(
self.integral,
-self.integral_limit,
self.integral_limit,
)
derivative = (error - self.prev_error) / dt
raw = self.kp * error + self.ki * self.integral + self.kd * derivative
output = clamp(raw, -self.output_limit, self.output_limit)
self.prev_error = error
return output
实际项目里还会加入:
dt过小或异常时跳过更新。- 目标突变时重置积分项。
- 根据模式切换清空历史状态。
- 输出命令做斜率限制,避免瞬间跳变。
2.7 排错表
| 现象 | 可能原因 | 优先处理 |
|---|---|---|
| 响应很慢 | 增大 P,检查限幅 | |
| 到目标附近来回振荡 | 降 P 或加 D | |
| 过冲很大 | 阻尼不足,积分过强 | 加 D,减 I |
| 长期差一点到不了 | 有负载或摩擦偏置 | 少量加 I 或加前馈 |
| 输出忽大忽小 | 微分放大噪声 | 过滤速度,减 D |
| 仿真好,真机抖 | 摩擦、延迟、结构柔性、频率不同 | 降增益,从低速开始 |
下一章进入状态空间和 LQR。它们不是替代 PID 的“高级玩具”,而是当系统状态耦合明显时,用模型系统化求反馈增益的方法。