GCC Code Coverage Report


Directory: ./
File: firmware/controllers/math/closed_loop_fuel.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 66.7% 52 0 78
Functions: 100.0% 10 0 10
Branches: 50.0% 27 0 54
Decisions: 53.8% 21 - 39

Line Branch Decision Exec Source
1 #include "pch.h"
2
3 #include "closed_loop_fuel.h"
4 #include "tunerstudio.h"
5
6 #if EFI_ENGINE_CONTROL
7
8 4688 SensorType ShortTermFuelTrim::getSensorForBankIndex(size_t index) {
9
2/3
✓ Branch 0 taken 2344 times.
✓ Branch 1 taken 2344 times.
✗ Branch 2 not taken.
4688 switch (index) {
10
1/1
✓ Decision 'true' taken 2344 times.
2344 case 0: return SensorType::Lambda1;
11
1/1
✓ Decision 'true' taken 2344 times.
2344 case 1: return SensorType::Lambda2;
12 default: return SensorType::Invalid;
13 }
14 }
15
16 12 size_t ShortTermFuelTrim::computeStftBin(float rpm, float load, stft_s& cfg) {
17 // Low RPM -> idle
18
2/2
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 9 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 9 times.
12 if (idleDeadband.lt(rpm, cfg.maxIdleRegionRpm))
19 {
20 3 return 0;
21 }
22
23 // Low load -> overrun
24
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 6 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 6 times.
9 if (overrunDeadband.lt(load, cfg.maxOverrunLoad))
25 {
26 3 return 1;
27 }
28
29 // High load -> power
30
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 3 times.
6 if (loadDeadband.gt(load, cfg.minPowerLoad))
31 {
32 3 return 2;
33 }
34
35 // Default -> normal "in the middle" cell
36 3 return 3;
37 }
38
39 1120 stft_state_e ShortTermFuelTrim::getCorrectionState() {
40 1120 const auto& cfg = engineConfiguration->stft;
41
42 // User disable bit
43
1/2
✓ Branch 0 taken 1120 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 1120 times.
✗ Decision 'false' not taken.
1120 if (!engineConfiguration->fuelClosedLoopCorrectionEnabled) {
44 1120 return stftDisabledSettings;
45 }
46
47 // Don't correct if tuning seems to be happening
48 if (checkIfTuningVeNow()) {
49 return stftDisabledTuning;
50 }
51
52 // Don't correct if not running
53 if (!engine->rpmCalculator.isRunning()) {
54 return stftDisabledRPM;
55 }
56
57 // Startup delay - allow O2 sensor to warm up, etc
58 if (cfg.startupDelay > engine->fuelComputer.running.timeSinceCrankingInSecs) {
59 return stftDisabledCrankingDelay;
60 }
61
62 // Check that the engine is hot enough (and clt not failed)
63 auto clt = Sensor::get(SensorType::Clt);
64 if (!clt.Valid || clt.Value < cfg.minClt) {
65 return stftDisabledClt;
66 }
67
68 // If all was well, then we're enabled!
69 return stftEnabled;
70 }
71
72 3 stft_state_e ShortTermFuelTrim::getLearningState(SensorType sensor) {
73 3 const auto& cfg = engineConfiguration->stft;
74
75 // TODO: add check for stftLearningDisabledSettings
76
77 // Pause (but don't reset) correction if the AFR is off scale.
78 // It's probably a transient and poorly tuned transient correction
79 // TODO: use getStoichiometricRatio() instead of STOICH_RATIO
80 3 auto afr = Sensor::getOrZero(sensor) * STOICH_RATIO;
81
7/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1 time.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1 time.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 1 time.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 1 time.
3 if (!afr || afr < cfg.minAfr || afr > cfg.maxAfr) {
82 2 return stftDisabledAfrOurOfRange;
83 }
84
85 // Pause correction if DFCO was active recently
86 1 auto timeSinceDfco = engine->module<DfcoController>()->getTimeSinceCut();
87
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1 time.
1 if (timeSinceDfco < engineConfiguration->noFuelTrimAfterDfcoTime) {
88 return stftDisabledDFCO;
89 }
90
91 // Pause correction if Accel enrichment was active recently
92 1 auto timeSinceAccel = engine->module<TpsAccelEnrichment>()->getTimeSinceAcell();
93
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1 time.
1 if (timeSinceAccel < engineConfiguration->noFuelTrimAfterAccelTime) {
94 return stftDisabledTpsAccel;
95 }
96
97 // Pause if some other cut was active recently
98 1 auto timeSinceFuelCut = engine->module<LimpManager>()->getTimeSinceAnyCut();
99 // TODO: should duration this be configurable?
100
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1 time.
1 if (timeSinceFuelCut < 2) {
101 return stftDisabledFuelCut;
102 }
103
104 1 return stftEnabled;
105 }
106
107 586 void ShortTermFuelTrim::init(stft_s *stftCfg) {
108
2/2
✓ Branch 0 taken 1172 times.
✓ Branch 1 taken 586 times.
2/2
✓ Decision 'true' taken 1172 times.
✓ Decision 'false' taken 586 times.
1758 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
109
2/2
✓ Branch 0 taken 4688 times.
✓ Branch 1 taken 1172 times.
2/2
✓ Decision 'true' taken 4688 times.
✓ Decision 'false' taken 1172 times.
5860 for (size_t bin = 0; bin < STFT_CELL_COUNT; bin++) {
110 4688 auto& cell = banks[bank].cells[bin];
111 4688 SensorType sensor = getSensorForBankIndex(bank);
112
113 4688 cell.configure(&stftCfg->cellCfgs[bin], sensor);
114 }
115 }
116 586 }
117
118 1120 ClosedLoopFuelResult ShortTermFuelTrim::getCorrection(float rpm, float fuelLoad) {
119
1/1
✓ Branch 1 taken 1120 times.
1120 stftCorrectionState = getCorrectionState();
120
1/2
✓ Branch 0 taken 1120 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 1120 times.
✗ Decision 'false' not taken.
1120 if (stftCorrectionState != stftEnabled) {
121 // Learning is also prohibited
122
2/2
✓ Branch 0 taken 2240 times.
✓ Branch 1 taken 1120 times.
2/2
✓ Decision 'true' taken 2240 times.
✓ Decision 'false' taken 1120 times.
3360 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
123 2240 stftLearningState[bank] = stftCorrectionState;
124 }
125 1120 return {};
126 }
127
128 stftCorrectionBinIdx = computeStftBin(rpm, fuelLoad, engineConfiguration->stft);
129
130 ClosedLoopFuelResult result;
131
132 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
133 auto& cell = banks[bank].cells[stftCorrectionBinIdx];
134
135 SensorType sensor = getSensorForBankIndex(bank);
136
137 stftLearningState[bank] = getLearningState(sensor);
138 if (stftLearningState[bank] == stftEnabled) {
139 stftInputError[bank] = cell.getLambdaError();
140 cell.update(engineConfiguration->stft.deadband * 0.01f, engineConfiguration->stftIgnoreErrorMagnitude);
141 stftLearningBinIdx = stftCorrectionBinIdx;
142 }
143
144 result.banks[bank] = cell.getAdjustment();
145 }
146
147 return result;
148 }
149
150 1085 void ShortTermFuelTrim::onSlowCallback() {
151 // Do some magic math here?
152 1085 }
153
154 1001 bool ShortTermFuelTrim::needsDelayedShutoff() {
155 1001 return false;
156 }
157
158 584 void initStft(void)
159 {
160 584 engine->module<ShortTermFuelTrim>()->init(&engineConfiguration->stft);
161 584 }
162
163 /* TODO: move out of here */
164 17 bool checkIfTuningVeNow() {
165 #if EFI_TUNER_STUDIO
166 17 const bool result = isTuningVeNow();
167 #else
168 const bool result = false;
169 #endif /* EFI_TUNER_STUDIO */
170 17 engine->outputChannels.isTuningNow = result;
171 17 return result;
172 }
173
174 #endif // EFI_ENGINE_CONTROL
175