322 lines
12 KiB
C++
Executable File
322 lines
12 KiB
C++
Executable File
#pragma once
|
|
|
|
#include <AH/Filters/EMA.hpp>
|
|
#include <AH/Filters/Hysteresis.hpp>
|
|
#include <AH/Hardware/ExtendedInputOutput/ExtendedInputOutput.hpp>
|
|
#include <AH/Hardware/Hardware-Types.hpp>
|
|
#include <AH/Math/IncreaseBitDepth.hpp>
|
|
#include <AH/Math/MinMaxFix.hpp>
|
|
#include <AH/STL/type_traits> // std::enable_if, std::is_constructible
|
|
#include <AH/STL/utility> // std::forward
|
|
#include <AH/Settings/SettingsWrapper.hpp>
|
|
|
|
BEGIN_AH_NAMESPACE
|
|
|
|
/**
|
|
* @brief Helper to determine how many of the remaining bits of the filter
|
|
* data types can be used to achieve higher precision.
|
|
*/
|
|
template <uint8_t FilterShiftFactor, class FilterType, class AnalogType>
|
|
struct MaximumFilteredAnalogIncRes {
|
|
constexpr static uint8_t value =
|
|
min(sizeof(FilterType) * CHAR_BIT - ADC_BITS - FilterShiftFactor,
|
|
sizeof(AnalogType) * CHAR_BIT - ADC_BITS);
|
|
};
|
|
|
|
/**
|
|
* @brief FilteredAnalog base class with generic MappingFunction.
|
|
*
|
|
* @see FilteredAnalog
|
|
*/
|
|
template <class MappingFunction, uint8_t Precision = 10,
|
|
uint8_t FilterShiftFactor = ANALOG_FILTER_SHIFT_FACTOR,
|
|
class FilterType = ANALOG_FILTER_TYPE, class AnalogType = analog_t,
|
|
uint8_t IncRes = MaximumFilteredAnalogIncRes<
|
|
FilterShiftFactor, FilterType, AnalogType>::value>
|
|
class GenericFilteredAnalog {
|
|
public:
|
|
/**
|
|
* @brief Construct a new GenericFilteredAnalog object.
|
|
*
|
|
* @param analogPin
|
|
* The analog pin to read from.
|
|
* @param mapFn
|
|
* The mapping function
|
|
* @param initial
|
|
* The initial value of the filter.
|
|
*/
|
|
GenericFilteredAnalog(pin_t analogPin, MappingFunction mapFn,
|
|
AnalogType initial = 0)
|
|
: analogPin(analogPin), mapFn(std::forward<MappingFunction>(mapFn)),
|
|
filter(increaseBitDepth<ADC_BITS + IncRes, Precision, AnalogType,
|
|
AnalogType>(initial)) {}
|
|
|
|
/// @copydoc GenericFilteredAnalog::GenericFilteredAnalog(pin_t,MappingFunction,AnalogType)
|
|
GenericFilteredAnalog(ArduinoPin_t analogPin, MappingFunction mapFn,
|
|
AnalogType initial = 0)
|
|
: GenericFilteredAnalog(pin_t(analogPin),
|
|
std::forward<MappingFunction>(mapFn), initial) {
|
|
}
|
|
|
|
/**
|
|
* @brief Reset the filter to the given value.
|
|
*
|
|
* @param value
|
|
* The value to reset the filter state to.
|
|
*
|
|
* @todo Should the filter be initialized to the first value that is read
|
|
* instead of to zero? This would require adding a `begin` method.
|
|
*/
|
|
void reset(AnalogType value = 0) {
|
|
AnalogType widevalue = increaseBitDepth<ADC_BITS + IncRes, Precision,
|
|
AnalogType, AnalogType>(value);
|
|
filter.reset(widevalue);
|
|
hysteresis.setValue(widevalue);
|
|
}
|
|
|
|
/**
|
|
* @brief Reset the filtered value to the value that's currently being
|
|
* measured at the analog input.
|
|
*
|
|
* This is useful to avoid transient effects upon initialization.
|
|
*/
|
|
void resetToCurrentValue() {
|
|
AnalogType widevalue = getRawValue();
|
|
filter.reset(widevalue);
|
|
hysteresis.setValue(widevalue);
|
|
}
|
|
|
|
/**
|
|
* @brief Specify a mapping function/functor that is applied to the analog
|
|
* value after filtering and before applying hysteresis.
|
|
*
|
|
* @param fn
|
|
* This functor should have a call operator that takes the filtered
|
|
* value (of ADC_BITS + IncRes bits wide) as a parameter,
|
|
* and returns a value of ADC_BITS + IncRes bits wide.
|
|
*
|
|
* @note Applying the mapping function before filtering could result in
|
|
* the noise being amplified to such an extent that filtering it
|
|
* afterwards would be ineffective.
|
|
* Applying it after hysteresis would result in a lower resolution.
|
|
* That's why the mapping function is applied after filtering and
|
|
* before hysteresis.
|
|
*/
|
|
void map(MappingFunction fn) { mapFn = std::forward<MappingFunction>(fn); }
|
|
|
|
/**
|
|
* @brief Get a reference to the mapping function.
|
|
*/
|
|
MappingFunction &getMappingFunction() { return mapFn; }
|
|
/**
|
|
* @brief Get a reference to the mapping function.
|
|
*/
|
|
const MappingFunction &getMappingFunction() const { return mapFn; }
|
|
|
|
/**
|
|
* @brief Read the analog input value, apply the mapping function, and
|
|
* update the average.
|
|
*
|
|
* @retval true
|
|
* The value changed since last time it was updated.
|
|
* @retval false
|
|
* The value is still the same.
|
|
*/
|
|
bool update() {
|
|
AnalogType input = getRawValue(); // read the raw analog input value
|
|
input = filter.filter(input); // apply a low-pass EMA filter
|
|
input = mapFnHelper(input); // apply the mapping function
|
|
return hysteresis.update(input); // apply hysteresis, and return true
|
|
// if the value changed since last time
|
|
}
|
|
|
|
/**
|
|
* @brief Get the filtered value of the analog input (with the mapping
|
|
* function applied).
|
|
*
|
|
* @note This function just returns the value from the last call to
|
|
* @ref update, it doesn't read the analog input again.
|
|
*
|
|
* @return The filtered value of the analog input, as a number
|
|
* of `Precision` bits wide.
|
|
*/
|
|
AnalogType getValue() const { return hysteresis.getValue(); }
|
|
|
|
/**
|
|
* @brief Get the filtered value of the analog input with the mapping
|
|
* function applied as a floating point number from 0.0 to 1.0.
|
|
*
|
|
* @return The filtered value of the analog input, as a number
|
|
* from 0.0 to 1.0.
|
|
*/
|
|
float getFloatValue() const {
|
|
return getValue() * (1.0f / (ldexpf(1.0f, Precision) - 1.0f));
|
|
}
|
|
|
|
/**
|
|
* @brief Read the raw value of the analog input without any filtering or
|
|
* mapping applied, but with its bit depth increased by @c IncRes.
|
|
*/
|
|
AnalogType getRawValue() const {
|
|
AnalogType value = ExtIO::analogRead(analogPin);
|
|
#ifdef ESP8266
|
|
if (value > 1023)
|
|
value = 1023;
|
|
#endif
|
|
return increaseBitDepth<ADC_BITS + IncRes, ADC_BITS, AnalogType>(value);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the maximum value that can be returned from @ref getRawValue.
|
|
*/
|
|
constexpr static AnalogType getMaxRawValue() {
|
|
return (1ul << (ADC_BITS + IncRes)) - 1ul;
|
|
}
|
|
|
|
/**
|
|
* @brief Select the configured ADC resolution. By default, it is set to
|
|
* the maximum resolution supported by the hardware.
|
|
*
|
|
* @see @ref ADC_BITS "ADC_BITS"
|
|
* @see @ref ADCConfig.hpp "ADCConfig.hpp"
|
|
*/
|
|
static void setupADC() {
|
|
#if HAS_ANALOG_READ_RESOLUTION
|
|
analogReadResolution(ADC_BITS);
|
|
#endif
|
|
}
|
|
|
|
private:
|
|
/// Helper function that applies the mapping function if it's enabled.
|
|
/// This function is only enabled if MappingFunction is explicitly
|
|
/// convertible to bool.
|
|
template <typename M = MappingFunction>
|
|
typename std::enable_if<std::is_constructible<bool, M>::value,
|
|
AnalogType>::type
|
|
mapFnHelper(AnalogType input) {
|
|
return bool(mapFn) ? mapFn(input) : input;
|
|
}
|
|
|
|
/// Helper function that applies the mapping function without checking if
|
|
/// it's enabled.
|
|
/// This function is only enabled if MappingFunction is not convertible to
|
|
/// bool.
|
|
template <typename M = MappingFunction>
|
|
typename std::enable_if<!std::is_constructible<bool, M>::value,
|
|
AnalogType>::type
|
|
mapFnHelper(AnalogType input) {
|
|
return mapFn(input);
|
|
}
|
|
|
|
private:
|
|
pin_t analogPin;
|
|
MappingFunction mapFn;
|
|
|
|
using EMA_t = EMA<FilterShiftFactor, AnalogType, FilterType>;
|
|
|
|
static_assert(
|
|
ADC_BITS + IncRes + FilterShiftFactor <= sizeof(FilterType) * CHAR_BIT,
|
|
"Error: FilterType is not wide enough to hold the maximum value");
|
|
static_assert(
|
|
ADC_BITS + IncRes <= sizeof(AnalogType) * CHAR_BIT,
|
|
"Error: AnalogType is not wide enough to hold the maximum value");
|
|
static_assert(
|
|
Precision <= ADC_BITS + IncRes,
|
|
"Error: Precision is larger than the increased ADC precision");
|
|
static_assert(EMA_t::supports_range(AnalogType(0), getMaxRawValue()),
|
|
"Error: EMA filter type doesn't support full ADC range");
|
|
|
|
EMA_t filter;
|
|
Hysteresis<ADC_BITS + IncRes - Precision, AnalogType, AnalogType>
|
|
hysteresis;
|
|
};
|
|
|
|
/**
|
|
* @brief A class that reads and filters an analog input.
|
|
*
|
|
* A map function can be applied to the analog value (e.g. to compensate for
|
|
* logarithmic taper potentiometers or to calibrate the range). The analog input
|
|
* value is filtered using an exponential moving average filter. The default
|
|
* settings for this filter can be changed in Settings.hpp.
|
|
* After filtering, hysteresis is applied to prevent flipping back and forth
|
|
* between two values when the input is not changing.
|
|
*
|
|
* @tparam Precision
|
|
* The number of bits of precision the output should have.
|
|
* @tparam FilterShiftFactor
|
|
* The number of bits used for the EMA filter.
|
|
* The pole location is
|
|
* @f$ 1 - \left(\frac{1}{2}\right)^{\text{FilterShiftFactor}} @f$.
|
|
* A lower shift factor means less filtering (@f$0@f$ is no filtering),
|
|
* and a higher shift factor means more filtering (and more latency).
|
|
* @tparam FilterType
|
|
* The type to use for the intermediate types of the filter.
|
|
* Should be at least
|
|
* @f$ \text{ADC_BITS} + \text{IncRes} +
|
|
* \text{FilterShiftFactor} @f$ bits wide.
|
|
* @tparam AnalogType
|
|
* The type to use for the analog values.
|
|
* Should be at least @f$ \text{ADC_BITS} + \text{IncRes} @f$
|
|
* bits wide.
|
|
* @tparam IncRes
|
|
* The number of bits to increase the resolution of the analog reading
|
|
* by.
|
|
*
|
|
* @ingroup AH_HardwareUtils
|
|
*/
|
|
template <uint8_t Precision = 10,
|
|
uint8_t FilterShiftFactor = ANALOG_FILTER_SHIFT_FACTOR,
|
|
class FilterType = ANALOG_FILTER_TYPE, class AnalogType = analog_t,
|
|
uint8_t IncRes = MaximumFilteredAnalogIncRes<
|
|
FilterShiftFactor, FilterType, AnalogType>::value>
|
|
class FilteredAnalog
|
|
: public GenericFilteredAnalog<AnalogType (*)(AnalogType), Precision,
|
|
FilterShiftFactor, FilterType, AnalogType,
|
|
IncRes> {
|
|
public:
|
|
/**
|
|
* @brief Construct a new FilteredAnalog object.
|
|
*
|
|
* @param analogPin
|
|
* The analog pin to read from.
|
|
* @param initial
|
|
* The initial value of the filter.
|
|
*/
|
|
FilteredAnalog(pin_t analogPin, AnalogType initial = 0)
|
|
: GenericFilteredAnalog<AnalogType (*)(AnalogType), Precision,
|
|
FilterShiftFactor, FilterType, AnalogType,
|
|
IncRes>(analogPin, nullptr, initial) {}
|
|
|
|
/// @copydoc FilteredAnalog(pin_t,AnalogType)
|
|
FilteredAnalog(ArduinoPin_t analogPin, AnalogType initial = 0)
|
|
: FilteredAnalog(pin_t(analogPin), initial) {}
|
|
|
|
/**
|
|
* @brief Construct a new FilteredAnalog object.
|
|
*
|
|
* **This constructor should not be used.**
|
|
* It is just a way to easily create arrays of FilteredAnalog objects, and
|
|
* initializing them later. Trying to update a default-constructed or
|
|
* uninitialized FilteredAnalog object will result in a fatal runtime error.
|
|
*/
|
|
FilteredAnalog() : FilteredAnalog(NO_PIN) {}
|
|
|
|
/// A function pointer to a mapping function to map analog values.
|
|
/// @see map()
|
|
using MappingFunction = AnalogType (*)(AnalogType);
|
|
|
|
/**
|
|
* @brief Invert the analog value. For example, if the precision is 10
|
|
* bits, when the analog input measures 1023, the output will be 0,
|
|
* and when the analog input measures 0, the output will be 1023.
|
|
*
|
|
* @note This overrides the mapping function set by the `map` method.
|
|
*/
|
|
void invert() {
|
|
constexpr AnalogType maxval = FilteredAnalog::getMaxRawValue();
|
|
this->map([](AnalogType val) -> AnalogType { return maxval - val; });
|
|
}
|
|
};
|
|
|
|
END_AH_NAMESPACE
|