Skip to content

Commit a174029

Browse files
committed
Formalized and cleaned up heater state machine
1 parent d7caf56 commit a174029

6 files changed

Lines changed: 992 additions & 217 deletions

File tree

firmware/heater_control.cpp

Lines changed: 168 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
using namespace wbo;
99

10+
#define HEATER_OVERVOLTAGE_THRESHOLD 23.0f
11+
#define MAX_HEATER_VOLTAGE 12.0f
12+
1013
static const PidConfig heaterPidConfig =
1114
{
1215
.kP = 0.3f, // kP
@@ -26,11 +29,14 @@ void HeaterControllerBase::Configure(float targetTempC, float targetEsr, struct
2629
m_targetTempC = targetTempC;
2730
m_targetEsr = targetEsr;
2831
m_configuration = configuration;
32+
m_retryTime = 0;
33+
34+
m_stateTimer.reset();
2935

30-
m_preheatTimer.reset();
31-
m_warmupTimer.reset();
3236
m_heaterStableTimer.reset();
33-
m_closedLoopStableTimer.reset();
37+
m_undervoltTimer.reset();
38+
m_underheatTimer.reset();
39+
m_overheatTimer.reset();
3440
}
3541

3642
bool HeaterControllerBase::IsRunningClosedLoop() const
@@ -53,87 +59,143 @@ HeaterState HeaterControllerBase::GetHeaterState() const
5359
return heaterState;
5460
}
5561

62+
HeaterState HeaterControllerBase::changeState(HeaterState newState, Status status)
63+
{
64+
m_stateTimer.reset();
65+
SetStatus(ch, status);
66+
return newState;
67+
}
68+
69+
HeaterState HeaterControllerBase::stopWithRetry(Status status, int retryTimeSec)
70+
{
71+
m_retryTime = retryTimeSec;
72+
return changeState(HeaterState::Stopped, status);
73+
}
74+
5675
HeaterState HeaterControllerBase::GetNextState(HeaterState currentState, HeaterAllow heaterAllowState, float heaterSupplyVoltage, float sensorTemp)
5776
{
58-
bool heaterAllowed = heaterAllowState == HeaterAllow::Allowed;
77+
const float overheatTemp = m_targetTempC + 100;
78+
const float closedLoopTemp = m_targetTempC - 30;
79+
const float underheatTemp = m_targetTempC - 100;
80+
81+
// Common checks that apply to all running states
82+
// This is here to avoid repeating these checks in every state - if any of these conditions are met, we want to stop heating immediately
83+
// There should be no code outside of specific states otherwise to keep the state machine logic clear and maintainable
5984

60-
// Check battery voltage for thresholds only if there is still no command over CAN
61-
if (heaterAllowState == HeaterAllow::Unknown)
85+
if (heaterSupplyVoltage > HEATER_OVERVOLTAGE_THRESHOLD)
6286
{
63-
// measured voltage too low to auto-start heating
64-
if (heaterSupplyVoltage < m_configuration->HeaterSupplyOffVoltage)
65-
{
66-
m_heaterStableTimer.reset();
67-
}
68-
else if (heaterSupplyVoltage > m_configuration->HeaterSupplyOnVoltage)
69-
{
70-
// measured voltage is high enougth to auto-start heating, wait some time to stabilize
71-
heaterAllowed = m_heaterStableTimer.hasElapsedSec(HEATER_BATTERY_STAB_TIME);
72-
}
87+
// If voltage is dangerously high, immediately stop heating to protect the sensor
88+
return stopWithRetry(Status::SensorOvervoltage, HEATER_OVERVOLT_RETRY_TIMEOUT);
7389
}
7490

75-
if (!heaterAllowed)
91+
// If voltage is back to normal, reset undervoltage timer
92+
if (heaterSupplyVoltage > m_configuration->HeaterSupplyOffVoltage)
7693
{
77-
// ECU hasn't allowed preheat yet, reset timer, and force preheat state
78-
m_preheatTimer.reset();
79-
SetStatus(ch, Status::Preheat);
80-
return HeaterState::Preheat;
94+
m_undervoltTimer.reset();
8195
}
8296

83-
float overheatTemp = m_targetTempC + 100;
84-
float closedLoopTemp = m_targetTempC - 30;
85-
float underheatTemp = m_targetTempC - 100;
97+
// Voltage has been too low for too long, stop heating and set fault
98+
if (m_undervoltTimer.hasElapsedSec(0.5f))
99+
{
100+
return stopWithRetry(Status::SensorUndervoltage, HEATER_UNDERVOLT_RETRY_TIMEOUT);
101+
}
86102

87103
switch (currentState)
88104
{
105+
case HeaterState::Stopped:
106+
{
107+
// If retry timer is running, wait until it elapses before allowing to start preheat again
108+
if ((m_retryTime) && !(m_stateTimer.hasElapsedSec(m_retryTime)))
109+
{
110+
break;
111+
}
112+
// Disable retry timer
113+
m_retryTime = 0;
114+
115+
if (heaterAllowState == HeaterAllow::NotAllowed)
116+
{
117+
// ECU has explicitly disallowed heating, stay stopped
118+
break;
119+
}
120+
121+
// If we haven't received any CAN message about whether heating is allowed or not
122+
// we should wait until heater supply voltage stabilizes above threshold before allowing to start heating
123+
if (heaterAllowState == HeaterAllow::Unknown)
124+
{
125+
// Reset voltage_stable timer if voltage is too low
126+
if (heaterSupplyVoltage < m_configuration->HeaterSupplyOnVoltage)
127+
{
128+
m_heaterStableTimer.reset();
129+
}
130+
131+
if (!m_heaterStableTimer.hasElapsedSec(HEATER_VOLTAGE_STAB_TIME))
132+
{
133+
// Continue to wait for voltage to stabilize before allowing heating
134+
break;
135+
}
136+
}
137+
138+
// Otherwise, start preheat
139+
return changeState(HeaterState::Preheat, Status::Preheat);
140+
break;
141+
}
89142
case HeaterState::Preheat:
90-
#ifdef HEATER_FAST_HEATING_THRESHOLD_T
91-
if (sensorTemp >= HEATER_FAST_HEATING_THRESHOLD_T) {
92-
// if sensor is already hot - we can start from higher heater voltage
93-
rampVoltage = 9;
143+
{
144+
bool startRamp = false;
94145

95-
// Reset the timer for the warmup phase
96-
m_warmupTimer.reset();
146+
rampVoltage = 7.0f;
97147

98-
SetStatus(ch, Status::Warmup);
99-
return HeaterState::WarmupRamp;
148+
#ifdef HEATER_FAST_HEATING_THRESHOLD_T
149+
// If the sensor is already above a certain temperature, skip preheat and go straight to warmup ramp
150+
if (sensorTemp > HEATER_FAST_HEATING_THRESHOLD_T)
151+
{
152+
// Start ramp at higher voltage to speed up heating for cases like hot restarts where sensor is already warm
153+
rampVoltage = 9.0f;
154+
startRamp = true;
100155
}
156+
101157
#endif
102158

103-
// If preheat timeout, or sensor is already hot (engine running?)
104-
if (m_preheatTimer.hasElapsedSec(m_configuration->PreheatTimeSec) || sensorTemp > closedLoopTemp)
159+
// If the sensor is already hot (engine running?), skip preheat and go straight to warmup ramp
160+
if (sensorTemp > closedLoopTemp)
105161
{
106-
// If enough time has elapsed, start the ramp
107-
// Start the ramp at 7 volts
108-
rampVoltage = 7;
162+
startRamp = true;
163+
}
109164

110-
// Reset the timer for the warmup phase
111-
m_warmupTimer.reset();
165+
// If enough time has elapsed in preheat (condensation phase has passed), start warmup ramp
166+
if (m_stateTimer.hasElapsedSec(m_configuration->PreheatTimeSec))
167+
{
168+
startRamp = true;
169+
}
112170

113-
SetStatus(ch, Status::Warmup);
114-
return HeaterState::WarmupRamp;
171+
if (startRamp)
172+
{
173+
// Reset the timer for the warmup phase
174+
return changeState(HeaterState::WarmupRamp, Status::Warmup);
115175
}
116176

117-
// Stay in preheat - wait for time to elapse
177+
// Stay in preheat
118178
break;
179+
}
119180
case HeaterState::WarmupRamp:
181+
{
182+
// Already hot enough, start closed loop
120183
if (sensorTemp > closedLoopTemp)
121184
{
122-
m_closedLoopStableTimer.reset();
123-
SetStatus(ch, Status::RunningClosedLoop);
124-
return HeaterState::ClosedLoop;
185+
return changeState(HeaterState::ClosedLoop, Status::RunningClosedLoop);
125186
}
126-
else if (m_warmupTimer.hasElapsedSec(HEATER_WARMUP_TIMEOUT))
187+
188+
// If we've been trying to warm up for too long without reaching the closed loop threshold,
189+
// something is wrong (like heater not working), so stop and set fault
190+
if (m_stateTimer.hasElapsedSec(HEATER_WARMUP_TIMEOUT))
127191
{
128-
SetStatus(ch, Status::SensorDidntHeat);
129-
// retry after timeout
130-
m_retryTime = HEATER_DIDNOTHEAT_RETRY_TIMEOUT;
131-
m_retryTimer.reset();
132-
return HeaterState::Stopped;
192+
return stopWithRetry(Status::SensorDidntHeat, HEATER_DIDNOTHEAT_RETRY_TIMEOUT);
133193
}
134194

135195
break;
196+
}
136197
case HeaterState::ClosedLoop:
198+
{
137199
// Over/under heat timers track how long it's been since
138200
// temperature was within normal range (then we abort if
139201
// it's been too long out of range)
@@ -147,47 +209,46 @@ HeaterState HeaterControllerBase::GetNextState(HeaterState currentState, HeaterA
147209
m_underheatTimer.reset();
148210
}
149211

150-
if (m_closedLoopStableTimer.hasElapsedSec(HEATER_CLOSED_LOOP_STAB_TIME)) {
151-
if (m_overheatTimer.hasElapsedSec(0.5f))
152-
{
153-
SetStatus(ch, Status::SensorOverheat);
154-
// retry after timeout
155-
m_retryTime = HEATER_OVERHEAT_RETRY_TIMEOUT;
156-
m_retryTimer.reset();
157-
return HeaterState::Stopped;
158-
}
159-
else if (m_underheatTimer.hasElapsedSec(0.5f))
160-
{
161-
SetStatus(ch, Status::SensorUnderheat);
162-
// retry after timeout
163-
m_retryTime = HEATER_UNDERHEAT_RETRY_TIMEOUT;
164-
m_retryTimer.reset();
165-
return HeaterState::Stopped;
166-
}
167-
} else {
212+
// Check for overheat befor checking for closed loop stable time
213+
// if we're overheating, we want to stop as soon as possible, even if we haven't been in closed loop for very long
214+
if (m_overheatTimer.hasElapsedSec(0.5f))
215+
{
216+
return stopWithRetry(Status::SensorOverheat, HEATER_OVERHEAT_RETRY_TIMEOUT);
217+
}
218+
219+
220+
if (!m_stateTimer.hasElapsedSec(HEATER_CLOSED_LOOP_STAB_TIME))
221+
{
168222
// give some time for stabilization...
169223
// looks like heavy ramped Ipump affects sensorTemp measure
170224
// and right after switch to closed loop sensorTemp drops below underhead threshold
225+
break;
171226
}
172227

173-
break;
174-
case HeaterState::Stopped:
175-
if ((m_retryTime) && (m_retryTimer.hasElapsedSec(m_retryTime))) {
176-
return HeaterState::Preheat;
228+
229+
else if (m_underheatTimer.hasElapsedSec(0.5f))
230+
{
231+
return stopWithRetry(Status::SensorUnderheat, HEATER_UNDERHEAT_RETRY_TIMEOUT);
177232
}
233+
178234
break;
235+
}
179236
}
180237

181238
return currentState;
182239
}
183240

184241
float HeaterControllerBase::GetVoltageForState(HeaterState state, float sensorEsr)
185242
{
243+
float heaterVoltage = 0;
244+
186245
switch (state)
187246
{
188247
case HeaterState::Preheat:
189248
// Max allowed during condensation phase (preheat) is 2v
190-
return 2.0f;
249+
heaterVoltage = 2.0f;
250+
break;
251+
191252
case HeaterState::WarmupRamp:
192253
if (rampVoltage < 12)
193254
{
@@ -197,20 +258,29 @@ float HeaterControllerBase::GetVoltageForState(HeaterState state, float sensorEs
197258
rampVoltage += (rampRateVoltPerSecond / heaterFrequency);
198259
}
199260

200-
return rampVoltage;
261+
heaterVoltage = rampVoltage;
262+
break;
263+
201264
case HeaterState::ClosedLoop:
202265
// "nominal" heater voltage is 7.5v, so apply correction around that point (instead of relying on integrator so much)
203266
// Negated because lower resistance -> hotter
204267

205268
// TODO: heater PID should operate on temperature, not ESR
206-
return 7.5f - m_pid.GetOutput(m_targetEsr, sensorEsr);
269+
heaterVoltage = 7.5f - m_pid.GetOutput(m_targetEsr, sensorEsr);
270+
break;
271+
207272
case HeaterState::Stopped:
208273
// Something has gone wrong, turn off the heater.
209-
return 0;
274+
heaterVoltage = 0;
275+
break;
210276
}
211277

212-
// should be unreachable
213-
return 0;
278+
// Limit to MAX_HEATER_VOLTAGE as per specification
279+
if (heaterVoltage > MAX_HEATER_VOLTAGE) {
280+
heaterVoltage = MAX_HEATER_VOLTAGE;
281+
}
282+
283+
return heaterVoltage;
214284
}
215285

216286
void HeaterControllerBase::Update(const ISampler& sampler, HeaterAllow heaterAllowState)
@@ -219,6 +289,9 @@ void HeaterControllerBase::Update(const ISampler& sampler, HeaterAllow heaterAll
219289
float sensorEsr = sampler.GetSensorInternalResistance();
220290
float sensorTemperature = sampler.GetSensorTemperature();
221291

292+
// TODO: Clean this up, looks like a mess
293+
// Move supply voltage reading logic into port so that it's cleaner and more testable
294+
// Also unify voltage reading logic across the codebase, so that everyting is consistent
222295
#if defined(HEATER_INPUT_DIVIDER)
223296
// if board has ability to measure heater supply localy - use it
224297
float heaterSupplyVoltage = sampler.GetInternalHeaterVoltage();
@@ -231,37 +304,25 @@ void HeaterControllerBase::Update(const ISampler& sampler, HeaterAllow heaterAll
231304

232305
// Run the state machine
233306
heaterState = GetNextState(heaterState, heaterAllowState, heaterSupplyVoltage, sensorTemperature);
234-
float heaterVoltage = GetVoltageForState(heaterState, sensorEsr);
307+
heaterVoltage = GetVoltageForState(heaterState, sensorEsr);
235308

236-
// Limit to 12 volts
237-
if (heaterVoltage > 12) {
238-
heaterVoltage = 12;
239-
}
240-
241-
// Very low supply voltage -> avoid divide by zero or very high duty
242-
if (heaterSupplyVoltage < 3) {
243-
heaterSupplyVoltage = 12;
244-
}
245-
246-
// duty = (V_eff / V_batt) ^ 2
247-
float voltageRatio = (heaterSupplyVoltage < 1.0f) ? 0 : heaterVoltage / heaterSupplyVoltage;
248-
float duty = voltageRatio * voltageRatio;
249-
250-
#ifdef HEATER_MAX_DUTY
251-
cycle++;
252-
// limit PWM each 10th cycle (2 time per second) to measure heater supply voltage throuth "Heater-"
253-
if ((cycle % 10) == 0) {
254-
if (duty > HEATER_MAX_DUTY) {
255-
duty = HEATER_MAX_DUTY;
256-
}
257-
}
258-
#endif
309+
float duty = 0;
259310

260-
// Protect the sensor in case of very high voltage
261-
if (heaterSupplyVoltage >= 23)
311+
if (heaterVoltage > 0)
262312
{
263-
duty = 0;
264-
heaterVoltage = 0;
313+
// duty = (V_eff / V_batt) ^ 2
314+
float voltageRatio = heaterVoltage / heaterSupplyVoltage;
315+
duty = voltageRatio * voltageRatio;
316+
317+
#ifdef HEATER_MAX_DUTY
318+
cycle++;
319+
// limit PWM each 10th cycle (2 time per second) to measure heater supply voltage through "Heater-"
320+
if ((cycle % 10) == 0) {
321+
if (duty > HEATER_MAX_DUTY) {
322+
duty = HEATER_MAX_DUTY;
323+
}
324+
}
325+
#endif
265326
}
266327

267328
// Pipe the output to the heater driver

0 commit comments

Comments
 (0)