GCC Code Coverage Report


Directory: ./
File: firmware/controllers/engine_cycle/rpm_calculator.cpp
Date: 2025-11-16 14:52:24
Coverage Exec Excl Total
Lines: 94.7% 143 0 151
Functions: 96.0% 24 0 25
Branches: 91.6% 87 0 95
Decisions: 91.5% 54 - 59

Line Branch Decision Exec Source
1 /**
2 * @file rpm_calculator.cpp
3 * @brief RPM calculator
4 *
5 * Here we listen to position sensor events in order to figure our if engine is currently running or not.
6 * Actual getRpm() is calculated once per crankshaft revolution, based on the amount of time passed
7 * since the start of previous shaft revolution.
8 *
9 * We also have 'instant RPM' logic separate from this 'cycle RPM' logic. Open question is why do we not use
10 * instant RPM instead of cycle RPM more often.
11 *
12 * @date Jan 1, 2013
13 * @author Andrey Belomutskiy, (c) 2012-2020
14 */
15
16 #include "pch.h"
17
18 #include "trigger_central.h"
19
20 #include "engine_sniffer.h"
21
22 // See RpmCalculator::checkIfSpinning()
23 #ifndef NO_RPM_EVENTS_TIMEOUT_SECS
24 #define NO_RPM_EVENTS_TIMEOUT_SECS 2
25 #endif /* NO_RPM_EVENTS_TIMEOUT_SECS */
26
27 531081 float RpmCalculator::getRpmAcceleration() const {
28 531081 return rpmRate;
29 }
30
31 38325 bool RpmCalculator::isStopped() const {
32 // Spinning-up with zero RPM means that the engine is not ready yet, and is treated as 'stopped'.
33
6/6
✓ Branch 0 taken 37065 times.
✓ Branch 1 taken 1260 times.
✓ Branch 2 taken 4311 times.
✓ Branch 3 taken 32754 times.
✓ Branch 4 taken 2596 times.
✓ Branch 5 taken 1715 times.
38325 return state == STOPPED || (state == SPINNING_UP && cachedRpmValue == 0);
34 }
35
36 459600 bool RpmCalculator::isCranking() const {
37 // Spinning-up with non-zero RPM is suitable for all engine math, as good as cranking
38
6/6
✓ Branch 0 taken 342693 times.
✓ Branch 1 taken 116907 times.
✓ Branch 2 taken 100742 times.
✓ Branch 3 taken 241951 times.
✓ Branch 4 taken 27854 times.
✓ Branch 5 taken 72888 times.
459600 return state == CRANKING || (state == SPINNING_UP && cachedRpmValue > 0);
39 }
40
41 169251 bool RpmCalculator::isSpinningUp() const {
42 169251 return state == SPINNING_UP;
43 }
44
45 742217 uint32_t RpmCalculator::getRevolutionCounterSinceStart(void) const {
46 742217 return revolutionCounterSinceStart;
47 }
48
49 66866 float RpmCalculator::getCachedRpm() const {
50 66866 return cachedRpmValue;
51 }
52
53 201040 operation_mode_e lookupOperationMode() {
54
2/2
✓ Branch 0 taken 15591 times.
✓ Branch 1 taken 185449 times.
2/2
✓ Decision 'true' taken 15591 times.
✓ Decision 'false' taken 185449 times.
201040 if (engineConfiguration->twoStroke) {
55 15591 return TWO_STROKE;
56 } else {
57
2/2
✓ Branch 0 taken 143782 times.
✓ Branch 1 taken 41667 times.
185449 return engineConfiguration->skippedWheelOnCam ? FOUR_STROKE_CAM_SENSOR : FOUR_STROKE_CRANK_SENSOR;
58 }
59 }
60
61 #if EFI_SHAFT_POSITION_INPUT
62 // see also in TunerStudio project '[doesTriggerImplyOperationMode] tag
63 // this is related to 'knownOperationMode' flag
64 1921200 static bool doesTriggerImplyOperationMode(trigger_type_e type) {
65
2/2
✓ Branch 0 taken 200126 times.
✓ Branch 1 taken 1721074 times.
1921200 switch (type) {
66
1/1
✓ Decision 'true' taken 200126 times.
200126 case trigger_type_e::TT_TOOTHED_WHEEL:
67 case trigger_type_e::TT_HALF_MOON:
68 case trigger_type_e::TT_3_1_CAM: // huh why is this trigger with CAM suffix right in the name on this exception list?!
69 case trigger_type_e::TT_36_2_2_2: // this trigger is special due to rotary application https://github.com/rusefi/rusefi/issues/5566
70 case trigger_type_e::TT_TOOTHED_WHEEL_60_2:
71 case trigger_type_e::TT_TOOTHED_WHEEL_36_1:
72 // These modes could be either cam or crank speed
73
1/1
✓ Decision 'true' taken 200126 times.
200126 return false;
74
1/1
✓ Decision 'true' taken 1721074 times.
1721074 default:
75 1721074 return true;
76 }
77 }
78 #endif // EFI_SHAFT_POSITION_INPUT
79
80 // todo: move to triggerCentral/triggerShape since has nothing to do with rotation state!
81 1921200 operation_mode_e RpmCalculator::getOperationMode() const {
82 #if EFI_SHAFT_POSITION_INPUT
83 // Ignore user-provided setting for well known triggers.
84
2/2
✓ Branch 1 taken 1721074 times.
✓ Branch 2 taken 200126 times.
2/2
✓ Decision 'true' taken 1721074 times.
✓ Decision 'false' taken 200126 times.
1921200 if (doesTriggerImplyOperationMode(engineConfiguration->trigger.type)) {
85 // For example for Miata NA, there is no reason to allow user to set FOUR_STROKE_CRANK_SENSOR
86 1721074 return engine->triggerCentral.triggerShape.getWheelOperationMode();
87 } else
88 #endif // EFI_SHAFT_POSITION_INPUT
89 {
90 // For example 36-1, could be on either cam or crank, so we have to ask the user
91 200126 return lookupOperationMode();
92 }
93 }
94
95
96 #if EFI_SHAFT_POSITION_INPUT
97
98 684 RpmCalculator::RpmCalculator() :
99 684 StoredValueSensor(SensorType::Rpm, 0)
100 {
101 684 assignRpmValue(0);
102 684 }
103
104 /**
105 * @return true if there was a full shaft revolution within the last second
106 */
107 275846 bool RpmCalculator::isRunning() const {
108 275846 return state == RUNNING;
109 }
110
111 /**
112 * @return true if engine is spinning (cranking or running)
113 */
114 2518 bool RpmCalculator::checkIfSpinning(efitick_t nowNt) const {
115
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 2518 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2518 times.
2518 if (getLimpManager()->shutdownController.isEngineStop(nowNt)) {
116 return false;
117 }
118
119 // Anything below 60 rpm is not running
120 2518 bool noRpmEventsForTooLong = lastTdcTimer.getElapsedSeconds(nowNt) > NO_RPM_EVENTS_TIMEOUT_SECS;
121
122 /**
123 * Also check if there were no trigger events
124 */
125 2518 bool noTriggerEventsForTooLong = !engine->triggerCentral.engineMovedRecently(nowNt);
126
127
3/4
✓ Branch 0 taken 2351 times.
✓ Branch 1 taken 167 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2351 times.
2/2
✓ Decision 'true' taken 167 times.
✓ Decision 'false' taken 2351 times.
2518 if (noRpmEventsForTooLong || noTriggerEventsForTooLong) {
128 167 return false;
129 }
130
131 2351 return true;
132 }
133
134 26093 void RpmCalculator::assignRpmValue(float floatRpmValue) {
135 26093 previousRpmValue = cachedRpmValue;
136
137 26093 cachedRpmValue = floatRpmValue;
138
139 26093 setValidValue(floatRpmValue, 0); // 0 for current time since RPM sensor never times out
140
2/2
✓ Branch 0 taken 901 times.
✓ Branch 1 taken 25192 times.
2/2
✓ Decision 'true' taken 901 times.
✓ Decision 'false' taken 25192 times.
26093 if (cachedRpmValue <= 0) {
141 901 oneDegreeUs = NAN;
142 } else {
143 // here it's really important to have more precise float RPM value, see #796
144 25192 oneDegreeUs = getOneDegreeTimeUs(floatRpmValue);
145
2/2
✓ Branch 0 taken 189 times.
✓ Branch 1 taken 25003 times.
2/2
✓ Decision 'true' taken 189 times.
✓ Decision 'false' taken 25003 times.
25192 if (previousRpmValue == 0) {
146 /**
147 * this would make sure that we have good numbers for first cranking revolution
148 * #275 cranking could be improved
149 */
150 189 engine->periodicFastCallback();
151 }
152 }
153 26093 }
154
155 24214 void RpmCalculator::setRpmValue(float value) {
156
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 24212 times.
2/2
✓ Decision 'true' taken 2 times.
✓ Decision 'false' taken 24212 times.
24214 if (value > MAX_ALLOWED_RPM) {
157 2 value = 0;
158 }
159
160 24214 assignRpmValue(value);
161 24214 spinning_state_e oldState = state;
162 // Change state
163
2/2
✓ Branch 0 taken 166 times.
✓ Branch 1 taken 24048 times.
2/2
✓ Decision 'true' taken 166 times.
✓ Decision 'false' taken 24048 times.
24214 if (cachedRpmValue == 0) {
164 166 state = STOPPED;
165
2/2
✓ Branch 0 taken 19373 times.
✓ Branch 1 taken 4675 times.
2/2
✓ Decision 'true' taken 19373 times.
✓ Decision 'false' taken 4675 times.
24048 } else if (cachedRpmValue >= engineConfiguration->cranking.rpm) {
166
2/2
✓ Branch 0 taken 118 times.
✓ Branch 1 taken 19255 times.
2/2
✓ Decision 'true' taken 118 times.
✓ Decision 'false' taken 19255 times.
19373 if (state != RUNNING) {
167 // Store the time the engine started
168 118 engineStartTimer.reset();
169 }
170
171 19373 state = RUNNING;
172
4/4
✓ Branch 0 taken 4640 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 43 times.
✓ Branch 3 taken 4597 times.
2/2
✓ Decision 'true' taken 78 times.
✓ Decision 'false' taken 4597 times.
4675 } else if (state == STOPPED || state == SPINNING_UP) {
173 /**
174 * We are here if RPM is above zero but we have not seen running RPM yet.
175 * This gives us cranking hysteresis - a drop of RPM during running is still running, not cranking.
176 */
177 78 state = CRANKING;
178 }
179 #if EFI_ENGINE_CONTROL
180 // This presumably fixes injection mode change for cranking-to-running transition.
181 // 'isSimultaneous' flag should be updated for events if injection modes differ for cranking and running.
182
3/4
✓ Branch 0 taken 255 times.
✓ Branch 1 taken 23959 times.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
2/2
✓ Decision 'true' taken 255 times.
✓ Decision 'false' taken 23959 times.
24214 if (state != oldState && engineConfiguration->crankingInjectionMode != engineConfiguration->injectionMode) {
183 // Reset the state of all injectors: when we change fueling modes, we could
184 // immediately reschedule an injection that's currently underway. That will cause
185 // the injector's overlappingCounter to get out of sync with reality. As the fix,
186 // every injector's state is forcibly reset just before we could cause that to happen.
187 255 engine->injectionEvents.resetOverlapping();
188
189 // reschedule all injection events now that we've reset them
190 255 engine->injectionEvents.addFuelEvents();
191 }
192 #endif
193 24214 }
194
195 5 spinning_state_e RpmCalculator::getState() const {
196 5 return state;
197 }
198
199 3573 void RpmCalculator::onNewEngineCycle() {
200 3573 revolutionCounterSinceBoot++;
201 3573 revolutionCounterSinceStart++;
202 3573 }
203
204 37055 uint32_t RpmCalculator::getRevolutionCounterM(void) const {
205 37055 return revolutionCounterSinceBoot;
206 }
207
208 void RpmCalculator::onSlowCallback() {
209 // Stop the engine if it's been too long since we got a trigger event
210 if (!engine->triggerCentral.engineMovedRecently(getTimeNowNt())) {
211 setStopSpinning();
212 }
213 }
214
215 130 void RpmCalculator::setStopSpinning() {
216 130 isSpinning = false;
217 130 revolutionCounterSinceStart = 0;
218 130 rpmRate = 0;
219
220
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 127 times.
2/2
✓ Decision 'true' taken 3 times.
✓ Decision 'false' taken 127 times.
130 if (cachedRpmValue != 0) {
221 3 assignRpmValue(0);
222 // needed by 'useNoiselessTriggerDecoder'
223 3 engine->triggerCentral.noiseFilter.resetAccumSignalData();
224 3 efiPrintf("engine stopped");
225 }
226 130 state = STOPPED;
227
228 130 engine->onEngineStopped();
229 130 }
230
231 38630 void RpmCalculator::setSpinningUp(efitick_t nowNt) {
232
2/2
✓ Branch 0 taken 1408 times.
✓ Branch 1 taken 37222 times.
2/2
✓ Decision 'true' taken 1408 times.
✓ Decision 'false' taken 37222 times.
38630 if (!engineConfiguration->isFasterEngineSpinUpEnabled)
233 1408 return;
234 // Only a completely stopped and non-spinning engine can enter the spinning-up state.
235
6/6
✓ Branch 1 taken 2827 times.
✓ Branch 2 taken 34395 times.
✓ Branch 3 taken 110 times.
✓ Branch 4 taken 2717 times.
✓ Branch 5 taken 110 times.
✓ Branch 6 taken 37112 times.
2/2
✓ Decision 'true' taken 110 times.
✓ Decision 'false' taken 37112 times.
37222 if (isStopped() && !isSpinning) {
236 110 state = SPINNING_UP;
237 110 engine->triggerCentral.instantRpm.spinningEventIndex = 0;
238 110 isSpinning = true;
239 }
240 // update variables needed by early instant RPM calc.
241
6/6
✓ Branch 1 taken 4411 times.
✓ Branch 2 taken 32811 times.
✓ Branch 4 taken 3138 times.
✓ Branch 5 taken 1273 times.
✓ Branch 6 taken 3138 times.
✓ Branch 7 taken 34084 times.
2/2
✓ Decision 'true' taken 3138 times.
✓ Decision 'false' taken 34084 times.
37222 if (isSpinningUp() && !engine->triggerCentral.triggerState.getShaftSynchronized()) {
242 3138 engine->triggerCentral.instantRpm.setLastEventTimeForInstantRpm(nowNt);
243 }
244 }
245
246 /**
247 * @brief Shaft position callback used by RPM calculation logic.
248 *
249 * This callback should always be the first of trigger callbacks because other callbacks depend of values
250 * updated here.
251 * This callback is invoked on interrupt thread.
252 */
253 33433 void rpmShaftPositionCallback(trigger_event_e ckpSignalType,
254 uint32_t trgEventIndex, efitick_t nowNt) {
255
256 33433 bool alwaysInstantRpm = engineConfiguration->alwaysInstantRpm;
257
258 33433 RpmCalculator *rpmState = &engine->rpmCalculator;
259
260
2/2
✓ Branch 0 taken 2518 times.
✓ Branch 1 taken 30915 times.
2/2
✓ Decision 'true' taken 2518 times.
✓ Decision 'false' taken 30915 times.
33433 if (trgEventIndex == 0) {
261
2/2
✓ Branch 1 taken 702 times.
✓ Branch 2 taken 1816 times.
2/2
✓ Decision 'true' taken 702 times.
✓ Decision 'false' taken 1816 times.
2518 if (HAVE_CAM_INPUT()) {
262 702 engine->triggerCentral.validateCamVvtCounters();
263 }
264
265
266 2518 bool hadRpmRecently = rpmState->checkIfSpinning(nowNt);
267
268 2518 float periodSeconds = engine->rpmCalculator.lastTdcTimer.getElapsedSecondsAndReset(nowNt);
269
270
2/2
✓ Branch 0 taken 2351 times.
✓ Branch 1 taken 167 times.
2/2
✓ Decision 'true' taken 2351 times.
✓ Decision 'false' taken 167 times.
2518 if (hadRpmRecently) {
271 /**
272 * Four stroke cycle is two crankshaft revolutions
273 *
274 * We always do '* 2' because the event signal is already adjusted to 'per engine cycle'
275 * and each revolution of crankshaft consists of two engine cycles revolutions
276 *
277 */
278
2/2
✓ Branch 0 taken 1418 times.
✓ Branch 1 taken 933 times.
2/2
✓ Decision 'true' taken 1418 times.
✓ Decision 'false' taken 933 times.
2351 if (!alwaysInstantRpm) {
279
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1418 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1418 times.
1418 if (periodSeconds == 0) {
280 rpmState->setRpmValue(0);
281 rpmState->rpmRate = 0;
282 } else {
283 // todo: extract utility method? see duplication with high_pressure_pump.cpp
284 1418 int mult = (int)getEngineCycle(getEngineRotationState()->getOperationMode()) / 360;
285 1418 float rpm = 60 * mult / periodSeconds;
286
287 1418 auto rpmDelta = rpm - rpmState->previousRpmValue;
288 1418 rpmState->rpmRate = rpmDelta / (mult * periodSeconds);
289
290 1418 rpmState->setRpmValue(rpm);
291 }
292 }
293 } else {
294 // we are here only once trigger is synchronized for the first time
295 // while transitioning from 'spinning' to 'running'
296 167 engine->triggerCentral.instantRpm.movePreSynchTimestamps();
297 }
298
299 2518 rpmState->onNewEngineCycle();
300 }
301
302
303 // Always update instant RPM even when not spinning up
304 33433 engine->triggerCentral.instantRpm.updateInstantRpm(
305 engine->triggerCentral.triggerState.currentCycle.current_index,
306
307 33433 engine->triggerCentral.triggerShape, &engine->triggerCentral.triggerFormDetails,
308 trgEventIndex, nowNt);
309
310 33433 float instantRpm = engine->triggerCentral.instantRpm.getInstantRpm();
311
2/2
✓ Branch 0 taken 22749 times.
✓ Branch 1 taken 10684 times.
2/2
✓ Decision 'true' taken 22749 times.
✓ Decision 'false' taken 10684 times.
33433 if (alwaysInstantRpm) {
312 22749 rpmState->setRpmValue(instantRpm);
313
2/2
✓ Branch 1 taken 1192 times.
✓ Branch 2 taken 9492 times.
2/2
✓ Decision 'true' taken 1192 times.
✓ Decision 'false' taken 9492 times.
10684 } else if (rpmState->isSpinningUp()) {
314 1192 rpmState->assignRpmValue(instantRpm);
315 #if 0
316 efiPrintf("** RPM: idx=%d sig=%d iRPM=%d", trgEventIndex, ckpSignalType, instantRpm);
317 #else
318 UNUSED(ckpSignalType);
319 #endif
320 }
321 33433 }
322
323 12 float RpmCalculator::getSecondsSinceEngineStart(efitick_t nowNt) const {
324 12 return engineStartTimer.getElapsedSeconds(nowNt);
325 }
326
327
328 /**
329 * This callback has nothing to do with actual engine control, it just sends a Top Dead Center mark to the rusEfi console
330 * digital sniffer.
331 */
332 1683 static void onTdcCallback() {
333 #if EFI_UNIT_TEST
334
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1683 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1683 times.
1683 if (!engine->needTdcCallback) {
335 return;
336 }
337 #endif /* EFI_UNIT_TEST */
338
339 1683 float rpm = Sensor::getOrZero(SensorType::Rpm);
340 1683 addEngineSnifferTdcEvent(rpm);
341 #if EFI_TOOTH_LOGGER
342 1683 LogTriggerTopDeadCenter(getTimeNowNt());
343 #endif /* EFI_TOOTH_LOGGER */
344 }
345
346 /**
347 * This trigger callback schedules the actual physical TDC callback in relation to trigger synchronization point.
348 */
349 33433 void tdcMarkCallback(
350 uint32_t trgEventIndex, efitick_t nowNt) {
351 33433 bool isTriggerSynchronizationPoint = trgEventIndex == 0;
352
5/6
✓ Branch 0 taken 2518 times.
✓ Branch 1 taken 30915 times.
✓ Branch 3 taken 2518 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2518 times.
✓ Branch 6 taken 30915 times.
2/2
✓ Decision 'true' taken 2518 times.
✓ Decision 'false' taken 30915 times.
33433 if (isTriggerSynchronizationPoint && getTriggerCentral()->isEngineSnifferEnabled) {
353
354 #if EFI_UNIT_TEST
355
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 2458 times.
2/2
✓ Decision 'true' taken 60 times.
✓ Decision 'false' taken 2458 times.
2518 if (!engine->tdcMarkEnabled) {
356 60 return;
357 }
358 #endif // EFI_UNIT_TEST
359
360
361 // two instances of scheduling_s are needed to properly handle event overlap
362 2458 int revIndex2 = getRevolutionCounter() % 2;
363 2458 float rpm = Sensor::getOrZero(SensorType::Rpm);
364 // todo: use tooth event-based scheduling, not just time-based scheduling
365
2/2
✓ Branch 0 taken 2381 times.
✓ Branch 1 taken 77 times.
2/2
✓ Decision 'true' taken 2381 times.
✓ Decision 'false' taken 77 times.
2458 if (rpm != 0) {
366
3/3
✓ Branch 2 taken 2381 times.
✓ Branch 4 taken 17 times.
✓ Branch 5 taken 2364 times.
2381 angle_t tdcPosition = tdcPosition();
367 // we need a positive angle offset here
368
1/1
✓ Branch 1 taken 2381 times.
2381 wrapAngle(tdcPosition, "tdcPosition", ObdCode::CUSTOM_ERR_6553);
369
1/1
✓ Branch 4 taken 2381 times.
2381 scheduleByAngle(&engine->tdcScheduler[revIndex2], nowNt, tdcPosition, action_s::make<onTdcCallback>());
370 }
371 }
372 }
373
374 /**
375 * Schedules a callback 'angle' degree of crankshaft from now.
376 * The callback would be executed once after the duration of time which
377 * it takes the crankshaft to rotate to the specified angle.
378 *
379 * @return tick time of scheduled action
380 */
381 22687 efitick_t scheduleByAngle(scheduling_s *timer, efitick_t nowNt, angle_t angle, action_s const& action) {
382 22687 float delayUs = engine->rpmCalculator.oneDegreeUs * angle;
383
384 22687 efitick_t actionTimeNt = sumTickAndFloat(nowNt, USF2NT(delayUs));
385
386 22687 engine->scheduler.schedule("angle", timer, actionTimeNt, action);
387
388 22687 return actionTimeNt;
389 }
390
391 #else
392 RpmCalculator::RpmCalculator() :
393 StoredValueSensor(SensorType::Rpm, 0)
394 {
395
396 }
397
398 #endif /* EFI_SHAFT_POSITION_INPUT */
399
400