| 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 |