GCC Code Coverage Report


Directory: ./
File: firmware/controllers/long_term_fuel_trim.cpp
Date: 2025-10-03 00:57:22
Warnings: 1 unchecked decisions!
Coverage Exec Excl Total
Lines: 61.8% 97 0 157
Functions: 50.0% 14 0 28
Branches: 57.1% 36 0 63
Decisions: 50.0% 21 - 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 589 void LtftState::reset() {
42
2/2
✓ Branch 0 taken 1178 times.
✓ Branch 1 taken 589 times.
2/2
✓ Decision 'true' taken 1178 times.
✓ Decision 'false' taken 589 times.
1767 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
43 1178 setTable(trims[bank], 0.0f);
44 }
45 589 }
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] = 0.01 * (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 two 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 586 void LongTermFuelTrim::init(LtftState *state) {
79 586 m_state = state;
80
81 #if EFI_PROD_CODE
82 ltftLoadPending = storageReqestReadID(EFI_LTFT_RECORD_ID);
83 #else
84 586 ltftLoadPending = false;
85 586 reset();
86 #endif
87 586 }
88
89 40407 float LongTermFuelTrim::getIntegratorGain() const
90 {
91 40407 const auto& cfg = engineConfiguration->ltft;
92
93 40407 return 1 / clampF(30, cfg.timeConstant, 3000);
94 }
95
96 80000 float LongTermFuelTrim::getMaxAdjustment() const {
97 80000 const auto& cfg = engineConfiguration->ltft;
98
99 80000 float raw = 0.01 * cfg.maxAdd;
100 // Don't allow maximum less than 0, or more than maximum add adjustment
101 80000 return clampF(0, raw, MAX_ADJ);
102 }
103
104 80000 float LongTermFuelTrim::getMinAdjustment() const {
105 80000 const auto& cfg = engineConfiguration->ltft;
106
107 80000 float raw = -0.01f * cfg.maxRemove;
108 // Don't allow minimum more than 0, or less than maximum remove adjustment
109 80000 return clampF(-MAX_ADJ, raw, 0);
110 }
111
112 61120 void LongTermFuelTrim::learn(ClosedLoopFuelResult clResult, float rpm, float fuelLoad) {
113 61120 const auto& cfg = engineConfiguration->ltft;
114
115
4/6
✓ Branch 0 taken 41120 times.
✓ Branch 1 taken 20000 times.
✓ Branch 2 taken 41120 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 41120 times.
2/2
✓ Decision 'true' taken 20000 times.
✓ Decision 'false' taken 41120 times.
61120 if ((!cfg.enabled) || (ltftSavePending) || (ltftLoadPending)) {
116 20000 ltftLearning = false;
117 20000 return;
118 }
119
120 // TODO: should we swap x and y here to keep aligned to wierd TS table definition?
121 // x - load, y - rpm
122 41120 auto x = priv::getClosestBin(fuelLoad, config->veLoadBins);
123 41120 auto y = priv::getClosestBin(rpm, config->veRpmBins);
124
125 // Skip learning if current load point falls far outside the table
126
5/6
✓ Branch 1 taken 41120 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 713 times.
✓ Branch 4 taken 40407 times.
✓ Branch 5 taken 713 times.
✓ Branch 6 taken 40407 times.
2/2
✓ Decision 'true' taken 713 times.
✓ Decision 'false' taken 81527 times.
82240 if ((abs(x.Frac) > 0.5) ||
127 41120 (abs(y.Frac) > 0.5)) {
128 // we are outside table
129 713 ltftCntMiss++;
130 713 ltftLearning = false;
131 713 return;
132 }
133
134 40407 bool adjusted = false;
135
136 // calculate weight depenting on distance from cell center
137 // Is this too heavy?
138 40407 float weight = 1.0 - hypotf(x.Frac, y.Frac) / hypotf(0.5, 0.5);
139 40407 float k = getIntegratorGain() * integrator_dt * weight;
140
141
2/2
✓ Branch 0 taken 80814 times.
✓ Branch 1 taken 40407 times.
2/2
✓ Decision 'true' taken 80814 times.
✓ Decision 'false' taken 40407 times.
121221 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
142 80814 float lambdaCorrection = clResult.banks[bank] - 1.0;
143
144 // If we're within the deadband, make no adjustment.
145
2/2
✓ Branch 2 taken 814 times.
✓ Branch 3 taken 80000 times.
2/2
✓ Decision 'true' taken 814 times.
✓ Decision 'false' taken 80000 times.
80814 if (std::abs(lambdaCorrection) < 0.01f * cfg.deadband) {
146 814 continue;
147 }
148
149 // get current trim
150 80000 float trim = m_state->trims[bank][x.Idx][y.Idx];
151
152 // Integrate
153 80000 float newTrim = trim + k * (lambdaCorrection - trim);
154
155 // TODO:
156 // rise OBD code if we hit trim limit
157
158 // Clamp to bounds and save
159 80000 newTrim = clampF(getMinAdjustment(), newTrim, getMaxAdjustment());
160
161 // accumulate
162 80000 ltftAccummulatedCorrection[bank] += newTrim - trim;
163
164 // store
165 80000 m_state->trims[bank][x.Idx][y.Idx] = newTrim;
166
167 80000 adjusted = true;
168 }
169
170 40407 ltftLearning = adjusted;
171
2/2
✓ Branch 0 taken 40000 times.
✓ Branch 1 taken 407 times.
2/2
✓ Decision 'true' taken 40000 times.
✓ Decision 'false' taken 407 times.
40407 if (adjusted) {
172 40000 ltftCntHit++;
173 40000 showUpdateToUser = true;
174 40000 if ((ltftCntHit % SAVE_AFTER_HITS) == 0) {
175 // request save
176 #if EFI_PROD_CODE
177 settingsLtftRequestWriteToFlash();
178 #endif
179 }
180 } else {
181 407 ltftCntDeadband++;
182 }
183 }
184
185 1125 ClosedLoopFuelResult LongTermFuelTrim::getTrims(float rpm, float fuelLoad) {
186 1125 const auto& cfg = engineConfiguration->ltft;
187
188
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1121 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
0/1
? Decision couldn't be analyzed.
1125 if ((!cfg.correctionEnabled) || (ltftLoadPending)) {
189
2/2
✓ Branch 0 taken 2242 times.
✓ Branch 1 taken 1121 times.
2/2
✓ Decision 'true' taken 2242 times.
✓ Decision 'false' taken 1121 times.
3363 for (size_t bank = 0; bank < FT_BANK_COUNT; bank++) {
190 2242 ltftCorrection[bank] = 1.0f;
191 }
192 1121 ltftCorrecting = false;
193 1121 return { };
194 }
195
196 // Keep calculating/applying correction even load point is far outside table
197 #if 0
198 // x - load, y - rpm
199 auto x = priv::getClosestBin(fuelLoad, config->veLoadBins);
200 auto y = priv::getClosestBin(rpm, config->veRpmBins);
201
202 // do not interpolate outside table...
203 if ((abs(x.Frac) > 0.5) ||
204 (abs(y.Frac) > 0.5)) {
205 // we are outside table
206 miss++;
207 return { };
208 }
209 #endif
210
211 // Is there any reason we should not apply LTFT?
212
213
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++) {
214 8 ltftCorrection[bank] = 1.0f + interpolate3d(
215 8 m_state->trims[bank],
216 8 config->veLoadBins, fuelLoad,
217
1/1
✓ Branch 1 taken 8 times.
8 config->veRpmBins, rpm
218 );
219 }
220
221 4 ClosedLoopFuelResult result;
222
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++) {
223 8 result.banks[bank] = ltftCorrection[bank];
224 }
225
226 4 ltftCorrecting = true;
227 4 return result;
228 }
229
230 // Called from storage manager thread when requested ID is ready
231 void LongTermFuelTrim::load() {
232 m_state->load();
233
234 ltftLoadPending = false;
235 }
236
237 void LongTermFuelTrim::store() {
238 // TODO: lock to avoid modification while writing
239 ltftSavePending = true;
240
241 if (m_state) {
242 m_state->save();
243 }
244
245 // TODO: unlock
246 ltftSavePending = false;
247 }
248
249 586 void LongTermFuelTrim::reset() {
250 586 m_state->reset();
251
252 586 ltftCntHit = 0;
253 586 ltftCntMiss = 0;
254 586 ltftCntDeadband = 0;
255
256
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++) {
257 1172 ltftAccummulatedCorrection[bank] = 0.0;
258 }
259 586 }
260
261 void LongTermFuelTrim::applyTrimsToVe() {
262 m_state->applyToVe();
263 m_state->reset();
264
265 veNeedRefresh = true;
266 }
267
268 522954 bool LongTermFuelTrim::isVeUpdated() {
269
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 522954 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 522954 times.
522954 if (veNeedRefresh) {
270 veNeedRefresh = false;
271 return true;
272 }
273 522954 return false;
274 }
275
276 void LongTermFuelTrim::onLiveDataRead() {
277 // rise refresh flag every second for one TS reading of livedata if we have something new...
278 if (ltftPageRefreshFlag) {
279 ltftPageRefreshFlag = false;
280 showUpdateToUser = false;
281 pageRefreshTimer.reset();
282 } else {
283 // was update to table and timeout
284 ltftPageRefreshFlag = showUpdateToUser && pageRefreshTimer.hasElapsedSec(1);
285 }
286 }
287
288 void LongTermFuelTrim::fillRandom() {
289 m_state->fillRandom();
290 }
291
292 1087 void LongTermFuelTrim::onSlowCallback() {
293 // we can wait some time for LTFT to be loaded from storage...
294 2176 if ((ltftLoadPending) &&
295 #if EFI_SHAFT_POSITION_INPUT
296
6/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1085 times.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1086 times.
1087 (engine->rpmCalculator.getSecondsSinceEngineStart(getTimeNowNt()) > 5.0) &&
297 #endif
298 (1)) {
299 1 efiPrintf("LTFT: failed to load calibrations");
300 1 m_state->reset();
301 1 ltftLoadPending = false;
302 1 ltftLoadError = true;
303 }
304 // Do some magic math here?
305
306 /* ... */
307 1087 }
308
309 1001 bool LongTermFuelTrim::needsDelayedShutoff() {
310 // TODO: We should delay power off until we store LTFT
311 1001 return false;
312 }
313
314 584 void initLtft(void)
315 {
316 584 engine->module<LongTermFuelTrim>()->init(&ltftState);
317 584 }
318
319 void resetLongTermFuelTrim() {
320 engine->module<LongTermFuelTrim>()->reset();
321 }
322
323 void applyLongTermFuelTrimToVe() {
324 engine->module<LongTermFuelTrim>()->applyTrimsToVe();
325 }
326
327 522954 bool ltftNeedVeRefresh() {
328 522954 return engine->module<LongTermFuelTrim>()->isVeUpdated();
329 }
330
331 void devPokeLongTermFuelTrim() {
332 engine->module<LongTermFuelTrim>()->fillRandom();
333 }
334
335 void *ltftGetTsPage() {
336 return (void *)ltftState.trims;
337 }
338
339 LtftState *ltftGetState() {
340 return &ltftState;
341 }
342
343 size_t ltftGetTsPageSize() {
344 return sizeof(ltftState.trims);
345 }
346
347 #endif // EFI_LTFT_CONTROL
348