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 \