diff --git a/firmware/controllers/algo/advance_map.cpp b/firmware/controllers/algo/advance_map.cpp index a10ac68564..9363aced60 100644 --- a/firmware/controllers/algo/advance_map.cpp +++ b/firmware/controllers/algo/advance_map.cpp @@ -19,13 +19,17 @@ */ #include "global.h" +#include "engine_configuration.h" +#include "engine.h" #include "advance_map.h" #include "interpolation.h" #include "efilib2.h" #include "engine_math.h" #include "tps.h" +#include "idle_thread.h" -EXTERN_ENGINE; +EXTERN_ENGINE +; #if EFI_TUNER_STUDIO || defined(__DOXYGEN__) extern TunerStudioOutputChannels tsOutputChannels; @@ -36,6 +40,10 @@ static ign_Map3D_t advanceMap("advance"); static ign_tps_Map3D_t advanceTpsMap("advanceTps", 1.0 / ADVANCE_TPS_STORAGE_MULT); static ign_Map3D_t iatAdvanceCorrectionMap("iat corr"); +// Init PID later (make it compatible with unit-tests) +static Pid idleTimingPid; +static bool shouldResetTimingPid = false; + static int minCrankingRpm = 0; static const float iatTimingRpmBins[IGN_LOAD_COUNT] = {880, 1260, 1640, 2020, 2400, 2780, 3000, 3380, 3760, 4140, 4520, 5000, 5700, 6500, 7200, 8000}; @@ -104,24 +112,56 @@ static angle_t getRunningAdvance(int rpm, float engineLoad DECLARE_ENGINE_PARAME return advanceAngle; } -static angle_t getAdvanceCorrections(int rpm DECLARE_ENGINE_PARAMETER_SUFFIX) { +angle_t getAdvanceCorrections(int rpm DECLARE_ENGINE_PARAMETER_SUFFIX) { float iatCorrection; if (cisnan(engine->sensors.iat)) { iatCorrection = 0; } else { iatCorrection = iatAdvanceCorrectionMap.getValue((float) rpm, engine->sensors.iat); } + // PID Ignition Advance angle correction + float pidTimingCorrection = 0.0f; + if (CONFIGB(useIdleTimingPidControl)) { + int targetRpm = getTargetRpmForIdleCorrection(PASS_ENGINE_PARAMETER_SIGNATURE); + int rpmDelta = absI(rpm - targetRpm); + float tps = getTPS(PASS_ENGINE_PARAMETER_SIGNATURE); + if (tps >= CONFIGB(idlePidDeactivationTpsThreshold)) { + // we are not in the idle mode anymore, so the 'reset' flag will help us when we return to the idle. + shouldResetTimingPid = true; + } + else if (rpmDelta > CONFIG(idleTimingPidDeadZone) && rpmDelta < CONFIG(idleTimingPidWorkZone) + CONFIG(idlePidFalloffDeltaRpm)) { + // We're now in the idle mode, and RPM is inside the Timing-PID regulator work zone! + // So if we need to reset the PID, let's do it now + if (shouldResetTimingPid) { + idleTimingPid.reset(); + shouldResetTimingPid = false; + } + // get PID value (this is not an actual Advance Angle, but just a additive correction!) + percent_t timingRawCorr = idleTimingPid.getOutput(targetRpm, rpm, engineConfiguration->idleTimingPid.periodMs); + // tps idle-running falloff + pidTimingCorrection = interpolateClamped(0.0f, timingRawCorr, CONFIGB(idlePidDeactivationTpsThreshold), 0.0f, tps); + // rpm falloff + pidTimingCorrection = interpolateClamped(0.0f, pidTimingCorrection, CONFIG(idlePidFalloffDeltaRpm), 0.0f, rpmDelta - CONFIG(idleTimingPidWorkZone)); + } else { + shouldResetTimingPid = true; + } + } else { + shouldResetTimingPid = true; + } + if (engineConfiguration->debugMode == DBG_IGNITION_TIMING) { #if EFI_TUNER_STUDIO || defined(__DOXYGEN__) tsOutputChannels.debugFloatField1 = iatCorrection; tsOutputChannels.debugFloatField2 = engine->engineState.cltTimingCorrection; tsOutputChannels.debugFloatField3 = engine->fsioState.fsioTimingAdjustment; + tsOutputChannels.debugFloatField4 = pidTimingCorrection; #endif /* EFI_TUNER_STUDIO */ } return iatCorrection + engine->fsioState.fsioTimingAdjustment + engine->engineState.cltTimingCorrection + + pidTimingCorrection // todo: uncomment once we get useable knock - engine->knockCount ; } @@ -197,6 +237,8 @@ void initTimingMap(DECLARE_ENGINE_PARAMETER_SIGNATURE) { config->ignitionRpmBins); iatAdvanceCorrectionMap.init(config->ignitionIatCorrTable, config->ignitionIatCorrLoadBins, config->ignitionIatCorrRpmBins); + // init timing PID + idleTimingPid = Pid(&CONFIG(idleTimingPid)); } /** diff --git a/firmware/controllers/algo/advance_map.h b/firmware/controllers/algo/advance_map.h index 4b1ae18322..58e4e0ab75 100644 --- a/firmware/controllers/algo/advance_map.h +++ b/firmware/controllers/algo/advance_map.h @@ -16,5 +16,6 @@ void initTimingMap(DECLARE_ENGINE_PARAMETER_SIGNATURE); float getTopAdvanceForBore(chamber_style_e style, int octane, double compression, double bore); float getInitialAdvance(int rpm, float map, float advanceMax); void buildTimingMap(float advanceMax DECLARE_CONFIG_PARAMETER_SUFFIX); +angle_t getAdvanceCorrections(int rpm DECLARE_ENGINE_PARAMETER_SUFFIX); #endif /* ADVANCE_H_ */ diff --git a/firmware/controllers/algo/engine2.cpp b/firmware/controllers/algo/engine2.cpp index 9431ae7392..d002aad508 100644 --- a/firmware/controllers/algo/engine2.cpp +++ b/firmware/controllers/algo/engine2.cpp @@ -202,7 +202,7 @@ void EngineState::periodicFastCallback(DECLARE_ENGINE_PARAMETER_SIGNATURE) { /** * *0.01 because of https://sourceforge.net/p/rusefi/tickets/153/ */ - float rawVe = veMap.getValue(rpm, map); + float rawVe = veMap.getValue(rpm, CONFIGB(useTPSBasedVeTable) ? tps : map); // get VE from the separate table for Idle if (CONFIG(useSeparateVeForIdle)) { float idleVe = interpolate2d("idleVe", rpm, config->idleVeBins, config->idleVe, IDLE_VE_CURVE_SIZE); diff --git a/firmware/controllers/algo/engine_configuration.cpp b/firmware/controllers/algo/engine_configuration.cpp index ae1691b094..6d643fc635 100644 --- a/firmware/controllers/algo/engine_configuration.cpp +++ b/firmware/controllers/algo/engine_configuration.cpp @@ -619,6 +619,18 @@ void setTargetRpmCurve(int rpm DECLARE_ENGINE_PARAMETER_SUFFIX) { setLinearCurve(engineConfiguration->cltIdleRpm, CLT_CURVE_SIZE, rpm, rpm, 10); } +int getTargetRpmForIdleCorrection(DECLARE_ENGINE_PARAMETER_SIGNATURE) { + float clt = engine->sensors.clt; + int targetRpm; + if (cisnan(clt)) { + // error is already reported, let's take first value from the table should be good enough error handing solution + targetRpm = CONFIG(cltIdleRpm)[0]; + } else { + targetRpm = interpolate2d("cltRpm", clt, CONFIG(cltIdleRpmBins), CONFIG(cltIdleRpm), CLT_CURVE_SIZE); + } + return targetRpm + engine->fsioState.fsioIdleTargetRPMAdjustment; +} + /** * @brief Global default engine configuration * This method sets the global engine configuration defaults. These default values are then diff --git a/firmware/controllers/algo/engine_configuration.h b/firmware/controllers/algo/engine_configuration.h index d221f1ec08..e589e937b3 100644 --- a/firmware/controllers/algo/engine_configuration.h +++ b/firmware/controllers/algo/engine_configuration.h @@ -51,6 +51,7 @@ typedef struct { void prepareVoidConfiguration(engine_configuration_s *activeConfiguration); void setDefaultConfiguration(DECLARE_ENGINE_PARAMETER_SIGNATURE); void setTargetRpmCurve(int rpm DECLARE_ENGINE_PARAMETER_SUFFIX); +int getTargetRpmForIdleCorrection(DECLARE_ENGINE_PARAMETER_SIGNATURE); void setAfrMap(afr_table_t table, float value); /** * See also setLinearCurve() diff --git a/firmware/controllers/idle_thread.cpp b/firmware/controllers/idle_thread.cpp index 2d413a0fb1..05bb0d893d 100644 --- a/firmware/controllers/idle_thread.cpp +++ b/firmware/controllers/idle_thread.cpp @@ -205,16 +205,7 @@ static percent_t automaticIdleController() { } // get Target RPM for Auto-PID from a separate table - float clt = engine->sensors.clt; - int targetRpm; - if (cisnan(clt)) { - // error is already reported, let's take first value from the table should be good enough error handing solution - targetRpm = CONFIG(cltIdleRpm)[0]; - } else { - targetRpm = interpolate2d("cltRpm", clt, CONFIG(cltIdleRpmBins), CONFIG(cltIdleRpm), CLT_CURVE_SIZE); - } - targetRpm += engine->fsioState.fsioIdleTargetRPMAdjustment; - + int targetRpm = getTargetRpmForIdleCorrection(PASS_ENGINE_PARAMETER_SIGNATURE); // check if within the dead zone int rpm = GET_RPM(); @@ -256,8 +247,8 @@ static percent_t automaticIdleController() { int idlePidLowerRpm = targetRpm + CONFIG(idlePidRpmDeadZone); if (CONFIG(idlePidRpmUpperLimit) > 0) { idleState = PID_UPPER; - if (CONFIGB(useIacTableForCoasting)) { - percent_t iacPosForCoasting = interpolate2d("iacCoasting", clt, CONFIG(iacCoastingBins), CONFIG(iacCoasting), CLT_CURVE_SIZE); + if (CONFIGB(useIacTableForCoasting) && !cisnan(engine->sensors.clt)) { + percent_t iacPosForCoasting = interpolate2d("iacCoasting", engine->sensors.clt, CONFIG(iacCoastingBins), CONFIG(iacCoasting), CLT_CURVE_SIZE); newValue = interpolateClamped(idlePidLowerRpm, newValue, idlePidLowerRpm + CONFIG(idlePidRpmUpperLimit), iacPosForCoasting, rpm); } else { // Well, just leave it as is, without PID regulation... diff --git a/firmware/integration/rusefi_config.txt b/firmware/integration/rusefi_config.txt index 13e80d6b16..1e4647864f 100644 --- a/firmware/integration/rusefi_config.txt +++ b/firmware/integration/rusefi_config.txt @@ -574,8 +574,8 @@ bit is_enabled_spi_2 bit coastingFuelCutEnabled bit useIacTableForCoasting bit useNoiselessTriggerDecoder - bit unused_board_984_24 - bit unused_board_984_25 + bit useIdleTimingPidControl + bit useTPSBasedVeTable bit unused_board_984_26 bit unused_board_984_27 bit unused_board_984_28 @@ -804,7 +804,7 @@ custom pin_mode_e 1 bits, U08, @OFFSET@, [0:5], @@pin_mode_e_enum@@ float throttlePedalWOTVoltage;+Pedal in the floor;"voltage", 1, 0, -6, 6, 2 int16_t startUpFuelPumpDuration;+on ECU start turn fuel pump on to build fuel pressure;"seconds", 1, 0, 0, 6000, 0 - int16_t idlePidRpmDeadZone;If RPM is close enough let's leave IAC alone;"RPM", 1, 0, 0, 1000, 0 + int16_t idlePidRpmDeadZone;If RPM is close enough let's leave IAC alone, and maybe engage timing PID correction;"RPM", 1, 0, 0, 1000, 0 float[CLT_CURVE_SIZE] cltIdleRpmBins;CLT-based target RPM for automatic idle controller;"C", 1, 0, -100.0, 250.0, 2 @@ -984,8 +984,15 @@ tChargeMode_e tChargeMode; int16_t etb_iTermMax;iTerm max value;"", 1, 0, -30000, 30000.0, 0 float etbDeadband;;"", 1, 0, 0, 100.0, 2 etb_io etb2 - int[622] mainUnusedEnd; + pid_s idleTimingPid;See useIdleTimingPidControl + int16_t idleTimingPidWorkZone;The timing correction works only if RPM is close enough, otherwise the IAC correction works;"RPM", 1, 0, 0, 1000, 0 + int16_t idleTimingPidDeadZone;If RPM is too perfect, let's leave the advance angle alone to avoid oscillation;"RPM", 1, 0, 0, 1000, 0 + int16_t idlePidFalloffDeltaRpm;Added to the work zone for smooth correction falloff;"RPM", 1, 0, 0, 1000, 0 + int16_t unusedIdleTimingPid; + + int[615] mainUnusedEnd; + end_struct diff --git a/firmware/tunerstudio/rusefi.input b/firmware/tunerstudio/rusefi.input index c14476dc27..cc81d2b992 100644 --- a/firmware/tunerstudio/rusefi.input +++ b/firmware/tunerstudio/rusefi.input @@ -7,15 +7,20 @@ ; this should stop TS from looking for the CAN ID in the 2nd byte location and allow the page reads to work correctly. enable2ndByteCanID = false -#unset tuneByMAF +;#unset tuneByMAF -;[SettingGroups] +[SettingGroups] ; the referenceName will over-ride previous, so if you are creating a ; settingGroup with a reference name of lambdaSensor, it will replace the ; setting group defined in the settingGroups.xml of the TunerStudio config ; folder. If is is an undefined referenceName, it will be added. ; keyword = referenceName, DisplayName + settingGroup = tuneVeMode, "VE Autotune Mode" + settingOption = tuneByTPS, "TPS-Based (See Injection -> Inj.Settings)" + settingOption = tuneByMAF, "MAF-Based" + settingOption = tuneByLoad, "Load-Based (Default)" + ; settingGroup = fAlgorithmSetting, "Fuel Logic / Tables" ; settingOption = FA_PLAIN_MAF, "Plain MAF" ; settingOption = FA_TPS, "AlphaN/TPS" @@ -685,7 +690,11 @@ fileVersion = { 20171101 } table = veTableTbl, veTableMap, "VE Table", 1 ; constant, variable xBins = veRpmBins, RPMValue +#if tuneByTPS + yBins = ignitionTpsBins, TPSValue +#else yBins = veLoadBins, MAPValue +#endif zBins = veTable ; gridHeight = 2.0 gridOrient = 250, 0, 340 ; Space 123 rotation of grid in degrees. @@ -1100,6 +1109,7 @@ menuDialog = main subMenu = idleVeCurve, "Idle VE", 0, {useSeparateVeForIdle == 1} subMenu = idleAdvanceCurve, "Idle Ignition Advance", 0, {useSeparateAdvanceForIdle == 1} subMenu = std_separator + subMenu = idleTimingPidCorrDialog, "Closed-loop Idle timing correction" subMenu = cltIdleCurve, "Warmup Idle multiplier" subMenu = iacCoastingCurve, "Coasting IAC Position for Auto-Idle", 0, {useIacTableForCoasting == 1} @@ -1291,6 +1301,12 @@ cmd_set_engine_type_default = "w\x00\x31\x00\x00" field = "Mode", injectionMode, {isInjectionEnabled == 1} field = "#Batch injection with individual wiring" field = "Two wire batch emulation", twoWireBatchInjection, {isInjectionEnabled == 1 && injectionMode == 2} +#if tuneByTPS + field = "Use TPS instead of Load for VE table", useTPSBasedVeTable, {isInjectionEnabled == 1 && fuelAlgorithm == LM_SPEED_DENSITY} +#else + field = "#Enabled for TPS-Based 'VE Autotune Mode' in Project Settings" + field = "Use TPS instead of Load for VE table", useTPSBasedVeTable, {0} +#endif dialog = ignitionSettings, "Ignition Settings" field = "Enabled", isIgnitionEnabled @@ -1804,6 +1820,22 @@ cmd_set_engine_type_default = "w\x00\x31\x00\x00" field = "#See Warmup idle multiplier" slider = "Manual IAC Position", manIdlePosition, horizontal + dialog = idleTimingPidCorrDialog, "", yAxis + field = "!This timing correction mode is Alpha Version" + field = "Use Auto-PID ignition advance control", useIdleTimingPidControl + field = "#See Idle Target RPM Curve" + field = "P-factor", idleTimingPid_pFactor, {useIdleTimingPidControl == 1} + field = "I-factor", idleTimingPid_iFactor, {useIdleTimingPidControl == 1} + field = "D-factor", idleTimingPid_dFactor, {useIdleTimingPidControl == 1} + field = "Offset", idleTimingPid_offset, {useIdleTimingPidControl == 1} + field = "Min Delta", idleTimingPid_minValue, {useIdleTimingPidControl == 1} + field = "Max Delta", idleTimingPid_maxValue, {useIdleTimingPidControl == 1} + field = "period", idleTimingPid_periodMs, {useIdleTimingPidControl == 1} + field = "#See RPM dead zone to deactivate IAC pid" + field = "RPM working zone for timing pid", idleTimingPidWorkZone, {useIdleTimingPidControl == 1} + field = "RPM working zone falloff", idlePidFalloffDeltaRpm, {useIdleTimingPidControl == 1} + field = "RPM dead zone to deactivate timing pid", idleTimingPidDeadZone, {useIdleTimingPidControl == 1} + ; Engine->Fan Settings dialog = fanSetting, "Fan Settings" field = "Fan On Temperature", fanOnTemperature diff --git a/unit_tests/tests/test_idle_controller.cpp b/unit_tests/tests/test_idle_controller.cpp index f5402f8465..5a2838ad8b 100644 --- a/unit_tests/tests/test_idle_controller.cpp +++ b/unit_tests/tests/test_idle_controller.cpp @@ -6,6 +6,8 @@ */ #include "engine_test_helper.h" +#include "advance_map.h" +#include "tps.h" #include "pid.h" TEST(idle, pid) { @@ -62,3 +64,84 @@ TEST(idle, pid) { ASSERT_EQ( 0, pid.iTerm) << "target=50, input=50 iTerm"; } + +TEST(idle, timingPid) { + print("******************************************* testTimingPidController\r\n"); + + WITH_ENGINE_TEST_HELPER(TEST_ENGINE); + // basic engine setup + setupSimpleTestEngineWithMafAndTT_ONE_trigger(ð); + + // set PID settings + pid_s pidS; + pidS.pFactor = 0.1; + pidS.iFactor = 0; + pidS.dFactor = 0; + pidS.offset = 0; + pidS.minValue = -20; + pidS.maxValue = +20; + pidS.periodMs = 1; + + // setup TimingPid settings + engineConfiguration->idleTimingPidDeadZone = 10; + engineConfiguration->idleTimingPidWorkZone = 100; + engineConfiguration->idlePidFalloffDeltaRpm = 30; + + // setup target rpm curve (we need only 1 value when CLT sensor is disabled) + const int idleRpmTarget = 700; + engineConfiguration->cltIdleRpm[0] = idleRpmTarget; + + // setup other settings + engineConfiguration->idleTimingPid = pidS; + eth.engine.fsioState.fsioTimingAdjustment = 0; + eth.engine.fsioState.fsioIdleTargetRPMAdjustment = 0; + eth.engine.engineState.cltTimingCorrection = 0; + + // configure TPS + engineConfiguration->tpsMin = 0; + engineConfiguration->tpsMax = 100; + engineConfiguration->bc.idlePidDeactivationTpsThreshold = 10; + setMockTpsPosition(0); + + // disable temperature sensors + eth.engine.sensors.clt = NAN; + eth.engine.sensors.iat = NAN; + + // all corrections disabled, should be 0 + engineConfiguration->bc.useIdleTimingPidControl = false; + angle_t corr = getAdvanceCorrections(idleRpmTarget PASS_ENGINE_PARAMETER_SUFFIX); + ASSERT_EQ(0, corr) << "getAdvanceCorrections#1"; + + // basic IDLE PID correction test + engineConfiguration->bc.useIdleTimingPidControl = true; + int baseTestRpm = idleRpmTarget + engineConfiguration->idleTimingPidWorkZone; + corr = getAdvanceCorrections(baseTestRpm PASS_ENGINE_PARAMETER_SUFFIX); + // (delta_rpm=-100) * (p-factor=0.1) = -10 degrees + ASSERT_EQ(-10, corr) << "getAdvanceCorrections#2"; + + // check if rpm is too close to the target + corr = getAdvanceCorrections((idleRpmTarget + engineConfiguration->idleTimingPidDeadZone) PASS_ENGINE_PARAMETER_SUFFIX); + ASSERT_EQ(0, corr) << "getAdvanceCorrections#3"; + + // check if rpm is too high (just outside the workzone and even falloff) so we disable the PID correction + int tooHighRpm = idleRpmTarget + engineConfiguration->idleTimingPidWorkZone + engineConfiguration->idlePidFalloffDeltaRpm; + corr = getAdvanceCorrections(tooHighRpm PASS_ENGINE_PARAMETER_SUFFIX); + ASSERT_EQ(0, corr) << "getAdvanceCorrections#4"; + + // check if rpm is within the falloff zone + int falloffRpm = idleRpmTarget + engineConfiguration->idleTimingPidWorkZone + (engineConfiguration->idlePidFalloffDeltaRpm / 2); + corr = getAdvanceCorrections(falloffRpm PASS_ENGINE_PARAMETER_SUFFIX); + // -(100+30/2) * 0.1 / 2 = -5.75 + ASSERT_FLOAT_EQ(-5.75f, corr) << "getAdvanceCorrections#5"; + + // check if PID correction is disabled in running mode (tps > threshold): + setMockTpsPosition(engineConfiguration->bc.idlePidDeactivationTpsThreshold + 1); + corr = getAdvanceCorrections(idleRpmTarget PASS_ENGINE_PARAMETER_SUFFIX); + ASSERT_EQ(0, corr) << "getAdvanceCorrections#6"; + + // check if PID correction is interpolated for transient idle-running TPS positions + setMockTpsPosition(engineConfiguration->bc.idlePidDeactivationTpsThreshold / 2); + corr = getAdvanceCorrections(baseTestRpm PASS_ENGINE_PARAMETER_SUFFIX); + ASSERT_FLOAT_EQ(-5.0f, corr) << "getAdvanceCorrections#7"; + +}