From e2974cfeda7e693d0d972ab7c99892417c27878e Mon Sep 17 00:00:00 2001 From: Matthew Kennedy Date: Thu, 7 May 2020 05:52:32 -0700 Subject: [PATCH] short term fuel trim: part 2 (#1405) * add tooltip * add other direction to deadband * add impl * test partitioning * makefile * wrong comment * fix include --- .../controllers/math/closed_loop_fuel.cpp | 105 ++++++++++++++++++ firmware/controllers/math/closed_loop_fuel.h | 7 ++ firmware/controllers/math/math.mk | 1 + firmware/integration/rusefi_config.txt | 2 +- firmware/util/math/deadband.h | 5 + unit_tests/tests/test_stft.cpp | 31 +++++- 6 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 firmware/controllers/math/closed_loop_fuel.cpp create mode 100644 firmware/controllers/math/closed_loop_fuel.h diff --git a/firmware/controllers/math/closed_loop_fuel.cpp b/firmware/controllers/math/closed_loop_fuel.cpp new file mode 100644 index 0000000000..6abfa49928 --- /dev/null +++ b/firmware/controllers/math/closed_loop_fuel.cpp @@ -0,0 +1,105 @@ +#include "closed_loop_fuel.h" +#include "closed_loop_fuel_cell.h" + +#include "engine.h" + +#include "sensor.h" +#include "engine_math.h" +#include "deadband.h" + +EXTERN_ENGINE; + +ClosedLoopFuelCellImpl cells[STFT_CELL_COUNT]; + +static Deadband<25> idleDeadband; +static Deadband<2> overrunDeadband; +static Deadband<2> loadDeadband; + +size_t computeStftBin(int rpm, float load, stft_s& cfg) { + // Low RPM -> idle + if (idleDeadband.lt(rpm, cfg.maxIdleRegionRpm * RPM_1_BYTE_PACKING_MULT)) + { + return 0; + } + + // Low load -> overrun + if (overrunDeadband.lt(load, cfg.maxOverrunLoad)) + { + return 1; + } + + // High load -> power + if (loadDeadband.gt(load, cfg.minPowerLoad)) + { + return 2; + } + + // Default -> normal "in the middle" cell + return 3; +} + +static bool shouldCorrect(DECLARE_ENGINE_PARAMETER_SIGNATURE) { + const auto& cfg = CONFIG(stft); + + // User disable bit + if (!CONFIG(fuelClosedLoopCorrectionEnabled)) { + return false; + } + + // Don't correct if not running + if (!ENGINE(rpmCalculator).isRunning(PASS_ENGINE_PARAMETER_SIGNATURE)) { + return false; + } + + // Startup delay - allow O2 sensor to warm up, etc + if (cfg.startupDelay > ENGINE(engineState.running.timeSinceCrankingInSecs)) { + return false; + } + + // Check that the engine is hot enough (and clt not failed) + auto clt = Sensor::get(SensorType::Clt); + if (!clt.Valid || clt.Value < cfg.minClt) { + return false; + } + + // If all was well, then we're enabled! + return true; +} + +static bool shouldUpdateCorrection(DECLARE_ENGINE_PARAMETER_SIGNATURE) { + const auto& cfg = CONFIG(stft); + + // Pause (but don't reset) correction if the AFR is off scale. + // It's probably a transient and poorly tuned transient correction + float afr = ENGINE(sensors.currentAfr); + if (afr < (cfg.minAfr * 0.1f) || afr > (cfg.maxAfr * 0.1f)) { + return false; + } + + return true; +} + +float fuelClosedLoopCorrection(DECLARE_ENGINE_PARAMETER_SIGNATURE) { + if (!shouldCorrect(PASS_ENGINE_PARAMETER_SIGNATURE)) { + return 1.0f; + } + + size_t binIdx = computeStftBin(GET_RPM(), getEngineLoadT(PASS_ENGINE_PARAMETER_SIGNATURE), CONFIG(stft)); + +#if EFI_TUNER_STUDIO + if (engineConfiguration->debugMode == DBG_FUEL_PID_CORRECTION) { + tsOutputChannels.debugIntField1 = binIdx; + } +#endif // EFI_TUNER_STUDIO + + auto& cell = cells[binIdx]; + + // todo: push configuration at startup + cell.configure(&CONFIG(stft.cellCfgs[binIdx])); + + if (shouldUpdateCorrection(PASS_ENGINE_PARAMETER_SIGNATURE)) { + cell.update(CONFIG(stft.deadband) * 0.001f, CONFIG(stftIgnoreErrorMagnitude) PASS_ENGINE_PARAMETER_SUFFIX); + } + + return cell.getAdjustment(); +} diff --git a/firmware/controllers/math/closed_loop_fuel.h b/firmware/controllers/math/closed_loop_fuel.h new file mode 100644 index 0000000000..aff0d2c426 --- /dev/null +++ b/firmware/controllers/math/closed_loop_fuel.h @@ -0,0 +1,7 @@ +#pragma once + +#include "globalaccess.h" +#include "engine_configuration_generated_structures.h" + +float fuelClosedLoopCorrection(DECLARE_ENGINE_PARAMETER_SIGNATURE); +size_t computeStftBin(int rpm, float load, stft_s& cfg); diff --git a/firmware/controllers/math/math.mk b/firmware/controllers/math/math.mk index 3a66d4acef..99e4cfd952 100644 --- a/firmware/controllers/math/math.mk +++ b/firmware/controllers/math/math.mk @@ -4,5 +4,6 @@ CONTROLLERS_MATH_SRC = CONTROLLERS_MATH_SRC_CPP = $(PROJECT_DIR)/controllers/math/engine_math.cpp \ $(PROJECT_DIR)/controllers/math/pid_auto_tune.cpp \ $(PROJECT_DIR)/controllers/math/speed_density.cpp \ + $(PROJECT_DIR)/controllers/math/closed_loop_fuel.cpp \ $(PROJECT_DIR)/controllers/math/closed_loop_fuel_cell.cpp \ diff --git a/firmware/integration/rusefi_config.txt b/firmware/integration/rusefi_config.txt index a29a238816..f8fed4619f 100644 --- a/firmware/integration/rusefi_config.txt +++ b/firmware/integration/rusefi_config.txt @@ -817,7 +817,7 @@ custom maf_sensor_type_e 4 bits, S32, @OFFSET@, [0:7], @@maf_sensor_type_e_enum@ bit enableCanVss bit enableInnovateLC2 bit showHumanReadableWarning - bit stftIgnoreErrorMagnitude + bit stftIgnoreErrorMagnitude;+If enabled, adjust at a constant rate instead of a rate proportional to the current lambda error. This mode may be easier to tune, and more tolerant of sensor noise. Use of this mode is required if you have a narrowband O2 sensor.; bit unusedBit_251_11 bit unusedBit_251_12 bit unusedBit_251_13 diff --git a/firmware/util/math/deadband.h b/firmware/util/math/deadband.h index 2a6a825fc8..01501941dd 100644 --- a/firmware/util/math/deadband.h +++ b/firmware/util/math/deadband.h @@ -24,6 +24,11 @@ public: return m_lastState; } + // Deadband has no concept of equal - only greater and less, so to compute gt, we just swap args + bool lt(float lhs, float rhs) { + return gt(rhs, lhs); + } + private: bool m_lastState =false; }; diff --git a/unit_tests/tests/test_stft.cpp b/unit_tests/tests/test_stft.cpp index 161dc149a4..0f442bde64 100644 --- a/unit_tests/tests/test_stft.cpp +++ b/unit_tests/tests/test_stft.cpp @@ -1,5 +1,6 @@ #include "closed_loop_fuel_cell.h" +#include "closed_loop_fuel.h" #include "engine.h" @@ -34,7 +35,6 @@ TEST(ClosedLoopCell, TestDeadband) { TEST(ClosedLoopFuelCell, AdjustRate) { StrictMock cl; - // Error is more than deadtime, so nothing else should be called EXPECT_CALL(cl, getLambdaError(_, _, _)) .WillOnce(Return(0.1f)); EXPECT_CALL(cl, getMinAdjustment()) @@ -50,3 +50,32 @@ TEST(ClosedLoopFuelCell, AdjustRate) { // dt = 1000.0f / FAST_CALLBACK_PERIOD_MS EXPECT_FLOAT_EQ(cl.getAdjustment(), 1 + (0.2f / (1000.0f / FAST_CALLBACK_PERIOD_MS))); } + +TEST(ClosedLoopFuel, CellSelection) { + stft_s cfg; + + // Sensible region config + cfg.maxIdleRegionRpm = 1500 / RPM_1_BYTE_PACKING_MULT; + cfg.minPowerLoad = 80; + cfg.maxOverrunLoad = 30; + + // Test idle + EXPECT_EQ(0, computeStftBin(1000, 10, cfg)); + EXPECT_EQ(0, computeStftBin(1000, 50, cfg)); + EXPECT_EQ(0, computeStftBin(1000, 90, cfg)); + + // Test overrun + EXPECT_EQ(1, computeStftBin(2000, 10, cfg)); + EXPECT_EQ(1, computeStftBin(4000, 10, cfg)); + EXPECT_EQ(1, computeStftBin(10000, 10, cfg)); + + // Test load + EXPECT_EQ(2, computeStftBin(2000, 90, cfg)); + EXPECT_EQ(2, computeStftBin(4000, 90, cfg)); + EXPECT_EQ(2, computeStftBin(10000, 90, cfg)); + + // Main cell + EXPECT_EQ(3, computeStftBin(2000, 50, cfg)); + EXPECT_EQ(3, computeStftBin(4000, 50, cfg)); + EXPECT_EQ(3, computeStftBin(10000, 50, cfg)); +}