动手实现控温电路

本篇继 [[信号处理之输出控制]] 后,从理论到实战

应用过程可以分为三步走:硬件闭环搭建 $\rightarrow$ 软件核心代码 $\rightarrow$ 定时调度

第一步:硬件闭环搭建

PID 是一个闭环控制,你的硬件必须形成一个圆。

  1. 感知层 (Input):PT100 $\to$ STM32
    • 难点: PT100 是电阻变化,STM32 的 ADC 只能测电压。而且 PT100 变化很微弱(0.385$\Omega$/°C)。
    • 方案 A (推荐): 使用专用芯片 MAX31865。它能直接读 PT100 电阻并通过 SPI 接口告诉 STM32 温度值,精度极高,省去模拟电路设计的麻烦。
    • 方案 B (省钱): 电桥电路 + 运算放大器 $\to$ STM32 ADC 引脚。
  2. 执行层 (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 填多少?这里有一套“傻瓜口诀”,按照顺序来:

  1. 先置零: 把 $K_i$ 和 $K_d$ 设为 0,只留 $K_p$。
  2. 调 P (弹簧):
    • 慢慢加大 $K_p$,直到温度开始在目标值(315°C)附近等幅震荡(像波浪一样不停)。
    • 此时把 $K_p$ 乘以 0.6,作为最终的 $K_p$。
    • 现象:温度能快速上去,但还有静差(比如卡在 310°C)。
  3. 调 I (推手):
    • 慢慢加大 $K_i$(从小数值开始,如 0.01)。
    • 观察那 5°C 的静差是不是慢慢消失了。
    • 注意:如果 $K_i$ 太大,温度会冲过头(超调)然后荡很久。
  4. 调 D (阻尼):
    • 如果你发现温度冲过头比较严重,或者震荡收敛太慢,加一点点 $K_d$。
    • 一般加热控制中,D 可以很小或者为 0(PI 控制通常就够用了)。

总结

要在 STM32 上应用 PID:

  1. 硬件: 买个 MAX31865 读 PT100,用 MOSFET/SSR 驱动加热棒。
  2. 软件: 复制上面的 C 结构体和函数。
  3. 架构: 把 PID_Compute 扔进 100ms 的定时器中断里,千万别放在 while(1) 里跑。
  4. 心态: PID 参数不是算出来的,是试出来的。准备好耐心,对着串口打印的波形慢慢调。
Licensed under CC BY-NC-SA 4.0