Heater: PID v0.1.14 - banded controller + sensor glitch guards

Rebuild PID as three-zone banded controller: far field P/D full power,
This commit is contained in:
2026-10-15 20:55:02 +08:00
parent 8035f9b7d3
commit 8900240c65
6 changed files with 473 additions and 85 deletions
+175 -68
View File
@@ -1,16 +1,26 @@
/**
* @file heat.c
* @brief 加热控制:慢 PWM 驱动(PB12)+ 分段 PID 控温闭环 + 安全保护。
* @brief 加热控制:慢 PWM 驱动(PB12)+ 斜坡设定值 + 分段 PID 控温闭环 + 安全保护。
* @details 硬件:PB12=HEAT_EN,高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V)。
* 开关频率约束(<=10Hz)由"2s 载波周期 + 5% 占空步进"保证——每档脉宽
* 恒为 100ms 的整数倍(0.5Hz 载波、20 档占空分辨率)。
* 控制为分段式:
* err >= 温和带 全量 PID(满功率爬升);
* 0 <= err < 温和带 正常 PID 但占空上限压到温和档(小功率缓升,
* 减少进带蓄热,降低越过目标后的惯性过冲);
* err < 0(超温) 强制关断 + 积分按负误差回退(执行器只能加热不能制冷)。
* @note 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT)强制关断;
* 过温(>= HEATER_OVERTEMP_C)滞回保护;未使能强制关断。
* 控制结构(平滑逼近目标,抑制过冲/震荡):
* 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"
@@ -23,17 +33,27 @@
/* 内部状态 */
static bool s_enable = false; /* 加热使能(M3) */
static float s_target = 0.0f; /* 目标温度(M4) */
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 积分项 */
static float s_prev_err = 0.0f; /* PID 上次误差 */
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
/******************************************************************************/
/* 底层输出 */
@@ -56,38 +76,74 @@ void heat_init(void) {
/******************************************************************************/
/* PID */
/**
* @brief 复位 PID 积分与历史误差(使能切换/传感器故障时调用,防积分残留冲击)。
*/
static void pid_reset(void) {
s_integral = 0.0f;
s_prev_err = 0.0f;
}
/**
* @brief 积分试算值(err 一步积分;供抗饱和逻辑条件采纳)。
*/
static float integ_step(float err) {
return s_integral + err;
}
/**
* @brief 分段 PID 计算(500ms 周期):误差 -> 占空档位 s_duty。
*/
static void pid_compute(void) {
float t = ntc_get_temperature();
float t_raw = ntc_get_temperature();
/* 传感器故障(开路/短路):安全第一,强制关断并复位 PID */
if (t <= -273.0f) {
s_duty = 0;
pid_reset();
return;
/* 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;
pid_reset();
s_integral = 0.0f;
s_prev_temp = t;
return;
}
@@ -99,52 +155,88 @@ static void pid_compute(void) {
}
if (s_overtemp) {
s_duty = 0;
pid_reset();
s_integral = 0.0f;
s_duty_acc = 0.0f;
s_prev_temp = t;
return;
}
float err = s_target - t;
/* 超温区间:执行器只能加热不能制冷,输出必须为 0;
积分按负误差回退,为温度回落后的重新加热保留正确初始态 */
if (err < 0.0f) {
s_integral = integ_step(err);
s_prev_err = err;
s_duty = 0;
return;
}
/* 位置式 PID(带抗积分饱和) */
float u = HEATER_PID_KP * err
+ HEATER_PID_KD * (err - s_prev_err)
+ HEATER_PID_KI * integ_step(err);
if (u > 100.0f) {
u = 100.0f;
/* 饱和:err>=0 分支积分不吸收(防 windup) */
/* 目标斜坡:内部设定值限速滑向 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_integral = integ_step(err);
s_ramp_target = s_target;
}
/* 温和带:距目标不足 HEATER_SLOWBAND_C 时,比例/微分即时响应受限
(小功率缓升,减少进带蓄热、降低惯性过冲);积分项不受限——
稳态维持功率由积分缓慢顶到实际需要值 */
if (err < HEATER_SLOWBAND_C) {
float u_pd = HEATER_PID_KP * err + HEATER_PID_KD * (err - s_prev_err);
if (u_pd > (float)HEATER_SLOWBAND_DUTY) {
u_pd = (float)HEATER_SLOWBAND_DUTY;
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_duty = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT);
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;
}
}
/******************************************************************************/
@@ -164,8 +256,10 @@ void heat_task(void) {
if (s_pid_divider >= HEATER_PID_PERIOD_MS) {
s_pid_divider = 0U;
pid_compute();
if (s_duty != prev_duty) {
s_pwm_phase_ms = 0U; /* 占空变更:从新周期起点开始,响应即时 */
if (s_duty > prev_duty) {
s_pwm_phase_ms = 0U; /* 仅升档复位相位:立即起算新 ON 窗口;
降档不复位——相位已过新窗口即刻关断,
不再额外补发一段 ON 脉冲 */
}
}
@@ -183,9 +277,22 @@ void heat_task(void) {
void heat_set_enable(bool on) {
if (s_enable == on) return; /* 幂等 */
s_enable = on;
if (!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;
pid_reset();
s_integral = 0.0f;
s_duty_acc = 0.0f;
heat_out(false);
}
}
+33 -1
View File
@@ -97,11 +97,43 @@ float ntc_read_temperature(void) {
static float s_ntc_last_temp = -273.15f; /* 最新温度(初值 = 开路标记) */
static uint16_t s_ntc_sample_divider = 0; /* 主循环 10ms 圈分频计数 */
/* 滑动中值滤波(V0.1.11):真机读数毛刺可持续 1.4s+(曾抓到 14 帧连续
OPEN),会穿透控制环的斜率限幅并触发猛拉。最近 NTC_MEDIAN_N 个样本取
中值——单点/短串毛刺被完全剔除,滞后仅 ~0.2s(远小于低通滤波)。
故障标记值(-273/-274)参与排序:偶发 OPEN 被中值滤掉,连续 >=3 帧才
成为中值(等效又一层去抖),故障判定语义不变。 */
#define NTC_MEDIAN_N 5U
static float s_ntc_win[NTC_MEDIAN_N];
static uint8_t s_ntc_win_idx = 0;
static uint8_t s_ntc_win_fill = 0;
void ntc_sample_task(void) {
/* 主循环周期 10ms:按 NTC_SAMPLE_PERIOD_MS 分频触发一次采样 */
if (++s_ntc_sample_divider >= (NTC_SAMPLE_PERIOD_MS / 10U)) {
s_ntc_sample_divider = 0;
s_ntc_last_temp = ntc_read_temperature();
float v = ntc_read_temperature();
s_ntc_win[s_ntc_win_idx] = v;
s_ntc_win_idx = (uint8_t)((s_ntc_win_idx + 1U) % NTC_MEDIAN_N);
if (s_ntc_win_fill < NTC_MEDIAN_N) {
s_ntc_win_fill++;
}
/* 插入排序后取中值(5 点,开销可忽略) */
float tmp[NTC_MEDIAN_N];
for (uint8_t i = 0; i < s_ntc_win_fill; i++) {
tmp[i] = s_ntc_win[i];
}
for (uint8_t i = 1; i < s_ntc_win_fill; i++) {
float key = tmp[i];
int8_t j = (int8_t)(i - 1);
while (j >= 0 && tmp[j] > key) {
tmp[j + 1] = tmp[j];
j--;
}
tmp[j + 1] = key;
}
s_ntc_last_temp = tmp[s_ntc_win_fill / 2U];
}
}