An embedded project my team is working on is having issues with sensor drift over time. To solve this, I thought it might work to use a high-pass filter, since the portion of the signal that we care about is generally at a significantly higher frequency than the drift. I'd like feedback on if this is a good general approach and if this implementation in particular follows good standards for performance, usability, and maintainability.
Side note: for logging, this project uses ArduinoLog.
drift_correct.hpp:
#include <cstdint>
/**
* @class DriftCorrector
* @brief Object offering drift-correction capabilities for real-valued signals
* @details Uses a high-pass filter to correct for unintended long-term drift in a signal,
* assuming that changes below a minimum frequency are to be considered drift
*/
class DriftCorrector
{
private:
float m_lastSampleUncorrected;
float m_lastSampleCorrected;
float m_timeSinceGoodSample; // to accurately keep track of time deltas even when samples
// need to be thrown out
float m_RC; // time constant (tau)
public:
/**
* @param cornerFrequency the maximum frequency (in Hz) of changes that should be
* considered drift
*/
DriftCorrector(float cornerFrequency = 0.1f) noexcept;
/**
* @brief Sends a value through for correction.
*
* @param value the uncorrected value, which will also be stored in the history
* @param timeDelta the time (in seconds) since the last value was stored
* @returns the value corrected for any drift that has been detected thus far.
*/
[[nodiscard]] float next(float value, float timeDelta) noexcept;
};
drift_correct.cpp:
#include "drift_correct.hpp"
#include <ArduinoLog.h>
#include <cmath>
#include <limits>
DriftCorrector::DriftCorrector(float cornerFrequency = 0.1f)
{
if (std::isnan(cornerFrequency) || std::isinf(cornerFrequency) || cornerFrequency <= 0.0f)
{
Log.errorln(
"invalid corner frequency given to drift corrector, substituting with 0.1 Hz");
cornerFrequency = 0.1f;
}
m_lastSampleCorrected = std::numeric_limits<float>::signaling_NaN();
m_lastSampleUncorrected = std::numeric_limits<float>::signaling_NaN();
m_timeSinceGoodSample = 0.0f;
m_RC = 1.0f / (2.0f * PI * cornerFrequency);
}
float DriftCorrector::next(float sample, float timeDelta)
{
// Algorithm from https://en.wikipedia.org/wiki/High-pass_filter (2023-02-28)
if (std::isnan(sample) || std::isinf(sample))
{
Log.warningln("invalid sample passed into drift correction, ignoring");
if (!std::isnan(timeDelta) && !std::isinf(timeDelta) && timeDelta > 0.0f)
{
m_timeSinceGoodSample += timeDelta;
}
return sample;
}
else if (std::isnan(timeDelta) || std::isinf(timeDelta) || timeDelta <= 0.0f)
{
Log.warningln("invalid time delta passed into drift correction, ignoring");
return sample;
}
timeDelta += m_timeSinceGoodSample;
m_timeSinceGoodSample = 0.0f;
if (std::isnan(m_lastSampleCorrected) || std::isnan(m_lastSampleUncorrected))
{
// First sample taken in, just pass it straight through
m_lastSampleCorrected = sample;
m_lastSampleUncorrected = sample;
return sample;
}
float alpha = m_RC / (m_RC + timeDelta);
float corrected = alpha * (m_lastSampleCorrected + sample - m_lastSampleUncorrected);
m_lastSampleCorrected = corrected;
m_lastSampleUncorrected = sample;
return corrected;
}
example usage:
DriftCorrector c(0.3f); // corner frequency of 0.3 Hz
for (;;)
{
std::cout << c.next(sampler.getValue(), timer.sinceLastTick()) << std::endl;
}