Heater: segmented PID (full-power / slow-band / overtemp-cutoff) + 2s/5pct PWM

This commit is contained in:
2026-10-10 14:49:24 +08:00
parent 974a077e29
commit c9aa41685f
10 changed files with 114 additions and 71 deletions
+68 -61
View File
@@ -1,10 +1,14 @@
/**
* @file heat.c
* @brief 加热控制:慢 PWM 驱动(PB12)+ PID 控温闭环 + 安全保护。
* @brief 加热控制:慢 PWM 驱动(PB12)+ 分段 PID 控温闭环 + 安全保护。
* @details 硬件:PB12=HEAT_EN,高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V)。
* 开关频率约束(<=10Hz)由"1s 载波周期 + 10% 占空步进"保证——每档脉宽
* 恒为 100ms 的整数倍。PID 以 500ms 周期计算(NTC 后台采样 100ms),
* 输出量化到 0~10 档占空。
* 开关频率约束(<=10Hz)由"2s 载波周期 + 5% 占空步进"保证——每档脉宽
* 恒为 100ms 的整数倍(0.5Hz 载波、20 档占空分辨率)。
* 控制为分段式:
* err >= 温和带 全量 PID(满功率爬升);
* 0 <= err < 温和带 正常 PID 但占空上限压到温和档(小功率缓升,
* 减少进带蓄热,降低越过目标后的惯性过冲);
* err < 0(超温) 强制关断 + 积分按负误差回退(执行器只能加热不能制冷)。
* @note 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT)强制关断;
* 过温(>= HEATER_OVERTEMP_C)滞回保护;未使能强制关断。
*/
@@ -19,20 +23,20 @@
/* 内部状态 */
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_duty10 = 0; /* 量化占空档位 0~10(x10%) */
static float s_target = 0.0f; /* 目标温度(M4) */
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 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)
/******************************************************************************/
/* 底层输出 */
static void heat_out(bool on);
static void heat_out(bool on) {
if (on) {
gpio_bit_set(HEATER_EN_PORT, HEATER_EN_PIN);
@@ -61,21 +65,28 @@ static void pid_reset(void) {
}
/**
* @brief PID 计算(500ms 周期):误差 -> 占空档位 s_duty10(0~10)。
* @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();
/* 传感器故障(开路/短路):安全第一,强制关断并复位 PID */
if (t <= -273.0f) {
s_duty10 = 0;
s_duty = 0;
pid_reset();
return;
}
/* 未使能:强制关断 */
if (!s_enable) {
s_duty10 = 0;
s_duty = 0;
pid_reset();
return;
}
@@ -87,57 +98,60 @@ static void pid_compute(void) {
s_overtemp = false;
}
if (s_overtemp) {
s_duty10 = 0;
s_duty = 0;
pid_reset();
return;
}
/* 目标斜坡:内部设定值以受限速率滑向 s_target。
满功率爬升段 ramp 超前于实际温升(不受影响);接近目标后 ramp 先到站停住,
误差随实际温度逼近而平滑收缩 -> 功率缓退,抑制热传导滞后造成的过冲。 */
float diff = s_target - s_ramp_target;
float step = HEATER_RAMP_CPS * (HEATER_PID_PERIOD_MS / 1000.0f);
if (diff > step) {
s_ramp_target += step;
} else if (diff < -step) {
s_ramp_target -= step;
} else {
s_ramp_target = s_target;
float err = s_target - t;
/* 超温区间:执行器只能加热不能制冷,输出必须为 0;
积分按负误差回退,为温度回落后的重新加热保留正确初始态 */
if (err < 0.0f) {
s_integral = integ_step(err);
s_prev_err = err;
s_duty = 0;
return;
}
/* 位置式 PID(带抗积分饱和):输出饱和时积分仅沿退饱和方向更新,
防止升温段积分顶满后长时间维持 100% 输出(此前实测目标 50℃ 冲到 59℃) */
float err = s_ramp_target - t;
float u_noi = HEATER_PID_KP * err
+ HEATER_PID_KD * (err - s_prev_err); /* 比例 + 微分 */
float integ_new = s_integral + err;
/* 超温区间(实际温度高于内部目标):加热器只能加热、不能制冷,
输出必须为 0(此前积分正残量会算出小占空在超调段续灌加热);
积分按负误差同步回退,为温度回落后的重新加热保留正确初始态 */
if (err < 0.0f) {
s_integral = integ_new;
s_prev_err = err;
s_duty10 = 0;
/* 位置式 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) */
} else {
/* 误差为正(未达/低于内部目标):正常 PID + 抗积分饱和 */
float u = u_noi + HEATER_PID_KI * integ_new;
s_integral = integ_step(err);
}
/* 温和带:距目标不足 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;
}
if (u_pd < 0.0f) {
u_pd = 0.0f;
}
u = u_pd + HEATER_PID_KI * s_integral;
if (u > 100.0f) {
u = 100.0f;
/* 超上限:仅误差为负才吸收——err>=0 分支不会发生,保持积分不动 */
} else {
s_integral = integ_new;
}
/* 输出量化到 10% 档位(四舍五入) */
s_duty10 = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT);
}
s_prev_err = err;
/* 输出量化到档位(四舍五入) */
s_duty = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT);
}
/******************************************************************************/
/* 公共接口 */
void heat_task(void) {
uint8_t prev_duty = s_duty10;
uint8_t prev_duty = s_duty;
/* PWM 载波相位推进(10ms tick) */
s_pwm_phase_ms += 10U;
@@ -150,16 +164,16 @@ void heat_task(void) {
if (s_pid_divider >= HEATER_PID_PERIOD_MS) {
s_pid_divider = 0U;
pid_compute();
if (s_duty10 != prev_duty) {
if (s_duty != prev_duty) {
s_pwm_phase_ms = 0U; /* 占空变更:从新周期起点开始,响应即时 */
}
}
/* 按占空档位输出:0 档常关、10 档常开、其余按相位斩波 */
uint16_t on_ms = (uint16_t)s_duty10 * (HEATER_PWM_PERIOD_MS / 10U);
if (s_duty10 == 0U) {
/* 按占空档位输出: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_duty10 >= 10U) {
} else if (s_duty >= HEATER_PWM_STEPS) {
heat_out(true);
} else {
heat_out(s_pwm_phase_ms < on_ms);
@@ -169,15 +183,8 @@ void heat_task(void) {
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 */
}
} else {
s_ramp_target = 0.0f;
s_duty10 = 0;
if (!on) {
s_duty = 0;
pid_reset();
heat_out(false);
}
@@ -196,5 +203,5 @@ float heat_get_target(void) {
}
uint8_t heat_get_duty(void) {
return s_duty10;
return s_duty;
}