/** * @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; }