diff --git a/Src/heat.c b/Src/heat.c index 9efbec5..5832708 100644 --- a/Src/heat.c +++ b/Src/heat.c @@ -91,21 +91,24 @@ static void pid_compute(void) { return; } - /* 位置式 PID */ + /* 位置式 PID(带抗积分饱和):输出饱和时积分仅沿退饱和方向更新, + 防止升温段积分顶满后长时间维持 100% 输出(此前实测目标 50℃ 冲到 59℃) */ float err = s_target - t; - s_integral += err; - { - /* 积分限幅(抗饱和):积分项贡献不超过满量程 */ - float imax = 100.0f / HEATER_PID_KI; - if (s_integral > imax) s_integral = imax; - if (s_integral < -imax) s_integral = -imax; - } - float deriv = err - s_prev_err; - s_prev_err = err; + float u_noi = HEATER_PID_KP * err + + HEATER_PID_KD * (err - s_prev_err); /* 比例 + 微分 */ + float integ_new = s_integral + err; + float u = u_noi + HEATER_PID_KI * integ_new; - float u = HEATER_PID_KP * err + HEATER_PID_KI * s_integral + HEATER_PID_KD * deriv; - if (u < 0.0f) u = 0.0f; /* 降温靠自然散热,输出不为负 */ - if (u > 100.0f) u = 100.0f; + if (u > 100.0f) { + u = 100.0f; + if (err < 0.0f) s_integral = integ_new; /* 超上限:仅误差为负(降温方向)才吸收 */ + } else if (u < 0.0f) { + u = 0.0f; + if (err > 0.0f) s_integral = integ_new; /* 超下限:仅误差为正(升温方向)才吸收 */ + } else { + s_integral = integ_new; /* 未饱和:正常积分 */ + } + s_prev_err = err; /* 输出量化到 10% 档位(四舍五入) */ s_duty10 = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT); diff --git a/temp_test_com.py b/temp_test_com.py new file mode 100644 index 0000000..bceeed0 --- /dev/null +++ b/temp_test_com.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +"""FilmForge PID anti-windup verification: target 50C, track steady-state.""" +import serial +import time +import threading + +def sum8(seq): + return sum(seq) & 0xFF + +def frame(cmd): + p = cmd.encode() + body = bytes([0xD5, 0x01, len(p)]) + p + return body + bytes([sum8(body[1:])]) + +rx = bytearray() +stop = threading.Event() + +def reader(buf, handle): + while not stop.is_set(): + data = handle.read(256) + if data: + buf.extend(data) + handle.close() + +def frames(buf): + out, i, b = [], 0, bytes(buf) + while i < len(b): + if b[i] == 0xB5 and i + 3 < len(b): + total = 3 + b[i + 2] + 1 + if i + total <= len(b): + out.append(b[i:i + total]); i += total; continue + i += 1 + return out + +s5 = serial.Serial('COM5', 115200, timeout=0.5) +t = threading.Thread(target=reader, args=(rx, s5)) +t.start() +time.sleep(0.5) + +def cmd(c, wait=0.8): + rx.clear() + s5.write(frame(c)) + time.sleep(wait) + fs = frames(rx) + payload = [f[3:3+f[2]].decode('ascii','replace') for f in fs if len(f) >= 4 and f[1] == 0xF0] + print('[%s] resp: %s' % (c, payload if payload else '')) + +cmd('M4S50') +cmd('M3S1') +cmd('M2S500') +print('--- heating to 50C, 150s sampling ---') +t0 = time.time() +last_print = -10 +peak = 0 +while time.time() - t0 < 150: + time.sleep(2.0) + fs = frames(rx) + vals = [] + for f in fs: + if len(f) >= 4 and f[1] == 0xF0 and f[2] >= 2: + try: + vals.append(float(f[3:3+f[2]])) + except ValueError: + pass + if vals: + peak = max(peak, max(vals)) + dt = time.time() - t0 + if dt >= last_print + 10 and vals: + print(' t+%03ds %.1f C (peak %.1f)' % (dt, vals[-1], peak)) + last_print = dt - (dt % 10) +cmd('M3S0', wait=1.0) +cmd('M2S0') + +stop.set(); t.join() +print('PEAK=%0.1f (target 50, overshoot %+.1f)' % (peak, peak - 50)) +print('DONE')