diff --git a/firmware/CHANGELOG.md b/firmware/CHANGELOG.md index e3b269f62e..93fa912d9c 100644 --- a/firmware/CHANGELOG.md +++ b/firmware/CHANGELOG.md @@ -31,6 +31,7 @@ Release template (copy/paste this for new release): - Manual electronic throttle synchronization #3680 - Delay before enabling AC compressor #4502 - Require full sync for odd cylinder count #4533 + - Hysteresis on some fuel cuts to avoid engine damage #4541 ### Fixed - Inverted vvt control #4464 diff --git a/firmware/controllers/limp_manager.cpp b/firmware/controllers/limp_manager.cpp index e2cf795403..4fef87ac28 100644 --- a/firmware/controllers/limp_manager.cpp +++ b/firmware/controllers/limp_manager.cpp @@ -45,7 +45,8 @@ void LimpManager::updateState(int rpm, efitick_t nowNt) { ? interpolate2d(Sensor::get(SensorType::Clt).value_or(0), engineConfiguration->cltRevLimitRpmBins, engineConfiguration->cltRevLimitRpm) : (float)engineConfiguration->rpmHardLimit; - if (rpm > revLimit) { + // Require 50 rpm drop before resuming + if (m_revLimitHysteresis.test(rpm, revLimit, revLimit - 50)) { if (engineConfiguration->cutFuelOnHardLimit) { allowFuel.clear(ClearReason::HardLimit); } @@ -72,8 +73,10 @@ void LimpManager::updateState(int rpm, efitick_t nowNt) { } // Limit fuel only on boost pressure (limiting spark bends valves) - if (engineConfiguration->boostCutPressure != 0) { - if (Sensor::getOrZero(SensorType::Map) > engineConfiguration->boostCutPressure) { + float mapCut = engineConfiguration->boostCutPressure; + if (mapCut != 0) { + // require drop of 20kPa to resume fuel + if (m_boostCutHysteresis.test(Sensor::getOrZero(SensorType::Map), mapCut, mapCut - 20)) { allowFuel.clear(ClearReason::BoostCut); } } @@ -118,7 +121,8 @@ void LimpManager::updateState(int rpm, efitick_t nowNt) { // If duty cycle is high, impose a fuel cut rev limiter. // This is safer than attempting to limp along with injectors or a pump that are out of flow. - if (getInjectorDutyCycle(rpm) > 96.0f) { + // only reset once below 20% duty to force the driver to lift + if (m_injectorDutyCutHysteresis.test(getInjectorDutyCycle(rpm), 96, 20)) { allowFuel.clear(ClearReason::InjectorDutyCycle); } diff --git a/firmware/controllers/limp_manager.h b/firmware/controllers/limp_manager.h index 098ef9ebd3..08b0e19cf8 100644 --- a/firmware/controllers/limp_manager.h +++ b/firmware/controllers/limp_manager.h @@ -54,6 +54,23 @@ struct LimpState { } }; +class Hysteresis { +public: + // returns true if value > rising, false if value < falling, previous if falling < value < rising. + bool test(float value, float rising, float falling) { + if (value > rising) { + m_state = true; + } else if (value < falling) { + m_state = false; + } + + return m_state; + } + +private: + bool m_state = false; +}; + class LimpManager { public: // This is called from periodicFastCallback to update internal state @@ -78,6 +95,10 @@ public: private: void setFaultRevLimit(int limit); + Hysteresis m_revLimitHysteresis; + Hysteresis m_boostCutHysteresis; + Hysteresis m_injectorDutyCutHysteresis; + // Start with no fault rev limit int32_t m_faultRevLimit = INT32_MAX; diff --git a/unit_tests/tests/test_deadband.cpp b/unit_tests/tests/test_deadband.cpp index c6303b986c..97f015690c 100644 --- a/unit_tests/tests/test_deadband.cpp +++ b/unit_tests/tests/test_deadband.cpp @@ -36,3 +36,20 @@ TEST(Deadband, InsideDeadband) { // Now it should flip back EXPECT_TRUE(d.gt(0, -5.01)); } + +TEST(Hysteresis, basic) { + Hysteresis h; + + // Below 'rising', should stay false + EXPECT_FALSE(h.test(15, 30, 20)); + EXPECT_FALSE(h.test(25, 30, 20)); + + // over 'rising', should go true + EXPECT_TRUE(h.test(31, 30, 20)); + + // drop back below 'rising', should stay true + EXPECT_TRUE(h.test(25, 30, 20)); + + // drop below 'falling', should go false + EXPECT_FALSE(h.test(15, 30, 20)); +} diff --git a/unit_tests/tests/test_limp.cpp b/unit_tests/tests/test_limp.cpp index 84746d2ef9..f62d68d9c2 100644 --- a/unit_tests/tests/test_limp.cpp +++ b/unit_tests/tests/test_limp.cpp @@ -106,13 +106,18 @@ TEST(limp, boostCut) { dut.updateState(1000, 0); EXPECT_TRUE(dut.allowInjection()); - // Above threshold, injection cut - Sensor::setMockValue(SensorType::Map, 120); + // Above rising threshold, injection cut + Sensor::setMockValue(SensorType::Map, 105); dut.updateState(1000, 0); EXPECT_FALSE(dut.allowInjection()); - // Below threshold, should recover - Sensor::setMockValue(SensorType::Map, 80); + // Below rising threshold, but should have hysteresis, so not cut yet + Sensor::setMockValue(SensorType::Map, 95); + dut.updateState(1000, 0); + EXPECT_FALSE(dut.allowInjection()); + + // Below falling threshold, fuel restored + Sensor::setMockValue(SensorType::Map, 79); dut.updateState(1000, 0); EXPECT_TRUE(dut.allowInjection());