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()
|