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 |