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

Generated by: LCOV version 1.14-6-g40580cd