GCC Code Coverage Report


Directory: ./
File: firmware/controllers/engine_cycle/high_pressure_fuel_pump.cpp
Date: 2025-11-16 14:52:24
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 192 float HpfpQuantity::calcFuelPercent(float rpm) {
79 192 float fuel_requested_cc_per_cycle =
80 192 engine->engineState.injectionMass[0] * (1.f / fuelDensity) * engineConfiguration->cylindersCount;
81 192 float fuel_requested_cc_per_lobe = fuel_requested_cc_per_cycle / engineConfiguration->hpfpCamLobes;
82 192 return 100.f *
83 192 fuel_requested_cc_per_lobe / engineConfiguration->hpfpPumpVolume +
84 384 interpolate3d(config->hpfpCompensation,
85 192 config->hpfpCompensationLoadBins, fuel_requested_cc_per_lobe,
86 192 config->hpfpCompensationRpmBins, rpm);
87 }
88
89 189 static float getLoad() {
90
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 189 times.
189 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 189 times.
189 default:
95 189 return Sensor::getOrZero(SensorType::Map);
96 }
97 }
98
99 189 float HpfpQuantity::calcPI(float rpm, float calc_fuel_percent, HpfpController *model) {
100
1/1
✓ Branch 1 taken 189 times.
189 float load = getLoad();
101
102 189 float possibleValue = model->m_pressureTarget_kPa - (engineConfiguration->hpfpTargetDecay *
103 189 (FAST_CALLBACK_PERIOD_MS / 1000.));
104
105 189 model->m_pressureTarget_kPa = std::max<float>(possibleValue,
106 189 interpolate3d(config->hpfpTarget,
107 189 config->hpfpTargetLoadBins, load,
108
1/1
✓ Branch 1 taken 189 times.
189 config->hpfpTargetRpmBins, rpm));
109
110
1/1
✓ Branch 2 taken 189 times.
189 auto fuelPressure = Sensor::get(SensorType::FuelPressureHigh);
111
2/2
✓ Branch 1 taken 178 times.
✓ Branch 2 taken 11 times.
2/2
✓ Decision 'true' taken 178 times.
✓ Decision 'false' taken 11 times.
189 if (!fuelPressure) {
112 178 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 177 angle_t HpfpQuantity::pumpAngleFuel(float rpm, HpfpController *model) {
140 // Math based on fuel requested
141 177 model->fuel_requested_percent = calcFuelPercent(rpm);
142
143 177 model->fuel_requested_percent_pi = calcPI(rpm, model->fuel_requested_percent, model);
144 // Apply PI control
145 177 float fuel_requested_percentTotal = model->fuel_requested_percent + model->fuel_requested_percent_pi;
146
147 // Convert to degrees
148 354 return interpolate2d(fuel_requested_percentTotal,
149 177 config->hpfpLobeProfileQuantityBins,
150 177 config->hpfpLobeProfileAngle);
151 }
152
153 90816 void HpfpController::onFastCallback() {
154 // Pressure current/target calculation
155 90816 float rpm = Sensor::getOrZero(SensorType::Rpm);
156
157
2/2
✓ Branch 0 taken 51008 times.
✓ Branch 1 taken 39808 times.
141824 isHpfpActive = !(rpm < rpm_spinning_cutoff ||
158
4/4
✓ Branch 1 taken 175 times.
✓ Branch 2 taken 50833 times.
✓ Branch 3 taken 174 times.
✓ Branch 4 taken 1 time.
51183 !isGdiEngine() ||
159 175 engineConfiguration->hpfpPumpVolume == 0 ||
160
1/2
✓ Branch 1 taken 174 times.
✗ Branch 2 not taken.
174 !enginePins.hpfpValve.isInitialized());
161 // What conditions can we not handle?
162
2/2
✓ Branch 0 taken 90642 times.
✓ Branch 1 taken 174 times.
2/2
✓ Decision 'true' taken 90642 times.
✓ Decision 'false' taken 174 times.
90816 if (!isHpfpActive) {
163 90642 resetQuantity();
164 90642 m_requested_pump = 0;
165 90642 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 174 times.
174 float deadtime_ms = interpolate2d(
172
1/1
✓ Branch 1 taken 174 times.
348 Sensor::get(SensorType::BatteryVoltage).value_or(VBAT_FALLBACK_VALUE),
173 174 config->hpfpDeadtimeVoltsBins,
174 174 config->hpfpDeadtimeMS);
175 174 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 174 m_requested_pump = m_quantity.pumpAngleFuel(rpm, this);
180
181
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 172 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 172 times.
174 if (!m_running) {
182 2 m_running = true;
183 2 scheduleNextCycle();
184 }
185 }
186 90816 }
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 309665 bool isGdiEngine() {
262 #if EFI_PROD_CODE
263 return enginePins.hpfpValve.isInitialized();
264 #else
265 309665 return engineConfiguration->hpfpCamLobes > 0;
266 #endif
267 }
268