Files
hulk 8900240c65 Heater: PID v0.1.14 - banded controller + sensor glitch guards
Rebuild PID as three-zone banded controller: far field P/D full power,
2026-10-15 20:55:02 +08:00

315 lines
13 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file heat.c
* @brief 加热控制:慢 PWM 驱动(PB12)+ 斜坡设定值 + 分段 PID 控温闭环 + 安全保护。
* @details 硬件:PB12=HEAT_EN,高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V)。
* 开关频率约束(<=10Hz)由"2s 载波周期 + 5% 占空步进"保证——每档脉宽
* 恒为 100ms 的整数倍(0.5Hz 载波、20 档占空分辨率)。
* 控制结构(平滑逼近目标,抑制过冲/震荡):
* 1. 目标斜坡:内部设定值以 HEATER_RAMP_CPS 限速滑向 M4 目标,
* 设定值阶跃/跳变不直接进入误差;
* 2. 全误差区间连续输出:err >= 温和带 P/D 全量;0 <= err < 温和带
* P/D 钳到 HEATER_SLOWBAND_DUTY(积分不参与钳);err < 0(超过
* 内部目标)P 负项直接抵扣积分后钳 0——P 参与使输出随超温幅度
* 立即收敛,积分只按剩余需要维持(若 P 铰 0 积分独撑,过线后
* 输出不随 err 变化,形成 sag->重建->过冲->排空自持极限环);
* 3. 微分基于温度变化率(D on measurement,-Kd*ΔT/拍):设定值
* 变化不产生微分踢;升温时提前退功率,回落越过目标时提前止跌;
* 4. 积分钳位 [0, HEATER_PID_I_MAX],吸收仅限目标近旁窄带
* (HEATER_PID_INTEG_ZONE_C)且 P/D 未被钳时(防爬升段
* windup);err<0 且 u>0 时小量回退,积分向稳态收敛;
* 稳态维持功率由积分承载。
* @note 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT,连续 3 拍去抖确认,
* 单拍毛刺不触发;短故障恢复无扰还原积分)强制关断;过温
* (>= HEATER_OVERTEMP_C)滞回保护;未使能强制关断。
*/
#include "heat.h"
#include "ntc.h"
#include "gd32f4xx_rcu.h"
#include "gd32f4xx_gpio.h"
#include "board_config.h"
/******************************************************************************/
/* 内部状态 */
static bool s_enable = false; /* 加热使能(M3) */
static float s_target = 0.0f; /* 目标温度(M4,外部设定值) */
static float s_ramp_target = 0.0f; /* 内部斜坡目标(以 HEATER_RAMP_CPS 滑向 s_target) */
static uint8_t s_duty = 0; /* 量化占空档位 0~HEATER_PWM_STEPS(x STEP%) */
static bool s_overtemp = false; /* 过温锁存(滞回恢复) */
static float s_integral = 0.0f; /* PID 积分累加(钳 [0, HEATER_PID_I_MAX]) */
static float s_prev_temp = 0.0f; /* 上拍温度(微分 on measurement 用) */
static float s_duty_acc = 0.0f; /* 占空抖动累加器(档位单位,<1) */
static uint8_t s_fault_debounce = 0; /* NTC 故障去抖(连续故障拍数,255 封顶记时长) */
static float s_fault_stash_I = 0.0f; /* 故障确认时刻积分暂存(短故障无扰还原用) */
static float s_fault_stash_T = 0.0f; /* 故障前末次正常温度(还原条件校验用) */
static float s_t_filt = 25.0f; /* 斜率限幅后的温度(读数毛刺防护) */
static uint8_t s_slew_cnt = 0; /* 连续超斜率拍数(真实快速变化放行计数) */
static uint16_t s_pwm_phase_ms = 0; /* PWM 载波相位 0..PERIOD-1 */
static uint16_t s_pid_divider = 0; /* PID 周期分频(10ms 圈) */
/* 档位总数 = 100% / 步进% */
#define HEATER_PWM_STEPS (100U / HEATER_PWM_STEP_PCT)
/* NTC 故障确认拍数(每拍 = 1 个 PID 周期 500ms;连续 3 拍 = 1.5s) */
#define NTC_FAULT_CONFIRM_PID_STEPS 3U
/******************************************************************************/
/* 底层输出 */
static void heat_out(bool on) {
if (on) {
gpio_bit_set(HEATER_EN_PORT, HEATER_EN_PIN);
} else {
gpio_bit_reset(HEATER_EN_PORT, HEATER_EN_PIN);
}
}
void heat_init(void) {
/* PB12 推挽输出低:上电默认加热关 */
rcu_periph_clock_enable(HEATER_EN_RCU);
gpio_mode_set(HEATER_EN_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, HEATER_EN_PIN);
gpio_output_options_set(HEATER_EN_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, HEATER_EN_PIN);
heat_out(false);
}
/******************************************************************************/
/* PID */
/**
* @brief 分段 PID 计算(500ms 周期):误差 -> 占空档位 s_duty。
*/
static void pid_compute(void) {
float t_raw = ntc_get_temperature();
/* NTC 读数斜率限幅(V0.1.9)+ 大偏差冻结(V0.1.13):真实热速率
<=0.2C/拍,单拍 |Δ|>0.5C 判为读数毛刺,沿限幅值过渡,连续 2 拍超限
后——若累计偏差 > HEATER_NTC_JUMP_C(秒级塌陷/卡死事件,真机实测
-2.5C/4.5s 塌陷与 +2.3C/10s 顶格,焊接点接触不良)则冻结滤波值
不再跟随,直到读数回弹到偏差带内自动恢复。真实温度变化永远不会
触发冻结(最大冷却 ~0.05C/拍)。过温方向冻结偏差最大 1.5C,安全。 */
float t = t_raw;
if (t_raw > -273.0f) {
float d_raw = t_raw - s_t_filt;
if (d_raw > HEATER_NTC_SLEW_C || d_raw < -HEATER_NTC_SLEW_C) {
if (s_slew_cnt < 2U) {
s_slew_cnt++;
t = s_t_filt + ((d_raw > 0.0f) ? HEATER_NTC_SLEW_C : -HEATER_NTC_SLEW_C);
} else if (d_raw > HEATER_NTC_JUMP_C || d_raw < -HEATER_NTC_JUMP_C) {
t = s_t_filt; /* 秒级坏值:冻结 */
} else {
s_slew_cnt = 2U; /* 偏差带内:放行 */
}
} else {
s_slew_cnt = 0U;
}
s_t_filt = t;
} else {
s_slew_cnt = 0U;
}
/* NTC 故障去抖:单拍毛刺(采样/EMI)保持输出不动;连续确认才安全关断。
毛刺拍不更新任何 PID 状态(含 s_prev_temp),下拍用真实温差继续。
确认沿暂存积分与故障前温度——短故障恢复时无扰还原(见恢复分支)。 */
if (t <= -273.0f) {
if (s_fault_debounce < 255U) {
s_fault_debounce++;
}
if (s_fault_debounce >= NTC_FAULT_CONFIRM_PID_STEPS) {
if (s_fault_debounce == NTC_FAULT_CONFIRM_PID_STEPS) {
s_fault_stash_I = s_integral;
s_fault_stash_T = s_prev_temp;
}
s_duty = 0;
s_integral = 0.0f;
s_duty_acc = 0.0f;
s_prev_temp = t;
}
return;
}
/* 故障恢复:短故障(<=HEATER_FAULT_RESTORE_MAX_S)且温度未显著漂移
(|ΔT|<2℃)时还原暂存积分——避免毛刺清零后稳态功率丢失引发
sag->重建->过冲循环;长故障/温度已漂移则从零重建(安全优先) */
if (s_fault_debounce >= NTC_FAULT_CONFIRM_PID_STEPS) {
float fault_dur_s = (float)s_fault_debounce * (HEATER_PID_PERIOD_MS / 1000.0f);
float drift = t - s_fault_stash_T;
if (fault_dur_s <= HEATER_FAULT_RESTORE_MAX_S && drift < 2.0f && drift > -2.0f) {
s_integral = s_fault_stash_I;
}
}
s_fault_debounce = 0U;
/* 未使能:强制关断(每拍刷新微分基线,恢复时无假温差) */
if (!s_enable) {
s_duty = 0;
s_integral = 0.0f;
s_prev_temp = t;
return;
}
/* 过温滞回保护:达到阈值锁存关断,降回阈值-滞回量后恢复 PID */
if (t >= HEATER_OVERTEMP_C) {
s_overtemp = true;
} else if (t <= (HEATER_OVERTEMP_C - HEATER_OVERTEMP_HYST)) {
s_overtemp = false;
}
if (s_overtemp) {
s_duty = 0;
s_integral = 0.0f;
s_duty_acc = 0.0f;
s_prev_temp = t;
return;
}
/* 目标斜坡:内部设定值限速滑向 s_target(升/降同限速)。
小步进(斜坡快于温升)全程落在温和带内缓升;大步进爬升段 ramp 超前、
到站后误差随实际温度平滑收缩,抑制热滞后过冲。 */
float diff = s_target - s_ramp_target;
float ramp_step = HEATER_RAMP_CPS * (HEATER_PID_PERIOD_MS / 1000.0f);
if (diff > ramp_step) {
s_ramp_target += ramp_step;
} else if (diff < -ramp_step) {
s_ramp_target -= ramp_step;
} else {
s_ramp_target = s_target;
}
float err = s_ramp_target - t;
float d_t = t - s_prev_temp; /* 本拍温度变化(D on measurement) */
/* 分区作用(V0.1.12):近场扩展区 [-BAND, SLOWBAND_C) 合并"带内退闸"
与"带外下方"为一条连续减速曲线——u = Ki*I(维持功率,不衰减)+
ASSIST 退闸曲线(err=+SLOWBAND_C 满档快爬、err=+BAND 处归零)+ Kd 减速。
原结构近场推力真空(P 随 err 缩小消失、I 未成熟,最后 2C 爬 141s),
ASSIST 提供与积分成熟度无关的确定推力。远场 err >= SLOWBAND_C:P/D
全量 + I。过温缘 err < -BAND:关断 + I 限速回退。 */
float u;
if (err >= HEATER_SLOWBAND_C) {
float u_pd = HEATER_PID_KP * err - HEATER_PID_KD * d_t;
if (u_pd > 100.0f) {
u_pd = 100.0f;
}
if (u_pd < 0.0f) {
u_pd = 0.0f;
}
u = u_pd + HEATER_PID_KI * s_integral;
if (u < 100.0f) {
s_integral += err * HEATER_PID_INTEG_GAIN_IN;
}
if (u > 100.0f) {
u = 100.0f;
}
} else if (err >= -HEATER_PID_BAND_C) {
float assist = (float)HEATER_SLOWBAND_DUTY * (err + HEATER_PID_BAND_C)
/ (HEATER_SLOWBAND_C + HEATER_PID_BAND_C);
u = HEATER_PID_KI * s_integral + assist - HEATER_PID_KD * d_t;
s_integral += err * HEATER_PID_INTEG_GAIN_IN;
if (u > (float)HEATER_NEAR_MAX_DUTY) {
u = (float)HEATER_NEAR_MAX_DUTY; /* 近场推力上限:快而不过冲 */
}
if (u < 0.0f) {
u = 0.0f;
}
} else {
s_integral -= HEATER_PID_INTEG_BLEED;
u = 0.0f;
}
if (s_integral > HEATER_PID_I_MAX) {
s_integral = HEATER_PID_I_MAX;
}
if (s_integral < 0.0f) {
s_integral = 0.0f;
}
s_prev_temp = t;
/* 输出 Bresenham 抖动量化:载波 2s = 4 个 PID 拍,档位余数按拍分摊,
平均分辨率 5%/4 = 1.25%。稳态维持功率(实测 ~10-12%)恰落在整数档
之间时,固定取整会迫使输出在相邻档间满幅切换(±1℃ 极限环残因);
抖动后 PI 可停在真实需要的平均功率上。脉宽仍为 100ms 整数倍,
开关频率仍 0.5Hz,硬件约束不变 */
s_duty_acc += u / (float)HEATER_PWM_STEP_PCT;
if (s_duty_acc >= (float)(HEATER_PWM_STEPS + 1)) {
s_duty_acc = (float)(HEATER_PWM_STEPS + 1);
}
{
uint8_t level = (uint8_t)s_duty_acc; /* u >= 0,截断即 floor */
s_duty_acc -= (float)level;
s_duty = level;
}
}
/******************************************************************************/
/* 公共接口 */
void heat_task(void) {
uint8_t prev_duty = s_duty;
/* PWM 载波相位推进(10ms tick) */
s_pwm_phase_ms += 10U;
if (s_pwm_phase_ms >= HEATER_PWM_PERIOD_MS) {
s_pwm_phase_ms = 0U;
}
/* PID 周期分频 */
s_pid_divider += 10U;
if (s_pid_divider >= HEATER_PID_PERIOD_MS) {
s_pid_divider = 0U;
pid_compute();
if (s_duty > prev_duty) {
s_pwm_phase_ms = 0U; /* 仅升档复位相位:立即起算新 ON 窗口;
降档不复位——相位已过新窗口即刻关断,
不再额外补发一段 ON 脉冲 */
}
}
/* 按占空档位输出:0 档常关、满档常开、其余按相位斩波 */
uint16_t on_ms = (uint16_t)s_duty * (HEATER_PWM_PERIOD_MS / HEATER_PWM_STEPS);
if (s_duty == 0U) {
heat_out(false);
} else if (s_duty >= HEATER_PWM_STEPS) {
heat_out(true);
} else {
heat_out(s_pwm_phase_ms < on_ms);
}
}
void heat_set_enable(bool on) {
if (s_enable == on) return; /* 幂等 */
s_enable = on;
if (on) {
/* 从当前实际温度起坡:避免内部目标从 0 跳变造成功率冲击;
微分基线/读数滤波基线同步取当前温度,首拍无假温差 */
s_ramp_target = ntc_get_temperature();
if (s_ramp_target < 0.0f) {
s_ramp_target = 0.0f; /* 传感器故障标记值非法,退化为 0 */
}
s_prev_temp = s_ramp_target;
s_t_filt = (s_ramp_target > 0.0f) ? s_ramp_target : 25.0f;
s_slew_cnt = 0U;
s_fault_debounce = 0U;
} else {
s_ramp_target = 0.0f;
s_duty = 0;
s_integral = 0.0f;
s_duty_acc = 0.0f;
heat_out(false);
}
}
bool heat_get_enable(void) {
return s_enable;
}
void heat_set_target(float temp_c) {
s_target = temp_c;
}
float heat_get_target(void) {
return s_target;
}
uint8_t heat_get_duty(void) {
return s_duty;
}