Noiseless trigger decoder (#592)

* fix typo

* better formatting & tiny fix

* add syncRatioAvg

* add useNoiselessTriggerDecoder setting

* resetAccumSignalData

* isUsefulSignal

* Impl. Noise Filtering for Trigger Decoder

* Unit-tests
This commit is contained in:
andreika-git 2018-04-26 09:11:51 +03:00 committed by rusefi
parent e83b70080c
commit 58b7fbb96e
11 changed files with 332 additions and 20 deletions

View File

@ -75,8 +75,6 @@ void addTriggerEventListener(ShaftPositionListener listener, const char *name, E
engine->triggerCentral.addEventListener(listener, name, engine);
}
uint32_t triggerHanlderEntryTime;
#if (EFI_PROD_CODE || EFI_SIMULATOR) || defined(__DOXYGEN__)
int triggerReentraint = 0;
@ -179,10 +177,14 @@ void hwHandleVvtCamSignal(trigger_value_e front) {
}
void hwHandleShaftSignal(trigger_event_e signal) {
if (!isUsefulSignal(signal, engineConfiguration)) {
return;
// for effective noise filtering, we need both signal edges,
// so we pass them to handleShaftSignal() and defer this test
if (!boardConfiguration->useNoiselessTriggerDecoder) {
if (!isUsefulSignal(signal, engineConfiguration)) {
return;
}
}
triggerHanlderEntryTime = GET_TIMESTAMP();
uint32_t triggerHandlerEntryTime = GET_TIMESTAMP();
isInsideTriggerHandler = true;
if (triggerReentraint > maxTriggerReentraint)
maxTriggerReentraint = triggerReentraint;
@ -190,7 +192,7 @@ void hwHandleShaftSignal(trigger_event_e signal) {
efiAssertVoid(getRemainingStack(chThdGetSelfX()) > 128, "lowstck#8");
engine->triggerCentral.handleShaftSignal(signal PASS_ENGINE_PARAMETER_SUFFIX);
triggerReentraint--;
triggerDuration = GET_TIMESTAMP() - triggerHanlderEntryTime;
triggerDuration = GET_TIMESTAMP() - triggerHandlerEntryTime;
isInsideTriggerHandler = false;
if (triggerDuration > triggerMaxDuration)
triggerMaxDuration = triggerDuration;
@ -207,6 +209,7 @@ TriggerCentral::TriggerCentral() {
memset(hwEventCounters, 0, sizeof(hwEventCounters));
clearCallbacks(&triggerListeneres);
triggerState.reset();
resetAccumSignalData();
}
int TriggerCentral::getHwEventCounter(int index) {
@ -218,6 +221,12 @@ void TriggerCentral::resetCounters() {
triggerState.resetRunningCounters();
}
void TriggerCentral::resetAccumSignalData() {
memset(lastSignalTimes, 0xff, sizeof(lastSignalTimes)); // = -1
memset(accumSignalPeriods, 0, sizeof(accumSignalPeriods));
memset(accumSignalPrevPeriods, 0, sizeof(accumSignalPrevPeriods));
}
static char shaft_signal_msg_index[15];
static bool isUpEvent[6] = { false, true, false, true, false, true };
@ -241,6 +250,68 @@ static ALWAYS_INLINE void reportEventToWaveChart(trigger_event_e ckpSignalType,
}
}
/**
* This is used to filter noise spikes (interference) in trigger signal. See
* The basic idea is to use not just edges, but the average amount of time the signal stays in '0' or '1'.
* So we update 'accumulated periods' to track where the signal is.
* And then compare between the current period and previous, with some tolerance (allowing for the wheel speed change).
* @return true if the signal is passed through.
*/
bool TriggerCentral::noiseFilter(efitick_t nowNt, trigger_event_e signal DECLARE_ENGINE_PARAMETER_SUFFIX) {
// todo: find a better place for these defs
static const trigger_event_e opposite[6] = { SHAFT_PRIMARY_RISING, SHAFT_PRIMARY_FALLING, SHAFT_SECONDARY_RISING, SHAFT_SECONDARY_FALLING,
SHAFT_3RD_RISING, SHAFT_3RD_FALLING };
static const trigger_wheel_e triggerIdx[6] = { T_PRIMARY, T_PRIMARY, T_SECONDARY, T_SECONDARY, T_CHANNEL_3, T_CHANNEL_3 };
// we process all trigger channels independently
trigger_wheel_e ti = triggerIdx[signal];
// falling is opposite to rising, and vise versa
trigger_event_e os = opposite[signal];
// todo: currently only primary channel is filtered, because there are some weird trigger types on other channels
if (ti != T_PRIMARY)
return true;
// update period accumulator: for rising signal, we update '0' accumulator, and for falling - '1'
if (lastSignalTimes[signal] != -1)
accumSignalPeriods[signal] += nowNt - lastSignalTimes[signal];
// save current time for this trigger channel
lastSignalTimes[signal] = nowNt;
// now we want to compare current accumulated period to the stored one
efitick_t currentPeriod = accumSignalPeriods[signal];
// the trick is to compare between different
efitick_t allowedPeriod = accumSignalPrevPeriods[os];
// but first check if we're expecting a gap
bool isGapExpected = TRIGGER_SHAPE(isSynchronizationNeeded) && triggerState.shaft_is_synchronized &&
(triggerState.currentCycle.eventCount[ti] + 1) == TRIGGER_SHAPE(expectedEventCount[ti]);
if (isGapExpected) {
// usually we need to extend the period for gaps, based on the trigger info
allowedPeriod *= TRIGGER_SHAPE(syncRatioAvg);
}
// also we need some margin for rapidly changing trigger-wheel speed,
// that's why we expect the period to be no less than 2/3 of the previous period (this is just an empirical 'magic' coef.)
efitick_t minAllowedPeriod = 2 * allowedPeriod / 3;
// but no longer than 5/4 of the previous 'normal' period
efitick_t maxAllowedPeriod = 5 * allowedPeriod / 4;
// above all, check if the signal comes not too early
if (currentPeriod >= minAllowedPeriod) {
// now we store this period as a reference for the next time,
// BUT we store only 'normal' periods, and ignore too long periods (i.e. gaps)
if (!isGapExpected && (maxAllowedPeriod == 0 || currentPeriod <= maxAllowedPeriod)) {
accumSignalPrevPeriods[signal] = currentPeriod;
}
// reset accumulator
accumSignalPeriods[signal] = 0;
return true;
}
// all premature or extra-long events are ignored - treated as interference
return false;
}
void TriggerCentral::handleShaftSignal(trigger_event_e signal DECLARE_ENGINE_PARAMETER_SUFFIX) {
efiAssertVoid(engine!=NULL, "configuration");
@ -252,6 +323,17 @@ void TriggerCentral::handleShaftSignal(trigger_event_e signal DECLARE_ENGINE_PAR
nowNt = getTimeNowNt();
// This code gathers some statistics on signals and compares accumulated periods to filter interference
if (boardConfiguration->useNoiselessTriggerDecoder) {
if (!noiseFilter(nowNt, signal PASS_ENGINE_PARAMETER_SUFFIX)) {
return;
}
// moved here from hwHandleShaftSignal()
if (!isUsefulSignal(signal, engineConfiguration)) {
return;
}
}
engine->onTriggerSignalEvent(nowNt);
#if EFI_HISTOGRAMS && EFI_PROD_CODE
@ -579,6 +661,7 @@ void onConfigurationChangeTriggerCallback(engine_configuration_s *previousConfig
#if EFI_ENGINE_CONTROL || defined(__DOXYGEN__)
engine->triggerCentral.triggerShape.initializeTriggerShape(logger PASS_ENGINE_PARAMETER_SUFFIX);
engine->triggerCentral.resetAccumSignalData();
#endif
}
#if EFI_DEFAILED_LOGGING

View File

@ -32,6 +32,8 @@ public:
void handleShaftSignal(trigger_event_e signal DECLARE_ENGINE_PARAMETER_SUFFIX);
int getHwEventCounter(int index);
void resetCounters();
void resetAccumSignalData();
bool noiseFilter(efitick_t nowNt, trigger_event_e signal DECLARE_ENGINE_PARAMETER_SUFFIX);
TriggerStateWithRunningStatistics triggerState;
efitick_t nowNt;
angle_t vvtPosition;
@ -46,6 +48,11 @@ public:
private:
IntListenerArray<15> triggerListeneres;
int hwEventCounters[HW_EVENT_TYPES];
// Used by 'useNoiselessTriggerDecoder', see handleShaftSignal()
efitick_t lastSignalTimes[HW_EVENT_TYPES];
efitick_t accumSignalPeriods[HW_EVENT_TYPES];
efitick_t accumSignalPrevPeriods[HW_EVENT_TYPES];
};
#endif

View File

@ -209,7 +209,7 @@ void TriggerState::decodeTriggerEvent(trigger_event_e const signal, efitime_t no
if (isLessImportant(type)) {
#if EFI_UNIT_TEST || defined(__DOXYGEN__)
if (printTriggerDebug) {
printf("%s isLessImportant %s now=%d index=%d\r\n",
printf("%s isLessImportant %s now=%lld index=%d\r\n",
getTrigger_type_e(engineConfiguration->trigger.type),
getTrigger_event_e(signal),
nowNt,
@ -307,12 +307,12 @@ void TriggerState::decodeTriggerEvent(trigger_event_e const signal, efitime_t no
*/
#if EFI_UNIT_TEST || defined(__DOXYGEN__)
if (printTriggerDebug) {
printf("sync=%d index=%d size=%d\r\n",
if (printTriggerDebug) {
printf("sync=%d index=%d size=%d\r\n",
shaft_is_synchronized,
currentCycle.current_index,
TRIGGER_SHAPE(size));
}
}
#endif /* EFI_UNIT_TEST */
int endOfCycleIndex = TRIGGER_SHAPE(size) - (CONFIG(useOnlyRisingEdgeForTrigger) ? 2 : 1);
@ -395,14 +395,15 @@ void TriggerState::decodeTriggerEvent(trigger_event_e const signal, efitime_t no
;
if (triggerCycleCallback != NULL) {
triggerCycleCallback(this);
}
startOfCycleNt = nowNt;
resetCurrentCycleState();
incrementTotalEventCounter();
runningRevolutionCounter++;
totalEventCountBase += TRIGGER_SHAPE(size);
if (triggerCycleCallback != NULL) {
triggerCycleCallback(this);
}
startOfCycleNt = nowNt;
resetCurrentCycleState();
incrementTotalEventCounter();
runningRevolutionCounter++;
totalEventCountBase += TRIGGER_SHAPE(size);
#if EFI_UNIT_TEST || defined(__DOXYGEN__)
@ -412,7 +413,7 @@ void TriggerState::decodeTriggerEvent(trigger_event_e const signal, efitime_t no
runningRevolutionCounter);
}
#endif /* EFI_UNIT_TEST */
} else {
} else { /* if (!isSynchronizationPoint) */
nextTriggerEvent()
;
}

View File

@ -463,6 +463,7 @@ void TriggerShape::setTriggerSynchronizationGap2(float syncRatioFrom, float sync
isSynchronizationNeeded = true;
this->syncRatioFrom = syncRatioFrom;
this->syncRatioTo = syncRatioTo;
this->syncRatioAvg = (int)efiRound((syncRatioFrom + syncRatioTo) * 0.5f, 1.0f);
#if EFI_UNIT_TEST || defined(__DOXYGEN__)
if (printTriggerDebug) {
printf("setTriggerSynchronizationGap2 %.2f %.2f\r\n", syncRatioFrom, syncRatioTo);

View File

@ -91,6 +91,10 @@ public:
float syncRatioFrom;
float syncRatioTo;
/**
* used by NoiselessTriggerDecoder (See TriggerCentral::handleShaftSignal())
*/
int syncRatioAvg;
/**
* Usually this is not needed, but some crazy triggers like 36-2-2-2 require two consecutive

View File

@ -551,7 +551,7 @@ bit is_enabled_spi_2
bit isFasterEngineSpinUpEnabled
bit coastingFuelCutEnabled
bit useIacTableForCoasting
bit unused_board_984_23
bit useNoiselessTriggerDecoder
bit unused_board_984_24
bit unused_board_984_25
bit unused_board_984_26

View File

@ -1177,6 +1177,7 @@ cmd_stop_engine = "w\x00\x99\x00\x00"
field = "Trigger error LED", triggerErrorPin
field = "Trigger error LED mode", triggerErrorPinMode
field = "print sync details to console", isPrintTriggerSynchDetails
field = "Enable noise filtering", useNoiselessTriggerDecoder, {trigger_type == 8}
dialog = triggerConfiguration
panel = triggerConfiguration_settings, North
panel = triggerConfiguration_IO, South

View File

@ -8,6 +8,7 @@ TEST_SRC_CPP = test_util.cpp \
test_fasterEngineSpinningUp.cpp \
test_idle_controller.cpp \
test_trigger_decoder.cpp \
test_trigger_noiseless.cpp \
test_fuel_map.cpp \
test_fuelCut.cpp \
engine_test_helper.cpp \

View File

@ -471,6 +471,8 @@ void testTriggerDecoder(void) {
assertEquals(s->wave.switchTimes[2], 0.75);
assertEquals(s->wave.switchTimes[3], 1);
testNoiselessDecoder();
testDodgeNeonDecoder();
testTriggerDecoder2("Dodge Neon 1995", DODGE_NEON_1995, 8, 0.4931, 0.2070);

View File

@ -21,5 +21,6 @@ void testStartupFuelPumping(void);
void test1995FordInline6TriggerDecoder(void);
void testTriggerDecoder2(const char *msg, engine_type_e type, int synchPointIndex, float channel1duty, float channel2duty);
void setupSimpleTestEngineWithMafAndTT_ONE_trigger(EngineTestHelper *eth, injection_mode_e injMode = IM_BATCH);
void testNoiselessDecoder(void);
#endif /* TEST_TRIGGER_DECODER_H_ */

View File

@ -0,0 +1,211 @@
/**
* @file test_trigger_noiseless.cpp
*
* @date Apr 20, 2018
*/
#include "main.h"
#include "test_trigger_decoder.h"
#include "trigger_decoder.h"
#include "engine_math.h"
#include "allsensors.h"
#include "rpm_calculator.h"
#include "event_queue.h"
#include "algo.h"
#include "trigger_central.h"
#include "main_trigger_callback.h"
#include "engine.h"
#include "advance_map.h"
#include "speed_density.h"
#include "fuel_math.h"
#include "spark_logic.h"
#include "trigger_universal.h"
extern int timeNowUs;
extern float unitTestValue;
extern float testMafValue;
extern int unitTestWarningCounter;
extern bool printTriggerDebug;
extern float actualSynchGap;
extern EventQueue schedulingQueue;
static void fireEvent(EngineTestHelper *eth, bool isRise) {
// mostly we fire only rise events (useOnlyRisingEdgeForTrigger=true).
// but for noise filtering, both edges should be processed, so we fire falling events too
if (isRise)
eth->firePrimaryTriggerRise();
else if (eth->engine.engineConfiguration->bc.useNoiselessTriggerDecoder)
eth->firePrimaryTriggerFall();
}
/* ________ __ _ _ __
* __|_|_|__| OR | | | |________
* spikes signal spikes signal
*/
static void noisyPulse(EngineTestHelper *eth, int idx, int durationUs, bool isRise, int noiseIdx, int durationNoiseUs, int offsetNoiseUs, int numSpikes) {
// skip some time at the beginning
timeNowUs += offsetNoiseUs;
durationUs -= offsetNoiseUs;
// add noise spikes
if (idx == noiseIdx) {
// calculate the distance between noise spikes (evenly spaced)
int noiseIntervalUs = (durationUs - durationNoiseUs * numSpikes) / numSpikes;
for (int i = 0; i < numSpikes; i++) {
// start spike
fireEvent(eth, isRise);
timeNowUs += durationNoiseUs;
durationUs -= durationNoiseUs;
// end spike
fireEvent(eth, !isRise);
// add space between spikes
timeNowUs += noiseIntervalUs;
durationUs -= noiseIntervalUs;
}
}
// add the rest of pulse period
timeNowUs += durationUs;
fireEvent(eth, isRise);
}
static void fireNoisyCycle60_2(EngineTestHelper *eth, int numCycles, int durationUs, int noiseIdx, int durationNoiseUs, int offsetNoiseUs, int numSpikes) {
int idx = 0;
for (int cycles = 0; cycles < numCycles; cycles++) {
int count = 60 - 2 - 1;
for (int i = 0; i < count; i++) {
// rising
noisyPulse(eth, idx++, durationUs, true, noiseIdx, durationNoiseUs, offsetNoiseUs, numSpikes);
// falling
noisyPulse(eth, idx++, durationUs, false, noiseIdx, durationNoiseUs, offsetNoiseUs, numSpikes);
}
// skip 2 teeth (durationUs * 4)
// and add 1st rising teeth of the next cycle
noisyPulse(eth, idx++, (durationUs * 4) + durationUs, true, noiseIdx, durationNoiseUs, offsetNoiseUs, numSpikes);
// falling
noisyPulse(eth, idx++, durationUs, false, noiseIdx, durationNoiseUs, offsetNoiseUs, numSpikes);
}
}
static void resetTrigger(EngineTestHelper &eth) {
timeNowUs = 0;
eth.applyTriggerShape();
eth.engine.triggerCentral.resetAccumSignalData();
// reset error cnt
eth.engine.triggerCentral.triggerState.totalTriggerErrorCounter = 0;
}
static void testNoiselessDecoderProcedure(EngineTestHelper &eth, int errorToleranceCnt DECLARE_ENGINE_PARAMETER_SUFFIX) {
printf("*** (bc->useNoiselessTriggerDecoder = %s)\r\n",
boardConfiguration->useNoiselessTriggerDecoder ? "true" : "false");
resetTrigger(eth);
// first, no noise
fireNoisyCycle60_2(&eth, 2, 1000, -1, 0, 0, 0);
// should be no errors anyway
assertEquals(0, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
// check if we're imitating the 60-2 signal correctly
assertEqualsM("index #1", 0, eth.engine.triggerCentral.triggerState.getCurrentIndex());
// check rpm (60secs / (1000us * 60teeth)) = 1000rpm
assertEqualsM("RPM", 1000, eth.engine.rpmCalculator.getRpm(PASS_ENGINE_PARAMETER_SIGNATURE));
// add noise1 - 1 spike in the middle of the 2nd rising pulse
fireNoisyCycle60_2(&eth, 2, 1000, 2, 10, 500, 1);
assertEqualsM("noise#1", errorToleranceCnt, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
resetTrigger(eth);
// add noise2 - 1 spike in the middle of the 2nd falling pulse
fireNoisyCycle60_2(&eth, 2, 1000, 3, 10, 500, 1);
//assertEqualsM("noise#2", errorToleranceCnt, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
resetTrigger(eth);
// add noise3 - in the middle of the sync.gap,
// so that we cannot tell for sure if it's a start of another 'extra' tooth or just a noise inside the gap,
// that's why we used expectedEventCount[] in our filtering algo to make a prediction about gap
fireNoisyCycle60_2(&eth, 2, 1000, 114, 10, 1500, 1);
// so everything runs smoothly!
assertEqualsM("noise#3", errorToleranceCnt, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
resetTrigger(eth);
// add noise4 - too close to the start of the real next signal, so the noise spike is accepted as a signal
// but when the real signal comes shortly afterwards, it we be treated as a noise spike,
fireNoisyCycle60_2(&eth, 2, 1000, 4, 10, 980, 1);
// and we won't get out of sync!
assertEqualsM("noise#4", errorToleranceCnt, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
resetTrigger(eth);
// add noise5 - one very long 333us noise spike
fireNoisyCycle60_2(&eth, 2, 1000, 4, 333, 10, 1);
// still ok
assertEqualsM("noise#5", errorToleranceCnt, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
resetTrigger(eth);
// add noise6 - 10 short spikes across the entire signal pulse
const int failProofNumSpikes = 10;
fireNoisyCycle60_2(&eth, 2, 1000, 4, 5, 10, failProofNumSpikes);
// we barely survived this time
assertEqualsM("noise#6", errorToleranceCnt, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
resetTrigger(eth);
// add noise7 - 34 short spikes across the entire signal pulse
fireNoisyCycle60_2(&eth, 2, 1000, 2, 10, 10, failProofNumSpikes + 1);
// alas, this is a hard case even for noiseless decoder, and it fails...
// but still we're close to 33% signal-noise ratio threshold - not bad!
// so here's an error anyway!
assertEqualsM("noise#7_fail_test", 1, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
}
void testNoiselessDecoder(void) {
printf("*************************************************** testNoiselessDecoder\r\n");
timeNowUs = 0;
schedulingQueue.clear();
EngineTestHelper eth(TEST_ENGINE);
EXPAND_EngineTestHelper
engineConfiguration->ignitionMode = IM_WASTED_SPARK;
engineConfiguration->useOnlyRisingEdgeForTrigger = true;
setupSimpleTestEngineWithMafAndTT_ONE_trigger(&eth);
// we'll test on 60-2 wheel
engineConfiguration->trigger.type = TT_TOOTHED_WHEEL_60_2;
incrementGlobalConfigurationVersion(PASS_ENGINE_PARAMETER_SIGNATURE);
eth.applyTriggerShape();
engine->updateSlowSensors(PASS_ENGINE_PARAMETER_SIGNATURE);
assertEquals(0, engine->triggerCentral.triggerState.totalTriggerErrorCounter);
assertEquals(0, eth.engine.rpmCalculator.getRpm(PASS_ENGINE_PARAMETER_SIGNATURE));
//printTriggerDebug = true;
#if 0
// try normal trigger mode, no noise filtering
boardConfiguration->useNoiselessTriggerDecoder = false;
// for test validation, it should be 1 trigger error
testNoiselessDecoderProcedure(eth, 1 PASS_ENGINE_PARAMETER_SUFFIX);
#endif
// now enable our noise filtering algo
boardConfiguration->useNoiselessTriggerDecoder = true;
// should be 0 errors!
testNoiselessDecoderProcedure(eth, 0 PASS_ENGINE_PARAMETER_SUFFIX);
//printTriggerDebug = false;
}