diff --git a/firmware/controllers/algo/fuel/injector_model.cpp b/firmware/controllers/algo/fuel/injector_model.cpp index 6a4d0c5425..bdb8045f0c 100644 --- a/firmware/controllers/algo/fuel/injector_model.cpp +++ b/firmware/controllers/algo/fuel/injector_model.cpp @@ -10,14 +10,46 @@ #include "fuel_computer.h" void InjectorModelBase::prepare() { - m_massFlowRate = getInjectorMassFlowRate(); + float flowRatio = getInjectorFlowRatio(); + + // "large pulse" flow rate + m_massFlowRate = flowRatio * getBaseFlowRate(); m_deadtime = getDeadtime(); + + if (getNonlinearMode() == INJ_FordModel) { + m_smallPulseFlowRate = flowRatio * getSmallPulseFlowRate(); + m_smallPulseBreakPoint = getSmallPulseBreakPoint(); + + // amount added to small pulses to correct for the "kink" from low flow region + m_smallPulseOffset = 1000 * ((m_smallPulseBreakPoint / m_massFlowRate) - (m_smallPulseBreakPoint / m_smallPulseFlowRate)); + } } constexpr float convertToGramsPerSecond(float ccPerMinute) { return ccPerMinute * (fuelDensity / 60.f); } +float InjectorModel::getBaseFlowRate() const { + if (engineConfiguration->injectorFlowAsMassFlow) { + return engineConfiguration->injector.flow; + } else { + return convertToGramsPerSecond(engineConfiguration->injector.flow); + } +} + +float InjectorModel::getSmallPulseFlowRate() const { + return engineConfiguration->fordInjectorSmallPulseSlope; +} + +float InjectorModel::getSmallPulseBreakPoint() const { + // convert milligrams -> grams + return 0.001f * engineConfiguration->fordInjectorSmallPulseBreakPoint; +} + +InjectorNonlinearMode InjectorModel::getNonlinearMode() const { + return engineConfiguration->injectorNonlinearMode; +} + expected InjectorModel::getAbsoluteRailPressure() const { switch (engineConfiguration->injectorCompensationMode) { case ICM_FixedRailPressure: @@ -79,15 +111,6 @@ float InjectorModel::getInjectorFlowRatio() { return flowRatio; } -float InjectorModel::getInjectorMassFlowRate() { - // TODO: injector flow dependent upon temperature/ethanol content? - auto injectorVolumeFlow = engineConfiguration->injector.flow; - - float flowRatio = getInjectorFlowRatio(); - - return flowRatio * convertToGramsPerSecond(injectorVolumeFlow); -} - float InjectorModel::getDeadtime() const { return interpolate2d( Sensor::get(SensorType::BatteryVoltage).value_or(VBAT_FALLBACK_VALUE), @@ -97,18 +120,15 @@ float InjectorModel::getDeadtime() const { } float InjectorModelBase::getInjectionDuration(float fuelMassGram) const { - // TODO: support injector nonlinearity correction - - floatms_t baseDuration = fuelMassGram / m_massFlowRate * 1000; - - if (baseDuration <= 0) { - // If 0 duration, don't correct or add deadtime, just skip the injection. + if (fuelMassGram <= 0) { + // If 0 mass, don't do any math, just skip the injection. return 0.0f; } - // Correct short pulses (if enabled) - baseDuration = correctShortPulse(baseDuration); + // Get the no-offset duration + float baseDuration = getBaseDurationImpl(fuelMassGram); + // Add deadtime offset return baseDuration + m_deadtime; } @@ -117,8 +137,18 @@ float InjectorModelBase::getFuelMassForDuration(floatms_t duration) const { return duration * m_massFlowRate * 0.001f; } -float InjectorModel::correctShortPulse(float baseDuration) const { - switch (engineConfiguration->injectorNonlinearMode) { +float InjectorModelBase::getBaseDurationImpl(float fuelMassGram) const { + floatms_t baseDuration = fuelMassGram / m_massFlowRate * 1000; + + switch (getNonlinearMode()) { + case INJ_FordModel: + if (fuelMassGram < m_smallPulseBreakPoint) { + // Small pulse uses a different slope, and adds the "zero fuel pulse" offset + return (fuelMassGram / m_smallPulseFlowRate * 1000) + m_smallPulseOffset; + } else { + // Large pulse + return baseDuration; + } case INJ_PolynomialAdder: return correctInjectionPolynomial(baseDuration); case INJ_None: @@ -127,7 +157,7 @@ float InjectorModel::correctShortPulse(float baseDuration) const { } } -float InjectorModel::correctInjectionPolynomial(float baseDuration) const { +float InjectorModelBase::correctInjectionPolynomial(float baseDuration) const { if (baseDuration > engineConfiguration->applyNonlinearBelowPulse) { // Large pulse, skip correction. return baseDuration; diff --git a/firmware/controllers/algo/fuel/injector_model.h b/firmware/controllers/algo/fuel/injector_model.h index 36712e2b7b..29a075e4a3 100644 --- a/firmware/controllers/algo/fuel/injector_model.h +++ b/firmware/controllers/algo/fuel/injector_model.h @@ -17,26 +17,48 @@ public: floatms_t getInjectionDuration(float fuelMassGram) const override; float getFuelMassForDuration(floatms_t duration) const override; - virtual float getInjectorMassFlowRate() = 0; virtual float getInjectorFlowRatio() = 0; virtual expected getAbsoluteRailPressure() const = 0; - virtual float correctShortPulse(float baseDuration) const = 0; + + virtual float getBaseFlowRate() const = 0; + + virtual InjectorNonlinearMode getNonlinearMode() const = 0; + float getBaseDurationImpl(float baseDuration) const; + virtual float correctInjectionPolynomial(float baseDuration) const; + + // Ford small pulse model + virtual float getSmallPulseFlowRate() const = 0; + virtual float getSmallPulseBreakPoint() const = 0; private: + // Mass flow rate for large-pulse flow, g/s float m_massFlowRate = 0; + + // Break point below which the "small pulse" slope is used, grams + float m_smallPulseBreakPoint = 0; + + // Flow rate for small pulses, g/s + float m_smallPulseFlowRate = 0; + + // Correction adder for small pulses to correct for small/large pulse kink, ms + float m_smallPulseOffset = 0; }; class InjectorModel : public InjectorModelBase { public: floatms_t getDeadtime() const override; - float getInjectorMassFlowRate() override; + float getBaseFlowRate() const override; float getInjectorFlowRatio() override; expected getAbsoluteRailPressure() const override; + InjectorNonlinearMode getNonlinearMode() const override; + + // Ford small pulse model + float getSmallPulseFlowRate() const override; + float getSmallPulseBreakPoint() const override; + // Small pulse correction logic - float correctShortPulse(float baseDuration) const override; - virtual float correctInjectionPolynomial(float baseDuration) const; using interface_t = IInjectorModel; // Mock interface }; diff --git a/firmware/controllers/algo/rusefi_enums.h b/firmware/controllers/algo/rusefi_enums.h index 14aa35a139..4d9f868eb4 100644 --- a/firmware/controllers/algo/rusefi_enums.h +++ b/firmware/controllers/algo/rusefi_enums.h @@ -620,6 +620,7 @@ typedef enum __attribute__ ((__packed__)) { typedef enum __attribute__ ((__packed__)) { INJ_None = 0, INJ_PolynomialAdder = 1, + INJ_FordModel = 2, } InjectorNonlinearMode; typedef enum __attribute__ ((__packed__)) { diff --git a/firmware/integration/rusefi_config.txt b/firmware/integration/rusefi_config.txt index 5c87ff850b..868af6fffa 100644 --- a/firmware/integration/rusefi_config.txt +++ b/firmware/integration/rusefi_config.txt @@ -742,7 +742,9 @@ pin_input_mode_e throttlePedalUpPinMode; float compressionRatio;Just for reference really, not taken into account by any logic at this point;"CR", 1, 0, 0, 300, 1 Gpio[TRIGGER_SIMULATOR_PIN_COUNT iterate] triggerSimulatorPins;Each rusEFI piece can provide synthetic trigger signal for external ECU. Sometimes these wires are routed back into trigger inputs of the same rusEFI board.\nSee also directSelfStimulation which is different. - uint16_t unusedTrig + + uint16_t autoscale fordInjectorSmallPulseSlope;;"g/s", 0.001, 0, 0, 65, 3 + pin_output_mode_e[TRIGGER_SIMULATOR_PIN_COUNT iterate] triggerSimulatorPinModes; uint8_t unusedTrigMode output_pin_e o2heaterPin;Narrow band o2 heater, not used for CJ125. 'ON' if engine is running, 'OFF' if stopped or cranking. See wboHeaterPin @@ -817,7 +819,7 @@ output_pin_e acFanPin;Optional Radiator Fan used with A/C brain_input_pin_e vehicleSpeedSensorInputPin; switch_input_pin_e clutchUpPin;Some vehicles have a switch to indicate that clutch pedal is all the way up - custom InjectorNonlinearMode 1 bits, U08, @OFFSET@, [0:0], "None", "Polynomial" + custom InjectorNonlinearMode 1 bits, U08, @OFFSET@, [0:1], "None", "Polynomial", "Ford (dual slope)" InjectorNonlinearMode injectorNonlinearMode pin_input_mode_e clutchUpPinMode; @@ -1212,7 +1214,7 @@ int16_t tps2Max;Full throttle#2. tpsMax value as 10 bit ADC value. Not Voltage!\ bit stepperDcInvertedPins;Enable if DC-motor driver (H-bridge) inverts the signals (eg. RZ7899 on Hellen boards) bit canOpenBLT; Allow OpenBLT on Primary CAN bit can2OpenBLT; Allow OpenBLT on Secondary CAN - bit unused1740b2 + bit injectorFlowAsMassFlow,"mass flow","volumetric flow";Select whether to configure injector flow in volumetric flow (defualt, cc/min) or mass flow (g/s). bit unused1127 bit unused1128 bit unused1129 @@ -1444,7 +1446,9 @@ tChargeMode_e tChargeMode; uint8_t autoscale dfcoDelay;Delay before cutting fuel. Set to 0 to cut immediately with no delay. May cause rumbles and pops out of your exhaust...;"sec", 0.1, 0, 0, 10, 1 uint8_t autoscale acDelay;Delay before engaging the AC compressor. Set to 0 to engage immediately with no delay. Use this to prevent bogging at idle when AC engages.;"sec", 0.1, 0, 0, 10, 1 - int8_t[9] unused4080;;"", 1, 0, 0, 0, 0 + uint16_t autoscale fordInjectorSmallPulseBreakPoint;;"mg", 0.001, 0, 0, 65, 3 + + int8_t[5] unused4080;;"", 1, 0, 0, 0, 0 ! Someday there will be a 6th option for BMW S55 that uses a separate shaft just for HPFP #define hpfp_cam_e_enum "NONE", "Intake 1", "Exhaust 1", "Intake 2", "Exhaust 2" diff --git a/firmware/tunerstudio/rusefi.input b/firmware/tunerstudio/rusefi.input index ac03b40e44..df2ad4efa9 100644 --- a/firmware/tunerstudio/rusefi.input +++ b/firmware/tunerstudio/rusefi.input @@ -2222,7 +2222,8 @@ cmd_set_engine_type_default = "@@TS_IO_TEST_COMMAND_char@@@@ts_command_e_TS_ ; Engine->Injection Settings dialog = injChars, "Injector Settings", yAxis - field = "Injector Flow", injector_flow, {isInjectionEnabled == 1} + field = "Injector flow", injector_flow, {isInjectionEnabled == 1} + field = "Injector flow units", injectorFlowAsMassFlow, {isInjectionEnabled == 1} field = "Fuel rail pressure sensor", injectorPressureType, { isInjectionEnabled && (highPressureFuel_hwChannel || lowPressureFuel_hwChannel) } field = "Injector flow compensation mode", injectorCompensationMode, { isInjectionEnabled } field = "Injector reference pressure", fuelReferencePressure, { isInjectionEnabled && injectorCompensationMode != 0 } @@ -2273,9 +2274,16 @@ cmd_set_engine_type_default = "@@TS_IO_TEST_COMMAND_char@@@@ts_command_e_TS_ field = "x^6", injectorCorrectionPolynomial7 field = "x^7", injectorCorrectionPolynomial8 + dialog = injectorNonlinearFord, "Ford-model Small Pulse Correction", yAxis + field = "Small pulse slope (ALOSL)", fordInjectorSmallPulseSlope + field = "Set this to 'mass flow'", injectorFlowAsMassFlow + field = "Large pulse slope (AHISL)", injector_flow + field = "Small pulse breakpoint (FUEL_BKPT)", fordInjectorSmallPulseBreakPoint + dialog = injectorNonlinear field = "Small pulse correction mode", injectorNonlinearMode - panel = injectorNonlinearPolynomial, {1}, { injectorNonlinearMode != 0 } + panel = injectorNonlinearPolynomial, {1}, { injectorNonlinearMode == 1 } + panel = injectorNonlinearFord, {1}, { injectorNonlinearMode == 2 } dialog = testLuaOut, "Lua Out Test" commandButton = "Lua Out #1", cmd_test_lua1 diff --git a/unit_tests/tests/ignition_injection/test_injector_model.cpp b/unit_tests/tests/ignition_injection/test_injector_model.cpp index 8ea7d71ba0..a504cc866c 100644 --- a/unit_tests/tests/ignition_injection/test_injector_model.cpp +++ b/unit_tests/tests/ignition_injection/test_injector_model.cpp @@ -7,17 +7,21 @@ using ::testing::StrictMock; class MockInjectorModel : public InjectorModelBase { public: MOCK_METHOD(floatms_t, getDeadtime, (), (const, override)); - MOCK_METHOD(float, getInjectorMassFlowRate, (), (override)); + MOCK_METHOD(float, getBaseFlowRate, (), (const, override)); MOCK_METHOD(float, getInjectorFlowRatio, (), (override)); MOCK_METHOD(expected, getAbsoluteRailPressure, (), (const, override)); - MOCK_METHOD(float, correctShortPulse, (float baseDuration), (const, override)); + MOCK_METHOD(float, getSmallPulseFlowRate, (), (const, override)); + MOCK_METHOD(float, getSmallPulseBreakPoint, (), (const, override)); + MOCK_METHOD(InjectorNonlinearMode, getNonlinearMode, (), (const, override)); }; TEST(InjectorModel, Prepare) { StrictMock dut; EXPECT_CALL(dut, getDeadtime()); - EXPECT_CALL(dut, getInjectorMassFlowRate()); + EXPECT_CALL(dut, getBaseFlowRate()); + EXPECT_CALL(dut, getNonlinearMode()); + EXPECT_CALL(dut, getInjectorFlowRatio()); dut.prepare(); } @@ -27,11 +31,12 @@ TEST(InjectorModel, getInjectionDuration) { EXPECT_CALL(dut, getDeadtime()) .WillOnce(Return(2.0f)); - EXPECT_CALL(dut, getInjectorMassFlowRate()) + EXPECT_CALL(dut, getInjectorFlowRatio()) + .WillOnce(Return(1.0f)); + EXPECT_CALL(dut, getBaseFlowRate()) .WillOnce(Return(4.8f)); // 400cc/min - EXPECT_CALL(dut, correctShortPulse(_)) - .Times(2) - .WillRepeatedly([](float b) { return b; }); + EXPECT_CALL(dut, getNonlinearMode()) + .WillRepeatedly(Return(INJ_None)); dut.prepare(); @@ -39,23 +44,65 @@ TEST(InjectorModel, getInjectionDuration) { EXPECT_NEAR(dut.getInjectionDuration(0.02f), 20 / 4.8f + 2.0f, EPS4D); } -TEST(InjectorModel, getInjectionDurationNonlinear) { +TEST(InjectorModel, getInjectionDurationWithFlowRatio) { StrictMock dut; EXPECT_CALL(dut, getDeadtime()) .WillOnce(Return(2.0f)); - EXPECT_CALL(dut, getInjectorMassFlowRate()) + EXPECT_CALL(dut, getInjectorFlowRatio()) + .WillOnce(Return(1.1f)); + EXPECT_CALL(dut, getBaseFlowRate()) .WillOnce(Return(4.8f)); // 400cc/min - - // Dummy nonlinearity correction: just doubles the pulse - EXPECT_CALL(dut, correctShortPulse(_)) - .Times(2) - .WillRepeatedly([](float b) { return 2 * b; }); + EXPECT_CALL(dut, getNonlinearMode()) + .WillRepeatedly(Return(INJ_None)); dut.prepare(); - EXPECT_NEAR(dut.getInjectionDuration(0.01f), 2 * 10 / 4.8f + 2.0f, EPS4D); - EXPECT_NEAR(dut.getInjectionDuration(0.02f), 2 * 20 / 4.8f + 2.0f, EPS4D); + EXPECT_NEAR(dut.getInjectionDuration(0.01f), 10 / (4.8f * 1.1f) + 2.0f, EPS4D); + EXPECT_NEAR(dut.getInjectionDuration(0.02f), 20 / (4.8f * 1.1f) + 2.0f, EPS4D); +} + +TEST(InjectorModel, nonLinearFordMode) { + StrictMock dut; + + EXPECT_CALL(dut, getDeadtime()) + .WillOnce(Return(0)); + EXPECT_CALL(dut, getInjectorFlowRatio()) + .WillOnce(Return(1.0f)); + + // 2005 F150 injectors + EXPECT_CALL(dut, getBaseFlowRate()) + .WillRepeatedly(Return(2.979f)); + EXPECT_CALL(dut, getSmallPulseFlowRate()) + .WillRepeatedly(Return(3.562f)); + EXPECT_CALL(dut, getSmallPulseBreakPoint()) + .WillRepeatedly(Return(0.00627f)); + EXPECT_CALL(dut, getNonlinearMode()) + .WillRepeatedly(Return(INJ_FordModel)); + + dut.prepare(); + + EXPECT_NEAR(dut.getBaseDurationImpl(0.000f), 0.344f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.001f), 0.625f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.002f), 0.906f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.003f), 1.187f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.004f), 1.467f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.005f), 1.748f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.006f), 2.029f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.007f), 2.350f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.008f), 2.685f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.009f), 3.021f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.010f), 3.357f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.011f), 3.693f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.012f), 4.028f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.013f), 4.364f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.014f), 4.700f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.015f), 5.035f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.016f), 5.371f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.017f), 5.707f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.018f), 6.042f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.019f), 6.378f, 1e-3); + EXPECT_NEAR(dut.getBaseDurationImpl(0.020f), 6.714f, 1e-3); } TEST(InjectorModel, nonlinearPolynomial) { @@ -117,22 +164,6 @@ INSTANTIATE_TEST_SUITE_P( ::testing::Values(0.1f, 0.5f, 1.0f, 2.0f, 10.0f) ); -TEST_P(FlowRateFixture, FlowRateRatio) { - float flowRatio = GetParam(); - - StrictMock dut; - EXPECT_CALL(dut, getInjectorFlowRatio()).WillOnce(Return(flowRatio)); - - EngineTestHelper eth(TEST_ENGINE); - engineConfiguration->injector.flow = 500; - - // 500 cc/min = 6g/s - float expectedFlow = flowRatio * 6.0f; - - // Check that flow is adjusted correctly - EXPECT_FLOAT_EQ(expectedFlow, dut.getInjectorMassFlowRate()); -} - TEST_P(FlowRateFixture, PressureRatio) { float pressureRatio = GetParam(); // Flow ratio should be the sqrt of pressure ratio