rusEFI
The most advanced open source ECU
Loading...
Searching...
No Matches
fuel_math.cpp
Go to the documentation of this file.
1/**
2 * @file fuel_math.cpp
3 * @brief Fuel amount calculation logic
4 *
5 *
6 * @date May 27, 2013
7 * @author Andrey Belomutskiy, (c) 2012-2020
8 *
9 * This file is part of rusEfi - see http://rusefi.com
10 *
11 * rusEfi is free software; you can redistribute it and/or modify it under the terms of
12 * the GNU General Public License as published by the Free Software Foundation; either
13 * version 3 of the License, or (at your option) any later version.
14 *
15 * rusEfi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
16 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along with this program.
20 * If not, see <http://www.gnu.org/licenses/>.
21 *
22 */
23
24#include "pch.h"
25
26#include "airmass.h"
27#include "alphan_airmass.h"
28#include "maf_airmass.h"
30#include "fuel_math.h"
31#include "fuel_computer.h"
32#include "injector_model.h"
33#include "speed_density.h"
34#include "speed_density_base.h"
35#include "lua_hooks.h"
36
37extern ve_Map3D_t veMap;
39
40#if EFI_ENGINE_CONTROL
41
42float getCrankingFuel3(float baseFuel, uint32_t revolutionCounterSinceStart) {
43
44 float baseCrankingFuel;
46 baseCrankingFuel = baseFuel;
47 } else {
48 // Cranking fuel changes over time
49 baseCrankingFuel = interpolate3d(
53 ) * 0.001f; // parameter is in milligrams, convert to grams
54 }
55
56 engine->engineState.crankingFuel.baseFuel = baseCrankingFuel * 1000; // convert to mg
57
58 /**
59 * Cranking fuel is different depending on engine coolant temperature
60 * If the sensor is failed, use 20 deg C
61 */
62 auto clt = Sensor::get(SensorType::Clt).value_or(20);
63 auto e0Mult = interpolate2d(clt, config->crankingFuelBins, config->crankingFuelCoef);
64
65 bool alreadyWarned = false;
66 if (e0Mult <= 0.1f) {
67 warning(ObdCode::CUSTOM_ERR_ZERO_E0_MULT, "zero e0 multiplier");
68 alreadyWarned = true;
69 }
70
72 auto e85Mult = interpolate2d(clt, config->crankingFuelBins, config->crankingFuelCoefE100);
73
74 if (e85Mult <= 0.1f) {
75 warning(ObdCode::CUSTOM_ERR_ZERO_E85_MULT, "zero e85 multiplier");
76 alreadyWarned = true;
77 }
78
79 // If failed flex sensor, default to 50% E
80 auto flex = Sensor::get(SensorType::FuelEthanolPercent).value_or(50);
81
84 0, e0Mult,
85 85, e85Mult,
86 flex
87 );
88 } else {
90 }
91
94 tps.Valid
95 ? interpolate2d(tps.Value, config->crankingTpsBins, config->crankingTpsCoef)
96 : 1; // in case of failed TPS, don't correct.
97
98 floatms_t crankingFuel = baseCrankingFuel
101
103
104 // don't re-warn for zero fuel when we already warned for a more specific problem
105 if (!alreadyWarned && crankingFuel <= 0) {
107 }
108 return crankingFuel;
109}
110
111float getRunningFuel(float baseFuel) {
113
116 float postCrankingFuelCorrection = engine->fuelComputer.running.postCrankingFuelCorrection;
118
119 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(iatCorrection), "NaN iatCorrection", 0);
120 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(cltCorrection), "NaN cltCorrection", 0);
121 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(postCrankingFuelCorrection), "NaN postCrankingFuelCorrection", 0);
122
123 float correction = baroCorrection * iatCorrection * cltCorrection * postCrankingFuelCorrection;
124
125#if EFI_ANTILAG_SYSTEM
126 correction *= (1 + engine->antilagController.fuelALSCorrection / 100);
127#endif /* EFI_ANTILAG_SYSTEM */
128
129#if EFI_LAUNCH_CONTROL
131 correction *= engine->module<NitrousController>().unmock().getFuelCoefficient();
132#endif
133
134#ifdef MODULE_VVL_CONTROLLER
135 correction *= engine->module<VvlController>().unmock().getFuelCoefficient();
136#endif /* MODULE_VVL_CONTROLLER */
137
138 correction *= getLimpManager()->getLimitingFuelCorrection();
139
140 float runningFuel = baseFuel * correction;
141
142 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(runningFuel), "NaN runningFuel", 0);
143
144 // Publish output state
145 engine->fuelComputer.running.baseFuel = baseFuel * 1000;
147 engine->fuelComputer.running.fuel = runningFuel * 1000;
148
149 return runningFuel;
150}
151
155
156AirmassModelBase* getAirmassModel(engine_load_mode_e mode) {
157#if EFI_UNIT_TEST
158 if (mode == engine_load_mode_e::UNSUPPORTED_ENUM_VALUE) {
159 return engine->mockAirmassModel;
160 }
161#endif
162
163 switch (mode) {
164 case engine_load_mode_e::LM_SPEED_DENSITY: return &sdAirmass;
165 case engine_load_mode_e::LM_REAL_MAF: return &mafAirmass;
166 case engine_load_mode_e::LM_ALPHA_N: return &alphaNAirmass;
167#if EFI_LUA
168 case engine_load_mode_e::LM_LUA: return &(getLuaAirmassModel());
169#endif
170 default:
172 return nullptr;
173 }
174}
175
176float getMaxAirflowAtMap(float map) {
178}
179
180#if EFI_ENGINE_CONTROL
181
182// Per-cylinder base fuel mass
183static float getBaseFuelMass(float rpm) {
185
186 // airmass modes - get airmass first, then convert to fuel
188 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, model != nullptr, "Invalid airmass mode", 0.0f);
189
190 auto airmass = model->getAirmass(rpm, true);
191
192 // Plop some state for others to read
193 float normalizedCylinderFilling = 100 * airmass.CylinderAirmass / getStandardAirCharge();
194 engine->fuelComputer.sdAirMassInOneCylinder = airmass.CylinderAirmass;
196 engine->engineState.fuelingLoad = airmass.EngineLoadPercent;
198
199 auto gramPerCycle = airmass.CylinderAirmass * engineConfiguration->cylindersCount;
200 auto gramPerMs = rpm == 0 ? 0 : gramPerCycle / getEngineCycleDuration(rpm);
201
202 // convert g/s -> kg/h
203 engine->engineState.airflowEstimate = gramPerMs * 3600000 /* milliseconds per hour */ / 1000 /* grams per kg */;
204
205 float baseFuelMass = engine->fuelComputer.getCycleFuel(airmass.CylinderAirmass, rpm, airmass.EngineLoadPercent);
206
207 engine->engineState.baseFuel = baseFuelMass;
208
209 if (std::isnan(baseFuelMass)) {
210 // todo: we should not have this here but https://github.com/rusefi/rusefi/issues/1690
211 return 0;
212 }
213
214 return baseFuelMass;
215}
216
217angle_t getInjectionOffset(float rpm, float load) {
218 if (std::isnan(rpm)) {
219 return 0; // error already reported
220 }
221
222 if (std::isnan(load)) {
223 return 0; // error already reported
224 }
225
226 angle_t value = interpolate3d(
230 );
231
232 if (std::isnan(value)) {
233 // we could be here while resetting configuration for example
234 // huh? what? when do we have RPM while resetting configuration? is that CI edge case? shall we fix CI?
235 warning(ObdCode::CUSTOM_ERR_6569, "phase map not ready");
236 return 0;
237 }
238
239 angle_t result = value;
240 wrapAngle(result, "inj offset#2", ObdCode::CUSTOM_ERR_6553);
241 return result;
242}
243
244/**
245 * Number of injections using each injector per engine cycle
246 * @see getNumberOfSparks
247 */
249 switch (mode) {
250 case IM_SIMULTANEOUS:
251 case IM_SINGLE_POINT:
253 case IM_BATCH:
254 return 2;
255 case IM_SEQUENTIAL:
256 return 1;
257 default:
258 firmwareError(ObdCode::CUSTOM_ERR_INVALID_INJECTION_MODE, "Unexpected injection_mode_e %d", mode);
259 return 1;
260 }
261}
262
265
266 switch (mode) {
267 case IM_SIMULTANEOUS: {
268 auto cylCount = engineConfiguration->cylindersCount;
269
270 if (cylCount == 0) {
271 // we can end up here during configuration reset
272 return 0;
273 }
274
275 return 1.0f / cylCount;
276 }
277 case IM_SEQUENTIAL:
278 case IM_SINGLE_POINT:
279 return 1;
280 case IM_BATCH:
281 return 0.5f;
282 default:
283 firmwareError(ObdCode::CUSTOM_ERR_INVALID_INJECTION_MODE, "Unexpected injection_mode_e %d", mode);
284 return 0;
285 }
286}
287
290 floatms_t engineCycleDuration = getEngineCycleDuration(rpm);
291 return 100 * totalInjectiorAmountPerCycle / engineCycleDuration;
292}
293
296 floatms_t engineCycleDuration = getEngineCycleDuration(rpm);
297 return 100 * totalInjectiorAmountPerCycle / engineCycleDuration;
298}
299
300float getCycleFuelMass(bool isCranking, float baseFuelMass) {
301 if (isCranking) {
302 return getCrankingFuel(baseFuelMass);
303 } else {
304 return getRunningFuel(baseFuelMass);
305 }
306}
307
308/**
309 * @returns Mass of each individual fuel injection, in grams
310 * in case of single point injection mode the amount of fuel into all cylinders, otherwise the amount for one cylinder
311 */
312float getInjectionMass(float rpm) {
314
315 // Always update base fuel - some cranking modes use it
316 float baseFuelMass = getBaseFuelMass(rpm);
317
318 bool isCranking = engine->rpmCalculator.isCranking();
319 float cycleFuelMass = getCycleFuelMass(isCranking, baseFuelMass);
320 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(cycleFuelMass), "NaN cycleFuelMass", 0);
321
322 if (engine->module<DfcoController>()->cutFuel()) {
323 // If decel fuel cut, zero out fuel
324 cycleFuelMass = 0;
325 }
326
327 float durationMultiplier = getInjectionModeDurationMultiplier();
328 float injectionFuelMass = cycleFuelMass * durationMultiplier;
329
330 // Prepare injector flow rate & deadtime
331 engine->module<InjectorModelPrimary>()->prepare();
332
334 engine->module<InjectorModelSecondary>()->prepare();
335 }
336
337 // This variable will hold the extra fuel mass from legacy AE modes
338 float tpsFuelMass = 0;
339
340 // Get the AE value (it will be 0 if predictive mode is active)
341 float tpsAccelEnrich = engine->module<TpsAccelEnrichment>()->getTpsEnrichment();
342 efiAssert(ObdCode::CUSTOM_ERR_ASSERT, !std::isnan(tpsAccelEnrich), "NaN tpsAccelEnrich", 0);
343 engine->engineState.tpsAccelEnrich = tpsAccelEnrich;
344
345 float tpsAccelPerInjection = durationMultiplier * tpsAccelEnrich;
346
347 // Use a switch to handle all AE modes
349 case AE_MODE_PERCENT_ADDER:
350 // Treat the tpsAccelEnrich value as a percentage
351 tpsFuelMass = injectionFuelMass * tpsAccelPerInjection;
352 break;
353
354 case AE_MODE_MS_ADDER:
355 // For legacy reasons, the TPS accel table is in units of milliseconds,
356 // so we have to convert BACK to mass
357 tpsFuelMass = engine->module<InjectorModelPrimary>()->getFuelMassForDuration(tpsAccelPerInjection);
358 break;
359
360 case AE_MODE_PREDICTIVE_MAP:
361 // Do nothing here. Fuel correction has already been handled by providing a
362 // corrected MAP value to the getBaseFuelMass() calculation.
363 break;
364
365 default:
366 criticalError("Invalid accelEnrichmentMode %d", engineConfiguration->accelEnrichmentMode);
367 break;
368 }
369
370 return injectionFuelMass + tpsFuelMass;
371}
372#endif
373
374/**
375 * @brief Initialize fuel map data structure
376 * @note this method has nothing to do with fuel map VALUES - it's job
377 * is to prepare the fuel map data structure for 3d interpolation
378 */
382
383/**
384 * @brief Engine warm-up fuel correction.
385 */
387 const auto clt = Sensor::get(SensorType::Clt);
388
389 if (!clt)
390 return 1; // this error should be already reported somewhere else, let's just handle it
391
392 return interpolate2d(clt.Value, config->cltFuelCorrBins, config->cltFuelCorr);
393}
394
396 const auto iat = Sensor::get(SensorType::Iat);
397
398 if (!iat)
399 return 1; // this error should be already reported somewhere else, let's just handle it
400
401 return interpolate2d(iat.Value, config->iatFuelCorrBins, config->iatFuelCorr);
402}
403
405 const auto revolutionCounter = engine->rpmCalculator.getRevolutionCounterSinceStart();
406 // if the engine run time is past the last bin, disable ASE in case the table is filled with values more than 1.0, helps with compatibility
407 if (revolutionCounter > config->postCrankingDurationBins[efi::size(config->postCrankingDurationBins) - 1])
408 return 1;
409
410 // TODO:
411 //const auto clt = Sensor::get(SensorType::Clt);
412 //if (!clt)
413 // return 1; // this error should be already reported somewhere else, let's just handle it
414
415 // post-cranking fuel enrichment.
416 float postCrankingFactor = interpolate3d(
419 config->postCrankingDurationBins, revolutionCounter
420 );
421
422 // for compatibility reasons, apply only if the factor is greater than unity (only allow adding fuel)
423 if (postCrankingFactor < 1.0f)
424 postCrankingFactor = 1.0f;
425
426 return postCrankingFactor;
427}
428
431 // Default to 1atm if failed
432 float pressure = Sensor::get(SensorType::BarometricPressure).value_or(STD_ATMOSPHERE);
433
434 float correction = interpolate3d(
436 config->baroCorrPressureBins, pressure,
438 );
439
440 if (std::isnan(correction) || correction < 0.01) {
441 warning(ObdCode::OBD_Barometric_Press_Circ_Range_Perf, "Invalid baro correction %f", correction);
442 return 1;
443 }
444
445 return correction;
446 } else {
447 return 1;
448 }
449}
450
452#if EFI_ANTILAG_SYSTEM
455 auto AlsFuelAdd = interpolate3d(
457 config->alsFuelAdjustmentLoadBins, throttleIntent,
459 );
460 return AlsFuelAdd;
461 } else
462#endif /* EFI_ANTILAG_SYSTEM */
463 {
464 return 0;
465 }
466}
467
468#if EFI_ENGINE_CONTROL
469/**
470 * @return Duration of fuel injection while craning
471 */
472float getCrankingFuel(float baseFuel) {
474}
475
476/**
477 * Standard cylinder air charge - 100% VE at standard temperature, grams per cylinder
478 *
479 * Should we bother caching 'getStandardAirCharge' result or can we afford to run the math every time we calculate fuel?
480 */
482 float totalDisplacement = engineConfiguration->displacement;
483 float cylDisplacement = totalDisplacement / engineConfiguration->cylindersCount;
484
485 // Calculation of 100% VE air mass in g/cyl - 1 cylinder filling at 1.204/L
486 // 101.325kpa, 20C
487 return idealGasLaw(cylDisplacement, STD_ATMOSPHERE, C_K_OFFSET + STD_IAT);
488}
489
490PUBLIC_API_WEAK_SOMETHING_WEIRD
491float getCylinderFuelTrim(size_t cylinderNumber, float rpm, float fuelLoad) {
492 auto trimPercent = interpolate3d(
493 config->fuelTrims[cylinderNumber].table,
494 config->fuelTrimLoadBins, fuelLoad,
496 );
497
498 // Convert from percent +- to multiplier
499 // 5% -> 1.05
500 // possible optimization: remove division by moving this scaling to TS level
501 return (100 + trimPercent) / 100;
502}
503
505
506float getStage2InjectionFraction(float rpm, float load) {
508 return 0;
509 }
510
511 float frac = 0.01f * interpolate3d(
515 );
516
517 // don't allow very small fraction, with some hysteresis
518 if (!stage2Hysteresis.test(frac, 0.1, 0.03)) {
519 return 0;
520 }
521
522 // Clamp to 90%
523 if (frac > 0.9) {
524 frac = 0.9;
525 }
526
527 return frac;
528}
529
530#endif
531#endif
bool cutFuel() const
Definition dfco.cpp:81
FuelComputer fuelComputer
Definition engine.h:139
LaunchControlBase launchController
Definition engine.h:220
EngineState engineState
Definition engine.h:344
RpmCalculator rpmCalculator
Definition engine.h:306
AntilagSystemBase antilagController
Definition engine.h:228
AirmassModelBase * mockAirmassModel
Definition engine.h:382
constexpr auto & module()
Definition engine.h:200
floatms_t tpsAccelEnrich
floatms_t baseFuel
float airflowEstimate
floatms_t injectionDuration
floatms_t injectionDurationStage2
mass_t getCycleFuel(mass_t airmass, float rpm, float load) override
bool test(float value, float rising, float falling)
Definition hysteresis.h:31
float getFuelCoefficient() const
float getLimitingFuelCorrection() const
void initTable(TValueInit(&table)[TRowNum][TColNum], const TXColumnInit(&columnBins)[TColNum], const TRowInit(&rowBins)[TRowNum])
float getFuelCoefficient() const
bool isCranking() const override
uint32_t getRevolutionCounterSinceStart(void) const
virtual bool hasSensor() const
Definition sensor.h:141
virtual SensorResult get() const =0
static float getOrZero(SensorType type)
Definition sensor.h:83
float getAirflow(float rpm, float map, bool postState)
float getFuelCoefficient() const
float interpolateClamped(float x1, float y1, float x2, float y2, float x)
LimpManager * getLimpManager()
Definition engine.cpp:596
injection_mode_e getCurrentInjectionMode()
Definition engine.cpp:548
static EngineAccessor engine
Definition engine.h:413
static constexpr persistent_config_s * config
static constexpr engine_configuration_s * engineConfiguration
floatms_t getEngineCycleDuration(float rpm)
bool warning(ObdCode code, const char *fmt,...)
void firmwareError(ObdCode code, const char *fmt,...)
percent_t getInjectorDutyCycleStage2(float rpm)
static mapEstimate_Map3D_t mapEstimationTable
Definition fuel_math.cpp:38
static float getBaseFuelMass(float rpm)
void initFuelMap()
Initialize fuel map data structure.
float getStage2InjectionFraction(float rpm, float load)
ve_Map3D_t veMap
float getPostCrankingFuelCorrection()
float getRunningFuel(float baseFuel)
int getNumberOfInjections(injection_mode_e mode)
static SpeedDensityAirmass sdAirmass(veMap, mapEstimationTable)
AirmassModelBase * getAirmassModel(engine_load_mode_e mode)
angle_t getInjectionOffset(float rpm, float load)
static MafAirmass mafAirmass(veMap)
float getIatFuelCorrection()
float getCrankingFuel(float baseFuel)
float getBaroCorrection()
float getCrankingFuel3(float baseFuel, uint32_t revolutionCounterSinceStart)
Definition fuel_math.cpp:42
static AlphaNAirmass alphaNAirmass(veMap)
float getMaxAirflowAtMap(float map)
float getCycleFuelMass(bool isCranking, float baseFuelMass)
percent_t getFuelALSCorrection(float rpm)
float getCltFuelCorrection()
Engine warm-up fuel correction.
PUBLIC_API_WEAK_SOMETHING_WEIRD float getCylinderFuelTrim(size_t cylinderNumber, float rpm, float fuelLoad)
static Hysteresis stage2Hysteresis
float getStandardAirCharge()
float getInjectionModeDurationMultiplier()
float getInjectionMass(float rpm)
percent_t getInjectorDutyCycle(float rpm)
static CCM_OPTIONAL FunctionalSensor iat(SensorType::Iat, MS2NT(10))
static CCM_OPTIONAL FunctionalSensor clt(SensorType::Clt, MS2NT(10))
AirmassModelBase & getLuaAirmassModel()
@ CUSTOM_ERR_INVALID_INJECTION_MODE
@ OBD_Barometric_Press_Circ_Range_Perf
@ CUSTOM_ERR_6553
@ CUSTOM_ERR_ZERO_E85_MULT
@ CUSTOM_ERR_ZERO_CRANKING_FUEL
@ CUSTOM_ERR_ASSERT
@ CUSTOM_ERR_6569
@ CUSTOM_ERR_ZERO_E0_MULT
@ GetInjectionDuration
@ GetBaseFuel
@ GetRunningFuel
injection_mode_e
accel_enrichment_mode_e
float floatms_t
float angle_t
float percent_t
@ FuelEthanolPercent
@ DriverThrottleIntent
@ BarometricPressure
revolutionCounterSinceStart("revolutionCounterSinceStart", SensorCategory.SENSOR_INPUTS, FieldType.INT16, 64, 1.0, 0.0, 0.0, "")
baroCorrection("Fuel: Barometric pressure mult", SensorCategory.SENSOR_INPUTS, FieldType.INT, 1364, 1.0, -1.0, -1.0, "")
normalizedCylinderFilling("Air: Normalized cyl filling", SensorCategory.SENSOR_INPUTS, FieldType.INT, 928, 1.0, 0.0, 100.0, "%")
crankingFuel("crankingFuel", SensorCategory.SENSOR_INPUTS, FieldType.INT, 1352, 1.0, -1.0, -1.0, "")
mass_t idealGasLaw(float volume, float pressure, float temperature)
float getLoadOverride(float defaultLoad, load_override_e overrideMode) const
scaled_channel< uint16_t, 100, 1 > baseFuel
scaled_channel< uint16_t, 100, 1 > fuel
cranking_fuel_s crankingFuel
scaled_channel< int8_t, 5, 1 > table[FUEL_TRIM_SIZE][FUEL_TRIM_SIZE]
scaled_channel< int16_t, 10, 1 > ALSFuelAdjustment[ALS_SIZE][ALS_SIZE]
uint8_t injectorStagingTable[INJ_STAGING_COUNT][INJ_STAGING_COUNT]
float crankingCycleBaseFuel[CRANKING_CYCLE_CLT_SIZE][CRANKING_CURVE_SIZE]
int16_t injectionPhase[INJ_PHASE_LOAD_COUNT][INJ_PHASE_RPM_COUNT]
scaled_channel< uint16_t, 100, 1 > mapEstimateTpsBins[MAP_EST_LOAD_COUNT]
float postCrankingFactor[CRANKING_ENRICH_CLT_COUNT][CRANKING_ENRICH_COUNT]
scaled_channel< uint16_t, 100, 1 > crankingFuelCoefE100[CRANKING_CURVE_SIZE]
scaled_channel< uint16_t, 100, 1 > mapEstimateTable[MAP_EST_LOAD_COUNT][MAP_EST_RPM_COUNT]
scaled_channel< uint16_t, 100, 1 > baseFuel
scaled_channel< uint16_t, 100, 1 > fuel
void wrapAngle(angle_t &angle, const char *msg, ObdCode code)