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