rusEFI
The most advanced open source ECU
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 "periodic_task.h"
20 #include "dc_motors.h"
21 
22 #if EFI_TUNER_STUDIO
23 #include "stepper.h"
24 #endif
25 
28 
29  // idle air Bump for AC
30  // Why do we bump based on button not based on actual A/C relay state?
31  // 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
32  // alternator duty cycle has a similar logic
34 
36  idleTarget = target;
37  return target;
38 }
39 
40 IIdleController::Phase IdleController::determinePhase(int rpm, int targetRpm, SensorResult tps, float vss, float crankingTaperFraction) {
41 #if EFI_SHAFT_POSITION_INPUT
42  if (!engine->rpmCalculator.isRunning()) {
43  return Phase::Cranking;
44  }
45  badTps = !tps;
46 
47  if (badTps) {
48  // If the TPS has failed, assume the engine is running
49  return Phase::Running;
50  }
51 
52  // if throttle pressed, we're out of the idle corner
54  return Phase::Running;
55  }
56 
57  // If rpm too high (but throttle not pressed), we're coasting
58  // ALSO, if still in the cranking taper, disable coasting
59  int maximumIdleRpm = targetRpm + engineConfiguration->idlePidRpmUpperLimit;
60  looksLikeCoasting = rpm > maximumIdleRpm;
61  looksLikeCrankToIdle = crankingTaperFraction < 1;
63  return Phase::Coasting;
64  }
65 
66  // If the vehicle is moving too quickly, disable CL idle
67  auto maxVss = engineConfiguration->maxIdleVss;
68  looksLikeRunning = maxVss != 0 && vss > maxVss;
69  if (looksLikeRunning) {
70  return Phase::Running;
71  }
72 
73  // If still in the cranking taper, disable closed loop idle
76  }
77 #endif // EFI_SHAFT_POSITION_INPUT
78 
79  // If we are entering idle, and the PID settings are aggressive, it's good to make a soft entry upon entering closed loop
80  if (m_crankTaperEndTime == 0.0f) {
83  }
84 
85  // No other conditions met, we are idling!
86  return Phase::Idling;
87 }
88 
91 }
92 
94  float mult =
96  // Override to separate table
98  // Otherwise use plain running table
99  : interpolate2d(clt, config->cltIdleCorrBins, config->cltIdleCorr);
100 
102 }
103 
105  float running =
106  engineConfiguration->manIdlePosition // Base idle position (slider)
107  * interpolate2d(clt, config->cltIdleCorrBins, config->cltIdleCorr);
108 
109  // Now we bump it by the AC/fan amount if necessary
113 
114  running += luaAdd;
115 
116 #if EFI_ANTILAG_SYSTEM
119 }
120 #endif /* EFI_ANTILAG_SYSTEM */
121 
122  // 'dashpot' (hold+decay) logic for coasting->idle
123  float tpsForTaper = tps.value_or(0);
124  efitimeus_t nowUs = getTimeNowUs();
125  if (phase == Phase::Running) {
126  lastTimeRunningUs = nowUs;
127  }
128  // imitate a slow pedal release for TPS taper (to avoid engine stalls)
129  if (tpsForTaper <= engineConfiguration->idlePidDeactivationTpsThreshold) {
130  // make sure the time is not zero
131  float timeSinceRunningPhaseSecs = (nowUs - lastTimeRunningUs + 1) / US_PER_SECOND_F;
132  // we shift the time to implement the hold correction (time can be negative)
133  float timeSinceRunningAfterHoldSecs = timeSinceRunningPhaseSecs - engineConfiguration->iacByTpsHoldTime;
134  // implement the decay correction (from tpsForTaper to 0)
135  tpsForTaper = interpolateClamped(0, engineConfiguration->idlePidDeactivationTpsThreshold, engineConfiguration->iacByTpsDecayTime, tpsForTaper, timeSinceRunningAfterHoldSecs);
136  }
137 
138  // Now bump it by the specified amount when the throttle is opened (if configured)
139  // nb: invalid tps will make no change, no explicit check required
141  0, 0,
143  tpsForTaper);
144 
146 
147  float airTaperRpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit;
150  airTaperRpmUpperLimit, engineConfiguration->airByRpmTaper,
151  rpm);
152 
154 
155  // are we clamping open loop part separately? should not we clamp once we have total value?
156  return clampPercentValue(running);
157 }
158 
159 percent_t IdleController::getOpenLoop(Phase phase, float rpm, float clt, SensorResult tps, float crankingTaperFraction) {
160  percent_t crankingValvePosition = getCrankingOpenLoop(clt);
161 
162  isCranking = phase == Phase::Cranking;
163  isIdleCoasting = phase == Phase::Coasting;
164 
165  // if we're cranking, nothing more to do.
166  if (isCranking) {
167  return crankingValvePosition;
168  }
169 
170  // If coasting (and enabled), use the coasting position table instead of normal open loop
172  if (isIacTableForCoasting) {
173  return interpolate2d(rpm, config->iacCoastingRpmBins, config->iacCoasting);
174  }
175 
176  percent_t running = getRunningOpenLoop(phase, rpm, clt, tps);
177 
178  // Interpolate between cranking and running over a short time
179  // This clamps once you fall off the end, so no explicit check for >1 required
180  return interpolateClamped(0, crankingValvePosition, 1, running, crankingTaperFraction);
181 }
182 
185 }
186 
187 float IdleController::getIdleTimingAdjustment(int rpm, int targetRpm, Phase phase) {
188  // if not enabled, do nothing
190  return 0;
191  }
192 
193  // If not idling, do nothing
194  if (phase != Phase::Idling) {
195  m_timingPid.reset();
196  return 0;
197  }
198 
200  // Use interpolation for correction taper
202  }
203 
204  // We're now in the idle mode, and RPM is inside the Timing-PID regulator work zone!
205  return m_timingPid.getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f);
206 }
207 
208 static void finishIdleTestIfNeeded() {
211 }
212 
213 static void undoIdleBlipIfNeeded() {
215  engine->timeToStopBlip = 0;
216  }
217 }
218 
219 /**
220  * @return idle valve position percentage for automatic closed loop mode
221  */
222 float IdleController::getClosedLoop(IIdleController::Phase phase, float tpsPos, int rpm, int targetRpm) {
223  auto idlePid = getIdlePid();
224 
225  if (shouldResetPid) {
226  needReset = idlePid->getIntegration() <= 0 || mustResetPid;
227  // we reset only if I-term is negative, because the positive I-term is good - it keeps RPM from dropping too low
228  if (needReset) {
229  idlePid->reset();
230  mustResetPid = false;
231  }
232  shouldResetPid = false;
233  wasResetPid = true;
234  }
235 
236  // todo: move this to pid_s one day
239 
240  efitimeus_t nowUs = getTimeNowUs();
241 
243  if (notIdling) {
244  // Don't store old I and D terms if PID doesn't work anymore.
245  // Otherwise they will affect the idle position much later, when the throttle is closed.
246  if (mightResetPid) {
247  mightResetPid = false;
248  shouldResetPid = true;
249  }
250 
251  idleState = TPS_THRESHOLD;
252 
253  // We aren't idling, so don't apply any correction. A positive correction could inhibit a return to idle.
255  return 0;
256  }
257 
258  // #1553 we need to give FSIO variable offset or minValue a chance
259  bool acToggleJustTouched = (US2MS(nowUs) - engine->module<AcController>().unmock().acSwitchLastChangeTimeMs) < 500/*ms*/;
260  // check if within the dead zone
261  isInDeadZone = !acToggleJustTouched && absI(rpm - targetRpm) <= engineConfiguration->idlePidRpmDeadZone;
262  if (isInDeadZone) {
263  idleState = RPM_DEAD_ZONE;
264  // current RPM is close enough, no need to change anything
266  }
267 
268  // When rpm < targetRpm, there's a risk of dropping RPM too low - and the engine dies out.
269  // So PID reaction should be increased by adding extra percent to PID-error:
270  percent_t errorAmpCoef = 1.0f;
271  if (rpm < targetRpm) {
272  errorAmpCoef += (float)engineConfiguration->pidExtraForLowRpm / PERCENT_MULT;
273  }
274 
275  // if PID was previously reset, we store the time when it turned on back (see errorAmpCoef correction below)
276  if (wasResetPid) {
278  wasResetPid = false;
279  }
280  // increase the errorAmpCoef slowly to restore the process correctly after the PID reset
281  // todo: move restoreAfterPidResetTimeUs to idle?
282  efitimeus_t timeSincePidResetUs = nowUs - restoreAfterPidResetTimeUs;
283  // todo: add 'pidAfterResetDampingPeriodMs' setting
284  errorAmpCoef = interpolateClamped(0, 0, MS2US(/*engineConfiguration->pidAfterResetDampingPeriodMs*/1000), errorAmpCoef, timeSincePidResetUs);
285  // If errorAmpCoef > 1.0, then PID thinks that RPM is lower than it is, and controls IAC more aggressively
286  idlePid->setErrorAmplification(errorAmpCoef);
287 
288  percent_t newValue = idlePid->getOutput(targetRpm, rpm, SLOW_CALLBACK_PERIOD_MS / 1000.0f);
289  idleState = PID_VALUE;
290 
291  // the state of PID has been changed, so we might reset it now, but only when needed (see idlePidDeactivationTpsThreshold)
292  mightResetPid = true;
293 
294  // Apply PID Multiplier if used
296  float engineLoad = getFuelingLoad();
297  float multCoef = interpolate3d(
299  config->iacPidMultLoadBins, engineLoad,
301  );
302  // PID can be completely disabled of multCoef==0, or it just works as usual if multCoef==1
303  newValue = interpolateClamped(0, 0, 1, newValue, multCoef);
304  }
305 
306  // Apply PID Deactivation Threshold as a smooth taper for TPS transients.
307  // if tps==0 then PID just works as usual, or we completely disable it if tps>=threshold
308  // TODO: should we just remove this? It reduces the gain if your zero throttle stop isn't perfect,
309  // which could give unstable results.
310  newValue = interpolateClamped(0, newValue, engineConfiguration->idlePidDeactivationTpsThreshold, 0, tpsPos);
311 
312  m_lastAutomaticPosition = newValue;
313  return newValue;
314 }
315 
317 #if EFI_SHAFT_POSITION_INPUT
318 
319  // Simplify hardware CI: we borrow the idle valve controller as a PWM source for various stimulation tasks
320  // The logic in this function is solidly unit tested, so it's not necessary to re-test the particulars on real hardware.
321  #ifdef HARDWARE_CI
323  #endif
324 
325  /*
326  * Here we have idle logic thread - actual stepper movement is implemented in a separate
327  * working thread see stepper.cpp
328  */
331 
332  // On failed sensor, use 0 deg C - should give a safe highish idle
335 
336  // Compute the target we're shooting for
337  auto targetRpm = getTargetRpm(clt);
338  m_lastTargetRpm = targetRpm;
339 
340  // Determine cranking taper
341  float crankingTaper = getCrankingTaperFraction();
342 
343  // Determine what operation phase we're in - idling or not
344  float vehicleSpeed = Sensor::getOrZero(SensorType::VehicleSpeed);
345  auto phase = determinePhase(rpm, targetRpm, tps, vehicleSpeed, crankingTaper);
346  m_lastPhase = phase;
347 
350 
351  percent_t iacPosition;
352 
354  if (isBlipping) {
355  iacPosition = engine->blipIdlePosition;
356  idleState = BLIP;
357  } else {
358  // Always apply open loop correction
359  iacPosition = getOpenLoop(phase, rpm, clt, tps, crankingTaper);
360  baseIdlePosition = iacPosition;
361 
362  useClosedLoop = tps.Valid && engineConfiguration->idleMode == IM_AUTO;
363  // If TPS is working and automatic mode enabled, add any closed loop correction
364  if (useClosedLoop) {
365  auto closedLoop = getClosedLoop(phase, tps.Value, rpm, targetRpm);
366  idleClosedLoop = closedLoop;
367  iacPosition += closedLoop;
368  }
369 
370  iacPosition = clampPercentValue(iacPosition);
371  }
372 
373 #if EFI_TUNER_STUDIO && (EFI_PROD_CODE || EFI_SIMULATOR)
374  isIdleClosedLoop = phase == Phase::Idling;
375 
376  if (engineConfiguration->idleMode == IM_AUTO) {
377  // see also tsOutputChannels->idlePosition
379  }
380 
381  extern StepperMotor iacMotor;
383 #endif /* EFI_TUNER_STUDIO */
384 
385  currentIdlePosition = iacPosition;
386  return iacPosition;
387 #else
388  return 0;
389 #endif // EFI_SHAFT_POSITION_INPUT
390 
391 }
392 
394 #if EFI_SHAFT_POSITION_INPUT
396  applyIACposition(position);
397 #endif // EFI_SHAFT_POSITION_INPUT
398 }
399 
401 #if ! EFI_UNIT_TEST
402  shouldResetPid = !getIdlePid()->isSame(&previousConfiguration->idleRpmPid);
404 #endif
405 }
406 
408  shouldResetPid = false;
409  mightResetPid = false;
410  wasResetPid = false;
413 }
414 
415 #endif /* EFI_IDLE_CONTROL */
FuelComputer fuelComputer
Definition: engine.h:118
TriggerCentral triggerCentral
Definition: engine.h:281
efitimeus_t timeToStopBlip
Definition: engine.h:319
RpmCalculator rpmCalculator
Definition: engine.h:268
constexpr auto & module()
Definition: engine.h:174
AntilagSystemBase antilagController
Definition: engine.h:196
percent_t blipIdlePosition
Definition: engine.h:318
efitimeus_t timeToStopIdleTest
Definition: engine.h:320
TunerStudioOutputChannels outputChannels
Definition: engine.h:96
RegisteredOutputPin fanRelay
Definition: efi_gpio.h:83
RegisteredOutputPin fanRelay2
Definition: efi_gpio.h:84
PidIndustrial industrialWithOverrideIdlePid
Definition: idle_thread.h:73
void onConfigurationChange(engine_configuration_s const *previousConfig) final
float getClosedLoop(IIdleController::Phase phase, float tpsPos, int rpm, int targetRpm) override
int getTargetRpm(float clt) override
Definition: idle_thread.cpp:26
Phase determinePhase(int rpm, int targetRpm, SensorResult tps, float vss, float crankingTaperFraction) override
Definition: idle_thread.cpp:40
Phase m_lastPhase
Definition: idle_thread.h:93
float m_idleTimingSoftEntryEndTime
Definition: idle_thread.h:100
float getCrankingTaperFraction() const override
Definition: idle_thread.cpp:89
float m_lastAutomaticPosition
Definition: idle_thread.h:103
float getIdlePosition(float rpm)
float getIdleTimingAdjustment(int rpm) override
efitimeus_t restoreAfterPidResetTimeUs
Definition: idle_thread.h:95
percent_t getOpenLoop(Phase phase, float rpm, float clt, SensorResult tps, float crankingTaperFraction) override
Pid * getIdlePid()
Definition: idle_thread.h:80
void onSlowCallback() final
efitimeus_t lastTimeRunningUs
Definition: idle_thread.h:97
percent_t getCrankingOpenLoop(float clt) const override
Definition: idle_thread.cpp:93
float m_crankTaperEndTime
Definition: idle_thread.h:99
percent_t getRunningOpenLoop(IIdleController::Phase phase, float rpm, float clt, SensorResult tps) override
bool getLogicValue() const
Definition: efi_gpio.cpp:646
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:31
void setErrorAmplification(float coef)
Definition: efi_pid.cpp:129
void postState(pid_status_s &pidStatus) const
Definition: efi_pid.cpp:135
virtual void reset()
Definition: efi_pid.cpp:94
void initPidClass(pid_s *parameters)
Definition: efi_pid.cpp:24
float getOutput(float target, float input)
Definition: efi_pid.cpp:48
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:92
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
Engine * engine
float getFuelingLoad()
Definition: engine_math.cpp:47
void applyIACposition(percent_t position)
Idle Air Control valve hardware.
static void finishIdleTestIfNeeded()
static void undoIdleBlipIfNeeded()
Idle Valve Control thread.
static CCM_OPTIONAL FunctionalSensor clt(SensorType::Clt, MS2NT(10))
persistent_config_s * config
engine_configuration_s * engineConfiguration
float percent_t
Definition: rusefi_types.h:73
expected< float > SensorResult
Definition: sensor.h:55
@ DriverThrottleIntent
running("running", SensorCategory.SENSOR_INPUTS, FieldType.INT, 836, 1.0, -1.0, -1.0, "")
percent_t currentIdlePosition
percent_t baseIdlePosition
percent_t idleClosedLoop
percent_t iacByTpsTaper
idle_state_e idleState
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]
scaled_channel< uint8_t, 1, 10 > iacPidMultRpmBins[IAC_PID_MULT_SIZE]
scaled_channel< int8_t, 1, 2 > cltIdleRpmBins[CLT_CURVE_SIZE]
scaled_channel< uint8_t, 2, 1 > iacCoasting[CLT_CURVE_SIZE]
scaled_channel< uint8_t, 1, 20 > cltIdleRpm[CLT_CURVE_SIZE]