GCC Code Coverage Report


Directory: ./
File: firmware/controllers/engine_cycle/high_pressure_fuel_pump.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 95.6% 109 0 114
Functions: 100.0% 10 0 10
Branches: 91.5% 43 0 47
Decisions: 83.3% 15 - 18

Line Branch Decision Exec Source
1 /*
2 * @file high_pressure_fuel_pump.cpp
3 * @brief High Pressure Fuel Pump controller for GDI applications
4 *
5 * TL,DR: we have constant displacement mechanical pump driven by camshaft
6 * here we control desired fuel high pressure by controlling relief/strain/spill valve electronically
7 *
8 * @date Nov 6, 2021
9 * @author Scott Smith, (c) 2021
10 */
11
12 /*
13 * This file is part of rusEfi - see http://rusefi.com
14 *
15 * rusEfi is free software; you can redistribute it and/or modify it under the terms of
16 * the GNU General Public License as published by the Free Software Foundation; either
17 * version 3 of the License, or (at your option) any later version.
18 *
19 * rusEfi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
20 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License along with this program.
24 * If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "pch.h"
28
29 #include "high_pressure_fuel_pump.h"
30 #include "spark_logic.h"
31 #include "fuel_computer.h"
32
33 #if EFI_HPFP
34 #if !EFI_SHAFT_POSITION_INPUT
35 fail("EFI_SHAFT_POSITION_INPUT required to have EFI_EMULATE_POSITION_SENSORS")
36 #endif
37
38 // A constant we use; doesn't seem important to hoist into engineConfiguration.
39 static constexpr int rpm_spinning_cutoff = 60; // Below this RPM, we don't run the logic
40
41 88 angle_t HpfpLobe::findNextLobe() {
42 // TODO: Ideally we figure out where we are in the engine cycle and pick the next lobe
43 // based on that. At least we should do that when cranking, so we can start that much
44 // sooner.
45
46 88 auto lobes = engineConfiguration->hpfpCamLobes;
47
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 88 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 88 times.
88 if (!lobes) {
48 return 0;
49 }
50
51 // Which lobe are we on?
52 88 int next_index = m_lobe_index + 1;
53 // Note, this will be insufficient if the # of cam lobes is
54 // dynamically changed rapidly by more than 2x, but it will
55 // correct itself rather quickly.
56
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 60 times.
2/2
✓ Decision 'true' taken 28 times.
✓ Decision 'false' taken 60 times.
88 if (next_index >= lobes) {
57 28 next_index -= lobes;
58 }
59 88 m_lobe_index = next_index;
60
61 // Calculate impact of VVT
62 88 angle_t vvt = 0;
63
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 22 times.
2/2
✓ Decision 'true' taken 66 times.
✓ Decision 'false' taken 22 times.
88 if (engineConfiguration->hpfpCam != HPFP_CAM_NONE) {
64 // pump operates in cam-angle domain which is different speed from crank-angle domain on 4 stroke engines
65 66 int mult = (int)getEngineCycle(getEngineRotationState()->getOperationMode()) / 360;
66 66 int camIndex = engineConfiguration->hpfpCam - 1;
67 // TODO: Is the sign correct here? + means ATDC?
68 132 vvt = engine->triggerCentral.getVVTPosition(
69 66 BANK_BY_INDEX(camIndex),
70 66 CAM_BY_INDEX(camIndex)) / mult;
71 }
72
73 // heh, looks like we assume 4 stroke here?
74 88 return engineConfiguration->hpfpPeakPos + vvt + next_index * 720 / lobes;
75 }
76
77 // As a percent of the full pump stroke
78 22 float HpfpQuantity::calcFuelPercent(float rpm) {
79 22 float fuel_requested_cc_per_cycle =
80 22 engine->engineState.injectionMass[0] * (1.f / fuelDensity) * engineConfiguration->cylindersCount;
81 22 float fuel_requested_cc_per_lobe = fuel_requested_cc_per_cycle / engineConfiguration->hpfpCamLobes;
82 22 return 100.f *
83 22 fuel_requested_cc_per_lobe / engineConfiguration->hpfpPumpVolume +
84 44 interpolate3d(config->hpfpCompensation,
85 22 config->hpfpCompensationLoadBins, fuel_requested_cc_per_lobe,
86 22 config->hpfpCompensationRpmBins, rpm);
87 }
88
89 19 static float getLoad() {
90
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 switch(engineConfiguration->fuelAlgorithm) {
91 // TODO: allow other load axis, like we claim to
92 case engine_load_mode_e::LM_ALPHA_N:
93 return Sensor::getOrZero(SensorType::DriverThrottleIntent);
94
1/1
✓ Decision 'true' taken 19 times.
19 default:
95 19 return Sensor::getOrZero(SensorType::Map);
96 }
97 }
98
99 19 float HpfpQuantity::calcPI(float rpm, float calc_fuel_percent, HpfpController *model) {
100
1/1
✓ Branch 1 taken 19 times.
19 float load = getLoad();
101
102 19 float possibleValue = model->m_pressureTarget_kPa - (engineConfiguration->hpfpTargetDecay *
103 19 (FAST_CALLBACK_PERIOD_MS / 1000.));
104
105 19 model->m_pressureTarget_kPa = std::max<float>(possibleValue,
106 19 interpolate3d(config->hpfpTarget,
107 19 config->hpfpTargetLoadBins, load,
108
1/1
✓ Branch 1 taken 19 times.
19 config->hpfpTargetRpmBins, rpm));
109
110
1/1
✓ Branch 2 taken 19 times.
19 auto fuelPressure = Sensor::get(SensorType::FuelPressureHigh);
111
2/2
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 11 times.
2/2
✓ Decision 'true' taken 8 times.
✓ Decision 'false' taken 11 times.
19 if (!fuelPressure) {
112 8 return 0;
113 }
114
115 11 float pressureError_kPa =
116 11 model->m_pressureTarget_kPa - fuelPressure.Value;
117
118 11 model->hpfp_p_control_percent = pressureError_kPa * engineConfiguration->hpfpPidP;
119 11 float i_factor_divisor =
120 1000. * // ms/sec
121 60. * // sec/min -> ms/min
122 2.; // rev/cycle -> (rev * ms) / (min * cycle)
123 float i_factor =
124 22 engineConfiguration->hpfpPidI * // % / (kPa * lobe)
125 11 rpm * // (% * revs) / (kPa * lobe * min)
126 11 engineConfiguration->hpfpCamLobes * // lobes/cycle -> (% * revs) / (kPa * min * cycles)
127 11 (FAST_CALLBACK_PERIOD_MS / // (% * revs * ms) / (kPa * min * cycles)
128 11 i_factor_divisor); // % / kPa
129 11 float unclamped_i_control_percent = model->hpfp_i_control_percent + pressureError_kPa * i_factor;
130 // Clamp the output so that calc_fuel_percent+i_control_percent is within 0% to 100%
131 // That way the I term can override any fuel calculations over the long term.
132 // The P term is still allowed to drive the total output over 100% or under 0% to react to
133 // short term errors.
134
1/1
✓ Branch 1 taken 11 times.
11 model->hpfp_i_control_percent = clampF(-calc_fuel_percent, unclamped_i_control_percent,
135 100.f - calc_fuel_percent);
136 11 return model->hpfp_p_control_percent + model->hpfp_i_control_percent;
137 }
138
139 7 angle_t HpfpQuantity::pumpAngleFuel(float rpm, HpfpController *model) {
140 // Math based on fuel requested
141 7 model->fuel_requested_percent = calcFuelPercent(rpm);
142
143 7 model->fuel_requested_percent_pi = calcPI(rpm, model->fuel_requested_percent, model);
144 // Apply PI control
145 7 float fuel_requested_percentTotal = model->fuel_requested_percent + model->fuel_requested_percent_pi;
146
147 // Convert to degrees
148 14 return interpolate2d(fuel_requested_percentTotal,
149 7 config->hpfpLobeProfileQuantityBins,
150 7 config->hpfpLobeProfileAngle);
151 }
152
153 1121 void HpfpController::onFastCallback() {
154 // Pressure current/target calculation
155 1121 float rpm = Sensor::getOrZero(SensorType::Rpm);
156
157
2/2
✓ Branch 0 taken 559 times.
✓ Branch 1 taken 562 times.
1680 isHpfpActive = !(rpm < rpm_spinning_cutoff ||
158
4/4
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 554 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 1 time.
564 !isGdiEngine() ||
159 5 engineConfiguration->hpfpPumpVolume == 0 ||
160
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 !enginePins.hpfpValve.isInitialized());
161 // What conditions can we not handle?
162
2/2
✓ Branch 0 taken 1117 times.
✓ Branch 1 taken 4 times.
2/2
✓ Decision 'true' taken 1117 times.
✓ Decision 'false' taken 4 times.
1121 if (!isHpfpActive) {
163 1117 resetQuantity();
164 1117 m_requested_pump = 0;
165 1117 m_deadangle = 0;
166 } else {
167 #if EFI_PROD_CODE && EFI_SHAFT_POSITION_INPUT
168 criticalAssertVoid(engine->triggerCentral.triggerShape.getSize() > engineConfiguration->hpfpCamLobes * 6, "Too few trigger tooth for this number of HPFP lobes");
169 #endif // EFI_PROD_CODE
170 // Convert deadtime from ms to degrees based on current RPM
171
1/1
✓ Branch 2 taken 4 times.
4 float deadtime_ms = interpolate2d(
172
1/1
✓ Branch 1 taken 4 times.
8 Sensor::get(SensorType::BatteryVoltage).value_or(VBAT_FALLBACK_VALUE),
173 4 config->hpfpDeadtimeVoltsBins,
174 4 config->hpfpDeadtimeMS);
175 4 m_deadangle = deadtime_ms * rpm * (360.f / 60.f / 1000.f);
176
177 // We set deadtime first, then pump, in case pump used to be 0. Pump is what
178 // determines whether we do anything or not.
179 4 m_requested_pump = m_quantity.pumpAngleFuel(rpm, this);
180
181
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 2 times.
4 if (!m_running) {
182 2 m_running = true;
183 2 scheduleNextCycle();
184 }
185 }
186 1121 }
187
188 #define HPFP_CONTROLLER "hpfp"
189
190 19 void HpfpController::pinTurnOn(HpfpController *self) {
191 19 enginePins.hpfpValve.setHigh(HPFP_CONTROLLER);
192 19 self->HpfpValveState = true;
193
3/3
✓ Branch 2 taken 19 times.
✓ Branch 5 taken 19 times.
✓ Branch 8 taken 19 times.
19 self->HpfdActivationPhase = getTriggerCentral()->getCurrentEnginePhase(getTimeNowNt()).value_or(-1);
194
195 // By scheduling the close after we already open, we don't have to worry if the engine
196 // stops, the valve will be turned off in a certain amount of time regardless.
197 //
198 // We do not need precise control of valve _duration_ since
199 // For the solenoid type pump, the pump has a certain volume. You can activate the solenoid to request that the pump start pressurizing.
200 // Once it reaches a certain pressure, it is effectively self running and won't unlatch until the pump reaches the top.
201 // Since the solenoid latches itself, you don't have to keep it activated for the whole lobe. You just need to activate it until it latches and then let it do the rest of the work.
202 // see also https://rusefi.com/forum/viewtopic.php?f=5&t=2192
203
204
1/1
✓ Branch 2 taken 19 times.
38 scheduleByAngle(&self->m_event.eventScheduling,
205 self->m_event.eventScheduling.getMomentNt(),
206 19 self->m_deadangle + engineConfiguration->hpfpActivationAngle,
207 38 action_s::make<pinTurnOff>( self ));
208 19 }
209
210 18 void HpfpController::pinTurnOff(HpfpController *self) {
211 18 enginePins.hpfpValve.setLow(HPFP_CONTROLLER);
212 18 self->HpfpValveState = false;
213
3/3
✓ Branch 2 taken 18 times.
✓ Branch 5 taken 18 times.
✓ Branch 8 taken 18 times.
18 self->HpfdDeactivationPhase = getTriggerCentral()->getCurrentEnginePhase(getTimeNowNt()).value_or(-1);
214
215 18 self->scheduleNextCycle();
216 18 }
217
218 20 void HpfpController::scheduleNextCycle() {
219
1/1
✓ Branch 1 taken 20 times.
20 noValve = !enginePins.hpfpValve.isInitialized();
220
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 20 times.
20 if (noValve) {
221 m_running = false;
222 return;
223 }
224
225
1/1
✓ Branch 2 taken 20 times.
20 angle_t lobeAngle = m_lobe.findNextLobe();
226 //TODO: integrate livedata into HpfpLobe
227 20 nextLobe = m_lobe.m_lobe_index;
228 20 angle_t angle_requested = m_requested_pump;
229
230 20 angleAboveMin = angle_requested > engineConfiguration->hpfpMinAngle;
231
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 1 time.
2/2
✓ Decision 'true' taken 19 times.
✓ Decision 'false' taken 1 time.
20 if (angleAboveMin) {
232 // TODO: some manuals suggest also substracting peak time (converted to angle)
233 19 di_nextStart = lobeAngle - angle_requested - m_deadangle;
234
1/1
✓ Branch 1 taken 19 times.
19 wrapAngle(di_nextStart, "di_nextStart", ObdCode::CUSTOM_ERR_6557);
235
236
237 /**
238 * We are good to use just one m_event instance because new events are scheduled when we turn off valve.
239 */
240
2/2
✓ Branch 1 taken 19 times.
✓ Branch 6 taken 19 times.
38 engine->module<TriggerScheduler>()->schedule(
241 "hpfp",
242 &m_event,
243 di_nextStart,
244 38 action_s::make<pinTurnOn>( this ));
245
246 // Off will be scheduled after turning the valve on
247 } else {
248
1/1
✓ Branch 1 taken 1 time.
1 wrapAngle(lobeAngle, "lobe", ObdCode::CUSTOM_ERR_6557);
249 // Schedule this, even if we aren't opening the valve this time, since this
250 // will schedule the next lobe.
251 // todo: schedule only 'scheduleNextCycle' directly since we do not need output control part of the logic here!
252
2/2
✓ Branch 1 taken 1 time.
✓ Branch 6 taken 1 time.
2 engine->module<TriggerScheduler>()->schedule(
253 HPFP_CONTROLLER,
254 &m_event, lobeAngle,
255 2 action_s::make<pinTurnOff>( this ));
256 }
257 }
258
259 #endif // EFI_HPFP
260
261 4593 bool isGdiEngine() {
262 #if EFI_PROD_CODE
263 return enginePins.hpfpValve.isInitialized();
264 #else
265 4593 return engineConfiguration->hpfpCamLobes > 0;
266 #endif
267 }
268