GCC Code Coverage Report


Directory: ./
File: firmware/controllers/algo/fuel/injector_model.cpp
Date: 2025-11-16 14:52:24
Coverage Exec Excl Total
Lines: 78.8% 119 0 151
Functions: 76.9% 20 0 26
Branches: 80.6% 58 0 72
Decisions: 82.0% 41 - 50

Line Branch Decision Exec Source
1 #include "pch.h"
2
3 #include "injector_model.h"
4 #include "fuel_computer.h"
5
6 90825 void InjectorModelBase::prepare() {
7 90825 float flowRatio = getInjectorFlowRatio();
8
9 // "large pulse" flow rate
10 90825 m_massFlowRate = flowRatio * getBaseFlowRate();
11 90825 m_deadtime = getDeadtime();
12
13
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 90824 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 90824 times.
90825 if (getNonlinearMode() == INJ_FordModel) {
14 1 m_smallPulseFlowRate = flowRatio * getSmallPulseFlowRate();
15 1 m_smallPulseBreakPoint = getSmallPulseBreakPoint();
16
17 // amount added to small pulses to correct for the "kink" from low flow region
18 1 m_smallPulseOffset = 1000 * ((m_smallPulseBreakPoint / m_massFlowRate) - (m_smallPulseBreakPoint / m_smallPulseFlowRate));
19 }
20 90825 }
21
22 90818 constexpr float convertToGramsPerSecond(float ccPerMinute) {
23 90818 return ccPerMinute * (fuelDensity / 60.f);
24 }
25
26 // returns: grams per second flow
27 90821 float InjectorModelWithConfig::getBaseFlowRate() const {
28
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 90818 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 90818 times.
90821 if (engineConfiguration->injectorFlowAsMassFlow) {
29 3 return m_cfg->flow;
30 } else {
31 90818 return convertToGramsPerSecond(m_cfg->flow);
32 }
33 }
34
35 float InjectorModelPrimary::getSmallPulseFlowRate() const {
36 return engineConfiguration->fordInjectorSmallPulseSlope;
37 }
38
39 float InjectorModelPrimary::getSmallPulseBreakPoint() const {
40 // convert milligrams -> grams
41 // todo: make UI deal with scaling?!
42 return 0.001f * engineConfiguration->fordInjectorSmallPulseBreakPoint;
43 }
44
45 121545 InjectorNonlinearMode InjectorModelPrimary::getNonlinearMode() const {
46 121545 return engineConfiguration->injectorNonlinearMode;
47 }
48
49 126070 injector_compensation_mode_e InjectorModelPrimary::getInjectorCompensationMode() const {
50 126070 return engineConfiguration->injectorCompensationMode;
51 }
52
53 9001 float InjectorModelPrimary::getFuelReferencePressure() const {
54 9001 return engineConfiguration->fuelReferencePressure;
55 }
56
57 float InjectorModelSecondary::getSmallPulseFlowRate() const {
58 // not supported on second bank
59 return 0;
60 }
61
62 float InjectorModelSecondary::getSmallPulseBreakPoint() const {
63 // not supported on second bank
64 return 0;
65 }
66
67 10 injector_compensation_mode_e InjectorModelSecondary::getInjectorCompensationMode() const {
68 10 return engineConfiguration->secondaryInjectorCompensationMode;
69 }
70
71 1 float InjectorModelSecondary::getFuelReferencePressure() const {
72 1 return engineConfiguration->secondaryInjectorFuelReferencePressure;
73 }
74
75 2 InjectorNonlinearMode InjectorModelSecondary::getNonlinearMode() const {
76 // nonlinear not supported on second bank
77 2 return InjectorNonlinearMode::INJ_None;
78 }
79
80 void InjectorModelWithConfig::updateState() {
81 // TODO: remove at the end of 2025
82 // hack not to break tunes before 2779925f54f5c2d23499cbb2797f71508c652f54 "injector lag lookup should be done based on differential pressure"
83 if (engineConfiguration->useAbsolutePressureForLagTime) {
84 pressureCorrectionReference = getFuelPressure().Value;
85 } else {
86 pressureCorrectionReference = getFuelDifferentialPressure().Value;
87 }
88 }
89
90 expected<float> InjectorModelWithConfig::getFuelPressure() const {
91 return getFuelDifferentialPressure().Value + Sensor::get(SensorType::Map).value_or(STD_ATMOSPHERE);
92 }
93
94 4523 expected<float> InjectorModelWithConfig::getFuelDifferentialPressure() const {
95
1/1
✓ Branch 2 taken 4523 times.
4523 auto map = Sensor::get(SensorType::Map);
96
1/1
✓ Branch 2 taken 4523 times.
4523 auto baro = Sensor::get(SensorType::BarometricPressure);
97
98 4523 float baroKpa = baro.Value;
99 // todo: extract baro sensor validation logic
100
6/8
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 4503 times.
✓ Branch 3 taken 20 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 20 times.
✓ Branch 7 taken 4503 times.
✓ Branch 8 taken 20 times.
2/2
✓ Decision 'true' taken 4503 times.
✓ Decision 'false' taken 20 times.
4523 if (!baro || baro.Value > 120 || baro.Value < 50) {
101 4503 baroKpa = STD_ATMOSPHERE;
102 }
103
104
4/4
✓ Branch 1 taken 4523 times.
✓ Branch 3 taken 4499 times.
✓ Branch 4 taken 20 times.
✓ Branch 5 taken 4 times.
4523 switch (getInjectorCompensationMode()) {
105
1/1
✓ Decision 'true' taken 4499 times.
4499 case ICM_FixedRailPressure:
106 // Add barometric pressure, as "fixed" really means "fixed pressure above atmosphere"
107
1/1
✓ Branch 1 taken 4499 times.
4499 return getFuelReferencePressure()
108 4499 + baroKpa
109 8998 - map.value_or(STD_ATMOSPHERE);
110
1/1
✓ Decision 'true' taken 20 times.
20 case ICM_SensedRailPressure: {
111
3/3
✓ Branch 1 taken 20 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 19 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 19 times.
20 if (!Sensor::hasSensor(SensorType::FuelPressureInjector)) {
112
1/1
✓ Branch 1 taken 1 time.
1 warning(ObdCode::OBD_Fuel_Pressure_Sensor_Missing, "Fuel pressure compensation is set to use a pressure sensor, but none is configured.");
113 1 return unexpected;
114 }
115
116
1/1
✓ Branch 2 taken 19 times.
19 auto fps = Sensor::get(SensorType::FuelPressureInjector);
117
118 // TODO: what happens when the sensor fails?
119
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 18 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 18 times.
19 if (!fps) {
120 1 return unexpected;
121 }
122
123
3/3
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 8 times.
18 switch (engineConfiguration->fuelPressureSensorMode) {
124
1/1
✓ Decision 'true' taken 5 times.
5 case FPM_Differential:
125 // This sensor directly measures delta-P, no math needed!
126 5 return fps.Value;
127
1/1
✓ Decision 'true' taken 5 times.
5 case FPM_Gauge:
128
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 5 times.
5 if (!map) {
129 return unexpected;
130 }
131
132 5 return fps.Value + baroKpa - map.Value;
133
1/1
✓ Decision 'true' taken 8 times.
8 case FPM_Absolute:
134 default:
135
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 8 times.
8 if (!map) {
136 return unexpected;
137 }
138
139 8 return fps.Value - map.Value;
140 }
141
1/1
✓ Decision 'true' taken 4 times.
4 } default: return unexpected;
142 }
143 }
144
145 90831 float InjectorModelWithConfig::getInjectorFlowRatio() {
146 // Compensation disabled, use reference flow.
147
1/1
✓ Branch 1 taken 90831 times.
90831 auto compensationMode = getInjectorCompensationMode();
148
4/4
✓ Branch 0 taken 4505 times.
✓ Branch 1 taken 86326 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4503 times.
2/2
✓ Decision 'true' taken 86328 times.
✓ Decision 'false' taken 4503 times.
90831 if (compensationMode == ICM_None || compensationMode == ICM_HPFP_Manual_Compensation) {
149 86328 return 1.0f;
150 }
151
152
1/1
✓ Branch 1 taken 4503 times.
4503 const float referencePressure = getFuelReferencePressure();
153
154
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4503 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 4503 times.
4503 if (referencePressure < 50) {
155 // impossibly low fuel ref pressure
156 criticalError("Impossible fuel reference pressure: %f", referencePressure);
157
158 return 1.0f;
159 }
160
161
1/1
✓ Branch 2 taken 4503 times.
4503 expected<float> diffPressure = getFuelDifferentialPressure();
162
163 // If sensor failed, best we can do is disable correction
164
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 4501 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 4501 times.
4503 if (!diffPressure) {
165 2 return 1.0f;
166 }
167
168 4501 pressureDelta = diffPressure.Value;
169
170 // Somehow pressure delta is less than 0, assume failed sensor and return default flow
171
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4500 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 4500 times.
4501 if (pressureDelta <= 0) {
172 1 return 1.0f;
173 }
174
175 4500 pressureRatio = pressureDelta / referencePressure;
176 // todo: live data model?
177 4500 float flowRatio = sqrtf(pressureRatio);
178
179 // TODO: should the flow ratio be clamped?
180 4500 return flowRatio;
181 }
182
183 90826 float InjectorModelWithConfig::getDeadtime() const {
184 181652 return interpolate3d(
185 90826 m_cfg->battLagCorrTable,
186 90826 m_cfg->battLagCorrPressBins, pressureCorrectionReference,
187
2/2
✓ Branch 1 taken 90826 times.
✓ Branch 4 taken 90826 times.
272478 m_cfg->battLagCorrBattBins, Sensor::get(SensorType::BatteryVoltage).value_or(VBAT_FALLBACK_VALUE)
188 181652 );
189 }
190
191 //TODO: only used in the tests, refactor pending to InjectorModelWithConfig
192 4 floatms_t InjectorModelBase::getInjectionDuration(float fuelMassGram) const {
193
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 4 times.
4 if (fuelMassGram <= 0) {
194 // If 0 mass, don't do any math, just skip the injection.
195 return 0.0f;
196 }
197
198 // Get the no-offset duration
199 4 floatms_t baseDuration = getBaseDurationImpl(fuelMassGram);
200
201 4 return baseDuration + m_deadtime;
202 }
203
204 100100 floatms_t InjectorModelWithConfig::getInjectionDuration(float fuelMassGram) const {
205
2/2
✓ Branch 0 taken 69374 times.
✓ Branch 1 taken 30726 times.
2/2
✓ Decision 'true' taken 69374 times.
✓ Decision 'false' taken 30726 times.
100100 if (fuelMassGram <= 0) {
206 // If 0 mass, don't do any math, just skip the injection.
207 69374 return 0.0f;
208 }
209
210 // hopefully one day we pick between useInjectorFlowLinearizationTable and ICM_HPFP_Manual_Compensation approaches
211 // and not more than one of these would stay
212
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 30726 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 30726 times.
30726 if (engineConfiguration->useInjectorFlowLinearizationTable) {
213 auto fps = Sensor::get(SensorType::FuelPressureInjector);
214 // todo: KPA vs BAR mess?!
215 return interpolate3d(config->injectorFlowLinearization,
216 config->injectorFlowLinearizationPressureBins, KPA2BAR(fps.Value),// array values are on bar
217 config->injectorFlowLinearizationFuelMassBins, fuelMassGram * 1000); // array values are on mg
218 }
219
220 // Get the no-offset duration
221 30726 floatms_t baseDuration = getBaseDurationImpl(fuelMassGram);
222
223 // default non GDI case
224
2/2
✓ Branch 1 taken 30724 times.
✓ Branch 2 taken 2 times.
2/2
✓ Decision 'true' taken 30724 times.
✓ Decision 'false' taken 2 times.
30726 if (getInjectorCompensationMode() != ICM_HPFP_Manual_Compensation) {
225 // Add deadtime offset
226 30724 return baseDuration + m_deadtime;
227 }
228
229
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
1/2
✓ Decision 'true' taken 2 times.
✗ Decision 'false' not taken.
2 if (!Sensor::hasSensor(SensorType::FuelPressureHigh)) {
230 2 return baseDuration + m_deadtime;
231 }
232
233 auto fps = Sensor::get(SensorType::FuelPressureHigh);
234 // todo: KPA vs BAR mess in code and UI?!
235 float fuelMassCompensation = interpolate3d(config->hpfpFuelMassCompensation,
236 config->hpfpFuelMassCompensationFuelPressure, KPA2BAR(fps.Value),// array values are on bar
237 config->hpfpFuelMassCompensationFuelMass, fuelMassGram * 1000); // array values are on mg
238
239 // recalculate base duration with fuel mass compensation
240 baseDuration = getBaseDurationImpl(fuelMassGram * fuelMassCompensation);
241 return baseDuration + m_deadtime;
242 }
243
244 90819 float InjectorModelBase::getFuelMassForDuration(floatms_t duration) const {
245 // Convert from ms -> grams
246 90819 return duration * m_massFlowRate * 0.001f;
247 }
248
249 // todo: all that *1000 and *0.001f is pretty annoying, we need a cleaner approach for units!
250 30751 floatms_t InjectorModelBase::getBaseDurationImpl(float fuelMassGram) const {
251 30751 floatms_t baseDuration = fuelMassGram / m_massFlowRate * 1000;
252
253
2/3
✓ Branch 1 taken 21 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 30730 times.
30751 switch (getNonlinearMode()) {
254
1/1
✓ Decision 'true' taken 21 times.
21 case INJ_FordModel:
255
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 14 times.
2/2
✓ Decision 'true' taken 7 times.
✓ Decision 'false' taken 14 times.
21 if (fuelMassGram < m_smallPulseBreakPoint) {
256 // Small pulse uses a different slope, and adds the "zero fuel pulse" offset
257 7 return (fuelMassGram / m_smallPulseFlowRate * 1000) + m_smallPulseOffset;
258 } else {
259 // Large pulse
260 14 return baseDuration;
261 }
262 case INJ_PolynomialAdder:
263 return correctInjectionPolynomial(baseDuration);
264
1/1
✓ Decision 'true' taken 30730 times.
30730 case INJ_None:
265 default:
266
1/1
✓ Decision 'true' taken 30730 times.
30730 return baseDuration;
267 }
268 }
269
270 9 floatms_t InjectorModelBase::correctInjectionPolynomial(floatms_t baseDuration) const {
271
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 8 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 8 times.
9 if (baseDuration > engineConfiguration->applyNonlinearBelowPulse) {
272 // Large pulse, skip correction.
273 1 return baseDuration;
274 }
275
276 8 auto& is = engineConfiguration->injectorCorrectionPolynomial;
277 8 float xi = 1;
278
279 8 float adder = 0;
280
281 // Add polynomial terms, starting with x^0
282
2/2
✓ Branch 1 taken 64 times.
✓ Branch 2 taken 8 times.
2/2
✓ Decision 'true' taken 64 times.
✓ Decision 'false' taken 8 times.
72 for (size_t i = 0; i < efi::size(is); i++) {
283 64 adder += is[i] * xi;
284 64 xi *= baseDuration;
285 }
286
287 8 return baseDuration + adder;
288 }
289
290 1385 InjectorModelWithConfig::InjectorModelWithConfig(const injector_s* const cfg)
291 1385 : m_cfg(cfg)
292 {
293 1385 }
294
295 701 InjectorModelPrimary::InjectorModelPrimary()
296 701 : InjectorModelWithConfig(&engineConfiguration->injector)
297 {
298 701 }
299
300 // TODO: actual separate config for second bank!
301 684 InjectorModelSecondary::InjectorModelSecondary()
302 684 : InjectorModelWithConfig(&engineConfiguration->injectorSecondary)
303 {
304 684 }
305