rusefi/firmware/controllers/sensors/impl/software_knock.cpp

261 lines
7.4 KiB
C++

#include "pch.h"
#if EFI_SOFTWARE_KNOCK
#include "biquad.h"
#include "thread_controller.h"
#include "knock_logic.h"
#include "software_knock.h"
#include "knock_config.h"
#include "ch.hpp"
#ifdef KNOCK_SPECTROGRAM
#include "fft/fft.hpp"
#define COMPRESSED_SPECTRUM_PROTOCOL_SIZE 16 // 16 * 4 = 64 byte for transport to TS
#define START_SPECTRORGAM_FREQUENCY 4000 // magic minimum Hz for draw spectrogram, use near value +next 64 freqs from fft
static size_t spectrogramStartIndex = 0;
static SpectrogramData spectrogramData0;
static SpectrogramData* spectrogramData = &spectrogramData0;
// TODO: use big_buffer
//static volatile bool enableKnockSpectrogram = false;
//static BigBufferHandle buffer;
//static SpectrogramData* spectrogramData = nullptr;
#endif //KNOCK_SPECTROGRAM
static NO_CACHE adcsample_t sampleBuffer[1800];
static int8_t currentCylinderNumber = 0;
static int8_t channelNumber = 0;
static efitick_t lastKnockSampleTime = 0;
static Biquad knockFilter;
static volatile bool knockIsSampling = false;
static volatile bool knockNeedsProcess = false;
static volatile size_t sampleCount = 0;
chibios_rt::BinarySemaphore knockSem(/* taken =*/ true);
void onKnockSamplingComplete() {
knockNeedsProcess = true;
// Notify the processing thread that it's time to process this sample
chSysLockFromISR();
knockSem.signalI();
chSysUnlockFromISR();
}
void onStartKnockSampling(uint8_t cylinderNumber, float samplingSeconds, uint8_t channelIdx) {
if (!engineConfiguration->enableSoftwareKnock) {
return;
}
// Cancel if ADC isn't ready
if (!((KNOCK_ADC.state == ADC_READY) ||
(KNOCK_ADC.state == ADC_COMPLETE) ||
(KNOCK_ADC.state == ADC_ERROR))) {
return;
}
// If there's pending processing, skip this event
if (knockNeedsProcess) {
return;
}
// Convert sampling time to number of samples
constexpr int sampleRate = KNOCK_SAMPLE_RATE;
sampleCount = 0xFFFFFFFE & static_cast<size_t>(clampF(100, samplingSeconds * sampleRate, efi::size(sampleBuffer)));
// Select the appropriate conversion group - it will differ depending on which sensor this cylinder should listen on
auto conversionGroup = getKnockConversionGroup(channelIdx);
//current chanel number for spectrum TS plugin
channelNumber = channelIdx;
// Stash the current cylinder's number so we can store the result appropriately
currentCylinderNumber = cylinderNumber;
adcStartConversionI(&KNOCK_ADC, conversionGroup, sampleBuffer, sampleCount);
lastKnockSampleTime = getTimeNowNt();
}
class KnockThread : public ThreadController<UTILITY_THREAD_STACK_SIZE> {
public:
KnockThread() : ThreadController("knock", PRIO_KNOCK_PROCESS) {}
void ThreadTask() override;
};
static KnockThread kt;
void initSoftwareKnock() {
if (engineConfiguration->enableSoftwareKnock) {
float frequencyHz = 1000 * bore2frequency(engineConfiguration->cylinderBore);
frequencyHz = engineConfiguration->knockDetectionUseDoubleFrequency ? 2 * frequencyHz : frequencyHz;
if(engineConfiguration->knockFrequency > 0.01)
{
frequencyHz = engineConfiguration->knockFrequency;
}
knockFilter.configureBandpass(KNOCK_SAMPLE_RATE, frequencyHz, 3);
#ifdef KNOCK_SPECTROGRAM
if(engineConfiguration->enableKnockSpectrogram)
{
// TODO: use big buffer
//buffer = getBigBuffer(BigBufferUser::KnockSpectrogram);
// if (!buffer) {
// engineConfiguration->enableKnockSpectrogram = false;
// return;
// }
//spectrogramData = buffer.get<SpectrogramData>();
fft::blackmanharris(spectrogramData->window, FFT_SIZE, true);
int freqStartConst = START_SPECTRORGAM_FREQUENCY;
int minFreqDiff = freqStartConst;
int freqStart = 0;
float freqStep = 0;
for (size_t i = 0; i < FFT_SIZE/2; i++)
{
float freq = float(i * KNOCK_SAMPLE_RATE) / FFT_SIZE;
int min = abs(freq - freqStartConst);
// next after freq start index
if(i == spectrogramStartIndex + 1) {
freqStep = abs(freq - freqStart);
}
if(min < minFreqDiff) {
minFreqDiff = min;
spectrogramStartIndex = i;
freqStart = freq;
}
}
engine->module<KnockController>()->m_knockFrequencyStart = (uint16_t)freqStart;
engine->module<KnockController>()->m_knockFrequencyStep = freqStep;
}
#endif
// fun fact: we do not offer any ADC channel flexibility like we have for many other kinds of inputs
efiSetPadMode("knock ch1", KNOCK_PIN_CH1, PAL_MODE_INPUT_ANALOG);
#if KNOCK_HAS_CH2
efiSetPadMode("knock ch2", KNOCK_PIN_CH2, PAL_MODE_INPUT_ANALOG);
#endif
kt.start();
}
}
#ifdef KNOCK_SPECTROGRAM
static uint8_t toDb(const float& voltage) {
float db = 200 * log10(voltage*voltage) + 40; // best scaling for view
db = clampF(0, db, 255);
return uint8_t(db);
}
#endif
static void processLastKnockEvent() {
if (!knockNeedsProcess) {
return;
}
float sumSq = 0;
// todo: reduce magic constants. engineConfiguration->adcVcc?
constexpr float ratio = 3.3f / 4095.0f;
size_t localCount = sampleCount;
// Prepare the steady state at vcc/2 so that there isn't a step
// when samples begin
// todo: reduce magic constants. engineConfiguration->adcVcc?
knockFilter.cookSteadyState(3.3f / 2);
// Compute the sum of squares
for (size_t i = 0; i < localCount; i++) {
float volts = ratio * sampleBuffer[i];
float filtered = knockFilter.filter(volts);
if (i == localCount - 1 && engineConfiguration->debugMode == DBG_KNOCK) {
engine->outputChannels.debugFloatField1 = volts;
engine->outputChannels.debugFloatField2 = filtered;
}
sumSq += filtered * filtered;
}
// take a local copy
auto lastKnockTime = lastKnockSampleTime;
// We're done with inspecting the buffer, another sample can be taken
knockNeedsProcess = false;
#ifdef KNOCK_SPECTROGRAM
if (engineConfiguration->enableKnockSpectrogram) {
ScopePerf perf(PE::KnockAnalyzer);
if(engineConfiguration->enableKnockSpectrogramFilter) {
fft::fft_adc_sample_filtered(knockFilter, spectrogramData->window, ratio, engineConfiguration->knockSpectrumSensitivity, sampleBuffer, spectrogramData->fftBuffer, FFT_SIZE);
}
else {
fft::fft_adc_sample(spectrogramData->window, ratio, engineConfiguration->knockSpectrumSensitivity, sampleBuffer, spectrogramData->fftBuffer, FFT_SIZE);
}
auto* spectrum = &engine->module<KnockController>()->m_knockSpectrum[0];
for(uint8_t i = 0; i < COMPRESSED_SPECTRUM_PROTOCOL_SIZE; ++i) {
uint8_t startIndex = spectrogramStartIndex + (i * 4);
uint8_t a = toDb(fft::amplitude(spectrogramData->fftBuffer[startIndex]));
uint8_t b = toDb(fft::amplitude(spectrogramData->fftBuffer[startIndex + 1]));
uint8_t c = toDb(fft::amplitude(spectrogramData->fftBuffer[startIndex + 2]));
uint8_t d = toDb(fft::amplitude(spectrogramData->fftBuffer[startIndex + 3]));
uint32_t compressed = uint32_t(a << 24 | b << 16 | c << 8 | d);
{
chibios_rt::CriticalSectionLocker csl;
spectrum[i] = compressed;
}
}
uint16_t compressedChannelCyl = uint16_t(channelNumber << 8 | currentCylinderNumber);
{
chibios_rt::CriticalSectionLocker csl;
engine->module<KnockController>()->m_knockSpectrumChannelCyl = compressedChannelCyl;
}
}
#endif
// mean of squares (not yet root)
float meanSquares = sumSq / localCount;
// RMS
float db = 10 * log10(meanSquares);
// clamp to reasonable range
db = clampF(-100, db, 100);
engine->module<KnockController>()->onKnockSenseCompleted(currentCylinderNumber, db, lastKnockTime);
}
void KnockThread::ThreadTask() {
while (1) {
knockSem.wait();
ScopePerf perf(PE::SoftwareKnockProcess);
processLastKnockEvent();
}
}
#endif // EFI_SOFTWARE_KNOCK