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 |