电机不会自动知道“应该转多快”。控制器需要不断比较目标值与反馈值,再根据误差修正电机命令。PID 正是完成这一闭环计算的基础算法。
本文以实际 pid.c 中的两种离散实现为例。重点不是记住一组参数,而是理解一次计算中哪些状态发生了变化,以及这些状态为什么会影响下一次输出。
本篇目标与可观察结果
读完后应能沿着一次函数调用,说明目标值、反馈值、历史误差和输出如何变化。若把控制器接到串口曲线工具,最小可观察量应至少包含 target、current、error 与 out。
源码中的控制器状态
代码通过 PID_T 指针访问控制器。虽然结构体定义不在本文件中,但成员使用关系可以从源码确认:
| 成员 | 作用 | 更新位置 |
|---|---|---|
target、current | 设定值与反馈值 | 初始化、每次计算 |
error | 当前误差 | 两种公式内部 |
last_error、last2_error | 一拍、两拍历史误差 | 计算结束后移位 |
integral | 位置式累计误差 | 位置式公式 |
p_out、i_out、d_out | 三项分量 | 位置式公式 |
out、limit | 最终输出与对称限幅 | 公式与限幅函数 |
实际工程还需要在头文件中给出这些成员的类型与结构体定义。下表只列出控制计算真正使用的状态,便于先建立最小心智模型。
初始化、复位与目标切换
pid_init() 写入 Kp/Ki/Kd、目标值和输出上限,同时清空积分、历史误差和各分量。pid_reset() 只清状态,保留参数,适合故障恢复或模式切换。
值得注意的是,pid_set_target() 不是单纯改目标:
_tpPID->target = _target;pid_init(_tpPID, _tpPID->kp, _tpPID->ki, _tpPID->kd, _tpPID->target, _tpPID->limit);它会重新初始化控制器。目标每改变一次,积分和误差历史都会归零。这能避免旧状态带入新任务,却会造成输出不连续,不适合要求“无扰切换”的轨迹控制。
位置式 PID 如何工作
位置式算法每拍重新计算完整控制量:
e[k] = target - currentI[k] = I[k-1] + e[k]u[k] = Kp·e[k] + Ki·I[k] + Kd·(e[k]-e[k-1])源码把三项分别保存到 p_out、i_out 与 d_out。这对调参很有价值:若输出振荡,可以判断是比例过激、积分累积,还是微分放大了噪声。
本实现没有显式乘除采样周期 dt。因此 Ki 和 Kd 已经隐含调用周期;任务周期从 5 ms 改为 10 ms 后,即使参数不变,控制器也不再等价。
增量式 PID 如何工作
增量式算法计算的是本拍输出变化量:
Δu[k] = Kp(e[k]-e[k-1]) + Ki·e[k] + Kd(e[k]-2e[k-1]+e[k-2])u[k] = u[k-1] + Δu[k]两拍历史误差使微分项成为二阶差分。输出以旧值为基础累加,更适合持续调节速度,但执行器限幅后,内部输出状态仍需与真实执行量保持一致。
位置式的积分存于 integral;增量式没有单独积分和,它通过持续累加 out 保留历史作用。二者不能在同一个控制器实例上随意交替调用,否则状态语义会混合。
输出限幅与积分饱和
pid_out_limit() 把最终输出夹在 [-limit, limit]。这只能保护执行器命令,不能阻止位置式 integral 在饱和期间继续增长。
文件末尾有 pid_app_limit_integral(),但它带有 unused 标记,位置式计算函数也没有调用它。因此“源码包含积分限幅函数”是事实,“当前位置式 PID 已抗积分饱和”不是事实。
更稳妥的改造顺序是:先限制积分状态,再做输出限幅;当输出已饱和且误差继续把输出推向饱和方向时,暂停积分。
最小接入方式
调用者需要固定周期地执行控制器,而不是在主循环中无节拍地调用:
pid_init(&speed_pid, kp, ki, kd, target, output_limit);
void ControlTask(void){ float feedback = Encoder_GetSpeed(); float command = pid_calculate_incremental(&speed_pid, feedback); Motor_SetCommand(command);}这里的 Encoder_GetSpeed() 与 Motor_SetCommand() 是接入示意,不是源文件中存在的 API。真实工程必须替换成自己的接口,并明确反馈单位、输出范围和任务周期。
调试顺序与常见故障
- 先令
Ki=Kd=0,确认误差方向和电机方向一致。 - 记录目标、反馈、P/I/D 分量和限幅后的输出。
- 若输出一上电就顶满,先检查单位、符号和反馈有效性,不要先降参数掩盖错误。
- 若改变目标后输出突然回落,要检查
pid_set_target()触发的状态清零。 - 若位置式长时间饱和后恢复很慢,应检查积分饱和,而不是只看最终输出限幅。
涉及电机时,首次测试应架空车轮、限制输出并保留物理断电路径。具体电压、电流与限幅值必须依据实际驱动板原理图和电机数据手册确定。
接入真实小车前还要确认什么
| 检查项 | 当前代码表现 | 接入要求 |
|---|---|---|
| 控制算法 | 已实现位置式与增量式两种入口 | 为每个控制对象选择一种,运行中不要混用状态 |
| 输出保护 | pid_out_limit() 提供对称限幅 | 限幅值必须匹配 PWM 与驱动器范围 |
| 目标切换 | pid_set_target() 会清空历史状态 | 需要平滑切换时,应另行设计无扰切换 |
| 积分保护 | 辅助限积分函数未进入位置式计算链 | 上板前补充并验证抗积分饱和 |
| 参数与周期 | 参数没有显式包含 dt | 固定控制周期后重新整定并记录响应曲线 |
本篇总结
- 位置式 PID 每拍生成完整输出,增量式 PID 每拍生成输出变化量。
- 该源码把采样周期隐含在参数中,调度周期变化后必须重新确认参数。
pid_set_target()会重置历史状态,便利但不支持无扰切换。- 最终输出限幅不等于抗积分饱和,未调用的辅助函数也不等于已启用能力。
- 上板前应先补齐结构体定义、固定周期、数据记录和安全限幅。