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}