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

Generated by: LCOV version 1.15