GCC Code Coverage Report


Directory: ./
File: firmware/controllers/long_term_fuel_trim.cpp
Date: 2025-11-16 14:52:24
Warnings: 1 unchecked decisions!
Coverage Exec Excl Total
Lines: 57.5% 88 0 153
Functions: 50.0% 14 0 28
Branches: 53.7% 36 0 67
Decisions: 42.9% 18 - 42

Line Branch Decision Exec Source
1 #include "pch.h"
2
3 #if EFI_LTFT_CONTROL
4
5 #include "storage.h"
6
7 #include "long_term_fuel_trim.h"
8
9 #include "board_overrides.h"
10
11 // +/-25% maximum
12 #define MAX_ADJ (0.25f)
13
14 #define SAVE_AFTER_HITS 1000
15
16 constexpr float integrator_dt = FAST_CALLBACK_PERIOD_MS * 0.001f;
17
18 // TODO: store in backup ram and validate on start
19 static LtftState ltftState;
20
21 // LTFT to VE table custom apply algo
22 std::optional<setup_custom_board_overrides_type> custom_board_LtftTrimToVeApply;
23
24 void LtftState::save() {
25 #if EFI_PROD_CODE
26 storageWrite(EFI_LTFT_RECORD_ID, (const uint8_t *)trims, sizeof(trims));
27 #endif //EFI_PROD_CODE
28 }
29
30 2 void LtftState::load() {
31 #if EFI_PROD_CODE
32 if (storageRead(EFI_LTFT_RECORD_ID, (uint8_t *)trims, sizeof(trims)) != StorageStatus::Ok) {
33 #else
34 if (1) {
35 #endif
36 //Reset to some defaules
37 2 reset();
38 }
39 2 }
40
41 596 void LtftState::reset() {
42
2/2
✓ Branch 0 taken 1192 times.
✓ Branch 1 taken 596 times.
2/2
✓ Decision 'true' taken 1192 times.
✓ Decision 'false' taken 596 times.
1788 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
43 1192 setTable(trims[bank], 0.0f);
44 }
45 596 }
46
47 void LtftState::fillRandom() {
48 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
49 for (size_t loadIndex = 0; loadIndex < VE_LOAD_COUNT; loadIndex++) {
50 for (size_t rpmIndex = 0; rpmIndex < VE_RPM_COUNT; rpmIndex++) {
51 trims[bank][loadIndex][rpmIndex] = PERCENT_DIV * (loadIndex + rpmIndex * 0.1);
52 }
53 }
54 }
55 }
56
57 void LtftState::applyToVe() {
58 // if we have custom implementation
59 if (call_board_override(custom_board_LtftTrimToVeApply)) {
60 return;
61 }
62
63 for (size_t loadIndex = 0; loadIndex < VE_LOAD_COUNT; loadIndex++) {
64 for (size_t rpmIndex = 0; rpmIndex < VE_RPM_COUNT; rpmIndex++) {
65 float k = 0;
66
67 /* We have single VE table, but FT_BANK_COUNT banks of trims */
68 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
69 k += 1.0f + trims[bank][loadIndex][rpmIndex];
70 }
71 k = k / FT_BANK_COUNT;
72
73 config->veTable[loadIndex][rpmIndex] = config->veTable[loadIndex][rpmIndex] * k;
74 }
75 }
76 }
77
78 593 void LongTermFuelTrim::init(LtftState *state) {
79 593 m_state = state;
80
81 #if EFI_PROD_CODE
82 ltftLoadPending = storageReqestReadID(EFI_LTFT_RECORD_ID);
83 #else
84 593 ltftLoadPending = false;
85 593 reset();
86 #endif
87 593 }
88
89 40000 float LongTermFuelTrim::getIntegratorGain(const ltft_s& cfg, ft_region_e region) const
90 {
91 40000 return 1 / clampF(1, cfg.timeConstant[region], 3000);
92 }
93
94 80000 float LongTermFuelTrim::getMaxAdjustment(const ltft_s& cfg) const {
95 // Don't allow maximum less than 0, or more than maximum add adjustment
96 80000 return clampF(0, PERCENT_DIV * cfg.maxAdd, MAX_ADJ);
97 }
98
99 80000 float LongTermFuelTrim::getMinAdjustment(const ltft_s& cfg) const {
100 // Don't allow minimum more than 0, or less than maximum remove adjustment
101 80000 return clampF(-MAX_ADJ, -PERCENT_DIV * cfg.maxRemove, 0);
102 }
103
104 150815 void LongTermFuelTrim::learn(ClosedLoopFuelResult clResult, float rpm, float fuelLoad) {
105 150815 const auto& cfg = engineConfiguration->ltft;
106
107 // LTFT uses STFT output, so if STFT is not correcting for some reason - LTFT also should not learn
108
6/8
✓ Branch 0 taken 130815 times.
✓ Branch 1 taken 20000 times.
✓ Branch 2 taken 130815 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 130815 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 110815 times.
✓ Branch 7 taken 40000 times.
2/2
✓ Decision 'true' taken 110815 times.
✓ Decision 'false' taken 170815 times.
281630 if ((!cfg.enabled) || (ltftSavePending) || (ltftLoadPending) ||
109
2/2
✓ Branch 2 taken 90815 times.
✓ Branch 3 taken 40000 times.
130815 (engine->module<ShortTermFuelTrim>()->stftCorrectionState != stftEnabled)) {
110 110815 ltftLearning = false;
111 110815 return;
112 }
113
114 // TODO: should we swap x and y here to keep aligned to wierd TS table definition?
115 // x - load, y - rpm
116 40000 auto x = priv::getClosestBin(fuelLoad, config->veLoadBins);
117 40000 auto y = priv::getClosestBin(rpm, config->veRpmBins);
118
119 // Skip learning if current load point falls far outside the table
120
3/6
✓ Branch 1 taken 40000 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 40000 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 40000 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 80000 times.
80000 if ((abs(x.Frac) > 0.5) ||
121 40000 (abs(y.Frac) > 0.5)) {
122 // we are outside table
123 ltftCntMiss++;
124 ltftLearning = false;
125 return;
126 }
127
128 40000 bool adjusted = false;
129
130 // calculate weight depenting on distance from cell center
131 // Is this too heavy?
132 40000 float weight = 1.0 - hypotf(x.Frac, y.Frac) / hypotf(0.5, 0.5);
133 40000 float k = getIntegratorGain(cfg, clResult.region) * integrator_dt * weight;
134
135
2/2
✓ Branch 0 taken 80000 times.
✓ Branch 1 taken 40000 times.
2/2
✓ Decision 'true' taken 80000 times.
✓ Decision 'false' taken 40000 times.
120000 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
136 80000 float lambdaCorrection = clResult.banks[bank] - 1.0;
137
138 // If we're within the deadband, make no adjustment.
139
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 80000 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 80000 times.
80000 if (std::abs(lambdaCorrection) < PERCENT_DIV * cfg.deadband) {
140 continue;
141 }
142
143 // get current trim
144 80000 float trim = m_state->trims[bank][x.Idx][y.Idx];
145
146 // Integrate
147 80000 float newTrim = trim + k * (lambdaCorrection - trim);
148
149 // TODO:
150 // rise OBD code if we hit trim limit
151
152 // Clamp to bounds and save
153 80000 newTrim = clampF(getMinAdjustment(cfg), newTrim, getMaxAdjustment(cfg));
154
155 // accumulate
156 80000 ltftAccummulatedCorrection[bank] += newTrim - trim;
157
158 // store
159 80000 m_state->trims[bank][x.Idx][y.Idx] = newTrim;
160
161 80000 adjusted = true;
162 }
163
164 40000 ltftLearning = adjusted;
165
1/2
✓ Branch 0 taken 40000 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 40000 times.
✗ Decision 'false' not taken.
40000 if (adjusted) {
166 40000 ltftCntHit++;
167 40000 showUpdateToUser = true;
168 40000 if ((ltftCntHit % SAVE_AFTER_HITS) == 0) {
169 // request save
170 #if EFI_PROD_CODE
171 settingsLtftRequestWriteToFlash();
172 #endif
173 }
174 } else {
175 ltftCntDeadband++;
176 }
177 }
178
179 90820 ClosedLoopFuelResult LongTermFuelTrim::getTrims(float rpm, float fuelLoad) {
180 90820 const auto& cfg = engineConfiguration->ltft;
181
182
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 90816 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
0/1
? Decision couldn't be analyzed.
90820 if ((!cfg.correctionEnabled) || (ltftLoadPending)) {
183
2/2
✓ Branch 0 taken 181632 times.
✓ Branch 1 taken 90816 times.
2/2
✓ Decision 'true' taken 181632 times.
✓ Decision 'false' taken 90816 times.
272448 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
184 181632 ltftCorrection[bank] = 1.0f;
185 }
186 90816 ltftCorrecting = false;
187 90816 return { };
188 }
189
190 // Keep calculating/applying correction even load point is far outside table
191 #if 0
192 // x - load, y - rpm
193 auto x = priv::getClosestBin(fuelLoad, config->veLoadBins);
194 auto y = priv::getClosestBin(rpm, config->veRpmBins);
195
196 // do not interpolate outside table...
197 if ((abs(x.Frac) > 0.5) ||
198 (abs(y.Frac) > 0.5)) {
199 // we are outside table
200 miss++;
201 return { };
202 }
203 #endif
204
205 // Is there any reason we should not apply LTFT?
206
207
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
2/2
✓ Decision 'true' taken 8 times.
✓ Decision 'false' taken 4 times.
12 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
208 8 ltftCorrection[bank] = 1.0f + interpolate3d(
209 8 m_state->trims[bank],
210 8 config->veLoadBins, fuelLoad,
211
1/1
✓ Branch 1 taken 8 times.
8 config->veRpmBins, rpm
212 );
213 }
214
215 4 ClosedLoopFuelResult result;
216
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
2/2
✓ Decision 'true' taken 8 times.
✓ Decision 'false' taken 4 times.
12 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
217 8 result.banks[bank] = ltftCorrection[bank];
218 }
219
220 4 ltftCorrecting = true;
221 4 return result;
222 }
223
224 // Called from storage manager thread when requested ID is ready
225 void LongTermFuelTrim::load() {
226 m_state->load();
227
228 ltftLoadPending = false;
229 }
230
231 void LongTermFuelTrim::store() {
232 // TODO: lock to avoid modification while writing
233 ltftSavePending = true;
234
235 if (m_state) {
236 m_state->save();
237 }
238
239 // TODO: unlock
240 ltftSavePending = false;
241 }
242
243 593 void LongTermFuelTrim::reset() {
244 593 m_state->reset();
245
246 593 ltftCntHit = 0;
247 593 ltftCntMiss = 0;
248 593 ltftCntDeadband = 0;
249
250
2/2
✓ Branch 0 taken 1186 times.
✓ Branch 1 taken 593 times.
2/2
✓ Decision 'true' taken 1186 times.
✓ Decision 'false' taken 593 times.
1779 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
251 1186 ltftAccummulatedCorrection[bank] = 0.0;
252 }
253 593 }
254
255 void LongTermFuelTrim::applyTrimsToVe() {
256 m_state->applyToVe();
257 m_state->reset();
258
259 veNeedRefresh = true;
260 }
261
262 531078 bool LongTermFuelTrim::isVeUpdated() {
263
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 531078 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 531078 times.
531078 if (veNeedRefresh) {
264 veNeedRefresh = false;
265 return true;
266 }
267 531078 return false;
268 }
269
270 void LongTermFuelTrim::onLiveDataRead() {
271 // rise refresh flag every second for one TS reading of livedata if we have something new...
272 if (ltftPageRefreshFlag) {
273 ltftPageRefreshFlag = false;
274 showUpdateToUser = false;
275 pageRefreshTimer.reset();
276 } else {
277 // was update to table and timeout
278 ltftPageRefreshFlag = showUpdateToUser && pageRefreshTimer.hasElapsedSec(1);
279 }
280 }
281
282 void LongTermFuelTrim::fillRandom() {
283 m_state->fillRandom();
284 }
285
286 1094 void LongTermFuelTrim::onSlowCallback() {
287 // we can wait some time for LTFT to be loaded from storage...
288 2190 if ((ltftLoadPending) &&
289 #if EFI_SHAFT_POSITION_INPUT
290
6/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1092 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1093 times.
1094 (engine->rpmCalculator.getSecondsSinceEngineStart(getTimeNowNt()) > 5.0) &&
291 #endif
292 (1)) {
293 1 efiPrintf("LTFT: failed to load calibrations");
294 1 m_state->reset();
295 1 ltftLoadPending = false;
296 1 ltftLoadError = true;
297 }
298 // Do some magic math here?
299
300 /* ... */
301 1094 }
302
303 1008 bool LongTermFuelTrim::needsDelayedShutoff() {
304 // TODO: We should delay power off until we store LTFT
305 1008 return false;
306 }
307
308 591 void initLtft(void)
309 {
310 591 engine->module<LongTermFuelTrim>()->init(&ltftState);
311 591 }
312
313 void resetLongTermFuelTrim() {
314 engine->module<LongTermFuelTrim>()->reset();
315 }
316
317 void applyLongTermFuelTrimToVe() {
318 engine->module<LongTermFuelTrim>()->applyTrimsToVe();
319 }
320
321 531078 bool ltftNeedVeRefresh() {
322 531078 return engine->module<LongTermFuelTrim>()->isVeUpdated();
323 }
324
325 void devPokeLongTermFuelTrim() {
326 engine->module<LongTermFuelTrim>()->fillRandom();
327 }
328
329 void *ltftGetTsPage() {
330 return (void *)ltftState.trims;
331 }
332
333 LtftState *ltftGetState() {
334 return &ltftState;
335 }
336
337 size_t ltftGetTsPageSize() {
338 return sizeof(ltftState.trims);
339 }
340
341 #endif // EFI_LTFT_CONTROL
342