code wiki / _hdl_build / nx_heater_pid.nx

nx_heater_pid.nx source

↩ module page · 82 lines · 3894 B

1// nx_heater_pid.nx -- SOVEREIGN PRINTER ARC rung 1b: integer PID heater control + a first-order 2// thermal plant simulator to prove it against (deterministic sim = the oracle until real hardware). 3// This is the loop that holds the hotend at temperature UNDER LOAD -- the flip side of the jam 4// physics (nx_jam_check says what temp is needed; this loop is what must HOLD it when melt load 5// rises). Units: temperature in MILLI-degC, PWM output 0..255, gains scaled by /1000. 6// PID: u = (Kp*e + Ki*acc_e + Kd*(e - e_prev)) / 1000, clamped 0..255 7// ANTI-WINDUP: the integral accumulator is clamped so a long heat-up can't overshoot-lock. 8// PLANT: T' = T + (u * k_heat - (T - T_amb) * k_loss / 1000) per tick (first-order RC model). 9// HONEST SCOPE: plant constants are a MODEL (sim oracle); the bare-metal port reads a real 10// thermistor ADC + drives a real MOSFET -- flagged next rung (cortex_m/esp32 backends exist). 11// LAWS: struct-free, integer-only. license_tier: ORIGINAL 12import "nx_syscalls.nx" 13const HP_MAGIC_1000000: i64 = 1000000 14 15const HP_PWM_MAX: i64 = 255 16const HP_ISEP_MC: i64 = 10000 // integral SEPARATION: only integrate within +/-10C of target 17 // (bounds windup structurally: far from target P+D drive alone) 18 19// PID state in an i64[4]: [0]=acc_e (integral), [1]=e_prev, [2]=acc_clamp, [3]=initialized 20func hp_pid_init(st: *i64, acc_clamp: i64) -> i64 { 21 st[0] = 0; st[1] = 0; st[2] = acc_clamp; st[3] = 0 22 return 0 23} 24 25// one PID tick: target/current in milli-degC; gains kp/ki/kd are milli-scaled; returns PWM 0..255 26func hp_pid_tick(st: *i64, target_mc: i64, current_mc: i64, kp: i64, ki: i64, kd: i64) -> i64 { 27 let e: i64 = target_mc - current_mc 28 var d: i64 = 0 29 if st[3] == 1 { d = e - st[1] } 30 st[1] = e 31 st[3] = 1 32 var emag: i64 = e 33 if emag < 0 { emag = 0 - emag } 34 var acc: i64 = st[0] 35 if emag < HP_ISEP_MC { acc = acc + e } // integral separation: integrate only near target 36 if acc > st[2] { acc = st[2] } // anti-windup clamp (both directions) 37 if acc < (0 - st[2]) { acc = 0 - st[2] } 38 st[0] = acc 39 var u: i64 = (kp * e + ki * acc + kd * d) / HP_MAGIC_1000000 // gains milli x error milli -> /1e6 to PWM 40 if u < 0 { u = 0 } 41 if u > HP_PWM_MAX { u = HP_PWM_MAX } 42 return u 43} 44 45// first-order plant tick: returns next temperature (milli-degC). 46// k_heat: milli-degC per tick per PWM count; k_loss: per-mil of (T - T_amb) lost per tick. 47func hp_plant_tick(t_mc: i64, t_amb_mc: i64, pwm: i64, k_heat: i64, k_loss: i64) -> i64 { 48 return t_mc + pwm * k_heat - ((t_mc - t_amb_mc) * k_loss) / 1000 49} 50 51// closed-loop simulation: run n ticks; out[0]=final T, out[1]=max T (overshoot probe), 52// out[2]=first tick index inside +/-band of target (-1 if never), out[3]=1 if it stayed in band 53// from out[2] to the end (steady), else 0. Returns out[3]. 54func hp_sim(target_mc: i64, t0_mc: i64, t_amb_mc: i64, 55 kp: i64, ki: i64, kd: i64, acc_clamp: i64, 56 k_heat: i64, k_loss: i64, band_mc: i64, n: i64, out: *i64) -> i64 { 57 let st: *i64 = sys_mmap(64) as *i64 58 hp_pid_init(st, acc_clamp) 59 var t: i64 = t0_mc 60 var tmax: i64 = t0_mc 61 var first_in: i64 = 0 - 1 62 var stayed: i64 = 0 63 var i: i64 = 0 64 while i < n { 65 let u: i64 = hp_pid_tick(st, target_mc, t, kp, ki, kd) 66 t = hp_plant_tick(t, t_amb_mc, u, k_heat, k_loss) 67 if t > tmax { tmax = t } 68 var inband: i64 = 0 69 if t >= target_mc - band_mc { if t <= target_mc + band_mc { inband = 1 } } 70 if inband == 1 { 71 if first_in < 0 { first_in = i; stayed = 1 } 72 } else { 73 if first_in >= 0 { stayed = 0; first_in = 0 - 1 } // left the band -> not settled yet 74 } 75 i = i + 1 76 } 77 out[0] = t 78 out[1] = tmax 79 out[2] = first_in 80 out[3] = stayed 81 return stayed 82}