GCC Code Coverage Report


Directory: ./
File: firmware/controllers/engine_cycle/spark_logic.cpp
Date: 2025-10-03 00:57:22
Warnings: 1 unchecked decisions!
Coverage Exec Excl Total
Lines: 87.3% 220 0 252
Functions: 100.0% 16 0 16
Branches: 76.0% 152 0 200
Decisions: 80.8% 80 - 99

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