Skip to main content
Became Hot Network Question
edited tags
Link
Source Link

Drift correction for sensor readings using a high-pass filter

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;
}