GCC Code Coverage Report


Directory: ./
File: firmware/controllers/engine_cycle/rpm_calculator.cpp
Date: 2025-10-03 00:57:22
Coverage Exec Excl Total
Lines: 94.0% 142 0 151
Functions: 96.0% 24 0 25
Branches: 90.5% 86 0 95
Decisions: 89.8% 53 - 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 522957 float RpmCalculator::getRpmAcceleration() const {
28 522957 return rpmRate;
29 }
30
31 35967 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 34723 times.
✓ Branch 1 taken 1244 times.
✓ Branch 2 taken 3979 times.
✓ Branch 3 taken 30744 times.
✓ Branch 4 taken 2196 times.
✓ Branch 5 taken 1783 times.
35967 return state == STOPPED || (state == SPINNING_UP && cachedRpmValue == 0);
34 }
35
36 19882 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 17795 times.
✓ Branch 1 taken 2087 times.
✓ Branch 2 taken 1020 times.
✓ Branch 3 taken 16775 times.
✓ Branch 4 taken 987 times.
✓ Branch 5 taken 33 times.
19882 return state == CRANKING || (state == SPINNING_UP && cachedRpmValue > 0);
39 }
40
41 167714 bool RpmCalculator::isSpinningUp() const {
42 167714 return state == SPINNING_UP;
43 }
44
45 525349 uint32_t RpmCalculator::getRevolutionCounterSinceStart(void) const {
46 525349 return revolutionCounterSinceStart;
47 }
48
49 62978 float RpmCalculator::getCachedRpm() const {
50 62978 return cachedRpmValue;
51 }
52
53 102089 operation_mode_e lookupOperationMode() {
54
2/2
✓ Branch 0 taken 287 times.
✓ Branch 1 taken 101802 times.
2/2
✓ Decision 'true' taken 287 times.
✓ Decision 'false' taken 101802 times.
102089 if (engineConfiguration->twoStroke) {
55 287 return TWO_STROKE;
56 } else {
57
2/2
✓ Branch 0 taken 87553 times.
✓ Branch 1 taken 14249 times.
101802 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 1577124 static bool doesTriggerImplyOperationMode(trigger_type_e type) {
65
2/2
✓ Branch 0 taken 101184 times.
✓ Branch 1 taken 1475940 times.
1577124 switch (type) {
66
1/1
✓ Decision 'true' taken 101184 times.
101184 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 101184 times.
101184 return false;
74
1/1
✓ Decision 'true' taken 1475940 times.
1475940 default:
75 1475940 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 1577124 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 1475940 times.
✓ Branch 2 taken 101184 times.
2/2
✓ Decision 'true' taken 1475940 times.
✓ Decision 'false' taken 101184 times.
1577124 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 1475940 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 101184 return lookupOperationMode();
92 }
93 }
94
95
96 #if EFI_SHAFT_POSITION_INPUT
97
98 677 RpmCalculator::RpmCalculator() :
99 677 StoredValueSensor(SensorType::Rpm, 0)
100 {
101 677 assignRpmValue(0);
102 677 }
103
104 /**
105 * @return true if there was a full shaft revolution within the last second
106 */
107 6740 bool RpmCalculator::isRunning() const {
108 6740 return state == RUNNING;
109 }
110
111 /**
112 * @return true if engine is spinning (cranking or running)
113 */
114 2335 bool RpmCalculator::checkIfSpinning(efitick_t nowNt) const {
115
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 2335 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 2335 times.
2335 if (getLimpManager()->shutdownController.isEngineStop(nowNt)) {
116 return false;
117 }
118
119 // Anything below 60 rpm is not running
120 2335 bool noRpmEventsForTooLong = lastTdcTimer.getElapsedSeconds(nowNt) > NO_RPM_EVENTS_TIMEOUT_SECS;
121
122 /**
123 * Also check if there were no trigger events
124 */
125 2335 bool noTriggerEventsForTooLong = !engine->triggerCentral.engineMovedRecently(nowNt);
126
127
3/4
✓ Branch 0 taken 2168 times.
✓ Branch 1 taken 167 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2168 times.
2/2
✓ Decision 'true' taken 167 times.
✓ Decision 'false' taken 2168 times.
2335 if (noRpmEventsForTooLong || noTriggerEventsForTooLong) {
128 167 return false;
129 }
130
131 2168 return true;
132 }
133
134 23817 void RpmCalculator::assignRpmValue(float floatRpmValue) {
135 23817 previousRpmValue = cachedRpmValue;
136
137 23817 cachedRpmValue = floatRpmValue;
138
139 23817 setValidValue(floatRpmValue, 0); // 0 for current time since RPM sensor never times out
140
2/2
✓ Branch 0 taken 885 times.
✓ Branch 1 taken 22932 times.
2/2
✓ Decision 'true' taken 885 times.
✓ Decision 'false' taken 22932 times.
23817 if (cachedRpmValue <= 0) {
141 885 oneDegreeUs = NAN;
142 } else {
143 // here it's really important to have more precise float RPM value, see #796
144 22932 oneDegreeUs = getOneDegreeTimeUs(floatRpmValue);
145
2/2
✓ Branch 0 taken 187 times.
✓ Branch 1 taken 22745 times.
2/2
✓ Decision 'true' taken 187 times.
✓ Decision 'false' taken 22745 times.
22932 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 187 engine->periodicFastCallback();
151 }
152 }
153 23817 }
154
155 21877 void RpmCalculator::setRpmValue(float value) {
156
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 21877 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 21877 times.
21877 if (value > MAX_ALLOWED_RPM) {
157 value = 0;
158 }
159
160 21877 assignRpmValue(value);
161 21877 spinning_state_e oldState = state;
162 // Change state
163
2/2
✓ Branch 0 taken 157 times.
✓ Branch 1 taken 21720 times.
2/2
✓ Decision 'true' taken 157 times.
✓ Decision 'false' taken 21720 times.
21877 if (cachedRpmValue == 0) {
164 157 state = STOPPED;
165
2/2
✓ Branch 0 taken 17035 times.
✓ Branch 1 taken 4685 times.
2/2
✓ Decision 'true' taken 17035 times.
✓ Decision 'false' taken 4685 times.
21720 } else if (cachedRpmValue >= engineConfiguration->cranking.rpm) {
166
2/2
✓ Branch 0 taken 114 times.
✓ Branch 1 taken 16921 times.
2/2
✓ Decision 'true' taken 114 times.
✓ Decision 'false' taken 16921 times.
17035 if (state != RUNNING) {
167 // Store the time the engine started
168 114 engineStartTimer.reset();
169 }
170
171 17035 state = RUNNING;
172
4/4
✓ Branch 0 taken 4650 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 45 times.
✓ Branch 3 taken 4605 times.
2/2
✓ Decision 'true' taken 80 times.
✓ Decision 'false' taken 4605 times.
4685 } 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 80 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 250 times.
✓ Branch 1 taken 21627 times.
✓ Branch 2 taken 250 times.
✗ Branch 3 not taken.
2/2
✓ Decision 'true' taken 250 times.
✓ Decision 'false' taken 21627 times.
21877 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 250 engine->injectionEvents.resetOverlapping();
188
189 // reschedule all injection events now that we've reset them
190 250 engine->injectionEvents.addFuelEvents();
191 }
192 #endif
193 21877 }
194
195 5 spinning_state_e RpmCalculator::getState() const {
196 5 return state;
197 }
198
199 3390 void RpmCalculator::onNewEngineCycle() {
200 3390 revolutionCounterSinceBoot++;
201 3390 revolutionCounterSinceStart++;
202 3390 }
203
204 43324 uint32_t RpmCalculator::getRevolutionCounterM(void) const {
205 43324 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 36279 void RpmCalculator::setSpinningUp(efitick_t nowNt) {
232
2/2
✓ Branch 0 taken 1408 times.
✓ Branch 1 taken 34871 times.
2/2
✓ Decision 'true' taken 1408 times.
✓ Decision 'false' taken 34871 times.
36279 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 2418 times.
✓ Branch 2 taken 32453 times.
✓ Branch 3 taken 110 times.
✓ Branch 4 taken 2308 times.
✓ Branch 5 taken 110 times.
✓ Branch 6 taken 34761 times.
2/2
✓ Decision 'true' taken 110 times.
✓ Decision 'false' taken 34761 times.
34871 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 4079 times.
✓ Branch 2 taken 30792 times.
✓ Branch 4 taken 2737 times.
✓ Branch 5 taken 1342 times.
✓ Branch 6 taken 2737 times.
✓ Branch 7 taken 32134 times.
2/2
✓ Decision 'true' taken 2737 times.
✓ Decision 'false' taken 32134 times.
34871 if (isSpinningUp() && !engine->triggerCentral.triggerState.getShaftSynchronized()) {
242 2737 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 31489 void rpmShaftPositionCallback(trigger_event_e ckpSignalType,
254 uint32_t trgEventIndex, efitick_t nowNt) {
255
256 31489 bool alwaysInstantRpm = engineConfiguration->alwaysInstantRpm;
257
258 31489 RpmCalculator *rpmState = &engine->rpmCalculator;
259
260
2/2
✓ Branch 0 taken 2335 times.
✓ Branch 1 taken 29154 times.
2/2
✓ Decision 'true' taken 2335 times.
✓ Decision 'false' taken 29154 times.
31489 if (trgEventIndex == 0) {
261
2/2
✓ Branch 1 taken 702 times.
✓ Branch 2 taken 1633 times.
2/2
✓ Decision 'true' taken 702 times.
✓ Decision 'false' taken 1633 times.
2335 if (HAVE_CAM_INPUT()) {
262 702 engine->triggerCentral.validateCamVvtCounters();
263 }
264
265
266 2335 bool hadRpmRecently = rpmState->checkIfSpinning(nowNt);
267
268 2335 float periodSeconds = engine->rpmCalculator.lastTdcTimer.getElapsedSecondsAndReset(nowNt);
269
270
2/2
✓ Branch 0 taken 2168 times.
✓ Branch 1 taken 167 times.
2/2
✓ Decision 'true' taken 2168 times.
✓ Decision 'false' taken 167 times.
2335 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 1428 times.
✓ Branch 1 taken 740 times.
2/2
✓ Decision 'true' taken 1428 times.
✓ Decision 'false' taken 740 times.
2168 if (!alwaysInstantRpm) {
279
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1428 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1428 times.
1428 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 1428 int mult = (int)getEngineCycle(getEngineRotationState()->getOperationMode()) / 360;
285 1428 float rpm = 60 * mult / periodSeconds;
286
287 1428 auto rpmDelta = rpm - rpmState->previousRpmValue;
288 1428 rpmState->rpmRate = rpmDelta / (mult * periodSeconds);
289
290 1428 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 2335 rpmState->onNewEngineCycle();
300 }
301
302
303 // Always update instant RPM even when not spinning up
304 31489 engine->triggerCentral.instantRpm.updateInstantRpm(
305 engine->triggerCentral.triggerState.currentCycle.current_index,
306
307 31489 engine->triggerCentral.triggerShape, &engine->triggerCentral.triggerFormDetails,
308 trgEventIndex, nowNt);
309
310 31489 float instantRpm = engine->triggerCentral.instantRpm.getInstantRpm();
311
2/2
✓ Branch 0 taken 20402 times.
✓ Branch 1 taken 11087 times.
2/2
✓ Decision 'true' taken 20402 times.
✓ Decision 'false' taken 11087 times.
31489 if (alwaysInstantRpm) {
312 20402 rpmState->setRpmValue(instantRpm);
313
2/2
✓ Branch 1 taken 1260 times.
✓ Branch 2 taken 9827 times.
2/2
✓ Decision 'true' taken 1260 times.
✓ Decision 'false' taken 9827 times.
11087 } else if (rpmState->isSpinningUp()) {
314 1260 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 31489 }
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 1503 static void onTdcCallback() {
333 #if EFI_UNIT_TEST
334
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1503 times.
1/2
✗ Decision 'true' not taken.
✓ Decision 'false' taken 1503 times.
1503 if (!engine->needTdcCallback) {
335 return;
336 }
337 #endif /* EFI_UNIT_TEST */
338
339 1503 float rpm = Sensor::getOrZero(SensorType::Rpm);
340 1503 addEngineSnifferTdcEvent(rpm);
341 #if EFI_TOOTH_LOGGER
342 1503 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 31489 void tdcMarkCallback(
350 uint32_t trgEventIndex, efitick_t nowNt) {
351 31489 bool isTriggerSynchronizationPoint = trgEventIndex == 0;
352
5/6
✓ Branch 0 taken 2335 times.
✓ Branch 1 taken 29154 times.
✓ Branch 3 taken 2335 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2335 times.
✓ Branch 6 taken 29154 times.
2/2
✓ Decision 'true' taken 2335 times.
✓ Decision 'false' taken 29154 times.
31489 if (isTriggerSynchronizationPoint && getTriggerCentral()->isEngineSnifferEnabled) {
353
354 #if EFI_UNIT_TEST
355
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 2275 times.
2/2
✓ Decision 'true' taken 60 times.
✓ Decision 'false' taken 2275 times.
2335 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 2275 int revIndex2 = getRevolutionCounter() % 2;
363 2275 float rpm = Sensor::getOrZero(SensorType::Rpm);
364 // todo: use tooth event-based scheduling, not just time-based scheduling
365
2/2
✓ Branch 0 taken 2200 times.
✓ Branch 1 taken 75 times.
2/2
✓ Decision 'true' taken 2200 times.
✓ Decision 'false' taken 75 times.
2275 if (rpm != 0) {
366
3/3
✓ Branch 2 taken 2200 times.
✓ Branch 4 taken 17 times.
✓ Branch 5 taken 2183 times.
2200 angle_t tdcPosition = tdcPosition();
367 // we need a positive angle offset here
368
1/1
✓ Branch 1 taken 2200 times.
2200 wrapAngle(tdcPosition, "tdcPosition", ObdCode::CUSTOM_ERR_6553);
369
1/1
✓ Branch 4 taken 2200 times.
2200 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 21406 efitick_t scheduleByAngle(scheduling_s *timer, efitick_t nowNt, angle_t angle, action_s const& action) {
382 21406 float delayUs = engine->rpmCalculator.oneDegreeUs * angle;
383
384 21406 efitick_t actionTimeNt = sumTickAndFloat(nowNt, USF2NT(delayUs));
385
386 21406 engine->scheduler.schedule("angle", timer, actionTimeNt, action);
387
388 21406 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