GCC Code Coverage Report


Directory: ./
File: firmware/controllers/actuators/idle_thread.cpp
Date: 2025-10-24 14:26:41
Coverage Exec Excl Total
Lines: 86.9% 193 0 222
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 1105 IIdleController::TargetInfo IdleController::getTargetRpm(float clt) {
28 1105 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 1105 times.
1105 targetRpmAc = engine->module<AcController>().unmock().acButtonState ? engineConfiguration->acIdleRpmTarget : 0;
36
37
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1105 times.
1105 float target = (targetRpmByClt < targetRpmAc) ? targetRpmAc : targetRpmByClt;
38 1105 float rpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit;
39 1105 float entryRpm = target + rpmUpperLimit;
40
41 // Higher exit than entry to add some hysteresis to avoid bouncing around upper threshold
42 1105 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 1105 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1105 times.
1105 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 1105 idleTarget = target;
56 1105 idleEntryRpm = entryRpm;
57 1105 idleExitRpm = exitRpm;
58 1105 return { target, entryRpm, exitRpm };
59 }
60
61 1111 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 1046 times.
✓ Branch 2 taken 65 times.
2/2
✓ Decision 'true' taken 1046 times.
✓ Decision 'false' taken 65 times.
1111 if (!engine->rpmCalculator.isRunning()) {
64 1046 return Phase::Cranking;
65 }
66 65 badTps = !tps;
67
68
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 8 times.
2/2
✓ Decision 'true' taken 57 times.
✓ Decision 'false' taken 8 times.
65 if (badTps) {
69 // If the TPS has failed, assume the engine is running
70 57 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 1105 float IdleController::getCrankingTaperFraction(float clt) const {
115 1105 float taperDuration = interpolate2d(clt, config->afterCrankingIACtaperDurationBins, config->afterCrankingIACtaperDuration);
116 1105 return (float)engine->rpmCalculator.getRevolutionCounterSinceStart() / taperDuration;
117 }
118
119 1105 float IdleController::getCrankingOpenLoop(float clt) const {
120 1105 return interpolate2d(clt, config->cltCrankingCorrBins, config->cltCrankingCorr);
121 }
122
123 76 percent_t IdleController::getRunningOpenLoop(IIdleController::Phase phase, float rpm, float clt, SensorResult tps) {
124 152 float running = interpolate3d(
125 76 config->cltIdleCorrTable,
126 76 config->rpmIdleCorrBins, m_lastTargetRpm,
127 76 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 72 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 73 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 73 times.
76 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 74 times.
76 running += enginePins.fanRelay.getLogicValue() ? engineConfiguration->fan1ExtraIdle : 0;
136
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 74 times.
76 running += enginePins.fanRelay2.getLogicValue() ? engineConfiguration->fan2ExtraIdle : 0;
137
138 76 running += luaAdd;
139
140 #if EFI_ANTILAG_SYSTEM
141
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 76 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 76 times.
76 if (engine->antilagController.isAntilagCondition) {
142 running += engineConfiguration->ALSIdleAdd;
143 }
144 #endif /* EFI_ANTILAG_SYSTEM */
145
146 // 'dashpot' (hold+decay) logic for coasting->idle
147 76 float tpsForTaper = tps.value_or(0);
148 76 efitimeus_t nowUs = getTimeNowUs();
149
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 19 times.
2/2
✓ Decision 'true' taken 57 times.
✓ Decision 'false' taken 19 times.
76 if (phase == Phase::Running) {
150 57 lastTimeRunningUs = nowUs;
151 }
152 // imitate a slow pedal release for TPS taper (to avoid engine stalls)
153
2/2
✓ Branch 0 taken 74 times.
✓ Branch 1 taken 2 times.
2/2
✓ Decision 'true' taken 74 times.
✓ Decision 'false' taken 2 times.
76 if (tpsForTaper <= engineConfiguration->idlePidDeactivationTpsThreshold) {
154 // make sure the time is not zero
155 74 float timeSinceRunningPhaseSecs = (nowUs - lastTimeRunningUs + 1) / US_PER_SECOND_F;
156 // we shift the time to implement the hold correction (time can be negative)
157 74 float timeSinceRunningAfterHoldSecs = timeSinceRunningPhaseSecs - engineConfiguration->iacByTpsHoldTime;
158 // implement the decay correction (from tpsForTaper to 0)
159 74 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 152 iacByTpsTaper = interpolateClamped(
165 0, 0,
166 76 engineConfiguration->idlePidDeactivationTpsThreshold, engineConfiguration->iacByTpsTaper,
167 tpsForTaper);
168
169 76 running += iacByTpsTaper;
170
171 76 float airTaperRpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit;
172 152 iacByRpmTaper = interpolateClamped(
173 76 engineConfiguration->idlePidRpmUpperLimit, 0,
174 airTaperRpmUpperLimit, engineConfiguration->airByRpmTaper,
175 rpm);
176
177 76 running += iacByRpmTaper;
178
179 // are we clamping open loop part separately? should not we clamp once we have total value?
180 76 return clampPercentValue(running);
181 }
182
183 1112 percent_t IdleController::getOpenLoop(Phase phase, float rpm, float clt, SensorResult tps, float crankingTaperFraction) {
184 1112 percent_t crankingValvePosition = getCrankingOpenLoop(clt);
185
186 1112 isCranking = phase == Phase::Cranking;
187
5/6
✓ Branch 0 taken 1110 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 60 times.
✓ Branch 3 taken 1050 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 60 times.
1112 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 1046 times.
✓ Branch 1 taken 66 times.
2/2
✓ Decision 'true' taken 1046 times.
✓ Decision 'false' taken 66 times.
1112 if (isCranking) {
191 1046 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 64 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
66 isIacTableForCoasting = engineConfiguration->useIacTableForCoasting && isIdleCoasting;
196
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 64 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 64 times.
66 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 64 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 64 return interpolateClamped(0, crankingValvePosition, 1, running, crankingTaperFraction);
214 }
215
216 941 float IdleController::getIdleTimingAdjustment(float rpm) {
217 941 return getIdleTimingAdjustment(rpm, m_lastTargetRpm, m_lastPhase);
218 }
219
220 951 float IdleController::getIdleTimingAdjustment(float rpm, float targetRpm, Phase phase) {
221 // if not enabled, do nothing
222
2/2
✓ Branch 0 taken 932 times.
✓ Branch 1 taken 19 times.
2/2
✓ Decision 'true' taken 932 times.
✓ Decision 'false' taken 19 times.
951 if (!engineConfiguration->useIdleTimingPidControl) {
223 932 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 1104 static void finishIdleTestIfNeeded() {
246
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 1104 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 1104 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1104 times.
1104 if (engine->timeToStopIdleTest != 0 && getTimeNowUs() > engine->timeToStopIdleTest)
247 engine->timeToStopIdleTest = 0;
248 1104 }
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 || shouldResetPid;
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 }
263 2 shouldResetPid = false;
264 2 wasResetPid = true;
265 }
266
267 // todo: move this to pid_s one day
268 12 industrialWithOverrideIdlePid.antiwindupFreq = engineConfiguration->idle_antiwindupFreq;
269 12 industrialWithOverrideIdlePid.derivativeFilterLoss = engineConfiguration->idle_derivativeFilterLoss;
270
271 12 efitimeus_t nowUs = getTimeNowUs();
272
273 12 isIdleClosedLoop = phase == IIdleController::Phase::Idling;
274
275
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) {
276 // Don't store old I and D terms if PID doesn't work anymore.
277 // Otherwise they will affect the idle position much later, when the throttle is closed.¿
278 3 shouldResetPid = true;
279 3 idleState = TPS_THRESHOLD;
280
281 // We aren't idling, so don't apply any correction. A positive correction could inhibit a return to idle.
282 3 m_lastAutomaticPosition = 0;
283 3 return 0;
284 }
285
286 9 bool acToggleJustTouched = engine->module<AcController>().unmock().timeSinceStateChange.getElapsedSeconds() < 0.5f /*second*/;
287 // check if within the dead zone
288
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;
289
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) {
290 3 idleState = RPM_DEAD_ZONE;
291 // current RPM is close enough, no need to change anything
292 3 return m_lastAutomaticPosition;
293 }
294
295 // When rpm < targetRpm, there's a risk of dropping RPM too low - and the engine dies out.
296 // So PID reaction should be increased by adding extra percent to PID-error:
297 6 percent_t errorAmpCoef = 1.0f;
298
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) {
299 4 errorAmpCoef += (float)engineConfiguration->pidExtraForLowRpm / PERCENT_MULT;
300 }
301
302 // if PID was previously reset, we store the time when it turned on back (see errorAmpCoef correction below)
303
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) {
304 1 restoreAfterPidResetTimeUs = nowUs;
305 1 wasResetPid = false;
306 }
307 // increase the errorAmpCoef slowly to restore the process correctly after the PID reset
308 // todo: move restoreAfterPidResetTimeUs to idle?
309 6 efitimeus_t timeSincePidResetUs = nowUs - restoreAfterPidResetTimeUs;
310 // todo: add 'pidAfterResetDampingPeriodMs' setting
311 6 errorAmpCoef = interpolateClamped(0, 0, MS2US(/*engineConfiguration->pidAfterResetDampingPeriodMs*/1000), errorAmpCoef, timeSincePidResetUs);
312 // If errorAmpCoef > 1.0, then PID thinks that RPM is lower than it is, and controls IAC more aggressively
313 6 idlePid->setErrorAmplification(errorAmpCoef);
314
315 6 percent_t newValue = idlePid->getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
316 6 idleState = PID_VALUE;
317
318 // the state of PID has been changed, so we might reset it now, but only when needed (see idlePidDeactivationTpsThreshold)
319 6 mightResetPid = true;
320
321 // Apply PID Multiplier if used
322
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) {
323 float engineLoad = getFuelingLoad();
324 float multCoef = interpolate3d(
325 config->iacPidMultTable,
326 config->iacPidMultLoadBins, engineLoad,
327 config->iacPidMultRpmBins, rpm
328 );
329 // PID can be completely disabled of multCoef==0, or it just works as usual if multCoef==1
330 newValue = interpolateClamped(0, 0, 1, newValue, multCoef);
331 }
332
333 // Apply PID Deactivation Threshold as a smooth taper for TPS transients.
334 // if tps==0 then PID just works as usual, or we completely disable it if tps>=threshold
335 // TODO: should we just remove this? It reduces the gain if your zero throttle stop isn't perfect,
336 // which could give unstable results.
337 6 newValue = interpolateClamped(0, newValue, engineConfiguration->idlePidDeactivationTpsThreshold, 0, tpsPos);
338
339 6 m_lastAutomaticPosition = newValue;
340 6 return newValue;
341 }
342
343 1104 float IdleController::getIdlePosition(float rpm) {
344 #if EFI_SHAFT_POSITION_INPUT
345
346 // Simplify hardware CI: we borrow the idle valve controller as a PWM source for various stimulation tasks
347 // The logic in this function is solidly unit tested, so it's not necessary to re-test the particulars on real hardware.
348 #ifdef HARDWARE_CI
349 return config->cltIdleCorrTable[0][0];
350 #endif
351
352 1104 bool useModeledFlow = engineConfiguration->modeledFlowIdle;
353
354 /*
355 * Here we have idle logic thread - actual stepper movement is implemented in a separate
356 * working thread see stepper.cpp
357 */
358 1104 getIdlePid()->iTermMin = engineConfiguration->idlerpmpid_iTermMin;
359 1104 getIdlePid()->iTermMax = engineConfiguration->idlerpmpid_iTermMax;
360
361 // On failed sensor, use 0 deg C - should give a safe highish idle
362
1/1
✓ Branch 1 taken 1104 times.
1104 float clt = Sensor::getOrZero(SensorType::Clt);
363
1/1
✓ Branch 2 taken 1104 times.
1104 auto tps = Sensor::get(SensorType::DriverThrottleIntent);
364
365 // Compute the target we're shooting for
366
1/1
✓ Branch 2 taken 1104 times.
1104 auto targetRpm = getTargetRpm(clt);
367 1104 m_lastTargetRpm = targetRpm.ClosedLoopTarget;
368
369 // Determine cranking taper (modeled flow does no taper of open loop)
370
2/3
✗ Branch 0 not taken.
✓ Branch 1 taken 1104 times.
✓ Branch 3 taken 1104 times.
1104 float crankingTaper = useModeledFlow ? 1 : getCrankingTaperFraction(clt);
371
372 // Determine what operation phase we're in - idling or not
373
1/1
✓ Branch 1 taken 1104 times.
1104 float vehicleSpeed = Sensor::getOrZero(SensorType::VehicleSpeed);
374
1/1
✓ Branch 1 taken 1104 times.
1104 auto phase = determinePhase(rpm, targetRpm, tps, vehicleSpeed, crankingTaper);
375
376 // update TS flag
377
3/4
✓ Branch 0 taken 1101 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1101 times.
1104 isIdling = (phase == Phase::Idling) || (phase == Phase::CrankToIdleTaper);
378
379
4/4
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 1053 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 48 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 1101 times.
1104 if (phase != m_lastPhase && phase == Phase::Idling) {
380 // Just entered idle, reset timer
381
1/1
✓ Branch 1 taken 3 times.
3 m_timeInIdlePhase.reset();
382 }
383
384 1104 m_lastPhase = phase;
385
386
1/1
✓ Branch 1 taken 1104 times.
1104 finishIdleTestIfNeeded();
387
388 // Always apply open loop correction
389
1/1
✓ Branch 1 taken 1104 times.
1104 percent_t iacPosition = getOpenLoop(phase, rpm, clt, tps, crankingTaper);
390 1104 baseIdlePosition = iacPosition;
391 // Force closed loop operation for modeled flow
392
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1104 times.
1104 auto idleMode = useModeledFlow ? IM_AUTO : engineConfiguration->idleMode;
393
394 // If TPS is working and automatic mode enabled, add any closed loop correction
395
4/4
✓ Branch 0 taken 163 times.
✓ Branch 1 taken 941 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 161 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 1102 times.
1104 if (tps.Valid && idleMode == IM_AUTO) {
396
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) {
397 auto idlePid = getIdlePid();
398 idlePid->reset();
399 }
400
1/1
✓ Branch 1 taken 2 times.
2 auto closedLoop = getClosedLoop(phase, tps.Value, rpm, targetRpm.ClosedLoopTarget);
401 2 idleClosedLoop = closedLoop;
402 2 iacPosition += closedLoop;
403 2 } else {
404 1102 isIdleClosedLoop = false;
405 }
406
407
1/1
✓ Branch 1 taken 1104 times.
1104 iacPosition = clampPercentValue(iacPosition);
408
409 // todo: while is below disabled for unit tests?
410 #if EFI_TUNER_STUDIO && (EFI_PROD_CODE || EFI_SIMULATOR)
411
412 // see also tsOutputChannels->idlePosition
413 getIdlePid()->postState(engine->outputChannels.idleStatus);
414
415
416 extern StepperMotor iacMotor;
417 engine->outputChannels.idleStepperTargetPosition = iacMotor.getTargetPosition();
418 #endif /* EFI_TUNER_STUDIO */
419
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1104 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1104 times.
1104 if (useModeledFlow && phase != Phase::Cranking) {
420 float totalAirmass = 0.01 * iacPosition * engineConfiguration->idleMaximumAirmass;
421 idleTargetAirmass = totalAirmass;
422
423 bool shouldAdjustTiming = engineConfiguration->useIdleTimingPidControl && phase == Phase::Idling;
424
425 // extract hiqh frequency content to be handled by timing
426 float timingAirmass = shouldAdjustTiming ? m_timingHpf.filter(totalAirmass) : 0;
427
428 // Convert from airmass delta -> timing
429 m_modeledFlowIdleTiming = interpolate2d(timingAirmass, engineConfiguration->airmassToTimingBins, engineConfiguration->airmassToTimingValues);
430
431 // Handle the residual low frequency content with airflow
432 float idleAirmass = totalAirmass - timingAirmass;
433 float airflowKgPerH = 3.6 * 0.001 * idleAirmass * rpm / 60 * engineConfiguration->cylindersCount / 2;
434 idleTargetFlow = airflowKgPerH;
435
436 // Convert from desired flow -> idle valve position
437 float idlePos = interpolate2d(
438 airflowKgPerH,
439 engineConfiguration->idleFlowEstimateFlow,
440 engineConfiguration->idleFlowEstimatePosition
441 );
442
443 iacPosition = idlePos;
444 }
445
446 1104 currentIdlePosition = iacPosition;
447
448
1/1
✓ Branch 1 taken 1104 times.
1104 bool acActive = engine->module<AcController>().unmock().acButtonState;
449
1/1
✓ Branch 1 taken 1104 times.
1104 bool fan1Active = enginePins.fanRelay.getLogicValue();
450
1/1
✓ Branch 1 taken 1104 times.
1104 bool fan2Active = enginePins.fanRelay2.getLogicValue();
451
2/2
✓ Branch 2 taken 1104 times.
✓ Branch 5 taken 1104 times.
1104 updateLtit(rpm, clt, acActive, fan1Active, fan2Active, getIdlePid()->getIntegration());
452
453 2208 return iacPosition;
454 #else
455 return 0;
456 #endif // EFI_SHAFT_POSITION_INPUT
457
458 }
459
460 1101 void IdleController::onFastCallback() {
461 #if EFI_SHAFT_POSITION_INPUT
462 1101 float position = getIdlePosition(engine->triggerCentral.instantRpm.getInstantRpm());
463 1101 applyIACposition(position);
464 // huh: why not onIgnitionStateChanged?
465 1101 engine->m_ltit.checkIfShouldSave();
466 #endif // EFI_SHAFT_POSITION_INPUT
467 1101 }
468
469 131 void IdleController::onEngineStop() {
470 131 getIdlePid()->reset();
471 131 }
472
473 221 void IdleController::onConfigurationChange(engine_configuration_s const * previousConfiguration) {
474 #if ! EFI_UNIT_TEST
475 shouldResetPid = !previousConfiguration || !getIdlePid()->isSame(&previousConfiguration->idleRpmPid);
476 #endif
477 221 }
478
479 589 void IdleController::init() {
480 589 shouldResetPid = false;
481 589 mightResetPid = false;
482 589 wasResetPid = false;
483 589 m_timingPid.initPidClass(&engineConfiguration->idleTimingPid);
484 589 m_timingHpf.configureHighpass(20, 1);
485 589 getIdlePid()->initPidClass(&engineConfiguration->idleRpmPid);
486 589 engine->m_ltit.loadLtitFromConfig();
487 589 }
488
489 1104 void IdleController::updateLtit(float rpm, float clt, bool acActive, bool fan1Active, bool fan2Active, float idleIntegral) {
490
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1104 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1104 times.
1104 if (engineConfiguration->ltitEnabled) {
491 engine->m_ltit.update(rpm, clt, acActive, fan1Active, fan2Active, idleIntegral);
492 }
493 1104 }
494
495 1 void IdleController::onIgnitionStateChanged(bool ignitionOn) {
496 1 engine->m_ltit.onIgnitionStateChanged(ignitionOn);
497 1 }
498
499 #endif /* EFI_IDLE_CONTROL */
500