rusEFI
The most advanced open source ECU
Loading...
Searching...
No Matches
idle_thread.cpp
Go to the documentation of this file.
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
25using enum idle_mode_e;
26
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
36
38 float rpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit;
39 float entryRpm = target + rpmUpperLimit;
40
41 // Higher exit than entry to add some hysteresis to avoid bouncing around upper threshold
42 float exitRpm = target + 1.5 * rpmUpperLimit;
43
44 // Ramp the target down from the transition RPM to normal over a few seconds
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,
51 timeSinceIdleEntry
52 );
53 }
54
55 idleTarget = target;
56 idleEntryRpm = entryRpm;
57 idleExitRpm = exitRpm;
58 return { target, entryRpm, exitRpm };
59}
60
61IIdleController::Phase IdleController::determinePhase(float rpm, IIdleController::TargetInfo targetRpm, SensorResult tps, float vss, float crankingTaperFraction) {
62#if EFI_SHAFT_POSITION_INPUT
64 return Phase::Cranking;
65 }
66 badTps = !tps;
67
68 if (badTps) {
69 // If the TPS has failed, assume the engine is running
70 return Phase::Running;
71 }
72
73 // if throttle pressed, we're out of the idle corner
75 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 if (rpm > targetRpm.IdleExitRpm) {
81 looksLikeCoasting = true;
82 } else if (rpm < targetRpm.IdleEntryRpm) {
83 looksLikeCoasting = false;
84 }
85
86 looksLikeCrankToIdle = crankingTaperFraction < 1;
88 return Phase::Coasting;
89 }
90
91 // If the vehicle is moving too quickly, disable CL idle
92 auto maxVss = engineConfiguration->maxIdleVss;
93 looksLikeRunning = maxVss != 0 && vss > maxVss;
94 if (looksLikeRunning) {
95 return Phase::Running;
96 }
97
98 // If still in the cranking taper, disable closed loop idle
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 if (m_crankTaperEndTime == 0.0f) {
108 }
109
110 // No other conditions met, we are idling!
111 return Phase::Idling;
112}
113
116 return (float)engine->rpmCalculator.getRevolutionCounterSinceStart() / taperDuration;
117}
118
120 return interpolate2d(clt, config->cltCrankingCorrBins, config->cltCrankingCorr);
121}
122
124 float running = interpolate3d(
128 );
129
130 // Now we bump it by the AC/fan amount if necessary
131 if (engine->module<AcController>().unmock().acButtonState && (phase == Phase::Idling || phase == Phase::CrankToIdleTaper)) {
133 }
134
137
138 running += luaAdd;
139
140#if EFI_ANTILAG_SYSTEM
143}
144#endif /* EFI_ANTILAG_SYSTEM */
145
146 // 'dashpot' (hold+decay) logic for coasting->idle
147 float tpsForTaper = tps.value_or(0);
148 efitimeus_t nowUs = getTimeNowUs();
149 if (phase == Phase::Running) {
150 lastTimeRunningUs = nowUs;
151 }
152 // imitate a slow pedal release for TPS taper (to avoid engine stalls)
153 if (tpsForTaper <= engineConfiguration->idlePidDeactivationTpsThreshold) {
154 // make sure the time is not zero
155 float timeSinceRunningPhaseSecs = (nowUs - lastTimeRunningUs + 1) / US_PER_SECOND_F;
156 // we shift the time to implement the hold correction (time can be negative)
157 float timeSinceRunningAfterHoldSecs = timeSinceRunningPhaseSecs - engineConfiguration->iacByTpsHoldTime;
158 // implement the decay correction (from tpsForTaper to 0)
159 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
165 0, 0,
167 tpsForTaper);
168
170
171 float airTaperRpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit;
174 airTaperRpmUpperLimit, engineConfiguration->airByRpmTaper,
175 rpm);
176
178
179 // are we clamping open loop part separately? should not we clamp once we have total value?
180 return clampPercentValue(running);
181}
182
183percent_t IdleController::getOpenLoop(Phase phase, float rpm, float clt, SensorResult tps, float crankingTaperFraction) {
184 percent_t crankingValvePosition = getCrankingOpenLoop(clt);
185
186 isCranking = phase == Phase::Cranking;
188
189 // if we're cranking, nothing more to do.
190 if (isCranking) {
191 return crankingValvePosition;
192 }
193
194 // If coasting (and enabled), use the coasting position table instead of normal open loop
197 percent_t coastingPosition = interpolate2d(rpm, config->iacCoastingRpmBins, config->iacCoasting);
198
199 // Add A/C offset if the A/C is on during coasting
200 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 return clampPercentValue(coastingPosition);
207 }
208
209 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 return interpolateClamped(0, crankingValvePosition, 1, running, crankingTaperFraction);
214}
215
219
220float IdleController::getIdleTimingAdjustment(float rpm, float targetRpm, Phase phase) {
221 // if not enabled, do nothing
223 return 0;
224 }
225
226 // If not idling, do nothing
227 if (phase != Phase::Idling) {
229 return 0;
230 }
231
233 // Use interpolation for correction taper
235 }
236
239 } else {
240 // We're now in the idle mode, and RPM is inside the Timing-PID regulator work zone!
241 return m_timingPid.getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
242 }
243}
244
249
250/**
251 * @return idle valve position percentage for automatic closed loop mode
252 */
253float IdleController::getClosedLoop(IIdleController::Phase phase, float tpsPos, float rpm, float targetRpm) {
254 auto idlePid = getIdlePid();
255
256 if (shouldResetPid && !wasResetPid) {
257 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 if (needReset) {
261 idlePid->reset();
262 }
263 shouldResetPid = false;
264 wasResetPid = true;
265 }
266
267 // todo: move this to pid_s one day
270
271 efitimeus_t nowUs = getTimeNowUs();
272
274
275 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 shouldResetPid = true;
279 idleState = TPS_THRESHOLD;
280
281 // We aren't idling, so don't apply any correction. A positive correction could inhibit a return to idle.
283 return 0;
284 }
285
286 bool acToggleJustTouched = engine->module<AcController>().unmock().timeSinceStateChange.getElapsedSeconds() < 0.5f /*second*/;
287 // check if within the dead zone
288 isInDeadZone = !acToggleJustTouched && std::abs(rpm - targetRpm) <= engineConfiguration->idlePidRpmDeadZone;
289 if (isInDeadZone) {
290 idleState = RPM_DEAD_ZONE;
291 // current RPM is close enough, no need to change anything
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 percent_t errorAmpCoef = 1.0f;
298 if (rpm < targetRpm) {
299 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 if (wasResetPid) {
305 wasResetPid = false;
306 }
307 // increase the errorAmpCoef slowly to restore the process correctly after the PID reset
308 // todo: move restoreAfterPidResetTimeUs to idle?
309 efitimeus_t timeSincePidResetUs = nowUs - restoreAfterPidResetTimeUs;
310 // todo: add 'pidAfterResetDampingPeriodMs' setting
311 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 idlePid->setErrorAmplification(errorAmpCoef);
314
315 percent_t newValue = idlePid->getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
316 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 mightResetPid = true;
320
321 // Apply PID Multiplier if used
323 float engineLoad = getFuelingLoad();
324 float multCoef = interpolate3d(
326 config->iacPidMultLoadBins, engineLoad,
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.
338
339 m_lastAutomaticPosition = newValue;
340 return newValue;
341}
342
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 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 */
360
361 // On failed sensor, use 0 deg C - should give a safe highish idle
364
365 // Compute the target we're shooting for
366 auto targetRpm = getTargetRpm(clt);
367 m_lastTargetRpm = targetRpm.ClosedLoopTarget;
368
369 // Determine cranking taper (modeled flow does no taper of open loop)
370 float crankingTaper = useModeledFlow ? 1 : getCrankingTaperFraction(clt);
371
372 // Determine what operation phase we're in - idling or not
373 float vehicleSpeed = Sensor::getOrZero(SensorType::VehicleSpeed);
374 auto phase = determinePhase(rpm, targetRpm, tps, vehicleSpeed, crankingTaper);
375
376 // update TS flag
377 isIdling = (phase == Phase::Idling) || (phase == Phase::CrankToIdleTaper);
378
379 if (phase != m_lastPhase && phase == Phase::Idling) {
380 // Just entered idle, reset timer
381 m_timeInIdlePhase.reset();
382 }
383
384 m_lastPhase = phase;
385
387
388 // Always apply open loop correction
389 percent_t iacPosition = getOpenLoop(phase, rpm, clt, tps, crankingTaper);
390 baseIdlePosition = iacPosition;
391 // Force closed loop operation for modeled flow
392 auto idleMode = useModeledFlow ? IM_AUTO : engineConfiguration->idleMode;
393
394 // If TPS is working and automatic mode enabled, add any closed loop correction
395 if (tps.Valid && idleMode == IM_AUTO) {
396 if (useModeledFlow && phase != Phase::Idling) {
397 auto idlePid = getIdlePid();
398 idlePid->reset();
399 }
400 auto closedLoop = getClosedLoop(phase, tps.Value, rpm, targetRpm.ClosedLoopTarget);
401 idleClosedLoop = closedLoop;
402 iacPosition += closedLoop;
403 } else {
404 isIdleClosedLoop = false;
405 }
406
407 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
414
415
416 extern StepperMotor iacMotor;
418#endif /* EFI_TUNER_STUDIO */
419 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
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,
441 );
442
443 iacPosition = idlePos;
444 }
445
446 currentIdlePosition = iacPosition;
447
448 bool acActive = engine->module<AcController>().unmock().acButtonState;
449 bool fan1Active = enginePins.fanRelay.getLogicValue();
450 bool fan2Active = enginePins.fanRelay2.getLogicValue();
451 updateLtit(rpm, clt, acActive, fan1Active, fan2Active, getIdlePid()->getIntegration());
452
453 return iacPosition;
454#else
455 return 0;
456#endif // EFI_SHAFT_POSITION_INPUT
457
458}
459
461#if EFI_SHAFT_POSITION_INPUT
463 applyIACposition(position);
464 // huh: why not onIgnitionStateChanged?
466#endif // EFI_SHAFT_POSITION_INPUT
467}
468
472
474#if ! EFI_UNIT_TEST
475 shouldResetPid = !previousConfiguration || !getIdlePid()->isSame(&previousConfiguration->idleRpmPid);
476#endif
477}
478
488
489void IdleController::updateLtit(float rpm, float clt, bool acActive, bool fan1Active, bool fan2Active, float idleIntegral) {
491 engine->m_ltit.update(rpm, clt, acActive, fan1Active, fan2Active, idleIntegral);
492 }
493}
494
497}
498
499#endif /* EFI_IDLE_CONTROL */
Timer timeSinceStateChange
Definition ac_control.h:16
float filter(float input)
Definition biquad.cpp:74
void configureHighpass(float samplingFrequency, float cutoffFrequency, float Q=0.54f)
Definition biquad.cpp:61
FuelComputer fuelComputer
Definition engine.h:139
TriggerCentral triggerCentral
Definition engine.h:318
LongTermIdleTrim m_ltit
Definition engine.h:135
RpmCalculator rpmCalculator
Definition engine.h:306
AntilagSystemBase antilagController
Definition engine.h:228
efitimeus_t timeToStopIdleTest
Definition engine.h:351
TunerStudioOutputChannels outputChannels
Definition engine.h:109
constexpr auto & module()
Definition engine.h:200
RegisteredOutputPin fanRelay
Definition efi_gpio.h:86
RegisteredOutputPin fanRelay2
Definition efi_gpio.h:87
float m_modeledFlowIdleTiming
PidIndustrial industrialWithOverrideIdlePid
void onFastCallback() override final
float getCrankingTaperFraction(float clt) const override
float getClosedLoop(IIdleController::Phase phase, float tpsPos, float rpm, float targetRpm) override
void onConfigurationChange(engine_configuration_s const *previousConfig) override final
Phase determinePhase(float rpm, TargetInfo targetRpm, SensorResult tps, float vss, float crankingTaperFraction) override
float m_idleTimingSoftEntryEndTime
Pid * getIdlePid()
float m_lastAutomaticPosition
float getIdlePosition(float rpm)
Timer m_timeInIdlePhase
void onEngineStop() override final
float getIdleTimingAdjustment(float rpm) override
efitimeus_t restoreAfterPidResetTimeUs
percent_t getOpenLoop(Phase phase, float rpm, float clt, SensorResult tps, float crankingTaperFraction) override
TargetInfo getTargetRpm(float clt) override
void onIgnitionStateChanged(bool ignitionOn) override
efitimeus_t lastTimeRunningUs
percent_t getCrankingOpenLoop(float clt) const override
float m_crankTaperEndTime
void updateLtit(float rpm, float clt, bool acActive, bool fan1Active, bool fan2Active, float idleIntegral)
percent_t getRunningOpenLoop(IIdleController::Phase phase, float rpm, float clt, SensorResult tps) override
virtual void loadLtitFromConfig()
void onIgnitionStateChanged(bool ignitionOn)
void update(float rpm, float clt, bool acActive, bool fan1Active, bool fan2Active, float idleIntegral)
bool getLogicValue() const
Definition efi_gpio.cpp:667
float iTermMax
Definition efi_pid.h:69
float iTermMin
Definition efi_pid.h:68
bool isSame(const pid_s *parameters) const
Definition efi_pid.cpp:39
void setErrorAmplification(float coef)
Definition efi_pid.cpp:138
void postState(pid_status_s &pidStatus) const
Definition efi_pid.cpp:144
virtual void reset()
Definition efi_pid.cpp:103
void initPidClass(pid_s *parameters)
Definition efi_pid.cpp:24
float getOutput(float target, float input)
Definition efi_pid.cpp:56
float antiwindupFreq
Definition efi_pid.h:123
float derivativeFilterLoss
Definition efi_pid.h:124
bool isRunning() const
uint32_t getRevolutionCounterSinceStart(void) const
virtual SensorResult get() const =0
static float getOrZero(SensorType type)
Definition sensor.h:83
float getTargetPosition() const
Definition stepper.cpp:14
InstantRpmCalculator instantRpm
EnginePins enginePins
Definition efi_gpio.cpp:24
float interpolateClamped(float x1, float y1, float x2, float y2, float x)
efitimeus_t getTimeNowUs()
Definition efitime.cpp:26
static EngineAccessor engine
Definition engine.h:413
static constexpr persistent_config_s * config
static constexpr engine_configuration_s * engineConfiguration
float getFuelingLoad()
Idle Air Control valve hardware.
static void finishIdleTestIfNeeded()
Idle Valve Control thread.
void applyIACposition(percent_t position)
percent_t getIdlePosition()
static CCM_OPTIONAL FunctionalSensor clt(SensorType::Clt, MS2NT(10))
static FuncSensPair idlePos(PACK_MULT_VOLTAGE, SensorType::IdlePosition)
idle_mode_e
expected< float > SensorResult
Definition sensor.h:46
@ DriverThrottleIntent
running("running", SensorCategory.SENSOR_INPUTS, FieldType.INT, 888, 1.0, -1.0, -1.0, "")
percent_t currentIdlePosition
percent_t baseIdlePosition
scaled_channel< uint16_t, 100, 1 > idleTargetFlow
idle_state_e idleState
float cltIdleCorrTable[CLT_IDLE_TABLE_RPM_SIZE][CLT_IDLE_TABLE_CLT_SIZE]
scaled_channel< int16_t, 1, 1 > cltIdleRpmBins[CLT_CURVE_SIZE]
scaled_channel< uint8_t, 1, 10 > iacPidMultRpmBins[IAC_PID_MULT_RPM_SIZE]
uint16_t afterCrankingIACtaperDuration[CLT_CRANKING_TAPER_CURVE_SIZE]
scaled_channel< uint8_t, 1, 100 > iacCoastingRpmBins[CLT_CURVE_SIZE]
scaled_channel< uint8_t, 20, 1 > iacPidMultTable[IAC_PID_MULT_SIZE][IAC_PID_MULT_SIZE]
float afterCrankingIACtaperDurationBins[CLT_CRANKING_TAPER_CURVE_SIZE]
scaled_channel< uint8_t, 1, 100 > rpmIdleCorrBins[CLT_IDLE_TABLE_RPM_SIZE]
scaled_channel< uint8_t, 2, 1 > iacCoasting[CLT_CURVE_SIZE]
scaled_channel< uint8_t, 1, 20 > cltIdleRpm[CLT_CURVE_SIZE]