GCC Code Coverage Report


Directory: ./
File: firmware/controllers/engine_cycle/spark_logic.cpp
Date: 2025-10-24 14:26:41
Warnings: 1 unchecked decisions!
Coverage Exec Excl Total
Lines: 89.0% 234 0 263
Functions: 100.0% 16 0 16
Branches: 77.2% 159 0 206
Decisions: 82.5% 85 - 103

Line Branch Decision Exec Source
1 /*
2 * @file spark_logic.cpp
3 *
4 * @date Sep 15, 2016
5 * @author Andrey Belomutskiy, (c) 2012-2020
6 */
7
8 #include "pch.h"
9
10 #include "spark_logic.h"
11
12 #include "utlist.h"
13 #include "event_queue.h"
14
15 #include "knock_logic.h"
16
17 #if EFI_ENGINE_CONTROL
18
19 #if EFI_UNIT_TEST
20 extern bool verboseMode;
21 #endif /* EFI_UNIT_TEST */
22
23 #if EFI_PRINTF_FUEL_DETAILS || FUEL_MATH_EXTREME_LOGGING
24 extern bool printFuelDebug;
25 #endif // EFI_PRINTF_FUEL_DETAILS
26
27 static const char *prevSparkName = nullptr;
28
29 6944 static void fireSparkBySettingPinLow(IgnitionEvent *event, IgnitionOutputPin *output) {
30 #if SPARK_EXTREME_LOGGING
31 6944 efiPrintf("[%s] %d spark goes low revolution %d tick %d current value %d",
32 event->getOutputForLoggins()->getName(), event->sparkCounter,
33 getRevolutionCounter(), time2print(getTimeNowUs()),
34 output->currentLogicValue);
35 #endif /* SPARK_EXTREME_LOGGING */
36
37 /**
38 * there are two kinds of 'out-of-order'
39 * 1) low goes before high, everything is fine afterwards
40 *
41 * 2) we have an un-matched low followed by legit pairs
42 */
43 6944 output->signalFallSparkId = event->sparkCounter;
44
45
6/6
✓ Branch 0 taken 665 times.
✓ Branch 1 taken 6279 times.
✓ Branch 2 taken 480 times.
✓ Branch 3 taken 185 times.
✓ Branch 4 taken 473 times.
✓ Branch 5 taken 7 times.
2/2
✓ Decision 'true' taken 473 times.
✓ Decision 'false' taken 6471 times.
6944 if (!output->currentLogicValue && !event->wasSparkLimited && !event->wasSparkCanceled) {
46 #if SPARK_EXTREME_LOGGING
47 473 efiPrintf("out-of-order coil off %s", output->getName());
48 #endif /* SPARK_EXTREME_LOGGING */
49 473 warning(ObdCode::CUSTOM_OUT_OF_ORDER_COIL, "out-of-order coil off %s", output->getName());
50 }
51 6944 output->setLow();
52 6944 }
53
54 7421 static void assertPinAssigned(IgnitionOutputPin* output) {
55
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 7421 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 7421 times.
7421 if (!output->isInitialized()) {
56 warning(ObdCode::CUSTOM_OBD_COIL_PIN_NOT_ASSIGNED, "Pin Not Assigned check configuration #%s", output->getName()); \
57 }
58 7421 }
59
60 /**
61 * @param cylinderIndex from 0 to cylinderCount, not cylinder number
62 */
63 7318 static int getIgnitionPinForIndex(int cylinderIndex, ignition_mode_e ignitionMode) {
64
4/5
✓ Branch 0 taken 6235 times.
✓ Branch 1 taken 617 times.
✓ Branch 2 taken 454 times.
✓ Branch 3 taken 12 times.
✗ Branch 4 not taken.
7318 switch (ignitionMode) {
65
1/1
✓ Decision 'true' taken 6235 times.
6235 case IM_ONE_COIL:
66 6235 return 0;
67
1/1
✓ Decision 'true' taken 617 times.
617 case IM_WASTED_SPARK: {
68
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 617 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 617 times.
617 if (engineConfiguration->cylindersCount == 1) {
69 // we do not want to divide by zero
70 return 0;
71 }
72 617 return cylinderIndex % (engineConfiguration->cylindersCount / 2);
73 }
74
1/1
✓ Decision 'true' taken 454 times.
454 case IM_INDIVIDUAL_COILS:
75 454 return cylinderIndex;
76
1/1
✓ Decision 'true' taken 12 times.
12 case IM_TWO_COILS:
77 12 return cylinderIndex % 2;
78
79 default:
80 firmwareError(ObdCode::CUSTOM_OBD_IGNITION_MODE, "Invalid ignition mode getIgnitionPinForIndex(): %d", engineConfiguration->ignitionMode);
81 return 0;
82 }
83 }
84
85 7318 static void prepareCylinderIgnitionSchedule(angle_t dwellAngleDuration, floatms_t sparkDwell, IgnitionEvent *event) {
86 // todo: clean up this implementation? does not look too nice as is.
87
88 // let's save planned duration so that we can later compare it with reality
89 7318 event->sparkDwell = sparkDwell;
90
91
1/1
✓ Branch 1 taken 7318 times.
7318 auto ignitionMode = getCurrentIgnitionMode();
92
93 // On an odd cylinder (or odd fire) wasted spark engine, map outputs as if in sequential.
94 // During actual scheduling, the events just get scheduled every 360 deg instead
95 // of every 720 deg.
96
4/4
✓ Branch 0 taken 689 times.
✓ Branch 1 taken 6629 times.
✓ Branch 2 taken 72 times.
✓ Branch 3 taken 617 times.
2/2
✓ Decision 'true' taken 72 times.
✓ Decision 'false' taken 7246 times.
7318 if (ignitionMode == IM_WASTED_SPARK && engine->engineState.useOddFireWastedSpark) {
97 72 ignitionMode = IM_INDIVIDUAL_COILS;
98 }
99
100
1/1
✓ Branch 1 taken 7318 times.
7318 const int index = getIgnitionPinForIndex(event->cylinderIndex, ignitionMode);
101
1/1
✓ Branch 1 taken 7318 times.
7318 const int coilIndex = getCylinderNumberAtIndex(index);
102
1/1
✓ Branch 1 taken 7318 times.
7318 angle_t finalIgnitionTiming = getEngineState()->timingAdvance[coilIndex];
103 // Stash which cylinder we're scheduling so that knock sensing knows which
104 // cylinder just fired
105 7318 event->coilIndex = coilIndex;
106
107 // 10 ATDC ends up as 710, convert it to -10 so we can log and clamp correctly
108
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 7287 times.
2/2
✓ Decision 'true' taken 31 times.
✓ Decision 'false' taken 7287 times.
7318 if (finalIgnitionTiming > 360) {
109 31 finalIgnitionTiming -= 720;
110 }
111
112 // Clamp the final ignition timing to the configured limits
113 // finalIgnitionTiming is deg BTDC
114 // minimumIgnitionTiming limits maximum retard
115 // maximumIgnitionTiming limits maximum advance
116 /*
117 https://github.com/rusefi/rusefi/issues/5894 disabling feature for now
118 finalIgnitionTiming = clampF(engineConfiguration->minimumIgnitionTiming, finalIgnitionTiming, engineConfiguration->maximumIgnitionTiming);
119 */
120
121 7318 engine->outputChannels.ignitionAdvanceCyl[event->cylinderIndex] = finalIgnitionTiming;
122
123 7318 angle_t sparkAngle =
124 // Negate because timing *before* TDC, and we schedule *after* TDC
125 - finalIgnitionTiming
126 // Offset by this cylinder's position in the cycle
127
1/1
✓ Branch 1 taken 7318 times.
7318 + getPerCylinderFiringOrderOffset(event->cylinderIndex, coilIndex);
128
129
1/3
✗ Branch 1 not taken.
✓ Branch 2 taken 7318 times.
✗ Branch 4 not taken.
7318 efiAssertVoid(ObdCode::CUSTOM_SPARK_ANGLE_1, !std::isnan(sparkAngle), "sparkAngle#1");
130
1/1
✓ Branch 1 taken 7318 times.
7318 wrapAngle(sparkAngle, "findAngle#2", ObdCode::CUSTOM_ERR_6550);
131 7318 event->sparkAngle = sparkAngle;
132
133 7318 engine->outputChannels.currentIgnitionMode = static_cast<uint8_t>(ignitionMode);
134
135 7318 IgnitionOutputPin *output = &enginePins.coils[coilIndex];
136 7318 event->outputs[0] = output;
137 IgnitionOutputPin *secondOutput;
138
139 // We need two outputs if:
140 // - we are running wasted spark, and have "two wire" mode enabled
141 // - We are running sequential mode, but we're cranking, so we should run in two wire wasted mode (not one wire wasted)
142
4/4
✓ Branch 0 taken 7312 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 550 times.
✓ Branch 3 taken 6762 times.
7318 bool isTwoWireWasted = engineConfiguration->twoWireBatchIgnition || (engineConfiguration->ignitionMode == IM_INDIVIDUAL_COILS);
143
4/4
✓ Branch 0 taken 617 times.
✓ Branch 1 taken 6701 times.
✓ Branch 2 taken 103 times.
✓ Branch 3 taken 514 times.
2/2
✓ Decision 'true' taken 103 times.
✓ Decision 'false' taken 7215 times.
7318 if (ignitionMode == IM_WASTED_SPARK && isTwoWireWasted) {
144 103 int secondIndex = index + engineConfiguration->cylindersCount / 2;
145
1/1
✓ Branch 1 taken 103 times.
103 int secondCoilIndex = getCylinderNumberAtIndex(secondIndex);
146 103 secondOutput = &enginePins.coils[secondCoilIndex];
147
1/1
✓ Branch 1 taken 103 times.
103 assertPinAssigned(secondOutput);
148 103 } else {
149 7215 secondOutput = nullptr;
150 }
151
152
1/1
✓ Branch 1 taken 7318 times.
7318 assertPinAssigned(output);
153
154 7318 event->outputs[1] = secondOutput;
155
156
157 7318 angle_t dwellStartAngle = sparkAngle - dwellAngleDuration;
158
1/3
✗ Branch 1 not taken.
✓ Branch 2 taken 7318 times.
✗ Branch 4 not taken.
7318 efiAssertVoid(ObdCode::CUSTOM_ERR_6590, !std::isnan(dwellStartAngle), "findAngle#5");
159
160
2/5
✓ Branch 0 taken 7318 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7318 times.
✗ Branch 5 not taken.
7318 assertAngleRange(dwellStartAngle, "findAngle dwellStartAngle", ObdCode::CUSTOM_ERR_6550);
161
1/1
✓ Branch 1 taken 7318 times.
7318 wrapAngle(dwellStartAngle, "findAngle#7", ObdCode::CUSTOM_ERR_6550);
162 7318 event->dwellAngle = dwellStartAngle;
163
164 #if FUEL_MATH_EXTREME_LOGGING
165
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7318 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 7318 times.
7318 if (printFuelDebug) {
166 efiPrintf("addIgnitionEvent %s angle=%.1f", output->getName(), dwellStartAngle);
167 }
168 // efiPrintf("addIgnitionEvent %s ind=%d", output->name, event->dwellPosition->eventIndex);
169 #endif /* FUEL_MATH_EXTREME_LOGGING */
170 }
171
172 1 static void chargeTrailingSpark(IgnitionOutputPin* pin) {
173 #if SPARK_EXTREME_LOGGING
174 1 efiPrintf("chargeTrailingSpark %s", pin->getName());
175 #endif /* SPARK_EXTREME_LOGGING */
176 1 pin->setHigh();
177 1 }
178
179 1 static void fireTrailingSpark(IgnitionOutputPin* pin) {
180 #if SPARK_EXTREME_LOGGING
181 1 efiPrintf("fireTrailingSpark %s", pin->getName());
182 #endif /* SPARK_EXTREME_LOGGING */
183 1 pin->setLow();
184 1 }
185
186 315 static void overFireSparkAndPrepareNextSchedule(IgnitionEvent *event) {
187 #if SPARK_EXTREME_LOGGING
188 315 efiPrintf("[%s] %d %s",
189 event->getOutputForLoggins()->getName(), event->sparkCounter,
190 __func__);
191 #endif /* SPARK_EXTREME_LOGGING */
192 315 float actualDwellMs = event->actualDwellTimer.getElapsedSeconds() * 1e3;
193
194 630 warning((ObdCode)((int)ObdCode::CUSTOM_Ignition_Coil_Overcharge_1 + event->cylinderIndex),
195 "cylinder %d %s overcharge %f ms",
196 315 event->cylinderIndex + 1, event->outputs[0]->getName(), actualDwellMs);
197
198 // kill pending fire
199 315 engine->module<TriggerScheduler>()->cancel(&event->sparkEvent);
200
201 315 engine->engineState.overDwellCanceledCounter++;
202 315 event->wasSparkCanceled = true;
203 315 fireSparkAndPrepareNextSchedule(event);
204 315 }
205
206 /**
207 * TL,DR: each IgnitionEvent is in charge of it's own scheduling forever, we plant next event while finishing handling of the current one
208 */
209 6867 void fireSparkAndPrepareNextSchedule(IgnitionEvent *event) {
210 #if EFI_UNIT_TEST
211
2/2
✓ Branch 1 taken 268 times.
✓ Branch 2 taken 6599 times.
2/2
✓ Decision 'true' taken 268 times.
✓ Decision 'false' taken 6599 times.
6867 if (engine->onIgnitionEvent) {
212 268 engine->onIgnitionEvent(event, false);
213 }
214 #endif
215
216
2/2
✓ Branch 0 taken 13734 times.
✓ Branch 1 taken 6867 times.
2/2
✓ Decision 'true' taken 13734 times.
✓ Decision 'false' taken 6867 times.
20601 for (int i = 0; i< MAX_OUTPUTS_FOR_IGNITION;i++) {
217 13734 IgnitionOutputPin *output = event->outputs[i];
218
219
2/2
✓ Branch 0 taken 6944 times.
✓ Branch 1 taken 6790 times.
2/2
✓ Decision 'true' taken 6944 times.
✓ Decision 'false' taken 6790 times.
13734 if (output) {
220 6944 fireSparkBySettingPinLow(event, output);
221 }
222 }
223
224 6867 efitick_t nowNt = getTimeNowNt();
225
226 #if EFI_TOOTH_LOGGER
227 6867 LogTriggerCoilState(nowNt, false, event->coilIndex);
228 #endif // EFI_TOOTH_LOGGER
229
2/2
✓ Branch 0 taken 6683 times.
✓ Branch 1 taken 184 times.
2/2
✓ Decision 'true' taken 6683 times.
✓ Decision 'false' taken 184 times.
6867 if (!event->wasSparkLimited) {
230 /**
231 * ratio of desired dwell duration to actual dwell duration gives us some idea of how good is input trigger jitter
232 */
233 6683 float actualDwellMs = event->actualDwellTimer.getElapsedSeconds(nowNt) * 1e3;
234 6683 float ratio = actualDwellMs / event->sparkDwell;
235
236
2/2
✓ Branch 0 taken 607 times.
✓ Branch 1 taken 6076 times.
2/2
✓ Decision 'true' taken 607 times.
✓ Decision 'false' taken 6076 times.
6683 if (ratio > 1.2) {
237 607 engine->engineState.dwellOverChargeCounter++;
238
2/2
✓ Branch 0 taken 2217 times.
✓ Branch 1 taken 3859 times.
2/2
✓ Decision 'true' taken 2217 times.
✓ Decision 'false' taken 3859 times.
6076 } else if (ratio < 0.8) {
239 2217 engine->engineState.dwellUnderChargeCounter++;
240 }
241 6683 engine->engineState.dwellActualRatio = ratio;
242 }
243
244 // now that we've just fired a coil let's prepare the new schedule for the next engine revolution
245
246 6867 angle_t dwellAngleDuration = engine->ignitionState.dwellDurationAngle;
247 6867 floatms_t sparkDwell = engine->ignitionState.getDwell();
248
3/6
✓ Branch 1 taken 6867 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6867 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 6867 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 6867 times.
6867 if (std::isnan(dwellAngleDuration) || std::isnan(sparkDwell)) {
249 // we are here if engine has just stopped
250 return;
251 }
252
253 // If there are more sparks to fire, schedule them
254
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6867 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 6867 times.
6867 if (event->sparksRemaining > 0) {
255 event->sparksRemaining--;
256
257 efitick_t nextDwellStart = nowNt + engine->engineState.multispark.delay;
258 efitick_t nextFiring = nextDwellStart + engine->engineState.multispark.dwell;
259 #if SPARK_EXTREME_LOGGING
260 efiPrintf("schedule multispark");
261 #endif /* SPARK_EXTREME_LOGGING */
262
263 // We can schedule both of these right away, since we're going for "asap" not "particular angle"
264 engine->scheduler.schedule("dwell", &event->dwellStartTimer, nextDwellStart, action_s::make<turnSparkPinHighStartCharging>( event ));
265 engine->scheduler.schedule("firing", &event->sparkEvent.eventScheduling, nextFiring, action_s::make<fireSparkAndPrepareNextSchedule>( event ));
266 } else {
267
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 6866 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 6866 times.
6867 if (engineConfiguration->enableTrailingSparks) {
268 #if SPARK_EXTREME_LOGGING
269 1 efiPrintf("scheduleByAngle TrailingSparks");
270 #endif /* SPARK_EXTREME_LOGGING */
271
272 // Trailing sparks are enabled - schedule an event for the corresponding trailing coil
273
1/1
✓ Branch 2 taken 1 time.
2 scheduleByAngle(
274 1 &event->trailingSparkFire, nowNt, engine->ignitionState.trailingSparkAngle,
275 2 action_s::make<fireTrailingSpark>( &enginePins.trailingCoils[event->coilIndex] )
276 );
277 }
278
279 // If all events have been scheduled, prepare for next time.
280 6867 prepareCylinderIgnitionSchedule(dwellAngleDuration, sparkDwell, event);
281 }
282
283 6867 engine->onSparkFireKnockSense(event->cylinderIndex, nowNt);
284 }
285
286 6759 static bool startDwellByTurningSparkPinHigh(IgnitionEvent *event, IgnitionOutputPin *output) {
287 // todo: no reason for this to be disabled in unit_test mode?!
288 #if ! EFI_UNIT_TEST
289
290 if (Sensor::getOrZero(SensorType::Rpm) > 2 * engineConfiguration->cranking.rpm) {
291 const char *outputName = output->getName();
292 if (prevSparkName == outputName && getCurrentIgnitionMode() != IM_ONE_COIL) {
293 warning(ObdCode::CUSTOM_OBD_SKIPPED_SPARK, "looks like skipped spark event revolution=%d [%s]", getRevolutionCounter(), outputName);
294 }
295 prevSparkName = outputName;
296 }
297 #endif /* EFI_UNIT_TEST */
298
299
300 #if SPARK_EXTREME_LOGGING
301 6759 efiPrintf("[%s] %d spark goes high revolution %d tick %d current value %d",
302 event->getOutputForLoggins()->getName(), event->sparkCounter,
303 getRevolutionCounter(), time2print(getTimeNowUs()),
304 output->currentLogicValue, event->sparkCounter);
305 #endif /* SPARK_EXTREME_LOGGING */
306
307 // Reset error flag(s)
308 6759 event->wasSparkCanceled = false;
309
310
2/2
✓ Branch 0 taken 477 times.
✓ Branch 1 taken 6282 times.
2/2
✓ Decision 'true' taken 477 times.
✓ Decision 'false' taken 6282 times.
6759 if (output->signalFallSparkId >= event->sparkCounter) {
311 /**
312 * fact: we schedule both start of dwell and spark firing using a combination of time and trigger event domain
313 * in case of bad/noisy signal we can get unexpected trigger events and a small time delay for spark firing before
314 * we even start dwell if it scheduled with a longer time-only delay with fewer trigger events
315 *
316 * here we are detecting such out-of-order processing and choose the safer route of not even starting dwell
317 * [tag] #6349
318 */
319
320 #if SPARK_EXTREME_LOGGING
321
1/1
✓ Decision 'true' taken 477 times.
477 efiPrintf("[%s] bail spark dwell\n", output->getName());
322 #endif /* SPARK_EXTREME_LOGGING */
323 // let's save this coil if things do not look right
324 477 engine->engineState.sparkOutOfOrderCounter++;
325 477 return true;
326 }
327
328 6282 output->setHigh();
329 6282 return false;
330 }
331
332 6683 void turnSparkPinHighStartCharging(IgnitionEvent *event) {
333 6683 efitick_t nowNt = getTimeNowNt();
334
335 6683 event->actualDwellTimer.reset(nowNt);
336
337 6683 bool skippedDwellDueToTriggerNoised = false;
338
2/2
✓ Branch 0 taken 13366 times.
✓ Branch 1 taken 6683 times.
2/2
✓ Decision 'true' taken 13366 times.
✓ Decision 'false' taken 6683 times.
20049 for (int i = 0; i< MAX_OUTPUTS_FOR_IGNITION;i++) {
339 13366 IgnitionOutputPin *output = event->outputs[i];
340
2/2
✓ Branch 0 taken 6759 times.
✓ Branch 1 taken 6607 times.
2/2
✓ Decision 'true' taken 6759 times.
✓ Decision 'false' taken 6607 times.
13366 if (output != NULL) {
341 // at the moment we have a funny xor as if outputs could have different destiny. That's probably an over exaggeration,
342 // realistically it should be enough to check the sequencing of only the first output but that would be less elegant
343 //
344 // maybe it would have need nicer if instead of an array of outputs we had a linked list of outputs? but that's just daydreaming.
345 6759 skippedDwellDueToTriggerNoised |= startDwellByTurningSparkPinHigh(event, output);
346 }
347 }
348
349 #if EFI_UNIT_TEST
350 6683 engine->incrementBailedOnDwellCount();
351 #endif
352
353
354
2/2
✓ Branch 0 taken 6211 times.
✓ Branch 1 taken 472 times.
2/2
✓ Decision 'true' taken 6211 times.
✓ Decision 'false' taken 472 times.
6683 if (!skippedDwellDueToTriggerNoised) {
355
356 #if EFI_UNIT_TEST
357
2/2
✓ Branch 1 taken 256 times.
✓ Branch 2 taken 5955 times.
2/2
✓ Decision 'true' taken 256 times.
✓ Decision 'false' taken 5955 times.
6211 if (engine->onIgnitionEvent) {
358 256 engine->onIgnitionEvent(event, true);
359 }
360 #endif
361
362 #if EFI_TOOTH_LOGGER
363 6211 LogTriggerCoilState(nowNt, true, event->coilIndex);
364 #endif // EFI_TOOTH_LOGGER
365 }
366
367
368
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 6682 times.
2/2
✓ Decision 'true' taken 1 time.
✓ Decision 'false' taken 6682 times.
6683 if (engineConfiguration->enableTrailingSparks) {
369 1 IgnitionOutputPin *output = &enginePins.trailingCoils[event->coilIndex];
370 // Trailing sparks are enabled - schedule an event for the corresponding trailing coil
371
1/1
✓ Branch 2 taken 1 time.
2 scheduleByAngle(
372 1 &event->trailingSparkCharge, nowNt, engine->ignitionState.trailingSparkAngle,
373 2 action_s::make<chargeTrailingSpark>( output )
374 );
375 }
376 6683 }
377
378
379 9650 static void scheduleSparkEvent(bool limitedSpark, IgnitionEvent *event,
380 float rpm, float dwellMs, float dwellAngle, float sparkAngle, efitick_t edgeTimestamp, float currentPhase, float nextPhase) {
381 UNUSED(rpm);
382
383 9650 float angleOffset = dwellAngle - currentPhase;
384
2/2
✓ Branch 0 taken 148 times.
✓ Branch 1 taken 9502 times.
2/2
✓ Decision 'true' taken 148 times.
✓ Decision 'false' taken 9502 times.
9650 if (angleOffset < 0) {
385 148 angleOffset += engine->engineState.engineCycle;
386 }
387
388 /**
389 * By the way 32-bit value should hold at least 400 hours of events at 6K RPM x 12 events per revolution
390 * [tag:duration_limit]
391 */
392 9650 event->sparkCounter = engine->engineState.globalSparkCounter++;
393 9650 event->wasSparkLimited = limitedSpark;
394
395 9650 efitick_t chargeTime = 0;
396
397 /**
398 * The start of charge is always within the current trigger event range, so just plain time-based scheduling
399 */
400
2/2
✓ Branch 0 taken 9458 times.
✓ Branch 1 taken 192 times.
2/2
✓ Decision 'true' taken 9458 times.
✓ Decision 'false' taken 192 times.
9650 if (!limitedSpark) {
401 #if SPARK_EXTREME_LOGGING
402 9458 efiPrintf("[%s] %d sparkUp scheduling revolution %d angle %.1f (+%.1f) later",
403 event->getOutputForLoggins()->getName(), event->sparkCounter,
404 getRevolutionCounter(), dwellAngle, angleOffset);
405 #endif /* SPARK_EXTREME_LOGGING */
406
407 /**
408 * Note how we do not check if spark is limited or not while scheduling 'spark down'
409 * This way we make sure that coil dwell started while spark was enabled would fire and not burn
410 * the coil.
411 */
412
1/1
✓ Branch 3 taken 9458 times.
9458 chargeTime = scheduleByAngle(&event->dwellStartTimer, edgeTimestamp, angleOffset, action_s::make<turnSparkPinHighStartCharging>( event ));
413
414 #if EFI_UNIT_TEST
415 9458 engine->onScheduleTurnSparkPinHighStartCharging(*event, edgeTimestamp, angleOffset, chargeTime);
416 #endif
417
418 #if SPARK_EXTREME_LOGGING
419 9458 efitimeus_t chargeTimeUs = NT2US(chargeTime);
420 9458 efiPrintf("[%s] %d sparkUp scheduled at %d ticks (%d.%06d)",
421 event->getOutputForLoggins()->getName(), event->sparkCounter,
422 time2print(chargeTime), time2print(chargeTimeUs / (1000 * 1000)), time2print(chargeTimeUs % (1000 * 1000)));
423 #endif /* SPARK_EXTREME_LOGGING */
424
425 9458 event->sparksRemaining = engine->engineState.multispark.count;
426 } else {
427 // don't fire multispark if spark is cut completely!
428 192 event->sparksRemaining = 0;
429
430 #if SPARK_EXTREME_LOGGING
431 192 efiPrintf("[%s] %d sparkUp NOT scheduled because of limitedSpark",
432 event->getOutputForLoggins()->getName(), event->sparkCounter);
433 #endif /* SPARK_EXTREME_LOGGING */
434 }
435
436 /**
437 * Spark event is often happening during a later trigger event timeframe
438 */
439
440
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9650 times.
9650 efiAssertVoid(ObdCode::CUSTOM_ERR_6591, !std::isnan(sparkAngle), "findAngle#4");
441
2/4
✓ Branch 0 taken 9650 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9650 times.
9650 assertAngleRange(sparkAngle, "findAngle#a5", ObdCode::CUSTOM_ERR_6549);
442
443 #if SPARK_EXTREME_LOGGING
444 9650 efiPrintf("[%s] %d sparkDown scheduling revolution %d angle %.1f",
445 event->getOutputForLoggins()->getName(), event->sparkCounter,
446 getRevolutionCounter(), sparkAngle);
447 #endif /* FUEL_MATH_EXTREME_LOGGING */
448
449
450
1/1
✓ Branch 3 taken 9650 times.
19300 bool isTimeScheduled = engine->module<TriggerScheduler>()->scheduleOrQueue(
451 "spark",
452 &event->sparkEvent, edgeTimestamp, sparkAngle,
453 19300 action_s::make<fireSparkAndPrepareNextSchedule>( event ),
454 9650 currentPhase, nextPhase);
455
456 #if SPARK_EXTREME_LOGGING
457
2/2
✓ Branch 0 taken 7613 times.
✓ Branch 1 taken 2037 times.
9650 efiPrintf("[%s] %d sparkDown scheduled %s",
458 event->getOutputForLoggins()->getName(), event->sparkCounter,
459 isTimeScheduled ? "later" : "to queue");
460 #endif /* FUEL_MATH_EXTREME_LOGGING */
461
462
2/2
✓ Branch 0 taken 2037 times.
✓ Branch 1 taken 7613 times.
2/2
✓ Decision 'true' taken 2037 times.
✓ Decision 'false' taken 7613 times.
9650 if (isTimeScheduled) {
463 // event was scheduled by time, we expect it to happen reliably
464 } else {
465 // event was queued in relation to some expected tooth event in the future which might just never come so we shall protect from over-dwell
466
2/2
✓ Branch 0 taken 2030 times.
✓ Branch 1 taken 7 times.
2/2
✓ Decision 'true' taken 2030 times.
✓ Decision 'false' taken 7 times.
2037 if (!limitedSpark) {
467 // auto fire spark at 1.5x nominal dwell
468 2030 efitick_t fireTime = sumTickAndFloat(chargeTime, MSF2NT(1.5f * dwellMs));
469
470 #if SPARK_EXTREME_LOGGING
471 2030 efitimeus_t fireTimeUs = NT2US(fireTime);
472 2030 efiPrintf("[%s] %d overdwell scheduling at %d ticks (%d.%06d)",
473 event->getOutputForLoggins()->getName(), event->sparkCounter,
474 time2print(fireTime), time2print(fireTimeUs / (1000 * 1000)), time2print(fireTimeUs % (1000 * 1000)));
475 #endif /* SPARK_EXTREME_LOGGING */
476
477 /**
478 * todo: can we please comprehend/document how this even works? we seem to be reusing 'sparkEvent.scheduling' instance
479 * and it looks like current (smart?) re-queuing is effectively cancelling out the overdwell? is that the way this was intended to work?
480 * [tag:overdwell]
481 */
482
1/1
✓ Branch 3 taken 2030 times.
2030 engine->scheduler.schedule("overdwell", &event->sparkEvent.eventScheduling, fireTime, action_s::make<overFireSparkAndPrepareNextSchedule>( event ));
483
484 #if EFI_UNIT_TEST
485 2030 engine->onScheduleOverFireSparkAndPrepareNextSchedule(*event, fireTime);
486 #endif
487 } else {
488 7 engine->engineState.overDwellNotScheduledCounter++;
489 }
490 }
491
492 #if EFI_UNIT_TEST
493
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9650 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 9650 times.
9650 if (verboseMode) {
494 efiPrintf("spark dwell@ %.1f spark@ %.2f id=%d sparkCounter=%d", event->dwellAngle,
495 event->sparkEvent.getAngle(),
496 event->coilIndex,
497 event->sparkCounter);
498 }
499 #endif
500 }
501
502 110 void initializeIgnitionActions() {
503 110 IgnitionEventList *list = &engine->ignitionEvents;
504 110 angle_t dwellAngle = engine->ignitionState.dwellDurationAngle;
505 110 floatms_t sparkDwell = engine->ignitionState.getDwell();
506
3/6
✓ Branch 1 taken 110 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 110 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 110 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 110 times.
110 if (std::isnan(engine->engineState.timingAdvance[0]) || std::isnan(dwellAngle)) {
507 // error should already be reported
508 // need to invalidate previous ignition schedule
509 list->isReady = false;
510 return;
511 }
512
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 110 times.
110 efiAssertVoid(ObdCode::CUSTOM_ERR_6592, engineConfiguration->cylindersCount > 0, "cylindersCount");
513
514
2/2
✓ Branch 0 taken 451 times.
✓ Branch 1 taken 110 times.
2/2
✓ Decision 'true' taken 451 times.
✓ Decision 'false' taken 110 times.
561 for (size_t cylinderIndex = 0; cylinderIndex < engineConfiguration->cylindersCount; cylinderIndex++) {
515 451 list->elements[cylinderIndex].cylinderIndex = cylinderIndex;
516 451 prepareCylinderIgnitionSchedule(dwellAngle, sparkDwell, &list->elements[cylinderIndex]);
517 }
518 110 list->isReady = true;
519 }
520
521 109 static void prepareIgnitionSchedule() {
522 109 ScopePerf perf(PE::PrepareIgnitionSchedule);
523
524
2/2
✓ Branch 1 taken 109 times.
✓ Branch 4 taken 109 times.
109 operation_mode_e operationMode = getEngineRotationState()->getOperationMode();
525 float maxAllowedDwellAngle;
526
527
3/3
✓ Branch 1 taken 109 times.
✓ Branch 3 taken 73 times.
✓ Branch 4 taken 36 times.
2/2
✓ Decision 'true' taken 73 times.
✓ Decision 'false' taken 36 times.
109 if (getCurrentIgnitionMode() == IM_ONE_COIL) {
528
1/1
✓ Branch 1 taken 73 times.
73 maxAllowedDwellAngle = getEngineCycle(operationMode) / engineConfiguration->cylindersCount / 1.1;
529 } else {
530
1/1
✓ Branch 1 taken 36 times.
36 maxAllowedDwellAngle = (int) (getEngineCycle(operationMode) / 2); // the cast is about making Coverity happy
531 }
532
533
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 109 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 109 times.
109 if (engine->ignitionState.dwellDurationAngle == 0) {
534 warning(ObdCode::CUSTOM_ZERO_DWELL, "dwell is zero?");
535 }
536
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 107 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 107 times.
109 if (engine->ignitionState.dwellDurationAngle > maxAllowedDwellAngle) {
537
1/1
✓ Branch 1 taken 2 times.
2 warning(ObdCode::CUSTOM_DWELL_TOO_LONG, "dwell angle too long: %.2f", engine->ignitionState.dwellDurationAngle);
538 }
539
540 // todo: add some check for dwell overflow? like 4 times 6 ms while engine cycle is less then that
541
542
1/1
✓ Branch 1 taken 109 times.
109 initializeIgnitionActions();
543 109 }
544
545 33524 void onTriggerEventSparkLogic(float rpm, efitick_t edgeTimestamp, float currentPhase, float nextPhase) {
546 33524 ScopePerf perf(PE::OnTriggerEventSparkLogic);
547
548
2/2
✓ Branch 0 taken 673 times.
✓ Branch 1 taken 32851 times.
2/2
✓ Decision 'true' taken 673 times.
✓ Decision 'false' taken 32851 times.
33524 if (!engineConfiguration->isIgnitionEnabled) {
549 673 return;
550 }
551
552
2/2
✓ Branch 1 taken 32851 times.
✓ Branch 4 taken 32851 times.
32851 LimpState limitedSparkState = getLimpManager()->allowIgnition();
553
554 // todo: eliminate state copy logic by giving limpManager it's owm limp_manager.txt and leveraging LiveData
555 32851 engine->outputChannels.sparkCutReason = (int8_t)limitedSparkState.reason;
556 32851 bool limitedSpark = !limitedSparkState.value;
557
558
1/1
✓ Branch 1 taken 32851 times.
32851 const floatms_t dwellMs = engine->ignitionState.getDwell();
559
3/6
✓ Branch 1 taken 32851 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 32851 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 32851 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 32851 times.
32851 if (std::isnan(dwellMs) || dwellMs <= 0) {
560 warning(ObdCode::CUSTOM_DWELL, "invalid dwell to handle: %.2f", dwellMs);
561 return;
562 }
563
564
2/2
✓ Branch 0 taken 109 times.
✓ Branch 1 taken 32742 times.
2/2
✓ Decision 'true' taken 109 times.
✓ Decision 'false' taken 32742 times.
32851 if (!engine->ignitionEvents.isReady) {
565
1/1
✓ Branch 1 taken 109 times.
109 prepareIgnitionSchedule();
566 }
567
568
569 /**
570 * Ignition schedule is defined once per revolution
571 * See initializeIgnitionActions()
572 */
573
574
575 // Only apply odd cylinder count wasted logic if:
576 // - odd cyl count
577 // - current mode is wasted spark
578 // - four stroke
579 bool enableOddCylinderWastedSpark =
580 32851 engine->engineState.useOddFireWastedSpark
581
5/5
✓ Branch 0 taken 2467 times.
✓ Branch 1 taken 30384 times.
✓ Branch 3 taken 2467 times.
✓ Branch 5 taken 680 times.
✓ Branch 6 taken 1787 times.
32851 && getCurrentIgnitionMode() == IM_WASTED_SPARK;
582
583
1/2
✓ Branch 0 taken 32851 times.
✗ Branch 1 not taken.
1/2
✓ Decision 'true' taken 32851 times.
✗ Decision 'false' not taken.
32851 if (engine->ignitionEvents.isReady) {
584
2/2
✓ Branch 0 taken 130951 times.
✓ Branch 1 taken 32851 times.
2/2
✓ Decision 'true' taken 130951 times.
✓ Decision 'false' taken 32851 times.
163802 for (size_t i = 0; i < engineConfiguration->cylindersCount; i++) {
585 130951 IgnitionEvent *event = &engine->ignitionEvents.elements[i];
586
587 130951 angle_t dwellAngle = event->dwellAngle;
588
589 130951 angle_t sparkAngle = event->sparkAngle;
590
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 130951 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 130951 times.
130951 if (std::isnan(sparkAngle)) {
591 warning(ObdCode::CUSTOM_ADVANCE_SPARK, "NaN advance");
592 continue;
593 }
594
595 130951 bool isOddCylWastedEvent = false;
596
2/2
✓ Branch 0 taken 1358 times.
✓ Branch 1 taken 129593 times.
2/2
✓ Decision 'true' taken 1358 times.
✓ Decision 'false' taken 129593 times.
130951 if (enableOddCylinderWastedSpark) {
597 1358 auto dwellAngleWastedEvent = dwellAngle + 360;
598
2/2
✓ Branch 0 taken 1349 times.
✓ Branch 1 taken 9 times.
2/2
✓ Decision 'true' taken 1349 times.
✓ Decision 'false' taken 9 times.
1358 if (dwellAngleWastedEvent > 720) {
599 1349 dwellAngleWastedEvent -= 720;
600 }
601
602 // Check whether this event hits 360 degrees out from now (ie, wasted spark),
603 // and if so, twiddle the dwell and spark angles so it happens now instead
604
1/1
✓ Branch 1 taken 1358 times.
1358 isOddCylWastedEvent = isPhaseInRange(dwellAngleWastedEvent, currentPhase, nextPhase);
605
606
2/2
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 1323 times.
2/2
✓ Decision 'true' taken 35 times.
✓ Decision 'false' taken 1323 times.
1358 if (isOddCylWastedEvent) {
607 35 dwellAngle = dwellAngleWastedEvent;
608
609 35 sparkAngle += 360;
610
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 1 time.
2/2
✓ Decision 'true' taken 34 times.
✓ Decision 'false' taken 1 time.
35 if (sparkAngle > 720) {
611 34 sparkAngle -= 720;
612 }
613 }
614 }
615
616
7/7
✓ Branch 0 taken 130916 times.
✓ Branch 1 taken 35 times.
✓ Branch 3 taken 130916 times.
✓ Branch 5 taken 121301 times.
✓ Branch 6 taken 9615 times.
✓ Branch 7 taken 121301 times.
✓ Branch 8 taken 9650 times.
2/2
✓ Decision 'true' taken 121301 times.
✓ Decision 'false' taken 9650 times.
130951 if (!isOddCylWastedEvent && !isPhaseInRange(dwellAngle, currentPhase, nextPhase)) {
617 121301 continue;
618 }
619
620
4/9
✓ Branch 0 taken 2391 times.
✓ Branch 1 taken 7259 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2391 times.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 9650 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 9650 times.
9650 if (i == 0 && engineConfiguration->artificialTestMisfire && (getRevolutionCounter() % ((int)engineConfiguration->scriptSetting[5]) == 0)) {
621 // artificial misfire on cylinder #1 for testing purposes
622 // enable artificialMisfire
623 warning(ObdCode::CUSTOM_ARTIFICIAL_MISFIRE, "artificial misfire on cylinder #1 for testing purposes %d", engine->engineState.globalSparkCounter);
624 continue;
625 }
626 #if EFI_LAUNCH_CONTROL
627
4/6
✓ Branch 1 taken 9650 times.
✓ Branch 3 taken 9650 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 9650 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 9650 times.
9650 bool sparkLimited = engine->softSparkLimiter.shouldSkip() || engine->hardSparkLimiter.shouldSkip();
628 9650 engine->ignitionState.luaIgnitionSkip = sparkLimited;
629
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9650 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 9650 times.
9650 if (sparkLimited) {
630 continue;
631 }
632 #endif // EFI_LAUNCH_CONTROL
633
634 #if EFI_ANTILAG_SYSTEM && EFI_LAUNCH_CONTROL
635 /*
636 if (engine->antilagController.isAntilagCondition) {
637 if (engine->ALSsoftSparkLimiter.shouldSkip()) {
638 continue;
639 }
640 }
641 float throttleIntent = Sensor::getOrZero(SensorType::DriverThrottleIntent);
642 engine->antilagController.timingALSSkip = interpolate3d(
643 config->ALSIgnSkipTable,
644 config->alsIgnSkipLoadBins, throttleIntent,
645 config->alsIgnSkiprpmBins, rpm
646 );
647
648 auto ALSSkipRatio = engine->antilagController.timingALSSkip;
649 engine->ALSsoftSparkLimiter.setTargetSkipRatio(ALSSkipRatio/100);
650 */
651 #endif // EFI_ANTILAG_SYSTEM
652
653
1/1
✓ Branch 1 taken 9650 times.
9650 scheduleSparkEvent(limitedSpark, event, rpm, dwellMs, dwellAngle, sparkAngle, edgeTimestamp, currentPhase, nextPhase);
654 }
655 }
656 }
657
658 /**
659 * Number of sparks per physical coil
660 * @see getNumberOfInjections
661 */
662 531079 int getNumberOfSparks(ignition_mode_e mode) {
663
3/5
✓ Branch 0 taken 397736 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31818 times.
✓ Branch 3 taken 101525 times.
✗ Branch 4 not taken.
531079 switch (mode) {
664
1/1
✓ Decision 'true' taken 397736 times.
397736 case IM_ONE_COIL:
665 397736 return engineConfiguration->cylindersCount;
666 case IM_TWO_COILS:
667 return engineConfiguration->cylindersCount / 2;
668
1/1
✓ Decision 'true' taken 31818 times.
31818 case IM_INDIVIDUAL_COILS:
669 31818 return 1;
670
1/1
✓ Decision 'true' taken 101525 times.
101525 case IM_WASTED_SPARK:
671 101525 return 2;
672 default:
673 firmwareError(ObdCode::CUSTOM_ERR_IGNITION_MODE, "Unexpected ignition_mode_e %d", mode);
674 return 1;
675 }
676 }
677
678 /**
679 * @see getInjectorDutyCycle
680 */
681 531079 percent_t getCoilDutyCycle(float rpm) {
682 531079 floatms_t totalPerCycle = engine->ignitionState.getDwell() * getNumberOfSparks(getCurrentIgnitionMode());
683
2/2
✓ Branch 3 taken 1382 times.
✓ Branch 4 taken 529697 times.
531079 floatms_t engineCycleDuration = getCrankshaftRevolutionTimeMs(rpm) * (getEngineRotationState()->getOperationMode() == TWO_STROKE ? 1 : 2);
684 531079 return 100 * totalPerCycle / engineCycleDuration;
685 }
686
687 #endif // EFI_ENGINE_CONTROL
688