/** * @file idle_thread.cpp * @brief Idle Air Control valve thread. * * This thread looks at current RPM and decides if it should increase or decrease IAC duty cycle. * This file has the hardware & scheduling logic, desired idle level lives separately. * * * @date May 23, 2013 * @author Andrey Belomutskiy, (c) 2012-2022 */ #include "pch.h" #if EFI_IDLE_CONTROL #include "idle_thread.h" #include "idle_hardware.h" #include "periodic_task.h" #include "dc_motors.h" #if EFI_TUNER_STUDIO #include "stepper.h" #endif int IdleController::getTargetRpm(float clt) { targetRpmByClt = interpolate2d(clt, config->cltIdleRpmBins, config->cltIdleRpm); // idle air Bump for AC // Why do we bump based on button not based on actual A/C relay state? // 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 // alternator duty cycle has a similar logic targetRpmAc = engine->module().unmock().acButtonState ? engineConfiguration->acIdleRpmTarget : 0; auto target = (targetRpmByClt < targetRpmAc) ? targetRpmAc : targetRpmByClt; idleTarget = target; return target; } IIdleController::Phase IdleController::determinePhase(int rpm, int targetRpm, SensorResult tps, float vss, float crankingTaperFraction) { #if EFI_SHAFT_POSITION_INPUT if (!engine->rpmCalculator.isRunning()) { return Phase::Cranking; } badTps = !tps; if (badTps) { // If the TPS has failed, assume the engine is running return Phase::Running; } // if throttle pressed, we're out of the idle corner if (tps.Value > engineConfiguration->idlePidDeactivationTpsThreshold) { return Phase::Running; } // If rpm too high (but throttle not pressed), we're coasting // ALSO, if still in the cranking taper, disable coasting int maximumIdleRpm = targetRpm + engineConfiguration->idlePidRpmUpperLimit; looksLikeCoasting = rpm > maximumIdleRpm; looksLikeCrankToIdle = crankingTaperFraction < 1; if (looksLikeCoasting && !looksLikeCrankToIdle) { return Phase::Coasting; } // If the vehicle is moving too quickly, disable CL idle auto maxVss = engineConfiguration->maxIdleVss; looksLikeRunning = maxVss != 0 && vss > maxVss; if (looksLikeRunning) { return Phase::Running; } // If still in the cranking taper, disable closed loop idle if (looksLikeCrankToIdle) { return Phase::CrankToIdleTaper; } #endif // EFI_SHAFT_POSITION_INPUT // If we are entering idle, and the PID settings are aggressive, it's good to make a soft entry upon entering closed loop if (m_crankTaperEndTime == 0.0f) { m_crankTaperEndTime = engine->fuelComputer.running.timeSinceCrankingInSecs; m_idleTimingSoftEntryEndTime = m_crankTaperEndTime + engineConfiguration->idleTimingSoftEntryTime; } // No other conditions met, we are idling! return Phase::Idling; } float IdleController::getCrankingTaperFraction() const { return (float)engine->rpmCalculator.getRevolutionCounterSinceStart() / engineConfiguration->afterCrankingIACtaperDuration; } float IdleController::getCrankingOpenLoop(float clt) const { float mult = engineConfiguration->overrideCrankingIacSetting // Override to separate table ? interpolate2d(clt, config->cltCrankingCorrBins, config->cltCrankingCorr) // Otherwise use plain running table : interpolate2d(clt, config->cltIdleCorrBins, config->cltIdleCorr); return engineConfiguration->crankingIACposition * mult; } percent_t IdleController::getRunningOpenLoop(IIdleController::Phase phase, float rpm, float clt, SensorResult tps) { float running = engineConfiguration->manIdlePosition // Base idle position (slider) * interpolate2d(clt, config->cltIdleCorrBins, config->cltIdleCorr); // Now we bump it by the AC/fan amount if necessary running += engine->module().unmock().acButtonState ? engineConfiguration->acIdleExtraOffset : 0; running += enginePins.fanRelay.getLogicValue() ? engineConfiguration->fan1ExtraIdle : 0; running += enginePins.fanRelay2.getLogicValue() ? engineConfiguration->fan2ExtraIdle : 0; running += luaAdd; #if EFI_ANTILAG_SYSTEM if (engine->antilagController.isAntilagCondition) { running += engineConfiguration->ALSIdleAdd; } #endif /* EFI_ANTILAG_SYSTEM */ // 'dashpot' (hold+decay) logic for coasting->idle float tpsForTaper = tps.value_or(0); efitimeus_t nowUs = getTimeNowUs(); if (phase == Phase::Running) { lastTimeRunningUs = nowUs; } // imitate a slow pedal release for TPS taper (to avoid engine stalls) if (tpsForTaper <= engineConfiguration->idlePidDeactivationTpsThreshold) { // make sure the time is not zero float timeSinceRunningPhaseSecs = (float)(nowUs - lastTimeRunningUs + 1) / US_PER_SECOND_F; // we shift the time to implement the hold correction (time can be negative) float timeSinceRunningAfterHoldSecs = timeSinceRunningPhaseSecs - engineConfiguration->iacByTpsHoldTime; // implement the decay correction (from tpsForTaper to 0) tpsForTaper = interpolateClamped(0, engineConfiguration->idlePidDeactivationTpsThreshold, engineConfiguration->iacByTpsDecayTime, tpsForTaper, timeSinceRunningAfterHoldSecs); } // Now bump it by the specified amount when the throttle is opened (if configured) // nb: invalid tps will make no change, no explicit check required iacByTpsTaper = interpolateClamped( 0, 0, engineConfiguration->idlePidDeactivationTpsThreshold, engineConfiguration->iacByTpsTaper, tpsForTaper); running += iacByTpsTaper; float airTaperRpmUpperLimit = engineConfiguration->idlePidRpmUpperLimit + engineConfiguration->airTaperRpmRange; iacByRpmTaper = interpolateClamped( engineConfiguration->idlePidRpmUpperLimit, 0, airTaperRpmUpperLimit, engineConfiguration->airByRpmTaper, rpm); running += iacByRpmTaper; return clampF(0, running, 100); } percent_t IdleController::getOpenLoop(Phase phase, float rpm, float clt, SensorResult tps, float crankingTaperFraction) { percent_t crankingValvePosition = getCrankingOpenLoop(clt); isCranking = phase == Phase::Cranking; isIdleCoasting = phase == Phase::Coasting; // if we're cranking, nothing more to do. if (isCranking) { return crankingValvePosition; } // If coasting (and enabled), use the coasting position table instead of normal open loop isIacTableForCoasting = engineConfiguration->useIacTableForCoasting && isIdleCoasting; if (isIacTableForCoasting) { return interpolate2d(rpm, config->iacCoastingRpmBins, config->iacCoasting); } percent_t running = getRunningOpenLoop(phase, rpm, clt, tps); // Interpolate between cranking and running over a short time // This clamps once you fall off the end, so no explicit check for >1 required return interpolateClamped(0, crankingValvePosition, 1, running, crankingTaperFraction); } float IdleController::getIdleTimingAdjustment(int rpm) { return getIdleTimingAdjustment(rpm, m_lastTargetRpm, m_lastPhase); } float IdleController::getIdleTimingAdjustment(int rpm, int targetRpm, Phase phase) { // if not enabled, do nothing if (!engineConfiguration->useIdleTimingPidControl) { return 0; } // If not idling, do nothing if (phase != Phase::Idling) { m_timingPid.reset(); return 0; } if (engineConfiguration->idleTimingSoftEntryTime > 0.0f) { // Use interpolation for correction taper m_timingPid.setErrorAmplification(interpolateClamped(m_crankTaperEndTime, 0.0f, m_idleTimingSoftEntryEndTime, 1.0f, engine->fuelComputer.running.timeSinceCrankingInSecs)); } // We're now in the idle mode, and RPM is inside the Timing-PID regulator work zone! return m_timingPid.getOutput(targetRpm, rpm, FAST_CALLBACK_PERIOD_MS / 1000.0f); } static void finishIdleTestIfNeeded() { if (engine->timeToStopIdleTest != 0 && getTimeNowUs() > engine->timeToStopIdleTest) engine->timeToStopIdleTest = 0; } static void undoIdleBlipIfNeeded() { if (engine->timeToStopBlip != 0 && getTimeNowUs() > engine->timeToStopBlip) { engine->timeToStopBlip = 0; } } /** * @return idle valve position percentage for automatic closed loop mode */ float IdleController::getClosedLoop(IIdleController::Phase phase, float tpsPos, int rpm, int targetRpm) { auto idlePid = getIdlePid(); if (shouldResetPid) { needReset = idlePid->getIntegration() <= 0 || mustResetPid; // we reset only if I-term is negative, because the positive I-term is good - it keeps RPM from dropping too low if (needReset) { idlePid->reset(); mustResetPid = false; } shouldResetPid = false; wasResetPid = true; } // todo: move this to pid_s one day industrialWithOverrideIdlePid.antiwindupFreq = engineConfiguration->idle_antiwindupFreq; industrialWithOverrideIdlePid.derivativeFilterLoss = engineConfiguration->idle_derivativeFilterLoss; efitimeus_t nowUs = getTimeNowUs(); notIdling = phase != IIdleController::Phase::Idling; if (notIdling) { // Don't store old I and D terms if PID doesn't work anymore. // Otherwise they will affect the idle position much later, when the throttle is closed. if (mightResetPid) { mightResetPid = false; shouldResetPid = true; } idleState = TPS_THRESHOLD; // We aren't idling, so don't apply any correction. A positive correction could inhibit a return to idle. m_lastAutomaticPosition = 0; return 0; } // #1553 we need to give FSIO variable offset or minValue a chance bool acToggleJustTouched = (US2MS(nowUs) - engine->module().unmock().acSwitchLastChangeTimeMs) < 500/*ms*/; // check if within the dead zone isInDeadZone = !acToggleJustTouched && absI(rpm - targetRpm) <= engineConfiguration->idlePidRpmDeadZone; if (isInDeadZone) { idleState = RPM_DEAD_ZONE; // current RPM is close enough, no need to change anything return m_lastAutomaticPosition; } // When rpm < targetRpm, there's a risk of dropping RPM too low - and the engine dies out. // So PID reaction should be increased by adding extra percent to PID-error: percent_t errorAmpCoef = 1.0f; if (rpm < targetRpm) { errorAmpCoef += (float)engineConfiguration->pidExtraForLowRpm / PERCENT_MULT; } // if PID was previously reset, we store the time when it turned on back (see errorAmpCoef correction below) if (wasResetPid) { restoreAfterPidResetTimeUs = nowUs; wasResetPid = false; } // increase the errorAmpCoef slowly to restore the process correctly after the PID reset // todo: move restoreAfterPidResetTimeUs to idle? efitimeus_t timeSincePidResetUs = nowUs - restoreAfterPidResetTimeUs; // todo: add 'pidAfterResetDampingPeriodMs' setting errorAmpCoef = interpolateClamped(0, 0, MS2US(/*engineConfiguration->pidAfterResetDampingPeriodMs*/1000), errorAmpCoef, timeSincePidResetUs); // If errorAmpCoef > 1.0, then PID thinks that RPM is lower than it is, and controls IAC more aggressively idlePid->setErrorAmplification(errorAmpCoef); percent_t newValue = idlePid->getOutput(targetRpm, rpm, SLOW_CALLBACK_PERIOD_MS / 1000.0f); idleState = PID_VALUE; // the state of PID has been changed, so we might reset it now, but only when needed (see idlePidDeactivationTpsThreshold) mightResetPid = true; // Apply PID Multiplier if used if (engineConfiguration->useIacPidMultTable) { float engineLoad = getFuelingLoad(); float multCoef = interpolate3d( config->iacPidMultTable, config->iacPidMultLoadBins, engineLoad, config->iacPidMultRpmBins, rpm ); // PID can be completely disabled of multCoef==0, or it just works as usual if multCoef==1 newValue = interpolateClamped(0, 0, 1, newValue, multCoef); } // Apply PID Deactivation Threshold as a smooth taper for TPS transients. // if tps==0 then PID just works as usual, or we completely disable it if tps>=threshold // TODO: should we just remove this? It reduces the gain if your zero throttle stop isn't perfect, // which could give unstable results. newValue = interpolateClamped(0, newValue, engineConfiguration->idlePidDeactivationTpsThreshold, 0, tpsPos); m_lastAutomaticPosition = newValue; return newValue; } float IdleController::getIdlePosition(float rpm) { #if EFI_SHAFT_POSITION_INPUT // Simplify hardware CI: we borrow the idle valve controller as a PWM source for various stimulation tasks // The logic in this function is solidly unit tested, so it's not necessary to re-test the particulars on real hardware. #ifdef HARDWARE_CI return engineConfiguration->manIdlePosition; #endif /* * Here we have idle logic thread - actual stepper movement is implemented in a separate * working thread see stepper.cpp */ getIdlePid()->iTermMin = engineConfiguration->idlerpmpid_iTermMin; getIdlePid()->iTermMax = engineConfiguration->idlerpmpid_iTermMax; // On failed sensor, use 0 deg C - should give a safe highish idle float clt = Sensor::getOrZero(SensorType::Clt); auto tps = Sensor::get(SensorType::DriverThrottleIntent); // Compute the target we're shooting for auto targetRpm = getTargetRpm(clt); m_lastTargetRpm = targetRpm; // Determine cranking taper float crankingTaper = getCrankingTaperFraction(); // Determine what operation phase we're in - idling or not float vehicleSpeed = Sensor::getOrZero(SensorType::VehicleSpeed); auto phase = determinePhase(rpm, targetRpm, tps, vehicleSpeed, crankingTaper); m_lastPhase = phase; finishIdleTestIfNeeded(); undoIdleBlipIfNeeded(); percent_t iacPosition; isBlipping = engine->timeToStopBlip != 0; if (isBlipping) { iacPosition = engine->blipIdlePosition; idleState = BLIP; } else { // Always apply open loop correction iacPosition = getOpenLoop(phase, rpm, clt, tps, crankingTaper); baseIdlePosition = iacPosition; useClosedLoop = tps.Valid && engineConfiguration->idleMode == IM_AUTO; // If TPS is working and automatic mode enabled, add any closed loop correction if (useClosedLoop) { auto closedLoop = getClosedLoop(phase, tps.Value, rpm, targetRpm); idleClosedLoop = closedLoop; iacPosition += closedLoop; } iacPosition = clampPercentValue(iacPosition); } #if EFI_TUNER_STUDIO && (EFI_PROD_CODE || EFI_SIMULATOR) isIdleClosedLoop = phase == Phase::Idling; if (engineConfiguration->idleMode == IM_AUTO) { // see also tsOutputChannels->idlePosition getIdlePid()->postState(engine->outputChannels.idleStatus); } extern StepperMotor iacMotor; engine->outputChannels.idleStepperTargetPosition = iacMotor.getTargetPosition(); #endif /* EFI_TUNER_STUDIO */ currentIdlePosition = iacPosition; return iacPosition; #else return 0; #endif // EFI_SHAFT_POSITION_INPUT } void IdleController::onSlowCallback() { #if EFI_SHAFT_POSITION_INPUT float position = getIdlePosition(engine->triggerCentral.instantRpm.getInstantRpm()); applyIACposition(position); #endif // EFI_SHAFT_POSITION_INPUT } void IdleController::onConfigurationChange(engine_configuration_s const * previousConfiguration) { #if ! EFI_UNIT_TEST shouldResetPid = !getIdlePid()->isSame(&previousConfiguration->idleRpmPid); mustResetPid = shouldResetPid; #endif } void IdleController::init() { shouldResetPid = false; mightResetPid = false; wasResetPid = false; m_timingPid.initPidClass(&engineConfiguration->idleTimingPid); getIdlePid()->initPidClass(&engineConfiguration->idleRpmPid); } #endif /* EFI_IDLE_CONTROL */