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 return interpolate2d(rpm, config->iacCoastingRpmBins, config->iacCoasting);
198 }
199
200 percent_t running = getRunningOpenLoop(phase, rpm, clt, tps);
201
202 // Interpolate between cranking and running over a short time
203 // This clamps once you fall off the end, so no explicit check for >1 required
204 return interpolateClamped(0, crankingValvePosition, 1, running, crankingTaperFraction);
205}
206
210
211float IdleController::getIdleTimingAdjustment(float rpm, float targetRpm, Phase phase) {
212 // if not enabled, do nothing
214 return 0;
215 }
216
217 // If not idling, do nothing
218 if (phase != Phase::Idling) {
220 return 0;
221 }
222
224 // Use interpolation for correction taper
226 }
227
230 } else {
231 // We're now in the idle mode, and RPM is inside the Timing-PID regulator work zone!
232 return m_timingPid.getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
233 }
234}
235
240
241/**
242 * @return idle valve position percentage for automatic closed loop mode
243 */
244float IdleController::getClosedLoop(IIdleController::Phase phase, float tpsPos, float rpm, float targetRpm) {
245 auto idlePid = getIdlePid();
246
247 if (shouldResetPid && !wasResetPid) {
248 needReset = idlePid->getIntegration() <= 0 || mustResetPid;
249 // this is not-so valid since we have open loop first for this?
250 // we reset only if I-term is negative, because the positive I-term is good - it keeps RPM from dropping too low
251 if (needReset) {
252 idlePid->reset();
253 mustResetPid = false;
254 }
255 shouldResetPid = false;
256 wasResetPid = true;
257 }
258
259 // todo: move this to pid_s one day
262
263 efitimeus_t nowUs = getTimeNowUs();
264
266
267 if (!isIdleClosedLoop) {
268 // Don't store old I and D terms if PID doesn't work anymore.
269 // Otherwise they will affect the idle position much later, when the throttle is closed.¿
270 shouldResetPid = true;
271 mustResetPid = true;
272 idleState = TPS_THRESHOLD;
273
274 // We aren't idling, so don't apply any correction. A positive correction could inhibit a return to idle.
276 return 0;
277 }
278
279 bool acToggleJustTouched = engine->module<AcController>().unmock().timeSinceStateChange.getElapsedSeconds() < 0.5f /*second*/;
280 // check if within the dead zone
281 isInDeadZone = !acToggleJustTouched && std::abs(rpm - targetRpm) <= engineConfiguration->idlePidRpmDeadZone;
282 if (isInDeadZone) {
283 idleState = RPM_DEAD_ZONE;
284 // current RPM is close enough, no need to change anything
286 }
287
288 // When rpm < targetRpm, there's a risk of dropping RPM too low - and the engine dies out.
289 // So PID reaction should be increased by adding extra percent to PID-error:
290 percent_t errorAmpCoef = 1.0f;
291 if (rpm < targetRpm) {
292 errorAmpCoef += (float)engineConfiguration->pidExtraForLowRpm / PERCENT_MULT;
293 }
294
295 // if PID was previously reset, we store the time when it turned on back (see errorAmpCoef correction below)
296 if (wasResetPid) {
298 wasResetPid = false;
299 }
300 // increase the errorAmpCoef slowly to restore the process correctly after the PID reset
301 // todo: move restoreAfterPidResetTimeUs to idle?
302 efitimeus_t timeSincePidResetUs = nowUs - restoreAfterPidResetTimeUs;
303 // todo: add 'pidAfterResetDampingPeriodMs' setting
304 errorAmpCoef = interpolateClamped(0, 0, MS2US(/*engineConfiguration->pidAfterResetDampingPeriodMs*/1000), errorAmpCoef, timeSincePidResetUs);
305 // If errorAmpCoef > 1.0, then PID thinks that RPM is lower than it is, and controls IAC more aggressively
306 idlePid->setErrorAmplification(errorAmpCoef);
307
308 percent_t newValue = idlePid->getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
309 idleState = PID_VALUE;
310
311 // the state of PID has been changed, so we might reset it now, but only when needed (see idlePidDeactivationTpsThreshold)
312 mightResetPid = true;
313
314 // Apply PID Multiplier if used
316 float engineLoad = getFuelingLoad();
317 float multCoef = interpolate3d(
319 config->iacPidMultLoadBins, engineLoad,
321 );
322 // PID can be completely disabled of multCoef==0, or it just works as usual if multCoef==1
323 newValue = interpolateClamped(0, 0, 1, newValue, multCoef);
324 }
325
326 // Apply PID Deactivation Threshold as a smooth taper for TPS transients.
327 // if tps==0 then PID just works as usual, or we completely disable it if tps>=threshold
328 // TODO: should we just remove this? It reduces the gain if your zero throttle stop isn't perfect,
329 // which could give unstable results.
331
332 m_lastAutomaticPosition = newValue;
333 return newValue;
334}
335
337#if EFI_SHAFT_POSITION_INPUT
338
339 // Simplify hardware CI: we borrow the idle valve controller as a PWM source for various stimulation tasks
340 // The logic in this function is solidly unit tested, so it's not necessary to re-test the particulars on real hardware.
341 #ifdef HARDWARE_CI
342 return config->cltIdleCorrTable[0][0];
343 #endif
344
345 bool useModeledFlow = engineConfiguration->modeledFlowIdle;
346
347 /*
348 * Here we have idle logic thread - actual stepper movement is implemented in a separate
349 * working thread see stepper.cpp
350 */
353
354 // On failed sensor, use 0 deg C - should give a safe highish idle
357
358 // Compute the target we're shooting for
359 auto targetRpm = getTargetRpm(clt);
360 m_lastTargetRpm = targetRpm.ClosedLoopTarget;
361
362 // Determine cranking taper (modeled flow does no taper of open loop)
363 float crankingTaper = useModeledFlow ? 1 : getCrankingTaperFraction(clt);
364
365 // Determine what operation phase we're in - idling or not
366 float vehicleSpeed = Sensor::getOrZero(SensorType::VehicleSpeed);
367 auto phase = determinePhase(rpm, targetRpm, tps, vehicleSpeed, crankingTaper);
368
369 // update TS flag
370 isIdling = (phase == Phase::Idling) || (phase == Phase::CrankToIdleTaper);
371
372 if (phase != m_lastPhase && phase == Phase::Idling) {
373 // Just entered idle, reset timer
374 m_timeInIdlePhase.reset();
375 }
376
377 m_lastPhase = phase;
378
380
381 // Always apply open loop correction
382 percent_t iacPosition = getOpenLoop(phase, rpm, clt, tps, crankingTaper);
383 baseIdlePosition = iacPosition;
384 // Force closed loop operation for modeled flow
385 auto idleMode = useModeledFlow ? IM_AUTO : engineConfiguration->idleMode;
386
387 // If TPS is working and automatic mode enabled, add any closed loop correction
388 if (tps.Valid && idleMode == IM_AUTO) {
389 if (useModeledFlow && phase != Phase::Idling) {
390 auto idlePid = getIdlePid();
391 idlePid->reset();
392 }
393 auto closedLoop = getClosedLoop(phase, tps.Value, rpm, targetRpm.ClosedLoopTarget);
394 idleClosedLoop = closedLoop;
395 iacPosition += closedLoop;
396 } else {
397 isIdleClosedLoop = false;
398 }
399
400 iacPosition = clampPercentValue(iacPosition);
401
402// todo: while is below disabled for unit tests?
403#if EFI_TUNER_STUDIO && (EFI_PROD_CODE || EFI_SIMULATOR)
404
405 // see also tsOutputChannels->idlePosition
407
408
409 extern StepperMotor iacMotor;
411#endif /* EFI_TUNER_STUDIO */
412 if (useModeledFlow && phase != Phase::Cranking) {
413 float totalAirmass = 0.01 * iacPosition * engineConfiguration->idleMaximumAirmass;
414 idleTargetAirmass = totalAirmass;
415
416 bool shouldAdjustTiming = engineConfiguration->useIdleTimingPidControl && phase == Phase::Idling;
417
418 // extract hiqh frequency content to be handled by timing
419 float timingAirmass = shouldAdjustTiming ? m_timingHpf.filter(totalAirmass) : 0;
420
421 // Convert from airmass delta -> timing
423
424 // Handle the residual low frequency content with airflow
425 float idleAirmass = totalAirmass - timingAirmass;
426 float airflowKgPerH = 3.6 * 0.001 * idleAirmass * rpm / 60 * engineConfiguration->cylindersCount / 2;
427 idleTargetFlow = airflowKgPerH;
428
429 // Convert from desired flow -> idle valve position
430 float idlePos = interpolate2d(
431 airflowKgPerH,
434 );
435
436 iacPosition = idlePos;
437 }
438
439 currentIdlePosition = iacPosition;
440
441 bool acActive = engine->module<AcController>().unmock().acButtonState;
442 bool fan1Active = enginePins.fanRelay.getLogicValue();
443 bool fan2Active = enginePins.fanRelay2.getLogicValue();
444 updateLtit(rpm, clt, acActive, fan1Active, fan2Active, getIdlePid()->getIntegration());
445
446 return iacPosition;
447#else
448 return 0;
449#endif // EFI_SHAFT_POSITION_INPUT
450
451}
452
454#if EFI_SHAFT_POSITION_INPUT
456 applyIACposition(position);
457 // huh: why not onIgnitionStateChanged?
459#endif // EFI_SHAFT_POSITION_INPUT
460}
461
465
467#if ! EFI_UNIT_TEST
468 shouldResetPid = !previousConfiguration || !getIdlePid()->isSame(&previousConfiguration->idleRpmPid);
470#endif
471}
472
482
483void IdleController::updateLtit(float rpm, float clt, bool acActive, bool fan1Active, bool fan2Active, float idleIntegral) {
485 engine->m_ltit.update(rpm, clt, acActive, fan1Active, fan2Active, idleIntegral);
486 }
487}
488
491}
492
493#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, 892, 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]