Field Control¶
The three nested control loops that turn sensor data into a PWM duty cycle on the alternator field. Lives in 6_functions.ino. Single entry point: AdjustFieldLearnMode(), called every loop iteration (and gated to fire on every fresh CH1 sample after that — see System Overview → CH1 freshness gate).
Protection logic that sits alongside this control path — what fires when temperature, voltage, or current exceeds safe limits — lives in Safeties and Protections. This page only documents the steady-state control architecture.
Three Control Loops¶
| Loop | Cadence | Library | Units in/out | Anti-windup |
|---|---|---|---|---|
Output Current PID (currentPID) |
every CH1 sample / PidSampleDivisor (~213 Hz default) |
Brett Beauregard fork PID_v1_xeng (DIRECT) |
input: amps · output: duty % | TrackAppliedOutput() called every tick with lastAppliedDuty |
Voltage CV Loop (cv_I + VoltageKp) |
VoltageLoopInterval (default 100 ms) |
Custom position-form PI | input: volts error · output: Icv amps |
Custom — saturation freeze + slope-aware bleed (SlopeBleedK) + AW cap with bleed (AwBleedRate/AwRecoverRate) |
Temperature PID (tempPID) |
TempPIDIntervalMs (5000 ms) |
PID_v1_xeng (REVERSE) |
input: filtered °F · output: thermalPenaltyAmps |
Implied-penalty back-calculation via TrackAppliedOutput(), outer-error gated |
The outer two (CV and temperature) compute current limits. The inner one (output current PID) drives the actuator. Together they cascade: temperature reduces the table ceiling, voltage trims the request below that ceiling, current PID drives duty to make measured current match the request.
Command Path¶
RPM ────────► getCapCurrentForRPM(RPM) ◄── rpmCapCurrentTable[] (or rpmCapPowerTable[] / IBV when capLimitMode=1)
│
│ I_cap
▼
thermalPenaltyAmps ─► subtract ──► clamp [0, MaxTableValue] ──► I_cmd
│
warmupCeiling (optional) │
MaintainMode → 0 │
▼
uTargetAmps
│
fastOvCurrentCap (Groups 1/2/3,
iExcess, LoadDump) clamp │
▼
voltageControlActive ? Icv = clamp(Kp·e + cv_I, 0, uTargetAmps)
!voltageControlActive ? setpointCommand = uTargetAmps
│
slew_limit_f(rise, fall) ◄──────────── setpointCommand
▼
setpointLimited
│
▼
currentPID.Compute() ──► pidOutput (duty %)
│
MANUAL: dutyRequest = ManualDutyTarget │
AUTO: dutyRequest = pidOutput │
▼
governor_apply(lastApplied, request, govMode,
rpmMinDuty, dtSec) ──► dutyNewFloat
│
apply_pwm_float()
▼
GPIO PWM
Every variable named on the diagram is a global in Xregulator.ino (some are static inside AdjustFieldLearnMode()). Grep-friendly.
RPM Tables¶
RPM_TABLE_SIZE = 10 evenly-spaced breakpoints at {100, 600, 1100, 1600, 2100, 2600, 3100, 3600, 4100, 4600} RPM. Linear interpolation between points; values at or below the first breakpoint use the first entry directly (no extrapolation below).
| Table | Variable | Default | Purpose |
|---|---|---|---|
| Target current (Normal mode) | rpmCurrentTable[] |
{0, 50, 50, …, 50} |
User-configured operating-point target (currently unused — superseded by cap table as the operating target since Learning Mode was deprecated) |
| Cap current | rpmCapCurrentTable[] |
{0, 50, 50, …, 50} |
Hard mechanical/electrical ceiling — getCapCurrentForRPM() returns this |
| Cap power (kW) | rpmCapPowerTable[] |
all zeros | Alternate ceiling expressed in kW. Active only when capLimitMode == 1. Converted to amps using live IBV |
| Minimum duty | rpmMinDutyTable[] |
{5, 5, 4, 4, 3, 3, 2, 2, 1, 1} % |
Protects LM2907 tach signal coupling cap from harsh transitions. Applied inside governor_apply() |
HiLow == 0 ("Low Charge Rate") loads a parallel set of NVS blobs (capTableLo, capPowerTableLo) via loadCapTablesForMode(0). Defaults to one-quarter of the Normal-mode current values. Switching modes calls loadCapTablesForMode() — no runtime halving.
First entry is always zero so the alternator commands no current at ≤ 100 RPM (effectively stopped) — guarantees field-off regardless of the rest of the chain.
Output Current PID (currentPID)¶
PID currentPID(&pidInput, &pidOutput, &pidSetpoint, PidKp, PidKi, PidKd, DIRECT);
currentPID.SetOutputLimits((double)MinDuty, (double)MaxDuty);
currentPID.SetSampleTime(100); // library's internal gate; we externally gate too
currentPID.SetTunings(PidKp, PidKi, PidKd);
currentPID.SetTrackingGain(PIDTrackingGain); // 4.0/sec for TrackAppliedOutput
Per-tick wiring¶
float pidSig = (OutputPIDSigSrc == 2) ? MeasuredAmps // raw
: (OutputPIDSigSrc == 1) ? g_pidMA_N // MA(OutputPIDMA_N)
: g_pidI_filtered; // EMA(OutputPIDFilterTC)
targetCurrent = (MaintainMode == 1) ? Bcur : pidSig;
pidInput = targetCurrent;
pidSetpoint = setpointLimited;
currentPID.Compute();
Notes:
- Feedback signal is selectable via
OutputPIDSigSrcso the user can trade noise rejection for response time without recompiling. EMA, MA, and raw all live on the CH1 record path so all three are always populated. - MaintainMode ignores
pidSigand usesBcur(INA228 net battery current) — the regulator is then chasing net-zero battery flow rather than alternator output. The dropdown for battery current source does not apply here;Bcuris always INA228 to avoid the 1–2 s Victron lag. - Term contributions (
innerTermP/I/D) are read after compute viaGetPterm()/GetIterm()/GetDterm()for telemetry display.
Tracking anti-windup (TrackAppliedOutput)¶
After the governor returns dutyNewFloat, the firmware calls:
currentPID.TrackAppliedOutput((double)dutyNewFloat, actualDtSec);
The library's tracking gain (PIDTrackingGain = 4 / sec) pulls outputSum toward what the actuator actually applied. So when the governor clamps the output to rpmMinDuty or MaxDuty, the integrator unwinds at a known rate and the loop snaps out of saturation cleanly instead of catching up over many ticks.
In MANUAL the integrator is reset to dutyNewFloat directly (ResetIntegratorTo) every tick so re-entering AUTO is bumpless.
Voltage CV Loop (custom position-form PI)¶
Runs every VoltageLoopInterval ms (default 100). Computes Icv — the current setpoint that gets fed to the output-current PID as setpointCommand when voltageControlActive is true.
Variables¶
| Variable | Meaning |
|---|---|
cv_I |
Integrator state (amps). Clamped to [0, uTargetAmps] |
Icv |
Position-form output clamp(VoltageKp·e + cv_I, 0, uTargetAmps) |
voltageTargetSlewed |
Rate-limited charging target — rises only as fast as cv_I can support given uTargetAmps. Falls instantly |
VoltageLoopInterval |
Compute cadence (default 100 ms) |
g_voltLoopActualIntervalMs |
Measured wall-clock interval between fires — telemetry-only |
VoltageKp, VoltageKi |
User-tunable PI gains |
cvDSlope |
Backward-diff of getFiltV() (V/s) — feeds SlopeBleedK only |
lastVoltageLoopMs |
Time of last fire; 0 → loop will fire on enteringCV rising edge |
Asymmetric integration¶
const float KiDown = 7.0f * VoltageKi;
float dI = (e >= 0.0f ? VoltageKi : KiDown) * e * dtSec;
Above target (e < 0), the integrator unwinds 7× faster than it winds up. Empirically necessary because charging behavior is asymmetric — voltage overshoot needs to clear faster than the loop pushes target up.
Slope-aware bleed¶
When cvDSlope (filtered V/s rate of rise) exceeds SlopeBleedThresh, an additional bleed is subtracted from cv_I:
proxGain = clamp(1 - e / SlopeBleedProxV, 0, 1); // 0 far below target, 1 at/above
slopeBleedAmps = SlopeBleedK · (cvDSlope - SlopeBleedThresh) · dt · proxGain;
cv_I -= slopeBleedAmps;
proxGain is the critical guard — it zeros the bleed when the battery is genuinely far below target and rising fast (a legitimate fast charge into a depleted bank). Only when the battery is near target does the bleed activate.
Bumpless transfer on CV entry¶
When voltageControlActive goes false → true (enteringCV):
seed = clamp(g_pidI_filtered - VoltageKp · e, 0, uTargetAmps);
cv_I = seed;
cv_I_track = seed;
cv_I_aw_cap = MaxTableValue; // clear any stale post-event cap
awSeedProtectStartMs = currentMillis; // engage seed-protection window (AwSeedProtectMs)
The seed value g_pidI_filtered - VoltageKp · e makes Icv == g_pidI_filtered immediately — so setpointCommand continues smoothly from wherever current was, no step. The AwSeedProtectMs window (default 150 ms) blocks the per-tick cv_I bleed (below) from wiping the freshly-seeded value if a new protection event fires immediately after.
Tracker during !voltageControlActive (bumpless waiting)¶
When CV is inactive, cv_I_track is dragged toward where the integrator would need to be if CV re-engaged this tick:
e_bt = ChargingVoltageTarget - IBV;
cv_I_target = clamp(g_pidI_filtered - VoltageKp · e_bt, 0, uTargetAmps);
cv_I_track += 2.0 · (cv_I_target - cv_I_track) · dtSec; // first-order tracker, Kt = 2/sec
cv_I = cv_I_track;
This is why re-entering CV from idle or MaintainMode is bumpless — the integrator is already in position. The AwSeedProtectMs window suppresses this tracker during seed-protection too.
voltageTargetSlewed — voltage target rise governor¶
Prevents the integrator from seeing a large voltage-target step when ChargingVoltageTarget jumps (bulk→absorption, override entry/exit, CVTuningMode toggles, TargetVoltageMode engagement):
e_needed = (uTargetAmps - cv_I) / VoltageKp; // how much error the current cv_I can absorb
e_needed = max(e_needed, 0.02V);
voltageTargetSlewed = min(ChargingVoltageTarget, IBV + e_needed);
Falls are instantaneous. voltageTargetSlewed is what the PI actually compares against — ChargingVoltageTarget is the destination, voltageTargetSlewed is the moving target.
Saturation behavior¶
satHi = (Icv >= uTargetAmps);
satLo = (Icv <= 0);
supervisorLimiting = fastOvClampActive && (uTargetAmps < uTargetRaw_cached - 0.01f);
Three states (exported as g_awState):
0— normal integration1— fast-OV supervisor limiting; freeze upward integration2— standard saturation (satHi && dI > 0orsatLo && dI < 0); freeze in offending direction3— active per-tickcv_Ibleed during fastOV (5 ms cadence, not 100 ms)4—!voltageControlActive(bumpless tracker ownscv_I)
The per-tick bleed (state 3) drains cv_I at AwBleedRate · MaxTableValue A/s every loop iteration during fastOvClampActive — necessary because the CV loop itself only fires every 100 ms and that's too slow to counteract a fast OV event that fires at 5 ms cadence.
cv_I_aw_cap¶
A second ceiling on cv_I that bleeds during fastOV events and slowly recovers afterward. Scales by MaxTableValue so the per-second amperage stays a fixed fraction of the alternator's rating:
awBleedAmpS = AwBleedRate · MaxTableValue; // active bleed
awRecoverAmpS = AwRecoverRate · MaxTableValue; // post-event recovery
AwBleedRate = 2.0 at 50 A table → 100 A/s; at 150 A → 300 A/s. Same proportional aggression regardless of alternator size. AwRecoverRate = 0.1 similarly: slow re-engagement so the battery has time to settle between OV events instead of bouncing.
Temperature PID (tempPID)¶
PID tempPID(&tempPIDInput_d, &thermalPenaltyAmps_d, &tempPIDSetpoint_d,
TempPIDKp, TempPIDKi, TempPIDKd, REVERSE);
tempPID.SetMode(AUTOMATIC);
tempPID.SetSampleTime((int)TempPIDIntervalMs); // library's own timer governs cadence
REVERSE mode means rising input (temperature) increases output (penalty). The output is subtracted from I_cap, so more penalty → less current → less heat.
Predictive process variable¶
Instead of using filtered temperature directly, the firmware projects forward:
projectedTempF = tempFiltered + thermalSlopeFPerSec · ThermalLookaheadSec;
thermalSlopeFPerSec comes from a 60-second ring buffer (thermalSlopeBuffer[THERMAL_SLOPE_BUF = 13] × 5 s cadence = 13 × 5 s = 65 s window — close enough). ThermalLookaheadSec (default 90 s) is user-tunable. So the PID's setpoint compare is "where will I be in 90 s at current rate-of-rise?" — derating begins before the limit is reached, not in response to it.
Output slew¶
thermalPenaltyAmps is asymmetrically slew-limited after tempPID.Compute():
thermalPenaltyAmps = slew_limit_f(thermalPenaltyAmps, thermalPenaltyAmps_d,
ThermalPenaltyRiseRate, ThermalPenaltyFallRate, dtSec);
Default ThermalPenaltyRiseRate = 60 A/s, ThermalPenaltyFallRate = 20 A/s. Penalty climbs faster than it falls so the loop doesn't bounce when temp briefly dips.
Stale-temperature safety¶
If tempFiltered is NaN or out of range, the PID halts and thermalPenaltyAmps holds its last value (rather than zeroing). The T5 staleness gate in Safeties cuts the field outright if temperature stays stale for 20 s.
Implied-penalty anti-windup¶
When non-thermal constraints (getCapCurrentForRPM, MaintainMode, etc.) are already limiting current below where the thermal penalty would put it, the integrator would wind up uselessly. The block parks it near where the constraints already imply:
outerError = (TemperatureLimitF - TempPIDMarginF) - tempFiltered;
if (impliedPenalty > thermalPenaltyAmps && outerError > 0.0f) {
trackTarget = max(0, impliedPenalty - TempPIDAntiWindupMarginA);
tempPID.TrackAppliedOutput(trackTarget, dtSec);
}
The outerError > 0 gate is critical — without it, when temperature is actively driving the loop (outerError ≤ 0), TrackAppliedOutput() would un-wind the integrator on every tick at ~213 Hz, faster than Compute() runs at 0.2 Hz. The loop would be unable to act.
Charging-Stage State Machine — updateChargingStage()¶
Drives inBulkStage, inAbsorptionStage, inIdleStage, and ChargingVoltageTarget. Suppressed during MaintainMode, TargetVoltageMode, CVTuningMode, and tick.manualMode — those override-modes set the target themselves.
BULK (CC)¶
voltageControlActive = false,ChargingVoltageTarget = BulkVoltage.bulkVoltageHoldTimerarms whenv ≥ BulkVoltage − BULK_V_BAND_ENTER (0.05 V); clears whenv < BulkVoltage − BULK_V_BAND_EXIT (0.10 V). Two-sided hysteresis stops 30–50 mV idle noise from constantly resetting the timer.- Transitions to ABSORPTION when held continuously for
bulkVoltageHoldMs(default 30 s).
ABSORPTION (CV)¶
voltageControlActive = true,ChargingVoltageTarget = AbsorptionVoltage.- Tail-current exit:
Bcur ≤ TailCurrent_Acontinuously forabsorptionCompleteTime(default 30 s). - Thermal constraint suppression of tail exit: if
thermalPenaltyAmps > 2 AANDuTargetAmps ≤ 2 × TailCurrent_A, tail detection is suppressed — the low current is from thermal derating, not "battery full." Resumed (with throttled console log) when the constraint clears. - Timeout exit:
now - absorptionStartTime ≥ AbsorptionTimeoutMs(default 60 min). - On exit:
UseFloat == 0→ IDLE; otherwise → FLOAT.
FLOAT¶
voltageControlActive = true,ChargingVoltageTarget = FloatVoltage.- Re-bulk after
MinFloatTime(default 5 min) whenv < RebulkVoltageORBcur < −RebulkCurrent_A(significant discharge), confirmed continuously forrebulkDebounceTime(default 60 s). - Also re-bulks unconditionally after
FLOAT_DURATION(43200 s = 12 h) elapsed in float. UseFloat == 0while already in float → immediate transition to IDLE.- SOC gating: if
SOC_percent ≥ SOC_BlockRebulk_percent, rebulk is blocked;≤ SOC_AllowRebulk_percent, always allowed; in between, voltage/current rules apply normally.
IDLE (UseFloat=0)¶
chargingEnabled = false(set inbuildTickSnapshot). Field is off, no PWM.- Same rebulk logic as FLOAT (rebulks back into BULK on sag).
enter_sys_auto() always starts in BULK regardless of current battery state — updateChargingStage() then fast-forwards within a few seconds if the battery is already at the upper voltages.
Governor (governor_apply)¶
Single function that sits between every duty request and the PWM hardware. Applies bounds (MinDuty, MaxDuty, rpmMinDuty), then applies slew based on govMode:
govMode |
Behavior |
|---|---|
GOV_NORMAL_SLEW |
slew_limit_f(lastApplied, requestClamped, DutyRampRate, DutyRampRate, dtSec) — symmetric rate limit (default 50 %/s) |
GOV_BYPASS_SLEW |
Instant — next = requestClamped. Effective bounds still enforced |
GOV_HOLD |
next = lastAppliedDuty (no change this tick) |
When GOV_BYPASS_SLEW is set¶
- Major OV:
currentBatteryVoltage > AlternatorHardShutdownV + 0.5 V— instant collapse during shutdown. - Voltage sensor failure:
!voltagePlausible || voltageDisagreementCritical. - CV overshoot during
voltageControlActive: latches whenfastOvClampActivefires, releases whenIBV < ChargingVoltageTarget + 0.02 V. Hysteresis stops chatter as the field collapses. - iExcess or LoadDump active (re-evaluated mid-loop after those supervisors vote).
sysIDRunning— plant-delay step test needs instant transitions.
The pre-clamp inside the governor (finalMin = max(dynMin, hardMin)) means even GOV_BYPASS_SLEW respects the rpmMinDuty floor and MinDuty/MaxDuty hard bounds. Only the descent speed is unrestricted.
Shutdown special case¶
runShutdownPath() calls governor_apply with effectiveMin = 0 so the duty can go below MinDuty. Phase 1 ramps to rpmMinDuty at DutyRampRate; Phase 3 ramps from there to 0 at DutySlowRampRate (default 1 %/s). Phase 4 settles at 0 for SettleTimeBeforeCut ms, then digitalWrite(4, LOW). See Safeties → Three shutdown paths.
Override Modes¶
All four bypass parts of the normal control architecture. Mutually exclusive in practice (the UI prevents simultaneous activation of MaintainMode / TargetVoltageMode), but the logic does not enforce that.
MaintainMode¶
voltageControlActive = true; // forces CV active even from idle
ChargingVoltageTarget = BulkVoltage; // CV runs at bulk target
uTargetAmps = 0; // ceiling enforcer: Icv → 0 because clamp [0, 0]
The PI runs at the bulk voltage so Groups 1/2 OV remain armed, but the current ceiling is zero so setpointCommand stays at 0. The PID then drives duty wherever needed to make Bcur ≈ 0 net battery current. Useful for: holding a battery at its present voltage indefinitely (e.g. paralleling shore power), running pure house-load draw through the alternator instead of the battery.
TargetVoltageMode¶
ChargingVoltageTarget = TargetVoltageSetpoint; // user-specified
voltageControlActive = true; // force CV active
// all current limits (RPM cap, thermal penalty, MaxTableValue) remain active
updateChargingStage() is suppressed — stage flags are irrelevant. On exit, enter_sys_auto() resets to BULK so the resumption path is well-defined.
TuningMode (inner current loop)¶
Square-wave setpoint generator. uTargetAmps toggles between 5 and 5 + waveAmplitude every wavePeriod / 2 seconds. Slew-limited by SetpointRiseRate/SetpointFallRate. Scoring window opens when slew rate drops below 1 A/s (so the score is independent of the slew rate itself). voltageControlActive = false while active — the test characterizes the inner PID against the alternator, not against the battery. Commits to tuningLog[] (50 PSRAM records) on manual button press.
CVTuningMode (outer voltage loop)¶
Square-wave on ChargingVoltageTarget. LOW phase = normal setpoint; HIGH phase = setpoint + cvWaveAmplitudeV. Asymmetric ISE/T scoring:
- HIGH phase overshoot weighted by
cvKOvershoot, undershoot (during rise) weighted ×1. First 25 mV of overshoot is "free" (CV_HIGH_DEADBAND_V). - LOW phase one-sided squared error normal weight, plus undershoot ramp-up over 10 s with 1 s grace (
CV_LOW_GRACE_SEC,CV_LOW_RAMP_SEC).
Settling time accumulates when |IBV − target| ≤ CV_SETTLE_V_THRESH for cvConsecutiveReads consecutive ticks. Commits to cvTuningLog[] on manual button press.
SystemID¶
Plant-delay measurement test. systemID_tick() returns true while running. On rising edge: snapshot pre-test state (mode, setpoint, applied duty, cv_I, CV active). During: govMode = GOV_BYPASS_SLEW, inner PID in MANUAL with integrator pinned to sysIDDutyOut, all setpoint machinery skipped. On falling edge: legality check (must still be in normal running state), restore setpoint and CV integrator if context unchanged, re-seed inner PID to lastAppliedDuty. Records timing of the step response against MeasuredAmps for the rise-time / dead-time / slope estimates shown on the Plant Delay tab.
Limp Home¶
User-toggled emergency mode. handleLimpHome() ramps to a fixed 30 % duty, bypasses every protection except the INA228 hardware ALERT latch. Used when sensors have failed completely but the user needs some charging to make it home. Heavily logged (one console message per 30 s).
Mode Entry Functions (enter_sys_*)¶
Bumpless transfer wrappers. Called when sysMode transitions.
| Function | Sets | PID action |
|---|---|---|
enter_sys_off() |
sysMode = SYS_MODE_OFF, govMode = GOV_NORMAL_SLEW |
currentPID.SetMode(MANUAL), pidOutput = lastAppliedDuty, integrator → lastAppliedDuty |
enter_sys_manual() |
SYS_MODE_MANUAL, GOV_NORMAL_SLEW (manual still respects tach floor) |
Same as off |
enter_sys_auto() |
SYS_MODE_AUTO, GOV_NORMAL_SLEW. Always sets inBulkStage = true, inAbsorptionStage = false, inIdleStage = false, resets timers |
SetOutputLimits(MinDuty, MaxDuty), SetSampleTime(100), SetTunings(PidKp, PidKi, PidKd), SetTrackingGain(PIDTrackingGain), SetMode(AUTOMATIC), integrator → lastAppliedDuty. Seeds setpointLimited = max(0, getTargetAmps()) |
enter_sys_fault() |
SYS_MODE_FAULT, govMode left to caller |
Same as off |
applyImmediateCut(tick, reason) is the universal immediate-cut entrypoint — used for INA228 hardware OV, hard overcurrent, RPM gate, and T3 critical temp. It sets GPIO4 LOW, zeros PWM, drops sysMode to FAULT, resets the PID, zeros setpointLimited, jumps shutdownPhase to PHASE_4 (so any later re-entry doesn't try to re-run the ramp), and aborts any running SystemID test.
Slew Rates and Time Constants — Quick Reference¶
| Parameter | Default | Owner | What it limits |
|---|---|---|---|
DutyRampRate |
50 %/s | governor_apply |
Symmetric PWM duty slew |
DutySlowRampRate |
1 %/s | runShutdownPath Phase 3 |
Slow ramp from rpmMinDuty to 0 during shutdown |
SetpointRiseRate |
30 A/s | slew_limit_f on setpointCommand → setpointLimited |
How fast the current target can rise |
SetpointFallRate |
50 A/s | same | How fast the current target can fall — overridden to 1e9 A/s when fastOvClampActive |
StartupRiseRate |
3 A/s | same | Override for the very first ticks after field turn-on; inStartupRamp clears when setpointLimited catches up |
FastSetpointRiseRate |
(multiplier) | same | Multiplies SetpointRiseRate during the post-protection-clear window (FastSetpointRiseWindowMs, gated by FastSetpointRiseHeadroomV) |
VoltageLoopInterval |
100 ms | CV loop | Compute cadence for cv_I |
TempPIDIntervalMs |
5000 ms | tempPID library timer | Compute cadence for thermalPenaltyAmps_d |
ThermalPenaltyRiseRate |
60 A/s | slew_limit_f after tempPID.Compute() |
Penalty up-slew |
ThermalPenaltyFallRate |
20 A/s | same | Penalty down-slew |
ThermalLookaheadSec |
90 s | projectedTempF |
Predictive horizon for tempPID setpoint compare |
TempPIDFilterAlpha |
0.2 | tempFiltered EMA |
Temperature input smoothing |
InputFilterTC |
(ms) | MeasuredAmps_filtered EMA |
iExcess and CV input filter |
OutputPIDFilterTC |
(ms) | g_pidI_filtered EMA |
Inner PID input filter |
DvdtTC |
58 ms | fastOV dvdt EMA |
Smooths INA228 rate-of-rise into Group 1 |
VoltageFilterTC |
(ms) | IBV_filtered EMA |
Used by getFiltV() — display + slope bleed only, never safety |
AwSeedProtectMs |
150 ms | CV seed-protection window | Suppresses per-tick cv_I bleed for this long after a fresh seed |
FIELD_COLLAPSE_DELAY |
30000 ms | Lockout cooldown | Post-shutdown wait before next attempt |
SettleTimeBeforeCut |
1000 ms | Phase 4 | Duty must be ≤ 0 % continuously for this long before GPIO4 LOW |
Cross-references¶
- Protection triggers, fastOv supervisor, immediate-cut reasons, threshold cascade → Safeties and Protections
- RPM-bucket histogramming, learning-mode legacy state → Field Bucketer
- CSV plumbing for any control variable → Important Functions
- Mode/reason priority chain, sysMode/govMode/shutdownPhase enums → System Overview