PID 控制的工程实践:从传递函数到自动调参

6.2k 词

本文基于 rm_pid_tuner 项目的实际开发经验。与大多数讲 PID 的文章不同,这里不满足于”知道三个环节的作用”,而是从数学模型 → 离散化 → 工程实现 → 性能评估 → 自动调参完整链路展开,让你看完能自己写一个带自动调参功能的 PID 控制器。

从传递函数说起:PID 到底在补偿什么

几乎所有”PID 入门”文章都会从三个环节的作用讲起,但很少有人问:为什么这三个环节是完备的?

考虑一个典型的直流电机速度环。忽略电感(电气时间常数远小于机械时间常数),电机模型为:

$$E = IR + K_e\omega$$

$$T = K_t I$$

合并得:

$$T = \frac{K_t(E - K_e\omega)}{R} = \frac{K_t}{R}E - \frac{K_t K_e}{R}\omega$$

注意这里的结构:**输入 E(电压)与输出 ω(转速)之间存在一个负反馈项 $\frac{K_t K_e}{R}\omega$**——电机本身就是一个天然的被控对象加内环反馈。加上负载惯量 J 和粘性阻尼 B:

$$J\frac{d\omega}{dt} + B\omega = T$$

得到从电压到转速的传递函数(一阶惯性环节):

$$G(s) = \frac{\Omega(s)}{E(s)} = \frac{\frac{K_t}{JR}}{s + \frac{B + K_tK_e/R}{J}} = \frac{K}{\tau s + 1}$$

关键结论:电机速度环是一阶系统。 这意味着:

  • 纯 P 控制下,稳态误差 $e_{ss} = \frac{1}{1+KK_p}$——永远不为零,这正是”只有 P 会留静差”的数学根源
  • 加入积分环节后,开环传递函数在原点增加一个极点,系统变为 I 型系统,阶跃响应稳态误差严格为零
  • D 环节不改变系统型别,但改变闭环极点位置,可以增大阻尼比

所以 PID 三个环节的”完备性”在于:P 提供响应速度,I 消除稳态误差(提升系统型别),D 提供阻尼(抑制超调)。三者在频域上分别对应增益、低频整形、高频整形。

离散化:连续 PID 到单片机上的形式

连续 PID:

$$u(t) = K_p e(t) + K_i\int_0^t e(\tau)d\tau + K_d\frac{de(t)}{dt}$$

在 MCU 上必须以固定周期 T 采样离散化。用一阶向后差分近似积分和微分:

$$\int_0^t e(\tau)d\tau \approx T\sum_{k=0}^{n} e(k), \quad \frac{de(t)}{dt} \approx \frac{e(n) - e(n-1)}{T}$$

得到位置式 PID:

$$u(n) = K_p e(n) + K_i T\sum_{k=0}^{n} e(k) + \frac{K_d}{T}[e(n) - e(n-1)]$$

实际工程中通常把 $K_i T$ 合并为一个系数 $K_i’$,$\frac{K_d}{T}$ 合并为 $K_d’$,写代码时就不用关心采样周期了(但注意:换采样频率必须重新整定参数,这是新手最常踩的坑)。

增量式 PID 是位置式的差分:

$$\Delta u(n) = u(n) - u(n-1) = K_p[e(n)-e(n-1)] + K_i’ e(n) + K_d’[e(n) - 2e(n-1) + e(n-2)]$$

增量式最大的工程优势不是”计算量小”(现代 MCU 无所谓),而是:

  1. 故障安全:输出的是增量,执行机构(如步进电机驱动器)保持上一步位置,不会因计算异常跳变
  2. 无扰动切换:手动/自动切换时只需把 $\Delta u$ 清零,不会产生输出突变
  3. 天然抗积分饱和:u 被限幅时增量累积效应弱

RoboMaster 的电机速度环普遍用增量式,因为裁判系统掉线/串口异常时,增量输出不会让电机飞车。

工程实现:一个生产级的位置式 PID

结合 rm_pid_tuner 的实现,一个生产级 PID 至少要处理:积分分离、抗积分饱和、微分滤波、输出限幅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
namespace rm_pid_tuner {

struct PidParams {
double kp, ki, kd; // 位置式系数(ki,kd 已含采样周期)
double integral_limit; // 积分限幅
double output_limit; // 输出限幅
double d_filter_alpha; // 微分低通滤波系数 0~1
bool enable_integral_separation; // 积分分离开关
double separation_threshold; // 偏差超过该值则切断积分
};

class PidController {
public:
explicit PidController(const PidParams& params) : p_(params) {}

void setTarget(double target) { target_ = target; }
void setFeedback(double fb) { feedback_ = fb; }

double update() {
double err = target_ - feedback_;

// 1. 微分:带低通滤波,抑制高频噪声放大
// (纯微分对传感器噪声极其敏感,必须滤波)
double derivative = p_.d_filter_alpha * (err - prev_err_)
+ (1 - p_.d_filter_alpha) * prev_derivative_;
prev_derivative_ = derivative;

// 2. 积分分离:大偏差时禁用积分,防止超调
bool integral_active = true;
if (p_.enable_integral_separation &&
std::fabs(err) > p_.separation_threshold) {
integral_active = false;
}

if (integral_active) {
integral_ += err;
// 抗积分饱和:积分也限幅,防止积分项主导输出
integral_ = std::clamp(integral_, -p_.integral_limit, p_.integral_limit);
}

// 3. 合成输出
double output = p_.kp * err
+ p_.ki * integral_
+ p_.kd * derivative;

// 4. 输出限幅(执行机构物理极限)
output = std::clamp(output, -p_.output_limit, p_.output_limit);
prev_err_ = err;
return output;
}

void reset() {
integral_ = 0.0; prev_err_ = 0.0; prev_derivative_ = 0.0;
}

private:
PidParams p_;
double target_ = 0.0, feedback_ = 0.0;
double integral_ = 0.0;
double prev_err_ = 0.0;
double prev_derivative_ = 0.0;
};

} // namespace rm_pid_tuner

几个容易被忽略的细节:

微分滤波为什么必须? 离散微分 $\frac{e(n)-e(n-1)}{T}$ 本质是一个高通滤波器,而传感器噪声正是高频成分。若 D 系数稍大,噪声被放大后会在输出端产生剧烈抖动,听起来就像”电机在发抖”。低通滤波后的微分才是”看趋势”,而不是”看噪声”。

积分限幅和输出限幅的区别? 输出限幅是物理约束(PWM 最大 100%),积分限幅是逻辑约束。如果不单独限幅积分,会发生:误差持续存在 → 积分持续累加 → 输出到顶 → 误差反向 → 积分需要很长时间才能”消化”掉累积值 → 系统在反向时迟钝,这就是积分饱和的典型症状。

性能评估:怎么量化”调得好不好”

手动调参时”感觉差不多了”很容易,但自动化调参必须把”好不好”变成数字。rm_pid_tuner 中定义了几个核心指标:

1
2
3
4
5
6
struct Metrics {
double avg_error; // 平均绝对误差 MAE —— 稳态精度
double error_stddev; // 误差标准差 —— 振荡程度
double max_error; // 最大误差 —— 超调/瞬态峰值
double itae; // 时间乘绝对误差积分 —— 综合响应质量
};

MAE 衡量稳态精度,标准差衡量振荡,max_error 衡量超调,ITAE 则是一个”既要快又要稳”的综合指标——它对后期的小误差惩罚轻、对早期的持续误差惩罚重,因此在工程上比 ISE(平方误差积分)更受青睐:ISE 会放大早期大误差,导致整定结果偏向”压超调”而牺牲响应速度。

评分函数:

1
2
3
4
5
6
7
8
9
double calculatePerformanceScore(const Metrics& m) {
double score = 100.0;
score -= std::min(m.avg_error * 10, 30.0); // 稳态精度
score -= std::min(m.error_stddev * 5, 30.0); // 振荡
score -= std::min(m.max_error * 2, 20.0); // 超调
if (m.itae > 0)
score -= std::min(m.itae * 0.01, 20.0); // 综合
return std::max(0.0, std::min(100.0, score)); // clamp 到 [0,100]
}

各指标设了扣分上限(30/30/20/20),保证任何单一指标不会主导评分,这样”某一方面极差但其他方面很好”的系统不会被误判为优良。

规则式自动调参:把试凑法变成状态机

rm_pid_tuner 的核心是一个规则分析器:根据指标组合,决定参数往哪个方向调、调多少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
AnalysisResult analyze(const Metrics& metrics,
double kp, double ki, double kd,
bool conservative_mode) {
// 1. 先判断是否已完成
if (metrics.avg_error < 0.3 && metrics.error_stddev < 0.5) {
return {.status = "DONE", .needs_tuning = false};
}

// 2. 诊断 + 开药方
if (metrics.error_stddev > oscillation_threshold_) {
// 振荡 → 降 Kp,升 Kd(阻尼)
kp *= conservative_mode ? 0.85 : 0.9;
kd *= conservative_mode ? 1.3 : 1.2;
}
if (metrics.avg_error > slow_response_threshold_) {
// 响应慢 → 升 Kp
kp *= conservative_mode ? 1.2 : 1.3;
} else if (metrics.avg_error > steady_error_threshold_) {
// 有稳态误差但不振荡 → 升 Ki
ki *= conservative_mode ? 1.3 : 1.4;
}
if (metrics.max_error > 2 * metrics.avg_error && metrics.max_error > 5.0) {
// 超调 → 降 Kp,升 Kd
kp *= conservative_mode ? 0.7 : 0.8;
kd *= conservative_mode ? 1.5 : 1.3;
}

// 3. 保守模式:参数变化量减半(防止调参过程本身引发危险)
if (conservative_mode) {
kp = kp + (kp - kp_old) * 0.5; // 实际实现中需保存旧值
}
return {.suggested_p = kp, .suggested_i = ki, .suggested_d = kd,
.needs_tuning = true};
}

这套规则的本质,是把人类调参的经验抽象成可执行逻辑:

症状 处方 背后的控制理论
振荡(stddev 大) 降 Kp、升 Kd 增大阻尼比,压低谐振峰
响应慢(MAE 大) 升 Kp 提高穿越频率
稳态误差(MAE 中等) 升 Ki 提高低频增益,压静差
超调(max_error 大) 降 Kp、升 Kd 减小闭环峰值

自动调参的完整流程是一个状态机:

1
2
3
监听误差数据 → 缓存200个采样点 → 计算指标
→ 评分 < 80? → 是: 按规则调整参数 → 发布新参数 → 冷却60s(等系统稳定) → 重新采样
→ 否: 标记 DONE

注意冷却时间的工程意义:参数调整后系统需要时间过渡,如果立即采样会读到”调整过程中的瞬态”,导致误判。冷却期本质上是给闭环系统留出 3~5 个过渡过程时间。

从 PWM 到扭矩:PID 输出之后的最后一公里

PID 输出的是”控制量”,但对电机而言最终指令是 PWM 占空比 m。反解过程:

$$E = V_d \times m, \quad T = \frac{K_t(E - K_e\omega)}{R}$$

$$m = \frac{T \cdot R + K_e\omega}{K_t \cdot V_d}$$

其中关键参数获取:

  • R(相电阻):万用表测任意两相电阻,一半为每相电阻
  • K_e(反电动势系数):示波器测手拨电机产生的正弦波,$K_e = \frac{V_{peak}}{\omega}$
  • K_t(转矩系数):$K_t = \frac{60}{2\pi \cdot PP} K_e$,PP 为极对数

实际工程中 PWM 频率、死区、续流二极管压降都会造成偏差,所以多数 RoboMaster 队直接做电流环闭环:用电流传感器(如 INA240 + 采样电阻)测实际电流,电流环 PID 输出直接给 PWM,速度环 PID 输出作为电流环目标——两级串联。这样 PWM 非线性被电流环”消化”掉了,速度环面对的是一个近似线性的对象,调参更容易。

总结:工程 PID 的六个认知

  1. 先建模再调参:知道对象是一阶还是二阶,才能预测 P/I/D 的效果
  2. 增量式 ≠ 更高级:位置式和增量式是应用场景的选择,不是水平高低的区别
  3. 微分必须滤波:不滤波的 D 在真实传感器噪声面前是灾难
  4. 积分要限幅:抗饱和不是可选项,是必须项
  5. 用数字说话:MAE / 标准差 / 超调 / ITAE 四个指标,让调参从玄学变科学
  6. 给系统留冷却时间:调参节奏比参数本身更重要