本篇继 [[信号处理之输出控制]] 后,从理论到实战
应用过程可以分为三步走:硬件闭环搭建 $\rightarrow$ 软件核心代码 $\rightarrow$ 定时调度。
第一步:硬件闭环搭建
PID 是一个闭环控制,你的硬件必须形成一个圆。
- 感知层 (Input):PT100 $\to$ STM32
- 难点: PT100 是电阻变化,STM32 的 ADC 只能测电压。而且 PT100 变化很微弱(0.385$\Omega$/°C)。
- 方案 A (推荐): 使用专用芯片 MAX31865。它能直接读 PT100 电阻并通过 SPI 接口告诉 STM32 温度值,精度极高,省去模拟电路设计的麻烦。
- 方案 B (省钱): 电桥电路 + 运算放大器 $\to$ STM32 ADC 引脚。
- 执行层 (Output):STM32 $\to$ 加热棒
- 难点: STM32 的 GPIO 只有 3.3V/20mA,带不动 220V 或 24V 的加热棒。
- 方案: 使用 PWM (脉冲宽度调制) 控制。
- 直流加热棒: GPIO $\to$ MOSFET (场效应管) $\to$ 加热棒。
- 交流加热棒 (220V): GPIO $\to$ SSR (固态继电器) $\to$ 加热棒。
- 控制原理:比如 PID 算出输出 50%,你就让 PWM 占空比为 50%(通电 0.5 秒,断电 0.5 秒)。
第二步:软件核心代码
这是你最关心的部分。我们需要把 Python 里的类变成 C 语言的结构体和函数。
这里有一份可以直接用在 STM32 上的标准 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
65
66
67
68
69
70
71
72
73
74
|
#include "main.h"
// 1. 定义 PID 对象结构体
typedef struct {
// --- 参数 (调参用) ---
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
// --- 限制 (保护用) ---
float Output_Max; // 最大输出 (比如 PWM 1000)
float Output_Min; // 最小输出 (比如 0)
float Integral_Max; // 积分限幅 (抗饱和用)
// --- 过程变量 (记忆用) ---
float Target_Temp; // 目标温度
float Last_Error; // 上一次的误差 (算D项用)
float Integral_Sum; // 积分累计值 (算I项用)
} PID_Controller;
// 2. 初始化函数
void PID_Init(PID_Controller *pid, float kp, float ki, float kd) {
pid->Kp = kp;
pid->Ki = ki;
pid->Kd = kd;
pid->Output_Max = 1000.0f; // 假设 PWM 周期是 1000
pid->Output_Min = 0.0f;
pid->Integral_Max = 500.0f; // 积分限制在一般量程的一半左右
pid->Target_Temp = 0.0f;
pid->Last_Error = 0.0f;
pid->Integral_Sum = 0.0f;
}
// 3. 核心计算函数 (放在定时器中断里调用)
// Input: 当前实测温度
// Return: 应该输出的 PWM 占空比
float PID_Compute(PID_Controller *pid, float current_temp) {
float error;
float p_term, i_term, d_term;
float output;
// A. 计算误差
error = pid->Target_Temp - current_temp;
// B. 计算 P 项 (弹簧)
p_term = pid->Kp * error;
// C. 计算 I 项 (记忆) - 关键:带抗饱和逻辑
pid->Integral_Sum += error;
// --> 积分限幅 (Clamping) <--
if (pid->Integral_Sum > pid->Integral_Max) {
pid->Integral_Sum = pid->Integral_Max;
} else if (pid->Integral_Sum < -pid->Integral_Max) {
pid->Integral_Sum = -pid->Integral_Max;
}
i_term = pid->Ki * pid->Integral_Sum;
// D. 计算 D 项 (预判)
d_term = pid->Kd * (error - pid->Last_Error);
pid->Last_Error = error; // 记住这次误差,下次用
// E. 汇总输出
output = p_term + i_term + d_term;
// F. 输出限幅 (防止 PWM 溢出)
if (output > pid->Output_Max) output = pid->Output_Max;
if (output < pid->Output_Min) output = pid->Output_Min;
return output;
}
|
第三步:定时调度 (The Timing)
这是新手最容易忽略的。PID 算法不仅涉及数学,还涉及时间。公式里的积分和微分,都隐含了一个前提:采样周期 ($dt$) 必须是固定的!
- 错误做法: 在
main 函数的 while(1) 里狂跑 PID。
- 后果: 如果你的程序有时候忙别的(比如处理屏幕显示),循环变慢了,积分项的时间基准就乱了,PID 参数会完全失效。
- 正确做法: 使用 STM32 的定时器中断 (Timer Interrupt)。
实战架构
假设我们控制频率为 10Hz (每 100ms 算一次)。
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
|
// 定义一个全局 PID 对象
PID_Controller HeaterPID;
// 主函数初始化
int main(void) {
HAL_Init();
// 初始化硬件:SPI(读温度), TIM(产PWM), TIM(调度中断)
MX_SPI1_Init();
MX_TIM2_Init(); // 用于 PWM 输出
MX_TIM3_Init(); // 用于 100ms 中断
// 初始化 PID 参数 (需要调试出来)
PID_Init(&HeaterPID, 50.0f, 0.5f, 10.0f);
HeaterPID.Target_Temp = 315.0f; // 设定目标
// 开启 PWM
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
// 开启调度中断
HAL_TIM_Base_Start_IT(&htim3);
while (1) {
// 主循环处理低优先级任务:屏幕显示、按键、Modbus通讯
// 不要在这里算 PID!
}
}
// 定时器中断回调函数 (每 100ms 触发一次)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM3) {
// 1. 读取传感器 (通过 SPI 读取 MAX31865)
float now_temp = MAX31865_Read_Temp();
// 2. 运算 PID
float pwm_duty = PID_Compute(&HeaterPID, now_temp);
// 3. 执行输出 (修改 PWM 寄存器 CCR)
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (uint32_t)pwm_duty);
}
}
|
第四步:如何调参 (Tuning) —— 那个“玄学”部分
代码写进去了,但 Kp, Ki, Kd 填多少?这里有一套“傻瓜口诀”,按照顺序来:
- 先置零: 把 $K_i$ 和 $K_d$ 设为 0,只留 $K_p$。
- 调 P (弹簧):
- 慢慢加大 $K_p$,直到温度开始在目标值(315°C)附近等幅震荡(像波浪一样不停)。
- 此时把 $K_p$ 乘以 0.6,作为最终的 $K_p$。
- 现象:温度能快速上去,但还有静差(比如卡在 310°C)。
- 调 I (推手):
- 慢慢加大 $K_i$(从小数值开始,如 0.01)。
- 观察那 5°C 的静差是不是慢慢消失了。
- 注意:如果 $K_i$ 太大,温度会冲过头(超调)然后荡很久。
- 调 D (阻尼):
- 如果你发现温度冲过头比较严重,或者震荡收敛太慢,加一点点 $K_d$。
- 一般加热控制中,D 可以很小或者为 0(PI 控制通常就够用了)。
总结
要在 STM32 上应用 PID:
- 硬件: 买个 MAX31865 读 PT100,用 MOSFET/SSR 驱动加热棒。
- 软件: 复制上面的 C 结构体和函数。
- 架构: 把
PID_Compute 扔进 100ms 的定时器中断里,千万别放在 while(1) 里跑。
- 心态: PID 参数不是算出来的,是试出来的。准备好耐心,对着串口打印的波形慢慢调。