From 5bc1949aef9fbc49d9dbbcb2267325bcb6ec709a Mon Sep 17 00:00:00 2001 From: Scott Smith Date: Fri, 19 Nov 2021 20:06:51 -0800 Subject: [PATCH] Basic framework for high pressure fuel pump control (#3476) * Basic framework for high pressure fuel pump control * Many changes Move calculations to fast callback Move main object into Engine Respond to pin changes without requiring a reboot * Use EngineModule for HpfpController Schedule pin off after executing pin on so we are sure it ends, even if the motor stops. Test scheduling. Less RAM use by only having one event and reordering fields. * Make scheduling test actually useful - need non-0 activation angle. Co-authored-by: rusefillc <48498823+rusefillc@users.noreply.github.com> --- firmware/controllers/algo/engine.h | 9 +- firmware/controllers/engine_controller.cpp | 3 - .../engine_cycle/high_pressure_fuel_pump.cpp | 182 ++++++++++- .../engine_cycle/high_pressure_fuel_pump.h | 92 +++++- .../system/timer/trigger_scheduler.cpp | 6 + firmware/integration/rusefi_config.txt | 2 +- firmware/util/math/interpolation.h | 6 +- simulator/simulator/efifeatures.h | 1 + unit_tests/efifeatures.h | 2 + unit_tests/tests/test_hpfp.cpp | 298 ++++++++++++++++++ unit_tests/tests/tests.mk | 1 + 11 files changed, 585 insertions(+), 17 deletions(-) create mode 100755 unit_tests/tests/test_hpfp.cpp diff --git a/firmware/controllers/algo/engine.h b/firmware/controllers/algo/engine.h index 286677268e..5e6e711fc6 100644 --- a/firmware/controllers/algo/engine.h +++ b/firmware/controllers/algo/engine.h @@ -8,6 +8,7 @@ #pragma once #include "globalaccess.h" +#include "engine_module.h" #include "engine_state.h" #include "rpm_calculator.h" #include "event_registry.h" @@ -18,6 +19,7 @@ #include "local_version_holder.h" #include "buttonshift.h" #include "gear_controller.h" +#include "high_pressure_fuel_pump.h" #include "limp_manager.h" #include "pin_repository.h" #include "ac_control.h" @@ -126,8 +128,12 @@ public: Mockable, #if EFI_IDLE_CONTROL IdleController, -#endif +#endif // EFI_IDLE_CONTROL TriggerScheduler, +#if EFI_HPFP && EFI_ENGINE_CONTROL + HpfpController, +#endif // EFI_HPFP && EFI_ENGINE_CONTROL + FuelPumpController, EngineModule // dummy placeholder so the previous entries can all have commas > engineModules; @@ -209,7 +215,6 @@ public: FuelSchedule injectionEvents; IgnitionEventList ignitionEvents; scheduling_s tdcScheduler[2]; - #endif /* EFI_ENGINE_CONTROL */ bool needToStopEngine(efitick_t nowNt) const; diff --git a/firmware/controllers/engine_controller.cpp b/firmware/controllers/engine_controller.cpp index 0e66697761..8a32df1fcf 100644 --- a/firmware/controllers/engine_controller.cpp +++ b/firmware/controllers/engine_controller.cpp @@ -578,9 +578,6 @@ void commonInitEngineController() { * This method adds trigger listener which actually schedules ignition */ initMainEventListener(); -#if EFI_HPFP - initHPFP(); -#endif // EFI_HPFP } #endif /* EFI_ENGINE_CONTROL */ diff --git a/firmware/controllers/engine_cycle/high_pressure_fuel_pump.cpp b/firmware/controllers/engine_cycle/high_pressure_fuel_pump.cpp index f117c759ce..56c1e58017 100644 --- a/firmware/controllers/engine_cycle/high_pressure_fuel_pump.cpp +++ b/firmware/controllers/engine_cycle/high_pressure_fuel_pump.cpp @@ -1,23 +1,195 @@ /* * @file high_pressure_fuel_pump.cpp + * @brief High Pressure Fuel Pump controller for GDI applications * + * @date Nov 6, 2021 + * @author Scott Smith, (c) 2021 + */ + +/* + * This file is part of rusEfi - see http://rusefi.com * - * todo: there is some similarity with aux_valves.cpp and even map_averaging.cpp maybe reduce code duplication? + * rusEfi is free software; you can redistribute it and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. * - * @date Nov 5, 2020 - * @author Andrey Belomutskiy, (c) 2012-2020 + * rusEfi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . */ #include "pch.h" #include "high_pressure_fuel_pump.h" #include "spark_logic.h" +#include "fuel_computer.h" #if EFI_HPFP +// A constant we use; doesn't seem important to hoist into engineConfiguration. +static constexpr int rpm_spinning_cutoff = 60; // Below this RPM, we don't run the logic -void initHPFP() { - // Deleted to make new code diff easier +angle_t HpfpLobe::findNextLobe() { + // TODO: Ideally we figure out where we are in the engine cycle and pick the next lobe + // based on that. At least we should do that when cranking, so we can start that much + // sooner. + + auto lobes = engineConfiguration->hpfpCamLobes; + if (!lobes) { + return 0; + } + + // Which lobe are we on? + int next_index = m_lobe_index + 1; + // Note, this will be insufficient if the # of cam lobes is + // dynamically changed rapidly by more than 2x, but it will + // correct itself rather quickly. + if (next_index >= lobes) { + next_index -= lobes; + } + m_lobe_index = next_index; + + // Calculate impact of VVT + angle_t vvt = 0; + if (engineConfiguration->hpfpCam != HPFP_CAM_NONE) { + // TODO: Is the sign correct here? + means ATDC? + vvt = engine->triggerCentral.getVVTPosition( + (engineConfiguration->hpfpCam - 1) / 2 & 1, // Bank + (engineConfiguration->hpfpCam - 1) & 1); // Cam + } + + return engineConfiguration->hpfpPeakPos + vvt + next_index * 720 / lobes; +} + +// As a percent of the full pump stroke +float HpfpQuantity::calcFuelPercent(int rpm) { + float fuel_requested_cc_per_cycle = + engine->injectionMass[0] * (1.f / fuelDensity) * engineConfiguration->specs.cylindersCount; + float fuel_requested_cc_per_lobe = fuel_requested_cc_per_cycle / engineConfiguration->hpfpCamLobes; + return 100.f * + fuel_requested_cc_per_lobe / engineConfiguration->hpfpPumpVolume + + interpolate3d(engineConfiguration->hpfpCompensation, + engineConfiguration->hpfpCompensationLoadBins, fuel_requested_cc_per_lobe, + engineConfiguration->hpfpCompensationRpmBins, rpm); +} + +float HpfpQuantity::calcPI(int rpm, float calc_fuel_percent) { + m_pressureTarget_kPa = std::max( + m_pressureTarget_kPa - (engineConfiguration->hpfpTargetDecay * + (FAST_CALLBACK_PERIOD_MS / 1000.)), + interpolate3d(engineConfiguration->hpfpTarget, + engineConfiguration->hpfpTargetLoadBins, Sensor::get(SensorType::Map).Value, // TODO: allow other load axis, like we claim to + engineConfiguration->hpfpTargetRpmBins, rpm)); + + float pressureError_kPa = + m_pressureTarget_kPa - Sensor::get(SensorType::FuelPressureHigh).Value; + + float p_control_percent = pressureError_kPa * engineConfiguration->hpfpPidP; + float i_factor_divisor = + 1000. * // ms/sec + 60. * // sec/min -> ms/min + 2.; // rev/cycle -> (rev * ms) / (min * cycle) + float i_factor = + engineConfiguration->hpfpPidI * // % / (kPa * lobe) + rpm * // (% * revs) / (kPa * lobe * min) + engineConfiguration->hpfpCamLobes * // lobes/cycle -> (% * revs) / (kPa * min * cycles) + (FAST_CALLBACK_PERIOD_MS / // (% * revs * ms) / (kPa * min * cycles) + i_factor_divisor); // % / kPa + float i_control_percent = m_I_sum_percent + pressureError_kPa * i_factor; + calc_fuel_percent += p_control_percent; + i_control_percent = clampF(-calc_fuel_percent, i_control_percent, + 100.f - calc_fuel_percent); + m_I_sum_percent = i_control_percent; + return p_control_percent + i_control_percent; +} + +angle_t HpfpQuantity::pumpAngleFuel(int rpm) { + // Math based on fuel requested + float fuel_requested_percent = calcFuelPercent(rpm); + + // Apply PI control + fuel_requested_percent += calcPI(rpm, fuel_requested_percent); + + // Convert to degrees + return interpolate2d(fuel_requested_percent, + engineConfiguration->hpfpLobeProfileQuantityBins, + engineConfiguration->hpfpLobeProfileAngle); +} + +void HpfpController::onFastCallback() { + // Pressure current/target calculation + int rpm = engine->rpmCalculator.getRpm(); + + // What conditions can we not handle? + if (rpm < rpm_spinning_cutoff || + engineConfiguration->hpfpCamLobes == 0 || + engineConfiguration->hpfpPumpVolume == 0 || + !enginePins.hpfpValve.isInitialized()) { + m_quantity.reset(); + m_requested_pump = 0; + m_deadtime = 0; + } else { + // Convert deadtime from ms to degrees based on current RPM + float deadtime_ms = interpolate2d( + Sensor::get(SensorType::BatteryVoltage).value_or(VBAT_FALLBACK_VALUE), + engineConfiguration->hpfpDeadtimeVoltsBins, + engineConfiguration->hpfpDeadtimeMS); + m_deadtime = deadtime_ms * rpm * (360.f / 60.f / 1000.f); + + // We set deadtime first, then pump, in case pump used to be 0. Pump is what + // determines whether we do anything or not. + m_requested_pump = m_quantity.pumpAngleFuel(rpm); + + if (!m_running) { + m_running = true; + scheduleNextCycle(); + } + } +} + +void HpfpController::pinTurnOn(HpfpController *self) { + enginePins.hpfpValve.setHigh(); + + // By scheduling the close after we already open, we don't have to worry if the engine + // stops, the valve will be turned off in a certain amount of time regardless. + scheduleByAngle(&self->m_event.scheduling, + self->m_event.scheduling.momentX, + self->m_deadtime + engineConfiguration->hpfpActivationAngle, + { pinTurnOff, self }); +} + +void HpfpController::pinTurnOff(HpfpController *self) { + enginePins.hpfpValve.setLow(); + + self->scheduleNextCycle(); +} + +void HpfpController::scheduleNextCycle() { + if (!enginePins.hpfpValve.isInitialized()) { + m_running = false; + return; + } + + angle_t lobe = m_lobe.findNextLobe(); + angle_t angle_requested = m_requested_pump; + + if (angle_requested > engineConfiguration->hpfpMinAngle) { + engine->module()->scheduleOrQueue( + &m_event, TRIGGER_EVENT_UNDEFINED, 0, + lobe - angle_requested - m_deadtime, + { pinTurnOn, this }); + + // Off will be scheduled after turning the valve on + } else { + // Schedule this, even if we aren't opening the valve this time, since this + // will schedule the next lobe. + engine->module()->scheduleOrQueue( + &m_event, TRIGGER_EVENT_UNDEFINED, 0, lobe, + { pinTurnOff, this }); + } } #endif // EFI_HPFP diff --git a/firmware/controllers/engine_cycle/high_pressure_fuel_pump.h b/firmware/controllers/engine_cycle/high_pressure_fuel_pump.h index 1e094dd6ea..2d9a0427f0 100644 --- a/firmware/controllers/engine_cycle/high_pressure_fuel_pump.h +++ b/firmware/controllers/engine_cycle/high_pressure_fuel_pump.h @@ -1,10 +1,96 @@ /* * @file high_pressure_fuel_pump.h + * @brief High Pressure Fuel Pump controller for GDI applications * - * @date Nov 5, 2020 - * @author Andrey Belomutskiy, (c) 2012-2020 + * @date Nov 6, 2021 + * @author Scott Smith, (c) 2021 + */ + +/* + * This file is part of rusEfi - see http://rusefi.com + * + * rusEfi is free software; you can redistribute it and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * rusEfi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. + * If not, see . */ #pragma once -void initHPFP(); +class HpfpLobe { +public: + uint8_t m_lobe_index = 0; ///< 0-based index of the last lobe returned + + angle_t findNextLobe(); ///< Calculate the angle (after crank TDC) for the top of the next lobe +}; + +class HpfpQuantity { +public: + float m_I_sum_percent = 0; + float m_pressureTarget_kPa = 0; + + /** + * Calculate where the pump should become active, in degrees before pump lobe TDC + */ + angle_t pumpAngleFuel(int rpm); + + /** + * Calculate the percent of the pump stroke needed to replace the fuel injected. Also + * includes pump compensation calculations. + * + * This is used by internal tests and shouldn't be called directly. Instead use + * pumpAngleFuel. + * + * Return value is nominally 0-100, but may be outside that range (including negative) if + * model parameters are not accurate. + */ + float calcFuelPercent(int rpm); + + /** + * Calculates the PI controller contribution as a percent. This amount should be added to + * calcFuelPercent() above. + * + * This is used by internal tests and shouldn't be called directly. Instead use + * pumpAngleFuel. + * + * Return value is nominally 0-100, but may be outside that range (including negative) if + * model parameters are not accurate. The sum of this and calc_fuel_percent will be 0-100. + */ + float calcPI(int rpm, float calc_fuel_percent); + + /** + * Reset internal state due to a stopped engine. + */ + void reset() { + m_I_sum_percent = 0; + m_pressureTarget_kPa = 0; + } +}; + +class HpfpController : public EngineModule { +public: + void onFastCallback() final; + +#if !EFI_UNIT_TEST +private: +#endif // EFI_UNIT_TEST + AngleBasedEvent m_event; + + HpfpQuantity m_quantity; + HpfpLobe m_lobe; + + volatile bool m_running = false; ///< Whether events are being scheduled or not + volatile angle_t m_requested_pump = 0; ///< Computed requested pump duration in degrees (not including deadtime) + volatile angle_t m_deadtime = 0; ///< Computed solenoid deadtime in degrees + + void scheduleNextCycle(); + + static void pinTurnOn(HpfpController *self); + static void pinTurnOff(HpfpController *self); +}; diff --git a/firmware/controllers/system/timer/trigger_scheduler.cpp b/firmware/controllers/system/timer/trigger_scheduler.cpp index a1efb2d203..601c6f585d 100644 --- a/firmware/controllers/system/timer/trigger_scheduler.cpp +++ b/firmware/controllers/system/timer/trigger_scheduler.cpp @@ -8,6 +8,12 @@ bool TriggerScheduler::assertNotInList(AngleBasedEvent *head, AngleBasedEvent *e } /** + * Schedules 'action' to occur at engine cycle angle 'angle'. + * + * If you know when a recent trigger occured, you can pass it in as 'trgEventIndex' and + * 'edgeTimestamp'. Otherwise pass in TRIGGER_EVENT_UNDEFINED and the work will be scheduled on + * the next trigger edge. + * * @return true if event corresponds to current tooth and was time-based scheduler * false if event was put into queue for scheduling at a later tooth */ diff --git a/firmware/integration/rusefi_config.txt b/firmware/integration/rusefi_config.txt index 20673e1359..3d45e44853 100644 --- a/firmware/integration/rusefi_config.txt +++ b/firmware/integration/rusefi_config.txt @@ -1495,7 +1495,7 @@ int8_t[MAX_CYLINDER_COUNT iterate] fuelTrim;;"Percent", @@PERCENT_TRIM_BYTE_PACK uint8_t[HPFP_LOBE_PROFILE_SIZE] autoscale hpfpLobeProfileQuantityBins;;"%", 0.5, 0, 0, 100, 1 uint8_t[HPFP_LOBE_PROFILE_SIZE] autoscale hpfpLobeProfileAngle;;"deg", 0.5, 0, 0, 125, 1 uint8_t[HPFP_DEADTIME_SIZE] hpfpDeadtimeVoltsBins;;"volts", 1, 0, 0, 255, 0 - uint16_t[HPFP_DEADTIME_SIZE] autoscale hpfpDeadtimeMS;;"ms", 0.001, 0, 0, 65, 0 + uint16_t[HPFP_DEADTIME_SIZE] autoscale hpfpDeadtimeMS;;"ms", 0.001, 0, 0, 65, 3 uint16_t[HPFP_TARGET_SIZE x HPFP_TARGET_SIZE] hpfpTarget;;"kPa", 1, 0, 0, 65000, 0 uint16_t[HPFP_TARGET_SIZE] autoscale hpfpTargetLoadBins;;"load", 0.1, 0, 0, 6500, 1 uint8_t[HPFP_TARGET_SIZE] autoscale hpfpTargetRpmBins;;"RPM", 50, 0, 0, 12500, 0 diff --git a/firmware/util/math/interpolation.h b/firmware/util/math/interpolation.h index 751fcb5aad..2e0fdfb8d2 100644 --- a/firmware/util/math/interpolation.h +++ b/firmware/util/math/interpolation.h @@ -94,9 +94,9 @@ BinResult getBin(float value, const TBin (&bins)[TSize]) { return { idx, fraction }; } -template -BinResult getBin(float value, const scaled_channel (&bins)[TSize]) { - return getBin(value * TMult, *reinterpret_cast(&bins)); +template +BinResult getBin(float value, const scaled_channel (&bins)[TSize]) { + return getBin(value * (float(TMult) / TDiv), *reinterpret_cast(&bins)); } static float linterp(float low, float high, float frac) diff --git a/simulator/simulator/efifeatures.h b/simulator/simulator/efifeatures.h index f09a8ed1bc..d5e8e835d7 100644 --- a/simulator/simulator/efifeatures.h +++ b/simulator/simulator/efifeatures.h @@ -50,6 +50,7 @@ #define EFI_CDM_INTEGRATION FALSE #define EFI_MC33816 FALSE +#define EFI_HPFP TRUE #define EFI_BLUETOOTH_SETUP FALSE diff --git a/unit_tests/efifeatures.h b/unit_tests/efifeatures.h index f1bd6b434e..8cad2dcf7d 100644 --- a/unit_tests/efifeatures.h +++ b/unit_tests/efifeatures.h @@ -74,3 +74,5 @@ #define EFI_MAP_AVERAGING TRUE #define EFI_LUA TRUE + +#define EFI_HPFP TRUE diff --git a/unit_tests/tests/test_hpfp.cpp b/unit_tests/tests/test_hpfp.cpp new file mode 100755 index 0000000000..6fcc9d3b85 --- /dev/null +++ b/unit_tests/tests/test_hpfp.cpp @@ -0,0 +1,298 @@ +#include "pch.h" + +#include "high_pressure_fuel_pump.h" +#include "fuel_computer.h" + +using ::testing::_; +using ::testing::StrictMock; + +TEST(HPFP, Lobe) { + EngineTestHelper eth(TEST_ENGINE); + + engineConfiguration->hpfpCam = HPFP_CAM_NONE; + engineConfiguration->hpfpPeakPos = 123; + engineConfiguration->hpfpCamLobes = 3; + + engine->triggerCentral.vvtPosition[0][0] = 20; // Bank 0 + engine->triggerCentral.vvtPosition[0][1] = 40; + engine->triggerCentral.vvtPosition[1][0] = 60; // Bank 1 + engine->triggerCentral.vvtPosition[1][1] = 80; + + HpfpLobe lobe; + + // Run through all five CAM modes + for (int cam = 0; cam < 5; cam++) { + static hpfp_cam_e map[5] = { HPFP_CAM_NONE, HPFP_CAM_IN1, HPFP_CAM_EX1, + HPFP_CAM_IN2, HPFP_CAM_EX2}; + engineConfiguration->hpfpCam = map[cam]; + + // Run through several cycles of the engine to make sure we keep wrapping around + for (int i = 0; i < 4; i++) { + EXPECT_EQ(lobe.findNextLobe(), 240 + 123 + cam * 20); + EXPECT_EQ(lobe.findNextLobe(), 480 + 123 + cam * 20); + EXPECT_EQ(lobe.findNextLobe(), 0 + 123 + cam * 20); + } + } + + // Can we change the number of lobes? + engineConfiguration->hpfpCam = HPFP_CAM_NONE; + engineConfiguration->hpfpCamLobes = 4; + EXPECT_EQ(lobe.findNextLobe(), 180 + 123); + EXPECT_EQ(lobe.findNextLobe(), 360 + 123); + EXPECT_EQ(lobe.findNextLobe(), 540 + 123); + EXPECT_EQ(lobe.findNextLobe(), 0 + 123); + + // Can we change the peak position? + engineConfiguration->hpfpPeakPos = 95; + EXPECT_EQ(lobe.findNextLobe(), 180 + 95); + EXPECT_EQ(lobe.findNextLobe(), 360 + 95); + EXPECT_EQ(lobe.findNextLobe(), 540 + 95); + EXPECT_EQ(lobe.findNextLobe(), 0 + 95); +} + +TEST(HPFP, InjectionReplacementFuel) { + EngineTestHelper eth(TEST_ENGINE); + + engineConfiguration->specs.cylindersCount = 4; + engineConfiguration->hpfpCamLobes = 4; + engine->injectionMass[0] = 0.05 /* cc/cyl */ * fuelDensity; + engineConfiguration->hpfpPumpVolume = 0.2; // cc/lobe + + HpfpQuantity math; + + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 25); + + // What if we change the # of lobes? + engineConfiguration->hpfpCamLobes = 3; + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 25 * 1.333333333f); + + // More fuel! + engine->injectionMass[0] = 0.1 /* cc/cyl */ * fuelDensity; + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 50 * 1.333333333f); + + // More cylinders! + engineConfiguration->specs.cylindersCount = 6; + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 50 * 2.); // Ooops we maxed out + + // Compensation testing + engineConfiguration->specs.cylindersCount = + engineConfiguration->hpfpCamLobes; // Make math easier + for (int i = 0; i < HPFP_COMPENSATION_SIZE; i++) { + // one bin every 1000 RPM + engineConfiguration->hpfpCompensationRpmBins[i] = std::min(i * 1000, 8000); + } + for (int i = 0; i < HPFP_COMPENSATION_SIZE; i++) { + // one bin every 0.05 cc/lobe + engineConfiguration->hpfpCompensationLoadBins[i] = std::min(i * 0.05, 60.); + } + + engineConfiguration->hpfpCompensation[2][1] = -10; + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 40); // -10, in cell + EXPECT_FLOAT_EQ(math.calcFuelPercent(1500), 45); // -5, half way + EXPECT_FLOAT_EQ(math.calcFuelPercent(2000), 50); // -0, in next cell + + engineConfiguration->hpfpCompensation[2][1] = 20; + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 70); // +20, in cell + EXPECT_FLOAT_EQ(math.calcFuelPercent(1500), 60); // +10, half way + EXPECT_FLOAT_EQ(math.calcFuelPercent(2000), 50); // +0, in next cell + + // 25% more fuel (1.25*50=62.5 base), but half way between comp of 20 and 0 (so comp of 10) + engine->injectionMass[0] = 0.125 /* cc/cyl */ * fuelDensity; + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 72.5); // +10 comp + EXPECT_FLOAT_EQ(math.calcFuelPercent(1500), 67.5); // +5, half way + EXPECT_FLOAT_EQ(math.calcFuelPercent(2000), 62.5); // +0 base +} + +TEST(HPFP, PI) { + EngineTestHelper eth(TEST_ENGINE); + + engineConfiguration->specs.cylindersCount = 4; + engineConfiguration->hpfpCamLobes = 4; + engine->injectionMass[0] = 0.05 /* cc/cyl */ * fuelDensity; + engineConfiguration->hpfpPumpVolume = 0.2; // cc/lobe + + HpfpQuantity math; + + for (int i = 0; i < HPFP_TARGET_SIZE; i++) { + // one bin every 1000 RPM + engineConfiguration->hpfpTargetRpmBins[i] = std::min(i * 1000, 8000); + } + for (int i = 0; i < HPFP_TARGET_SIZE; i++) { + // one bin every 20kPa + engineConfiguration->hpfpTargetLoadBins[i] = std::min(i * 20, 200); + } + for (int r = 0; r < HPFP_TARGET_SIZE; r++) { + for (int c = 0; c < HPFP_TARGET_SIZE; c++) { + engineConfiguration->hpfpTarget[r][c] = 1000 * r + 10 * c; + } + } + + Sensor::setMockValue(SensorType::Map, 40); + Sensor::setMockValue(SensorType::FuelPressureHigh, 1000); + EXPECT_FLOAT_EQ(math.calcPI(1000, 120), -20); // Test integral clamp + EXPECT_FLOAT_EQ(math.m_I_sum_percent, -20); // and again + EXPECT_FLOAT_EQ(math.m_pressureTarget_kPa, 2010); + EXPECT_FLOAT_EQ(math.calcPI(1000, -40), 40); // Test integral clamp + EXPECT_FLOAT_EQ(math.m_I_sum_percent, 40); // and again + EXPECT_FLOAT_EQ(math.m_pressureTarget_kPa, 2010); + + // Test pressure decay + math.calcPI(4000, 0); + EXPECT_FLOAT_EQ(math.m_pressureTarget_kPa, 2040); + math.calcPI(1000, 0); + EXPECT_FLOAT_EQ(math.m_pressureTarget_kPa, 2040); + engineConfiguration->hpfpTargetDecay = 1000; + math.calcPI(1000, 0); + EXPECT_FLOAT_EQ(math.m_pressureTarget_kPa, 2035); // 5ms of decay + + // Proportional gain + math.reset(); // Reset for ease of testing + EXPECT_FLOAT_EQ(math.calcPI(1000, 0), 0); // Validate reset + EXPECT_FLOAT_EQ(math.m_pressureTarget_kPa, 2010); + engineConfiguration->hpfpPidP = 0.01; + EXPECT_FLOAT_EQ(math.calcPI(1000, 0), 10.10); + engineConfiguration->hpfpPidP = 0.02; + EXPECT_FLOAT_EQ(math.calcPI(1000, 0), 20.20); + + // Integral gain + engineConfiguration->hpfpPidI = 0.001; + EXPECT_FLOAT_EQ(math.calcPI(1000, 0), 20.368334); + EXPECT_FLOAT_EQ(math.m_I_sum_percent, 0.168333333); +} + +TEST(HPFP, Angle) { + EngineTestHelper eth(TEST_ENGINE); + + engineConfiguration->specs.cylindersCount = 4; + engineConfiguration->hpfpCamLobes = 4; + engine->injectionMass[0] = 0.05 /* cc/cyl */ * fuelDensity; + engineConfiguration->hpfpPumpVolume = 0.2; // cc/lobe + + HpfpQuantity math; + + for (int i = 0; i < HPFP_TARGET_SIZE; i++) { + // one bin every 1000 RPM + engineConfiguration->hpfpTargetRpmBins[i] = std::min(i * 1000, 8000); + } + for (int i = 0; i < HPFP_TARGET_SIZE; i++) { + // one bin every 20kPa + engineConfiguration->hpfpTargetLoadBins[i] = std::min(i * 20, 200); + } + for (int r = 0; r < HPFP_TARGET_SIZE; r++) { + for (int c = 0; c < HPFP_TARGET_SIZE; c++) { + engineConfiguration->hpfpTarget[r][c] = 1000 * r + 10 * c; + } + } + for (int i = 0; i < HPFP_LOBE_PROFILE_SIZE; i++) { + engineConfiguration->hpfpLobeProfileQuantityBins[i] = 100. * i / (HPFP_LOBE_PROFILE_SIZE - 1); + engineConfiguration->hpfpLobeProfileAngle[i] = 150. * i / (HPFP_LOBE_PROFILE_SIZE - 1); + } + + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 25); // Double check baseline + EXPECT_FLOAT_EQ(math.calcPI(1000, 10), 0); // Validate no PI + EXPECT_NEAR(math.pumpAngleFuel(1000), 37.5, 0.4); // Given the profile, should be 50% higher + + engine->injectionMass[0] = 0.08 /* cc/cyl */ * fuelDensity; + EXPECT_FLOAT_EQ(math.calcFuelPercent(1000), 40); // Double check baseline + EXPECT_FLOAT_EQ(math.calcPI(1000, 10), 0); // Validate no PI + EXPECT_NEAR(math.pumpAngleFuel(1000), 60, 0.4); // Given the profile, should be 50% higher + + engineConfiguration->hpfpPidP = 0.01; + Sensor::setMockValue(SensorType::Map, 40); + Sensor::setMockValue(SensorType::FuelPressureHigh, 1000); + EXPECT_FLOAT_EQ(math.calcPI(1000, 10), 10.1); + EXPECT_NEAR(math.pumpAngleFuel(1000), 50.1 * 1.5, 0.4); // Given the profile, should be 50% higher +} + +TEST(HPFP, Schedule) { + EngineTestHelper eth(TEST_ENGINE); + + engineConfiguration->specs.cylindersCount = 4; + engineConfiguration->hpfpCamLobes = 4; + engineConfiguration->hpfpPumpVolume = 0.2; // cc/lobe + + for (int i = 0; i < HPFP_TARGET_SIZE; i++) { + // one bin every 1000 RPM + engineConfiguration->hpfpTargetRpmBins[i] = std::min(i * 1000, 8000); + } + for (int i = 0; i < HPFP_TARGET_SIZE; i++) { + // one bin every 20kPa + engineConfiguration->hpfpTargetLoadBins[i] = std::min(i * 20, 200); + } + for (int r = 0; r < HPFP_TARGET_SIZE; r++) { + for (int c = 0; c < HPFP_TARGET_SIZE; c++) { + engineConfiguration->hpfpTarget[r][c] = 1000 * r + 10 * c; + } + } + for (int i = 0; i < HPFP_LOBE_PROFILE_SIZE; i++) { + engineConfiguration->hpfpLobeProfileQuantityBins[i] = 100. * i / (HPFP_LOBE_PROFILE_SIZE - 1); + engineConfiguration->hpfpLobeProfileAngle[i] = 150. * i / (HPFP_LOBE_PROFILE_SIZE - 1); + } + + auto & hpfp = *engine->module(); + + StrictMock mockExec; + engine->executor.setMockExecutor(&mockExec); + engineConfiguration->hpfpActivationAngle = 30; + + constexpr angle_t angle0 = 90; + constexpr angle_t angle1 = 270 - 37.6923065; + constexpr angle_t angle2 = angle1 + 30; + + constexpr float tick_per_deg = USF2NT(1000000.)*60/360/1000; + + constexpr efitick_t nt0 = tick_per_deg * angle0; + constexpr efitick_t nt1 = tick_per_deg * angle1; + constexpr efitick_t nt2 = tick_per_deg * angle2; + + { + testing::InSequence is; + + // First call to assignRpmValue will cause a dummy call to fast periodic timer. + // Injection Mass will be 0 so expect a no-op. + EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), &hpfp.m_event.scheduling, nt0, action_s(HpfpController::pinTurnOff, &hpfp))); + + // Second call will be the start of a real pump event. + EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), &hpfp.m_event.scheduling, nt1, action_s(HpfpController::pinTurnOn, &hpfp))); + + // Third call will be off event + EXPECT_CALL(mockExec, scheduleByTimestampNt(testing::NotNull(), &hpfp.m_event.scheduling, nt2, action_s(HpfpController::pinTurnOff, &hpfp))); + } + EXPECT_CALL(mockExec, cancel(_)).Times(2); + + // For HPFP to work, events need to be scheduled after the next tooth. This makes sure the + // peak pos occurs after the next tooth. + engineConfiguration->hpfpPeakPos = 90; + // This will call the fast callback routine + engine->rpmCalculator.assignRpmValue(1000); + engine->injectionMass[0] = 0.05 /* cc/cyl */ * fuelDensity; + engineConfiguration->hpfpValvePin = GPIOA_2; // arbitrary + + hpfp.onFastCallback(); + // First event was scheduled by assignRpmValue with 0 injection mass. So, it's off. + eth.assertTriggerEvent("h0", 0, &hpfp.m_event, (void*)&HpfpController::pinTurnOff, + 1, angle0 - 0); + + // Make the previous event happen, schedule the next. + engine->module()->scheduleEventsUntilNextTriggerTooth( + 1000, 1, tick_per_deg * 0); + // Mock executor doesn't run events, so we run it manually + HpfpController::pinTurnOff(&hpfp); + + // Now we should have a regular on/off event. + eth.assertTriggerEvent("h1", 0, &hpfp.m_event, (void*)&HpfpController::pinTurnOn, + 2, angle1 - 180); + + // Make it happen + engine->module()->scheduleEventsUntilNextTriggerTooth( + 1000, 2, tick_per_deg * 180); + + // Since we have a mock scheduler, lets insert the correct timestamp in the scheduling + // struct. + hpfp.m_event.scheduling.momentX = nt1; + + HpfpController::pinTurnOn(&hpfp); + + // The off event goes directly to scheduleByAngle and is tested by the last EXPECT_CALL + // above. +} diff --git a/unit_tests/tests/tests.mk b/unit_tests/tests/tests.mk index c767160fca..68ed905d19 100644 --- a/unit_tests/tests/tests.mk +++ b/unit_tests/tests/tests.mk @@ -80,6 +80,7 @@ TESTS_SRC_CPP = \ tests/test_stft.cpp \ tests/test_boost.cpp \ tests/test_gppwm.cpp \ + tests/test_hpfp.cpp \ tests/test_fuel_math.cpp \ tests/test_binary_log.cpp \ tests/test_dynoview.cpp \