rusefi/firmware/controllers/engine_cycle/rpm_calculator.cpp

415 lines
13 KiB
C++

/**
* @file rpm_calculator.cpp
* @brief RPM calculator
*
* Here we listen to position sensor events in order to figure our if engine is currently running or not.
* Actual getRpm() is calculated once per crankshaft revolution, based on the amount of time passed
* since the start of previous shaft revolution.
*
* We also have 'instant RPM' logic separate from this 'cycle RPM' logic. Open question is why do we not use
* instant RPM instead of cycle RPM more often.
*
* @date Jan 1, 2013
* @author Andrey Belomutskiy, (c) 2012-2020
*/
#include "pch.h"
#include "trigger_central.h"
#if EFI_SENSOR_CHART
#include "sensor_chart.h"
#endif // EFI_SENSOR_CHART
#include "engine_sniffer.h"
// See RpmCalculator::checkIfSpinning()
#ifndef NO_RPM_EVENTS_TIMEOUT_SECS
#define NO_RPM_EVENTS_TIMEOUT_SECS 2
#endif /* NO_RPM_EVENTS_TIMEOUT_SECS */
float RpmCalculator::getRpmAcceleration() const {
return rpmRate;
}
bool RpmCalculator::isStopped() const {
// Spinning-up with zero RPM means that the engine is not ready yet, and is treated as 'stopped'.
return state == STOPPED || (state == SPINNING_UP && cachedRpmValue == 0);
}
bool RpmCalculator::isCranking() const {
// Spinning-up with non-zero RPM is suitable for all engine math, as good as cranking
return state == CRANKING || (state == SPINNING_UP && cachedRpmValue > 0);
}
bool RpmCalculator::isSpinningUp() const {
return state == SPINNING_UP;
}
uint32_t RpmCalculator::getRevolutionCounterSinceStart(void) const {
return revolutionCounterSinceStart;
}
/**
* @return -1 in case of isNoisySignal(), current RPM otherwise
* See NOISY_RPM
*/
float RpmCalculator::getCachedRpm() const {
return cachedRpmValue;
}
operation_mode_e lookupOperationMode() {
if (engineConfiguration->twoStroke) {
return TWO_STROKE;
} else {
return engineConfiguration->skippedWheelOnCam ? FOUR_STROKE_CAM_SENSOR : FOUR_STROKE_CRANK_SENSOR;
}
}
#if EFI_SHAFT_POSITION_INPUT
// see also in TunerStudio project '[doesTriggerImplyOperationMode] tag
// this is related to 'knownOperationMode' flag
static bool doesTriggerImplyOperationMode(trigger_type_e type) {
switch (type) {
case trigger_type_e::TT_TOOTHED_WHEEL:
case trigger_type_e::TT_HALF_MOON:
case trigger_type_e::TT_3_1_CAM: // huh why is this trigger with CAM suffix right in the name on this exception list?!
case trigger_type_e::TT_36_2_2_2: // this trigger is special due to rotary application https://github.com/rusefi/rusefi/issues/5566
case trigger_type_e::TT_TOOTHED_WHEEL_60_2:
case trigger_type_e::TT_TOOTHED_WHEEL_36_1:
// These modes could be either cam or crank speed
return false;
default:
return true;
}
}
#endif // EFI_SHAFT_POSITION_INPUT
// todo: move to triggerCentral/triggerShape since has nothing to do with rotation state!
operation_mode_e RpmCalculator::getOperationMode() const {
#if EFI_SHAFT_POSITION_INPUT
// Ignore user-provided setting for well known triggers.
if (doesTriggerImplyOperationMode(engineConfiguration->trigger.type)) {
// For example for Miata NA, there is no reason to allow user to set FOUR_STROKE_CRANK_SENSOR
return engine->triggerCentral.triggerShape.getWheelOperationMode();
} else
#endif // EFI_SHAFT_POSITION_INPUT
{
// For example 36-1, could be on either cam or crank, so we have to ask the user
return lookupOperationMode();
}
}
#if EFI_SHAFT_POSITION_INPUT
RpmCalculator::RpmCalculator() :
StoredValueSensor(SensorType::Rpm, 0)
{
assignRpmValue(0);
}
/**
* @return true if there was a full shaft revolution within the last second
*/
bool RpmCalculator::isRunning() const {
return state == RUNNING;
}
/**
* @return true if engine is spinning (cranking or running)
*/
bool RpmCalculator::checkIfSpinning(efitick_t nowNt) const {
if (getLimpManager()->shutdownController.isEngineStop(nowNt)) {
return false;
}
// Anything below 60 rpm is not running
bool noRpmEventsForTooLong = lastTdcTimer.getElapsedSeconds(nowNt) > NO_RPM_EVENTS_TIMEOUT_SECS;
/**
* Also check if there were no trigger events
*/
bool noTriggerEventsForTooLong = !engine->triggerCentral.engineMovedRecently(nowNt);
if (noRpmEventsForTooLong || noTriggerEventsForTooLong) {
return false;
}
return true;
}
void RpmCalculator::assignRpmValue(float floatRpmValue) {
previousRpmValue = cachedRpmValue;
cachedRpmValue = floatRpmValue;
setValidValue(floatRpmValue, 0); // 0 for current time since RPM sensor never times out
if (cachedRpmValue <= 0) {
oneDegreeUs = NAN;
} else {
// here it's really important to have more precise float RPM value, see #796
oneDegreeUs = getOneDegreeTimeUs(floatRpmValue);
if (previousRpmValue == 0) {
/**
* this would make sure that we have good numbers for first cranking revolution
* #275 cranking could be improved
*/
engine->periodicFastCallback();
}
}
}
void RpmCalculator::setRpmValue(float value) {
assignRpmValue(value);
spinning_state_e oldState = state;
// Change state
if (cachedRpmValue == 0) {
state = STOPPED;
} else if (cachedRpmValue >= engineConfiguration->cranking.rpm) {
if (state != RUNNING) {
// Store the time the engine started
engineStartTimer.reset();
}
state = RUNNING;
} else if (state == STOPPED || state == SPINNING_UP) {
/**
* We are here if RPM is above zero but we have not seen running RPM yet.
* This gives us cranking hysteresis - a drop of RPM during running is still running, not cranking.
*/
state = CRANKING;
}
#if EFI_ENGINE_CONTROL
// This presumably fixes injection mode change for cranking-to-running transition.
// 'isSimultaneous' flag should be updated for events if injection modes differ for cranking and running.
if (state != oldState && engineConfiguration->crankingInjectionMode != engineConfiguration->injectionMode) {
// Reset the state of all injectors: when we change fueling modes, we could
// immediately reschedule an injection that's currently underway. That will cause
// the injector's overlappingCounter to get out of sync with reality. As the fix,
// every injector's state is forcibly reset just before we could cause that to happen.
engine->injectionEvents.resetOverlapping();
// reschedule all injection events now that we've reset them
engine->injectionEvents.addFuelEvents();
}
#endif
}
spinning_state_e RpmCalculator::getState() const {
return state;
}
void RpmCalculator::onNewEngineCycle() {
revolutionCounterSinceBoot++;
revolutionCounterSinceStart++;
}
uint32_t RpmCalculator::getRevolutionCounterM(void) const {
return revolutionCounterSinceBoot;
}
void RpmCalculator::onSlowCallback() {
// Stop the engine if it's been too long since we got a trigger event
if (!engine->triggerCentral.engineMovedRecently(getTimeNowNt())) {
setStopSpinning();
}
}
void RpmCalculator::setStopped() {
revolutionCounterSinceStart = 0;
rpmRate = 0;
if (cachedRpmValue != 0) {
assignRpmValue(0);
// needed by 'useNoiselessTriggerDecoder'
engine->triggerCentral.noiseFilter.resetAccumSignalData();
efiPrintf("engine stopped");
}
state = STOPPED;
}
void RpmCalculator::setStopSpinning() {
isSpinning = false;
setStopped();
}
void RpmCalculator::setSpinningUp(efitick_t nowNt) {
if (!engineConfiguration->isFasterEngineSpinUpEnabled)
return;
// Only a completely stopped and non-spinning engine can enter the spinning-up state.
if (isStopped() && !isSpinning) {
state = SPINNING_UP;
engine->triggerCentral.instantRpm.spinningEventIndex = 0;
isSpinning = true;
}
// update variables needed by early instant RPM calc.
if (isSpinningUp() && !engine->triggerCentral.triggerState.getShaftSynchronized()) {
engine->triggerCentral.instantRpm.setLastEventTimeForInstantRpm(nowNt);
}
}
/**
* @brief Shaft position callback used by RPM calculation logic.
*
* This callback should always be the first of trigger callbacks because other callbacks depend of values
* updated here.
* This callback is invoked on interrupt thread.
*/
void rpmShaftPositionCallback(trigger_event_e ckpSignalType,
uint32_t trgEventIndex, efitick_t nowNt) {
bool alwaysInstantRpm = engineConfiguration->alwaysInstantRpm;
RpmCalculator *rpmState = &engine->rpmCalculator;
if (trgEventIndex == 0) {
if (HAVE_CAM_INPUT()) {
engine->triggerCentral.validateCamVvtCounters();
}
bool hadRpmRecently = rpmState->checkIfSpinning(nowNt);
float periodSeconds = engine->rpmCalculator.lastTdcTimer.getElapsedSecondsAndReset(nowNt);
if (hadRpmRecently) {
/**
* Four stroke cycle is two crankshaft revolutions
*
* We always do '* 2' because the event signal is already adjusted to 'per engine cycle'
* and each revolution of crankshaft consists of two engine cycles revolutions
*
*/
if (!alwaysInstantRpm) {
if (periodSeconds == 0) {
rpmState->setRpmValue(NOISY_RPM);
rpmState->rpmRate = 0;
} else {
// todo: extract utility method? see duplication with high_pressure_pump.cpp
int mult = (int)getEngineCycle(getEngineRotationState()->getOperationMode()) / 360;
float rpm = 60 * mult / periodSeconds;
auto rpmDelta = rpm - rpmState->previousRpmValue;
rpmState->rpmRate = rpmDelta / (mult * periodSeconds);
rpmState->setRpmValue(rpm > UNREALISTIC_RPM ? NOISY_RPM : rpm);
}
}
} else {
// we are here only once trigger is synchronized for the first time
// while transitioning from 'spinning' to 'running'
engine->triggerCentral.instantRpm.movePreSynchTimestamps();
}
rpmState->onNewEngineCycle();
}
#if EFI_SENSOR_CHART
// this 'index==0' case is here so that it happens after cycle callback so
// it goes into sniffer report into the first position
if (getEngineState()->sensorChartMode == SC_TRIGGER) {
angle_t crankAngle = engine->triggerCentral.getCurrentEnginePhase(nowNt).value_or(0);
int signal = 1000 * ckpSignalType + trgEventIndex;
scAddData(crankAngle, signal);
}
#endif /* EFI_SENSOR_CHART */
// Always update instant RPM even when not spinning up
engine->triggerCentral.instantRpm.updateInstantRpm(
engine->triggerCentral.triggerState.currentCycle.current_index,
engine->triggerCentral.triggerShape, &engine->triggerCentral.triggerFormDetails,
trgEventIndex, nowNt);
float instantRpm = engine->triggerCentral.instantRpm.getInstantRpm();
if (alwaysInstantRpm) {
rpmState->setRpmValue(instantRpm);
} else if (rpmState->isSpinningUp()) {
rpmState->assignRpmValue(instantRpm);
#if 0
efiPrintf("** RPM: idx=%d sig=%d iRPM=%d", trgEventIndex, ckpSignalType, instantRpm);
#endif
}
}
float RpmCalculator::getSecondsSinceEngineStart(efitick_t nowNt) const {
return engineStartTimer.getElapsedSeconds(nowNt);
}
/**
* This callback has nothing to do with actual engine control, it just sends a Top Dead Center mark to the rusEfi console
* digital sniffer.
*/
static void onTdcCallback(void *) {
#if EFI_UNIT_TEST
if (!engine->needTdcCallback) {
return;
}
#endif /* EFI_UNIT_TEST */
int rpm = Sensor::getOrZero(SensorType::Rpm);
addEngineSnifferTdcEvent(rpm);
#if EFI_TOOTH_LOGGER
LogTriggerTopDeadCenter(getTimeNowNt());
#endif /* EFI_TOOTH_LOGGER */
}
/**
* This trigger callback schedules the actual physical TDC callback in relation to trigger synchronization point.
*/
void tdcMarkCallback(
uint32_t trgEventIndex, efitick_t edgeTimestamp) {
bool isTriggerSynchronizationPoint = trgEventIndex == 0;
if (isTriggerSynchronizationPoint && getTriggerCentral()->isEngineSnifferEnabled) {
#if EFI_UNIT_TEST
if (!engine->tdcMarkEnabled) {
return;
}
#endif // EFI_UNIT_TEST
// two instances of scheduling_s are needed to properly handle event overlap
int revIndex2 = getRevolutionCounter() % 2;
int rpm = Sensor::getOrZero(SensorType::Rpm);
// todo: use tooth event-based scheduling, not just time-based scheduling
if (isValidRpm(rpm)) {
angle_t tdcPosition = tdcPosition();
// we need a positive angle offset here
wrapAngle(tdcPosition, "tdcPosition", ObdCode::CUSTOM_ERR_6553);
scheduleByAngle(&engine->tdcScheduler[revIndex2], edgeTimestamp, tdcPosition, onTdcCallback);
}
}
}
/**
* Schedules a callback 'angle' degree of crankshaft from now.
* The callback would be executed once after the duration of time which
* it takes the crankshaft to rotate to the specified angle.
*/
efitick_t scheduleByAngle(scheduling_s *timer, efitick_t edgeTimestamp, angle_t angle,
action_s action) {
float delayUs = engine->rpmCalculator.oneDegreeUs * angle;
// 'delayNt' is below 10 seconds here so we use 32 bit type for performance reasons
int32_t delayNt = USF2NT(delayUs);
efitick_t delayedTime = edgeTimestamp + delayNt;
engine->executor.scheduleByTimestampNt("angle", timer, delayedTime, action);
return delayedTime;
}
#else
RpmCalculator::RpmCalculator() :
StoredValueSensor(SensorType::Rpm, 0)
{
}
#endif /* EFI_SHAFT_POSITION_INPUT */