generated from hulk/gd32f4xx_template_cmake_vscode
315 lines
13 KiB
C
315 lines
13 KiB
C
/**
|
||
* @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;
|
||
}
|