LCOV - code coverage report
Current view: top level - src/AH/Hardware - FilteredAnalog.hpp (source / functions) Hit Total Coverage
Test: ffed98f648fe78e7aa7bdd228474317d40dadbec Lines: 38 38 100.0 %
Date: 2022-05-28 15:22:59 Functions: 51 51 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : #pragma once
       2             : 
       3             : #include <AH/Settings/Warnings.hpp>
       4             : AH_DIAGNOSTIC_WERROR() // Enable errors on warnings
       5             : 
       6             : #include <AH/Filters/EMA.hpp>
       7             : #include <AH/Filters/Hysteresis.hpp>
       8             : #include <AH/Hardware/ExtendedInputOutput/ExtendedInputOutput.hpp>
       9             : #include <AH/Hardware/Hardware-Types.hpp>
      10             : #include <AH/Math/IncreaseBitDepth.hpp>
      11             : #include <AH/Math/MinMaxFix.hpp>
      12             : #include <AH/STL/type_traits> // std::enable_if, std::is_constructible
      13             : #include <AH/STL/utility> // std::forward
      14             : #include <AH/Settings/SettingsWrapper.hpp>
      15             : 
      16             : BEGIN_AH_NAMESPACE
      17             : 
      18             : /**
      19             :  * @brief   Helper to determine how many of the remaining bits of the filter 
      20             :  *          data types can be used to achieve higher precision.
      21             :  */
      22             : template <uint8_t FilterShiftFactor, class FilterType, class AnalogType>
      23             : struct MaximumFilteredAnalogIncRes {
      24             :     constexpr static uint8_t value =
      25             :         min(sizeof(FilterType) * CHAR_BIT - ADC_BITS - FilterShiftFactor,
      26             :             sizeof(AnalogType) * CHAR_BIT - ADC_BITS);
      27             : };
      28             : 
      29             : /**
      30             :  * @brief   FilteredAnalog base class with generic MappingFunction.
      31             :  * 
      32             :  * @see FilteredAnalog
      33             :  */
      34             : template <class MappingFunction, uint8_t Precision = 10,
      35             :           uint8_t FilterShiftFactor = ANALOG_FILTER_SHIFT_FACTOR,
      36             :           class FilterType = ANALOG_FILTER_TYPE, class AnalogType = analog_t,
      37             :           uint8_t IncRes = MaximumFilteredAnalogIncRes<
      38             :               FilterShiftFactor, FilterType, AnalogType>::value>
      39             : class GenericFilteredAnalog {
      40             :   public:
      41             :     /**
      42             :      * @brief   Construct a new GenericFilteredAnalog object.
      43             :      *
      44             :      * @param   analogPin
      45             :      *          The analog pin to read from.
      46             :      * @param   mapFn
      47             :      *          The mapping function
      48             :      * @param   initial
      49             :      *          The initial value of the filter.
      50             :      */
      51          21 :     GenericFilteredAnalog(pin_t analogPin, MappingFunction mapFn,
      52             :                           AnalogType initial = 0)
      53          21 :         : analogPin(analogPin), mapFn(std::forward<MappingFunction>(mapFn)),
      54             :           filter(increaseBitDepth<ADC_BITS + IncRes, Precision, AnalogType,
      55          24 :                                   AnalogType>(initial)) {}
      56             : 
      57             :     /// @copydoc GenericFilteredAnalog(pin_t,MappingFunction,AnalogType)
      58             :     GenericFilteredAnalog(ArduinoPin_t analogPin, MappingFunction mapFn,
      59             :                           AnalogType initial = 0)
      60             :         : GenericFilteredAnalog(pin_t(analogPin),
      61             :                                 std::forward<MappingFunction>(mapFn), initial) {
      62             :     }
      63             : 
      64             :     /**
      65             :      * @brief   Reset the filter to the given value.
      66             :      * 
      67             :      * @param   value 
      68             :      *          The value to reset the filter state to.
      69             :      * 
      70             :      * @todo    Should the filter be initialized to the first value that is read
      71             :      *          instead of to zero? This would require adding a `begin` method.
      72             :      */
      73           1 :     void reset(AnalogType value = 0) {
      74             :         AnalogType widevalue = increaseBitDepth<ADC_BITS + IncRes, Precision,
      75           1 :                                                 AnalogType, AnalogType>(value);
      76           1 :         filter.reset(widevalue);
      77           1 :         hysteresis.setValue(widevalue);
      78           1 :     }
      79             : 
      80             :     /**
      81             :      * @brief   Reset the filtered value to the value that's currently being
      82             :      *          measured at the analog input.
      83             :      * 
      84             :      * This is useful to avoid transient effects upon initialization.
      85             :      */
      86           7 :     void resetToCurrentValue() {
      87           7 :         AnalogType widevalue = getRawValue();
      88           7 :         filter.reset(widevalue);
      89           7 :         hysteresis.setValue(widevalue);
      90           7 :     }
      91             : 
      92             :     /**
      93             :      * @brief   Specify a mapping function/functor that is applied to the analog
      94             :      *          value after filtering and before applying hysteresis.
      95             :      *
      96             :      * @param   fn
      97             :      *          This functor should have a call operator that takes the filtered
      98             :      *          value (of ADC_BITS + IncRes bits wide) as a parameter, 
      99             :      *          and returns a value of ADC_BITS + IncRes bits wide.
     100             :      * 
     101             :      * @note    Applying the mapping function before filtering could result in
     102             :      *          the noise being amplified to such an extent that filtering it
     103             :      *          afterwards would be ineffective.  
     104             :      *          Applying it after hysteresis would result in a lower resolution.  
     105             :      *          That's why the mapping function is applied after filtering and
     106             :      *          before hysteresis.
     107             :      */
     108           7 :     void map(MappingFunction fn) { mapFn = std::forward<MappingFunction>(fn); }
     109             : 
     110             :     /**
     111             :      * @brief   Get a reference to the mapping function.
     112             :      */
     113             :     MappingFunction &getMappingFunction() { return mapFn; }
     114             :     /**
     115             :      * @brief   Get a reference to the mapping function.
     116             :      */
     117             :     const MappingFunction &getMappingFunction() const { return mapFn; }
     118             : 
     119             :     /**
     120             :      * @brief   Read the analog input value, apply the mapping function, and
     121             :      *          update the average.
     122             :      *
     123             :      * @retval  true
     124             :      *          The value changed since last time it was updated.
     125             :      * @retval  false
     126             :      *          The value is still the same.
     127             :      */
     128          42 :     bool update() {
     129          42 :         AnalogType input = getRawValue(); // read the raw analog input value
     130          42 :         input = filter.filter(input);     // apply a low-pass EMA filter
     131          42 :         input = mapFnHelper(input);       // apply the mapping function
     132          42 :         return hysteresis.update(input);  // apply hysteresis, and return true
     133             :         // if the value changed since last time
     134             :     }
     135             : 
     136             :     /**
     137             :      * @brief   Get the filtered value of the analog input (with the mapping 
     138             :      *          function applied).
     139             :      * 
     140             :      * @note    This function just returns the value from the last call to
     141             :      *          @ref update, it doesn't read the analog input again.
     142             :      *
     143             :      * @return  The filtered value of the analog input, as a number
     144             :      *          of `Precision` bits wide.
     145             :      */
     146          65 :     AnalogType getValue() const { return hysteresis.getValue(); }
     147             : 
     148             :     /**
     149             :      * @brief   Get the filtered value of the analog input with the mapping 
     150             :      *          function applied as a floating point number from 0.0 to 1.0.
     151             :      * 
     152             :      * @return  The filtered value of the analog input, as a number
     153             :      *          from 0.0 to 1.0.
     154             :      */
     155          21 :     float getFloatValue() const {
     156          21 :         return getValue() * (1.0f / (ldexpf(1.0f, Precision) - 1.0f));
     157             :     }
     158             : 
     159             :     /**
     160             :      * @brief   Read the raw value of the analog input without any filtering or
     161             :      *          mapping applied, but with its bit depth increased by @c IncRes.
     162             :      */
     163          49 :     AnalogType getRawValue() const {
     164          49 :         AnalogType value = ExtIO::analogRead(analogPin);
     165             : #ifdef ESP8266
     166             :         if (value > 1023)
     167             :             value = 1023;
     168             : #endif
     169          49 :         return increaseBitDepth<ADC_BITS + IncRes, ADC_BITS, AnalogType>(value);
     170             :     }
     171             : 
     172             :     /**
     173             :      * @brief   Get the maximum value that can be returned from @ref getRawValue.
     174             :      */
     175             :     constexpr static AnalogType getMaxRawValue() {
     176             :         return (1ul << (ADC_BITS + IncRes)) - 1ul;
     177             :     }
     178             : 
     179             :     /**
     180             :      * @brief   Select the configured ADC resolution. By default, it is set to
     181             :      *          the maximum resolution supported by the hardware.
     182             :      * 
     183             :      * @see     @ref ADC_BITS "ADC_BITS"
     184             :      * @see     @ref ADCConfig.hpp "ADCConfig.hpp"
     185             :      */
     186           7 :     static void setupADC() {
     187             : #if HAS_ANALOG_READ_RESOLUTION
     188           7 :         analogReadResolution(ADC_BITS);
     189             : #endif
     190           7 :     }
     191             : 
     192             :   private:
     193             :     /// Helper function that applies the mapping function if it's enabled.
     194             :     /// This function is only enabled if MappingFunction is explicitly
     195             :     /// convertible to bool.
     196             :     template <typename M = MappingFunction>
     197             :     typename std::enable_if<std::is_constructible<bool, M>::value,
     198             :                             AnalogType>::type
     199          41 :     mapFnHelper(AnalogType input) {
     200          41 :         return bool(mapFn) ? mapFn(input) : input;
     201             :     }
     202             : 
     203             :     /// Helper function that applies the mapping function without checking if
     204             :     /// it's enabled.
     205             :     /// This function is only enabled if MappingFunction is not convertible to
     206             :     /// bool.
     207             :     template <typename M = MappingFunction>
     208             :     typename std::enable_if<!std::is_constructible<bool, M>::value,
     209             :                             AnalogType>::type
     210           1 :     mapFnHelper(AnalogType input) {
     211           1 :         return mapFn(input);
     212             :     }
     213             : 
     214             :   private:
     215             :     pin_t analogPin;
     216             :     MappingFunction mapFn;
     217             : 
     218             :     using EMA_t = EMA<FilterShiftFactor, AnalogType, FilterType>;
     219             : 
     220             :     static_assert(
     221             :         ADC_BITS + IncRes + FilterShiftFactor <= sizeof(FilterType) * CHAR_BIT,
     222             :         "Error: FilterType is not wide enough to hold the maximum value");
     223             :     static_assert(
     224             :         ADC_BITS + IncRes <= sizeof(AnalogType) * CHAR_BIT,
     225             :         "Error: AnalogType is not wide enough to hold the maximum value");
     226             :     static_assert(
     227             :         Precision <= ADC_BITS + IncRes,
     228             :         "Error: Precision is larger than the increased ADC precision");
     229             :     static_assert(EMA_t::supports_range(AnalogType(0), getMaxRawValue()),
     230             :                   "Error: EMA filter type doesn't support full ADC range");
     231             : 
     232             :     EMA_t filter;
     233             :     Hysteresis<ADC_BITS + IncRes - Precision, AnalogType, AnalogType>
     234             :         hysteresis;
     235             : };
     236             : 
     237             : /**
     238             :  * @brief   A class that reads and filters an analog input.
     239             :  *
     240             :  * A map function can be applied to the analog value (e.g. to compensate for
     241             :  * logarithmic taper potentiometers or to calibrate the range). The analog input
     242             :  * value is filtered using an exponential moving average filter. The default
     243             :  * settings for this filter can be changed in Settings.hpp.  
     244             :  * After filtering, hysteresis is applied to prevent flipping back and forth 
     245             :  * between two values when the input is not changing.
     246             :  * 
     247             :  * @tparam  Precision
     248             :  *          The number of bits of precision the output should have.
     249             :  * @tparam  FilterShiftFactor
     250             :  *          The number of bits used for the EMA filter.
     251             :  *          The pole location is
     252             :  *          @f$ 1 - \left(\frac{1}{2}\right)^{\text{FilterShiftFactor}} @f$.  
     253             :  *          A lower shift factor means less filtering (@f$0@f$ is no filtering),
     254             :  *          and a higher shift factor means more filtering (and more latency).
     255             :  * @tparam  FilterType
     256             :  *          The type to use for the intermediate types of the filter.  
     257             :  *          Should be at least 
     258             :  *          @f$ \text{ADC_BITS} + \text{IncRes} + 
     259             :  *          \text{FilterShiftFactor} @f$ bits wide.
     260             :  * @tparam  AnalogType
     261             :  *          The type to use for the analog values.  
     262             :  *          Should be at least @f$ \text{ADC_BITS} + \text{IncRes} @f$ 
     263             :  *          bits wide.
     264             :  * @tparam  IncRes
     265             :  *          The number of bits to increase the resolution of the analog reading
     266             :  *          by.
     267             :  * 
     268             :  * @ingroup AH_HardwareUtils
     269             :  */
     270             : template <uint8_t Precision = 10,
     271             :           uint8_t FilterShiftFactor = ANALOG_FILTER_SHIFT_FACTOR,
     272             :           class FilterType = ANALOG_FILTER_TYPE, class AnalogType = analog_t,
     273             :           uint8_t IncRes = MaximumFilteredAnalogIncRes<
     274             :               FilterShiftFactor, FilterType, AnalogType>::value>
     275             : class FilteredAnalog
     276             :     : public GenericFilteredAnalog<AnalogType (*)(AnalogType), Precision,
     277             :                                    FilterShiftFactor, FilterType, AnalogType,
     278             :                                    IncRes> {
     279             :   public:
     280             :     /**
     281             :      * @brief   Construct a new FilteredAnalog object.
     282             :      *
     283             :      * @param   analogPin
     284             :      *          The analog pin to read from.
     285             :      * @param   initial 
     286             :      *          The initial value of the filter.
     287             :      */
     288          17 :     FilteredAnalog(pin_t analogPin, AnalogType initial = 0)
     289             :         : GenericFilteredAnalog<AnalogType (*)(AnalogType), Precision,
     290             :                                 FilterShiftFactor, FilterType, AnalogType,
     291          17 :                                 IncRes>(analogPin, nullptr, initial) {}
     292             : 
     293             :     /// @copydoc FilteredAnalog(pin_t,AnalogType)
     294             :     FilteredAnalog(ArduinoPin_t analogPin, AnalogType initial = 0)
     295             :         : FilteredAnalog(pin_t(analogPin), initial) {}
     296             : 
     297             :     /**
     298             :      * @brief   Construct a new FilteredAnalog object.
     299             :      * 
     300             :      * **This constructor should not be used.**  
     301             :      * It is just a way to easily create arrays of FilteredAnalog objects, and
     302             :      * initializing them later. Trying to update a default-constructed or 
     303             :      * uninitialized FilteredAnalog object will result in a fatal runtime error.
     304             :      */
     305             :     FilteredAnalog() : FilteredAnalog(NO_PIN) {}
     306             : 
     307             :     /// A function pointer to a mapping function to map analog values.
     308             :     /// @see    map()
     309             :     using MappingFunction = AnalogType (*)(AnalogType);
     310             : 
     311             :     /**
     312             :      * @brief   Invert the analog value. For example, if the precision is 10 
     313             :      *          bits, when the analog input measures 1023, the output will be 0,
     314             :      *          and when the analog input measures 0, the output will be 1023.
     315             :      * 
     316             :      * @note    This overrides the mapping function set by the `map` method.
     317             :      */
     318           3 :     void invert() {
     319           3 :         constexpr AnalogType maxval = FilteredAnalog::getMaxRawValue();
     320          16 :         this->map([](AnalogType val) -> AnalogType { return maxval - val; });
     321           3 :     }
     322             : };
     323             : 
     324             : END_AH_NAMESPACE
     325             : 
     326             : AH_DIAGNOSTIC_POP()

Generated by: LCOV version 1.15