GCC Code Coverage Report


Directory: ./
File: firmware/controllers/actuators/idle_thread.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 87.1% 195 0 224
Functions: 100.0% 17 0 17
Branches: 72.7% 112 0 154
Decisions: 82.4% 56 - 68

Line Branch Decision Exec Source
1 /**
2 * @file idle_thread.cpp
3 * @brief Idle Air Control valve thread.
4 *
5 * This thread looks at current RPM and decides if it should increase or decrease IAC duty cycle.
6 * This file has the hardware & scheduling logic, desired idle level lives separately.
7 *
8 *
9 * @date May 23, 2013
10 * @author Andrey Belomutskiy, (c) 2012-2022
11 */
12
13 #include "pch.h"
14
15 #if EFI_IDLE_CONTROL
16 #include "idle_thread.h"
17 #include "idle_hardware.h"
18
19 #include "dc_motors.h"
20
21 #if EFI_TUNER_STUDIO
22 #include "stepper.h"
23 #endif
24
25 using enum idle_mode_e;
26
27 1124 IIdleController::TargetInfo IdleController::getTargetRpm(float clt) {
28 1124 targetRpmByClt = interpolate2d(clt, config->cltIdleRpmBins, config->cltIdleRpm);
29
30 // FIXME: this is running as "RPM target" not "RPM bump" [ie adding to the CLT rpm target]
31 // idle air Bump for AC
32 // Why do we bump based on button not based on actual A/C relay state?
33 // Because AC output has a delay to allow idle bump to happen first, so that the airflow increase gets a head start on the load increase
34 // alternator duty cycle has a similar logic
35
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 1124 times.
1124 targetRpmAc = engine->module<AcController>().unmock().acButtonState ? engineConfiguration->acIdleRpmTarget : 0;
36
37
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1124 times.
1124 float target = (targetRpmByClt < targetRpmAc) ? targetRpmAc : targetRpmByClt;
38 1124 float rpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit;
39 1124 float entryRpm = target + rpmUpperLimit;
40
41 // Higher exit than entry to add some hysteresis to avoid bouncing around upper threshold
42 1124 float exitRpm = target + 1.5 * rpmUpperLimit;
43
44 // Ramp the target down from the transition RPM to normal over a few seconds
45
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1124 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1124 times.
1124 if (engineConfiguration->idleReturnTargetRamp) {
46 // Ramp the target down from the transition RPM to normal over a few seconds
47 float timeSinceIdleEntry = m_timeInIdlePhase.getElapsedSeconds();
48 target += interpolateClamped(
49 0, rpmUpperLimit,
50 engineConfiguration->idleReturnTargetRampDuration, 0,
51 timeSinceIdleEntry
52 );
53 }
54
55 1124 idleTarget = target;
56 1124 idleEntryRpm = entryRpm;
57 1124 idleExitRpm = exitRpm;
58 1124 return { target, entryRpm, exitRpm };
59 }
60
61 1130 IIdleController::Phase IdleController::determinePhase(float rpm, IIdleController::TargetInfo targetRpm, SensorResult tps, float vss, float crankingTaperFraction) {
62 #if EFI_SHAFT_POSITION_INPUT
63
2/2
✓ Branch 1 taken 1066 times.
✓ Branch 2 taken 64 times.
2/2
✓ Decision 'true' taken 1066 times.
✓ Decision 'false' taken 64 times.
1130 if (!engine->rpmCalculator.isRunning()) {
64 1066 return Phase::Cranking;
65 }
66 64 badTps = !tps;
67
68
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 8 times.
2/2
✓ Decision 'true' taken 56 times.
✓ Decision 'false' taken 8 times.
64 if (badTps) {
69 // If the TPS has failed, assume the engine is running
70 56 return Phase::Running;
71 }
72
73 // if throttle pressed, we're out of the idle corner
74
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 6 times.
8 if (tps.Value > engineConfiguration->idlePidDeactivationTpsThreshold) {
75 2 return Phase::Running;
76 }
77
78 // If rpm too high (but throttle not pressed), we're coasting
79 // ALSO, if still in the cranking taper, disable coasting
80
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 4 times.
6 if (rpm > targetRpm.IdleExitRpm) {
81 2 looksLikeCoasting = true;
82
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 4 times.
✗ Decision 'false' not taken.
4 } else if (rpm < targetRpm.IdleEntryRpm) {
83 4 looksLikeCoasting = false;
84 }
85
86 6 looksLikeCrankToIdle = crankingTaperFraction < 1;
87
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 4 times.
6 if (looksLikeCoasting && !looksLikeCrankToIdle) {
88 2 return Phase::Coasting;
89 }
90
91 // If the vehicle is moving too quickly, disable CL idle
92 4 auto maxVss = engineConfiguration->maxIdleVss;
93
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 3 times.
4 looksLikeRunning = maxVss != 0 && vss > maxVss;
94
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 3 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 3 times.
4 if (looksLikeRunning) {
95 1 return Phase::Running;
96 }
97
98 // If still in the cranking taper, disable closed loop idle
99
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 2 times.
3 if (looksLikeCrankToIdle) {
100 1 return Phase::CrankToIdleTaper;
101 }
102 #endif // EFI_SHAFT_POSITION_INPUT
103
104 // If we are entering idle, and the PID settings are aggressive, it's good to make a soft entry upon entering closed loop
105
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 1 time.
2 if (m_crankTaperEndTime == 0.0f) {
106 1 m_crankTaperEndTime = engine->fuelComputer.running.timeSinceCrankingInSecs;
107 1 m_idleTimingSoftEntryEndTime = m_crankTaperEndTime + engineConfiguration->idleTimingSoftEntryTime;
108 }
109
110 // No other conditions met, we are idling!
111 2 return Phase::Idling;
112 }
113
114 1124 float IdleController::getCrankingTaperFraction(float clt) const {
115 1124 float taperDuration = interpolate2d(clt, config->afterCrankingIACtaperDurationBins, config->afterCrankingIACtaperDuration);
116 1124 return (float)engine->rpmCalculator.getRevolutionCounterSinceStart() / taperDuration;
117 }
118
119 1124 float IdleController::getCrankingOpenLoop(float clt) const {
120 1124 return interpolate2d(clt, config->cltCrankingCorrBins, config->cltCrankingCorr);
121 }
122
123 75 percent_t IdleController::getRunningOpenLoop(IIdleController::Phase phase, float rpm, float clt, SensorResult tps) {
124 150 float running = interpolate3d(
125 75 config->cltIdleCorrTable,
126 75 config->rpmIdleCorrBins, m_lastTargetRpm,
127 75 config->cltIdleCorrBins, clt
128 );
129
130 // Now we bump it by the AC/fan amount if necessary
131
7/8
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 71 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 time.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 72 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 72 times.
75 if (engine->module<AcController>().unmock().acButtonState && (phase == Phase::Idling || phase == Phase::CrankToIdleTaper)) {
132 3 running += engineConfiguration->acIdleExtraOffset;
133 }
134
135
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 73 times.
75 running += enginePins.fanRelay.getLogicValue() ? engineConfiguration->fan1ExtraIdle : 0;
136
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 73 times.
75 running += enginePins.fanRelay2.getLogicValue() ? engineConfiguration->fan2ExtraIdle : 0;
137
138 75 running += luaAdd;
139
140 #if EFI_ANTILAG_SYSTEM
141
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 75 times.
75 if (engine->antilagController.isAntilagCondition) {
142 running += engineConfiguration->ALSIdleAdd;
143 }
144 #endif /* EFI_ANTILAG_SYSTEM */
145
146 // 'dashpot' (hold+decay) logic for coasting->idle
147 75 float tpsForTaper = tps.value_or(0);
148 75 efitimeus_t nowUs = getTimeNowUs();
149
2/2
✓ Branch 0 taken 56 times.
✓ Branch 1 taken 19 times.
2/2
✓ Decision 'true' taken 56 times.
✓ Decision 'false' taken 19 times.
75 if (phase == Phase::Running) {
150 56 lastTimeRunningUs = nowUs;
151 }
152 // imitate a slow pedal release for TPS taper (to avoid engine stalls)
153
2/2
✓ Branch 0 taken 73 times.
✓ Branch 1 taken 2 times.
2/2
✓ Decision 'true' taken 73 times.
✓ Decision 'false' taken 2 times.
75 if (tpsForTaper <= engineConfiguration->idlePidDeactivationTpsThreshold) {
154 // make sure the time is not zero
155 73 float timeSinceRunningPhaseSecs = (nowUs - lastTimeRunningUs + 1) / US_PER_SECOND_F;
156 // we shift the time to implement the hold correction (time can be negative)
157 73 float timeSinceRunningAfterHoldSecs = timeSinceRunningPhaseSecs - engineConfiguration->iacByTpsHoldTime;
158 // implement the decay correction (from tpsForTaper to 0)
159 73 tpsForTaper = interpolateClamped(0, engineConfiguration->idlePidDeactivationTpsThreshold, engineConfiguration->iacByTpsDecayTime, tpsForTaper, timeSinceRunningAfterHoldSecs);
160 }
161
162 // Now bump it by the specified amount when the throttle is opened (if configured)
163 // nb: invalid tps will make no change, no explicit check required
164 150 iacByTpsTaper = interpolateClamped(
165 0, 0,
166 75 engineConfiguration->idlePidDeactivationTpsThreshold, engineConfiguration->iacByTpsTaper,
167 tpsForTaper);
168
169 75 running += iacByTpsTaper;
170
171 75 float airTaperRpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit;
172 150 iacByRpmTaper = interpolateClamped(
173 75 engineConfiguration->idlePidRpmUpperLimit, 0,
174 airTaperRpmUpperLimit, engineConfiguration->airByRpmTaper,
175 rpm);
176
177 75 running += iacByRpmTaper;
178
179 // are we clamping open loop part separately? should not we clamp once we have total value?
180 75 return clampPercentValue(running);
181 }
182
183 1131 percent_t IdleController::getOpenLoop(Phase phase, float rpm, float clt, SensorResult tps, float crankingTaperFraction) {
184 1131 percent_t crankingValvePosition = getCrankingOpenLoop(clt);
185
186 1131 isCranking = phase == Phase::Cranking;
187
5/6
✓ Branch 0 taken 1129 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 59 times.
✓ Branch 3 taken 1070 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 59 times.
1131 isIdleCoasting = phase == Phase::Coasting || (phase == Phase::Running && engineConfiguration->modeledFlowIdle);
188
189 // if we're cranking, nothing more to do.
190
2/2
✓ Branch 0 taken 1066 times.
✓ Branch 1 taken 65 times.
2/2
✓ Decision 'true' taken 1066 times.
✓ Decision 'false' taken 65 times.
1131 if (isCranking) {
191 1066 return crankingValvePosition;
192 }
193
194 // If coasting (and enabled), use the coasting position table instead of normal open loop
195
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 63 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
65 isIacTableForCoasting = engineConfiguration->useIacTableForCoasting && isIdleCoasting;
196
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 63 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 63 times.
65 if (isIacTableForCoasting) {
197 2 percent_t coastingPosition = interpolate2d(rpm, config->iacCoastingRpmBins, config->iacCoasting);
198
199 // Add A/C offset if the A/C is on during coasting
200
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2 times.
2 if (engine->module<AcController>().unmock().acButtonState) {
201 coastingPosition += engineConfiguration->acIdleExtraOffset;
202 }
203
204 // We return here, bypassing the final interpolation, so we should clamp the value
205 // to ensure it's a valid percentage.
206 2 return clampPercentValue(coastingPosition);
207 }
208
209 63 percent_t running = getRunningOpenLoop(phase, rpm, clt, tps);
210
211 // Interpolate between cranking and running over a short time
212 // This clamps once you fall off the end, so no explicit check for >1 required
213 63 return interpolateClamped(0, crankingValvePosition, 1, running, crankingTaperFraction);
214 }
215
216 961 float IdleController::getIdleTimingAdjustment(float rpm) {
217 961 return getIdleTimingAdjustment(rpm, m_lastTargetRpm, m_lastPhase);
218 }
219
220 971 float IdleController::getIdleTimingAdjustment(float rpm, float targetRpm, Phase phase) {
221 // if not enabled, do nothing
222
2/2
✓ Branch 0 taken 952 times.
✓ Branch 1 taken 19 times.
2/2
✓ Decision 'true' taken 952 times.
✓ Decision 'false' taken 19 times.
971 if (!engineConfiguration->useIdleTimingPidControl) {
223 952 return 0;
224 }
225
226 // If not idling, do nothing
227
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 6 times.
2/2
✓ Decision 'true' taken 13 times.
✓ Decision 'false' taken 6 times.
19 if (phase != Phase::Idling) {
228 13 m_timingPid.reset();
229 13 return 0;
230 }
231
232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 6 times.
6 if (engineConfiguration->idleTimingSoftEntryTime > 0.0f) {
233 // Use interpolation for correction taper
234 m_timingPid.setErrorAmplification(interpolateClamped(m_crankTaperEndTime, 0.0f, m_idleTimingSoftEntryEndTime, 1.0f, engine->fuelComputer.running.timeSinceCrankingInSecs));
235 }
236
237
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 6 times.
6 if (engineConfiguration->modeledFlowIdle) {
238 return m_modeledFlowIdleTiming;
239 } else {
240 // We're now in the idle mode, and RPM is inside the Timing-PID regulator work zone!
241 6 return m_timingPid.getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
242 }
243 }
244
245 1123 static void finishIdleTestIfNeeded() {
246
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1123 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 1123 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1123 times.
1123 if (engine->timeToStopIdleTest != 0 && getTimeNowUs() > engine->timeToStopIdleTest)
247 engine->timeToStopIdleTest = 0;
248 1123 }
249
250 /**
251 * @return idle valve position percentage for automatic closed loop mode
252 */
253 12 float IdleController::getClosedLoop(IIdleController::Phase phase, float tpsPos, float rpm, float targetRpm) {
254 12 auto idlePid = getIdlePid();
255
256
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 10 times.
12 if (shouldResetPid && !wasResetPid) {
257
3/4
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
2 needReset = idlePid->getIntegration() <= 0 || mustResetPid;
258 // this is not-so valid since we have open loop first for this?
259 // we reset only if I-term is negative, because the positive I-term is good - it keeps RPM from dropping too low
260
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 2 times.
✗ Decision 'false' not taken.
2 if (needReset) {
261 2 idlePid->reset();
262 2 mustResetPid = false;
263 }
264 2 shouldResetPid = false;
265 2 wasResetPid = true;
266 }
267
268 // todo: move this to pid_s one day
269 12 industrialWithOverrideIdlePid.antiwindupFreq = engineConfiguration->idle_antiwindupFreq;
270 12 industrialWithOverrideIdlePid.derivativeFilterLoss = engineConfiguration->idle_derivativeFilterLoss;
271
272 12 efitimeus_t nowUs = getTimeNowUs();
273
274 12 isIdleClosedLoop = phase == IIdleController::Phase::Idling;
275
276
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 9 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 9 times.
12 if (!isIdleClosedLoop) {
277 // Don't store old I and D terms if PID doesn't work anymore.
278 // Otherwise they will affect the idle position much later, when the throttle is closed.¿
279 3 shouldResetPid = true;
280 3 mustResetPid = true;
281 3 idleState = TPS_THRESHOLD;
282
283 // We aren't idling, so don't apply any correction. A positive correction could inhibit a return to idle.
284 3 m_lastAutomaticPosition = 0;
285 3 return 0;
286 }
287
288 9 bool acToggleJustTouched = engine->module<AcController>().unmock().timeSinceStateChange.getElapsedSeconds() < 0.5f /*second*/;
289 // check if within the dead zone
290
3/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 6 times.
9 isInDeadZone = !acToggleJustTouched && std::abs(rpm - targetRpm) <= engineConfiguration->idlePidRpmDeadZone;
291
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 6 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 6 times.
9 if (isInDeadZone) {
292 3 idleState = RPM_DEAD_ZONE;
293 // current RPM is close enough, no need to change anything
294 3 return m_lastAutomaticPosition;
295 }
296
297 // When rpm < targetRpm, there's a risk of dropping RPM too low - and the engine dies out.
298 // So PID reaction should be increased by adding extra percent to PID-error:
299 6 percent_t errorAmpCoef = 1.0f;
300
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
2/2
✓ Decision 'true' taken 4 times.
✓ Decision 'false' taken 2 times.
6 if (rpm < targetRpm) {
301 4 errorAmpCoef += (float)engineConfiguration->pidExtraForLowRpm / PERCENT_MULT;
302 }
303
304 // if PID was previously reset, we store the time when it turned on back (see errorAmpCoef correction below)
305
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 5 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 5 times.
6 if (wasResetPid) {
306 1 restoreAfterPidResetTimeUs = nowUs;
307 1 wasResetPid = false;
308 }
309 // increase the errorAmpCoef slowly to restore the process correctly after the PID reset
310 // todo: move restoreAfterPidResetTimeUs to idle?
311 6 efitimeus_t timeSincePidResetUs = nowUs - restoreAfterPidResetTimeUs;
312 // todo: add 'pidAfterResetDampingPeriodMs' setting
313 6 errorAmpCoef = interpolateClamped(0, 0, MS2US(/*engineConfiguration->pidAfterResetDampingPeriodMs*/1000), errorAmpCoef, timeSincePidResetUs);
314 // If errorAmpCoef > 1.0, then PID thinks that RPM is lower than it is, and controls IAC more aggressively
315 6 idlePid->setErrorAmplification(errorAmpCoef);
316
317 6 percent_t newValue = idlePid->getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
318 6 idleState = PID_VALUE;
319
320 // the state of PID has been changed, so we might reset it now, but only when needed (see idlePidDeactivationTpsThreshold)
321 6 mightResetPid = true;
322
323 // Apply PID Multiplier if used
324
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 6 times.
6 if (engineConfiguration->useIacPidMultTable) {
325 float engineLoad = getFuelingLoad();
326 float multCoef = interpolate3d(
327 config->iacPidMultTable,
328 config->iacPidMultLoadBins, engineLoad,
329 config->iacPidMultRpmBins, rpm
330 );
331 // PID can be completely disabled of multCoef==0, or it just works as usual if multCoef==1
332 newValue = interpolateClamped(0, 0, 1, newValue, multCoef);
333 }
334
335 // Apply PID Deactivation Threshold as a smooth taper for TPS transients.
336 // if tps==0 then PID just works as usual, or we completely disable it if tps>=threshold
337 // TODO: should we just remove this? It reduces the gain if your zero throttle stop isn't perfect,
338 // which could give unstable results.
339 6 newValue = interpolateClamped(0, newValue, engineConfiguration->idlePidDeactivationTpsThreshold, 0, tpsPos);
340
341 6 m_lastAutomaticPosition = newValue;
342 6 return newValue;
343 }
344
345 1123 float IdleController::getIdlePosition(float rpm) {
346 #if EFI_SHAFT_POSITION_INPUT
347
348 // Simplify hardware CI: we borrow the idle valve controller as a PWM source for various stimulation tasks
349 // The logic in this function is solidly unit tested, so it's not necessary to re-test the particulars on real hardware.
350 #ifdef HARDWARE_CI
351 return config->cltIdleCorrTable[0][0];
352 #endif
353
354 1123 bool useModeledFlow = engineConfiguration->modeledFlowIdle;
355
356 /*
357 * Here we have idle logic thread - actual stepper movement is implemented in a separate
358 * working thread see stepper.cpp
359 */
360 1123 getIdlePid()->iTermMin = engineConfiguration->idlerpmpid_iTermMin;
361 1123 getIdlePid()->iTermMax = engineConfiguration->idlerpmpid_iTermMax;
362
363 // On failed sensor, use 0 deg C - should give a safe highish idle
364
1/1
✓ Branch 1 taken 1123 times.
1123 float clt = Sensor::getOrZero(SensorType::Clt);
365
1/1
✓ Branch 2 taken 1123 times.
1123 auto tps = Sensor::get(SensorType::DriverThrottleIntent);
366
367 // Compute the target we're shooting for
368
1/1
✓ Branch 2 taken 1123 times.
1123 auto targetRpm = getTargetRpm(clt);
369 1123 m_lastTargetRpm = targetRpm.ClosedLoopTarget;
370
371 // Determine cranking taper (modeled flow does no taper of open loop)
372
2/3
✗ Branch 0 not taken.
✓ Branch 1 taken 1123 times.
✓ Branch 3 taken 1123 times.
1123 float crankingTaper = useModeledFlow ? 1 : getCrankingTaperFraction(clt);
373
374 // Determine what operation phase we're in - idling or not
375
1/1
✓ Branch 1 taken 1123 times.
1123 float vehicleSpeed = Sensor::getOrZero(SensorType::VehicleSpeed);
376
1/1
✓ Branch 1 taken 1123 times.
1123 auto phase = determinePhase(rpm, targetRpm, tps, vehicleSpeed, crankingTaper);
377
378 // update TS flag
379
3/4
✓ Branch 0 taken 1120 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1120 times.
1123 isIdling = (phase == Phase::Idling) || (phase == Phase::CrankToIdleTaper);
380
381
4/4
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 1074 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 46 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 1120 times.
1123 if (phase != m_lastPhase && phase == Phase::Idling) {
382 // Just entered idle, reset timer
383
1/1
✓ Branch 1 taken 3 times.
3 m_timeInIdlePhase.reset();
384 }
385
386 1123 m_lastPhase = phase;
387
388
1/1
✓ Branch 1 taken 1123 times.
1123 finishIdleTestIfNeeded();
389
390 // Always apply open loop correction
391
1/1
✓ Branch 1 taken 1123 times.
1123 percent_t iacPosition = getOpenLoop(phase, rpm, clt, tps, crankingTaper);
392 1123 baseIdlePosition = iacPosition;
393 // Force closed loop operation for modeled flow
394
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1123 times.
1123 auto idleMode = useModeledFlow ? IM_AUTO : engineConfiguration->idleMode;
395
396 // If TPS is working and automatic mode enabled, add any closed loop correction
397
4/4
✓ Branch 0 taken 163 times.
✓ Branch 1 taken 960 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 161 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 1121 times.
1123 if (tps.Valid && idleMode == IM_AUTO) {
398
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2 times.
2 if (useModeledFlow && phase != Phase::Idling) {
399 auto idlePid = getIdlePid();
400 idlePid->reset();
401 }
402
1/1
✓ Branch 1 taken 2 times.
2 auto closedLoop = getClosedLoop(phase, tps.Value, rpm, targetRpm.ClosedLoopTarget);
403 2 idleClosedLoop = closedLoop;
404 2 iacPosition += closedLoop;
405 2 } else {
406 1121 isIdleClosedLoop = false;
407 }
408
409
1/1
✓ Branch 1 taken 1123 times.
1123 iacPosition = clampPercentValue(iacPosition);
410
411 // todo: while is below disabled for unit tests?
412 #if EFI_TUNER_STUDIO && (EFI_PROD_CODE || EFI_SIMULATOR)
413
414 // see also tsOutputChannels->idlePosition
415 getIdlePid()->postState(engine->outputChannels.idleStatus);
416
417
418 extern StepperMotor iacMotor;
419 engine->outputChannels.idleStepperTargetPosition = iacMotor.getTargetPosition();
420 #endif /* EFI_TUNER_STUDIO */
421
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1123 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1123 times.
1123 if (useModeledFlow && phase != Phase::Cranking) {
422 float totalAirmass = 0.01 * iacPosition * engineConfiguration->idleMaximumAirmass;
423 idleTargetAirmass = totalAirmass;
424
425 bool shouldAdjustTiming = engineConfiguration->useIdleTimingPidControl && phase == Phase::Idling;
426
427 // extract hiqh frequency content to be handled by timing
428 float timingAirmass = shouldAdjustTiming ? m_timingHpf.filter(totalAirmass) : 0;
429
430 // Convert from airmass delta -> timing
431 m_modeledFlowIdleTiming = interpolate2d(timingAirmass, engineConfiguration->airmassToTimingBins, engineConfiguration->airmassToTimingValues);
432
433 // Handle the residual low frequency content with airflow
434 float idleAirmass = totalAirmass - timingAirmass;
435 float airflowKgPerH = 3.6 * 0.001 * idleAirmass * rpm / 60 * engineConfiguration->cylindersCount / 2;
436 idleTargetFlow = airflowKgPerH;
437
438 // Convert from desired flow -> idle valve position
439 float idlePos = interpolate2d(
440 airflowKgPerH,
441 engineConfiguration->idleFlowEstimateFlow,
442 engineConfiguration->idleFlowEstimatePosition
443 );
444
445 iacPosition = idlePos;
446 }
447
448 1123 currentIdlePosition = iacPosition;
449
450
1/1
✓ Branch 1 taken 1123 times.
1123 bool acActive = engine->module<AcController>().unmock().acButtonState;
451
1/1
✓ Branch 1 taken 1123 times.
1123 bool fan1Active = enginePins.fanRelay.getLogicValue();
452
1/1
✓ Branch 1 taken 1123 times.
1123 bool fan2Active = enginePins.fanRelay2.getLogicValue();
453
2/2
✓ Branch 2 taken 1123 times.
✓ Branch 5 taken 1123 times.
1123 updateLtit(rpm, clt, acActive, fan1Active, fan2Active, getIdlePid()->getIntegration());
454
455 2246 return iacPosition;
456 #else
457 return 0;
458 #endif // EFI_SHAFT_POSITION_INPUT
459
460 }
461
462 1120 void IdleController::onFastCallback() {
463 #if EFI_SHAFT_POSITION_INPUT
464 1120 float position = getIdlePosition(engine->triggerCentral.instantRpm.getInstantRpm());
465 1120 applyIACposition(position);
466 // huh: why not onIgnitionStateChanged?
467 1120 engine->m_ltit.checkIfShouldSave();
468 #endif // EFI_SHAFT_POSITION_INPUT
469 1120 }
470
471 130 void IdleController::onEngineStop() {
472 130 getIdlePid()->reset();
473 130 }
474
475 219 void IdleController::onConfigurationChange(engine_configuration_s const * previousConfiguration) {
476 #if ! EFI_UNIT_TEST
477 shouldResetPid = !previousConfiguration || !getIdlePid()->isSame(&previousConfiguration->idleRpmPid);
478 mustResetPid = shouldResetPid;
479 #endif
480 219 }
481
482 588 void IdleController::init() {
483 588 shouldResetPid = false;
484 588 mightResetPid = false;
485 588 wasResetPid = false;
486 588 m_timingPid.initPidClass(&engineConfiguration->idleTimingPid);
487 588 m_timingHpf.configureHighpass(20, 1);
488 588 getIdlePid()->initPidClass(&engineConfiguration->idleRpmPid);
489 588 engine->m_ltit.loadLtitFromConfig();
490 588 }
491
492 1123 void IdleController::updateLtit(float rpm, float clt, bool acActive, bool fan1Active, bool fan2Active, float idleIntegral) {
493
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1123 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1123 times.
1123 if (engineConfiguration->ltitEnabled) {
494 engine->m_ltit.update(rpm, clt, acActive, fan1Active, fan2Active, idleIntegral);
495 }
496 1123 }
497
498 1 void IdleController::onIgnitionStateChanged(bool ignitionOn) {
499 1 engine->m_ltit.onIgnitionStateChanged(ignitionOn);
500 1 }
501
502 #endif /* EFI_IDLE_CONTROL */
503