Line data Source code
1 : /* ✔ */
2 :
3 : #pragma once
4 :
5 : #include <stdint.h>
6 : #include <AH/STL/limits>
7 : #include <AH/STL/type_traits>
8 :
9 : #include <AH/Settings/NamespaceSettings.hpp>
10 :
11 : BEGIN_AH_NAMESPACE
12 :
13 : /**
14 : * @brief Exponential moving average filter.
15 : *
16 : * Fast integer EMA implementation where the weight factor is a power of two.
17 : *
18 : * Difference equation: @f$ y[n] = \alpha·x[n]+(1-\alpha)·y[n-1] @f$
19 : * where @f$ \alpha = \left(\frac{1}{2}\right)^{K} @f$, @f$ x @f$ is the
20 : * input sequence, and @f$ y @f$ is the output sequence.
21 : *
22 : * [An in-depth explanation of the EMA filter](https://tttapa.github.io/Pages/Mathematics/Systems-and-Control-Theory/Digital-filters/Exponential%20Moving%20Average/)
23 : *
24 : * @tparam K
25 : * The amount of bits to shift by. This determines the location
26 : * of the pole in the EMA transfer function, and therefore the
27 : * cut-off frequency.
28 : * The higher this number, the more filtering takes place.
29 : * The pole location is @f$ 1 - 2^{-K} @f$.
30 : * @tparam input_t
31 : * The integer type to use for the input and output of the filter.
32 : * Can be signed or unsigned.
33 : * @tparam state_t
34 : * The unsigned integer type to use for the internal state of the
35 : * filter. A fixed-point representation with @f$ K @f$ fractional
36 : * bits is used, so this type should be at least @f$ M + K @f$ bits
37 : * wide, where @f$ M @f$ is the maximum number of bits of the input.
38 : *
39 : * Some examples of different combinations of template parameters:
40 : *
41 : * 1. Filtering the result of `analogRead`: analogRead returns an integer
42 : * between 0 and 1023, which can be represented using 10 bits, so
43 : * @f$ M = 10 @f$. If `input_t` and `output_t` are both `uint16_t`,
44 : * the maximum shift factor `K` is @f$ 16 - M = 6 @f$. If `state_t`
45 : * is increased to `uint32_t`, the maximum shift factor `K` is
46 : * @f$ 32 - M = 22 @f$.
47 : * 2. Filtering a signed integer between -32768 and 32767: this can be
48 : * represented using a 16-bit signed integer, so `input_t` is `int16_t`,
49 : * and @f$ M = 16 @f$. (2¹⁵ = 32768)
50 : * Let's say the shift factor `K` is 1, then the minimum width of
51 : * `state_t` should be @f$ M + K = 17 @f$ bits, so `uint32_t` would be
52 : * a sensible choice.
53 : *
54 : * @ingroup AH_Filters
55 : */
56 : template <uint8_t K,
57 : class input_t = uint_fast16_t,
58 : class state_t = typename std::make_unsigned<input_t>::type>
59 : class EMA {
60 : public:
61 : /// Constructor: initialize filter to zero or optional given value.
62 25 : EMA(input_t initial = input_t{0}) { reset(initial); }
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 34 : void reset(input_t value = input_t(0)) {
71 34 : state_t value_s = static_cast<state_t>(value);
72 34 : state = zero + (value_s << K) - value_s;
73 34 : }
74 :
75 : /**
76 : * @brief Filter the input: Given @f$ x[n] @f$, calculate @f$ y[n] @f$.
77 : *
78 : * @param input
79 : * The new raw input value.
80 : * @return The new filtered output value.
81 : */
82 2063 : input_t filter(input_t input) {
83 2063 : state += static_cast<state_t>(input);
84 2063 : state_t output = (state + half) >> K;
85 2063 : output -= zero >> K;
86 2063 : state -= output;
87 2063 : return static_cast<input_t>(output);
88 : }
89 :
90 : /// @copydoc EMA::filter(input_t)
91 2021 : input_t operator()(input_t input) {
92 2021 : return filter(input);
93 : }
94 :
95 : constexpr static state_t
96 : max_state = std::numeric_limits<state_t>::max(),
97 : half_state = max_state / 2 + 1,
98 : zero = std::is_unsigned<input_t>::value ? state_t{0} : half_state,
99 : half = K > 0 ? state_t{1} << (K - 1) : state_t{0};
100 :
101 : static_assert(std::is_unsigned<state_t>::value,
102 : "state type should be unsigned");
103 :
104 : static_assert(max_state >= std::numeric_limits<input_t>::max(),
105 : "state type cannot be narrower than input type");
106 :
107 : /// Verify the input range to make sure it's compatible with the shift
108 : /// factor and the width of the state type.
109 : ///
110 : /// Examples:
111 : /// ~~~cpp
112 : /// EMA<5, int_fast16_t, uint_fast16_t> filter;
113 : /// static_assert(filter.supports_range(-1024, 1023),
114 : /// "use a wider state or input type, or a smaller shift factor");
115 : /// ~~~
116 : /// ~~~cpp
117 : /// EMA<5, uint_fast16_t, uint_fast16_t> filter;
118 : /// static_assert(filter.supports_range(0u, 2047u),
119 : /// "use a wider state or input type, or a smaller shift factor");
120 : /// ~~~
121 : template <class T>
122 : constexpr static bool supports_range(T min, T max) {
123 : using sstate_t = typename std::make_signed<state_t>::type;
124 : return min <= max &&
125 : min >= std::numeric_limits<input_t>::min() &&
126 : max <= std::numeric_limits<input_t>::max() &&
127 : (std::is_unsigned<input_t>::value
128 : ? state_t(max) <= (max_state >> K)
129 : : min >= -static_cast<sstate_t>(max_state >> (K + 1)) - 1 &&
130 : max <= static_cast<sstate_t>(max_state >> (K + 1)));
131 : }
132 :
133 : private:
134 : state_t state;
135 : };
136 :
137 : // -------------------------------------------------------------------------- //
138 :
139 : /**
140 : * @brief A class for single-pole infinite impulse response filters
141 : * or exponential moving average filters.
142 : *
143 : * This version uses floating point maths.
144 : *
145 : * Difference equation: @f$ y[n] = \alpha·x[n]+(1-\alpha)·y[n-1] @f$
146 : * @f$ x @f$ is the input sequence, and @f$ y @f$ is the output sequence.
147 : *
148 : * [An in-depth explanation of the EMA filter]
149 : * (https://tttapa.github.io/Pages/Mathematics/Systems-and-Control-Theory/Digital-filters/Exponential%20Moving%20Average/)
150 : *
151 : * @ingroup AH_Filters
152 : */
153 : class EMA_f {
154 : public:
155 : /**
156 : * @brief Create an exponential moving average filter with a pole at the
157 : * given location.
158 : *
159 : * @param pole
160 : * The pole of the filter (@f$1-\alpha@f$).
161 : * Should be a value in the range
162 : * @f$ \left[0,1\right) @f$.
163 : * Zero means no filtering, and closer to one means more filtering.
164 : */
165 1 : EMA_f(float pole) : alpha(1 - pole) {}
166 :
167 : /**
168 : * @brief Filter the input: Given @f$ x[n] @f$, calculate @f$ y[n] @f$.
169 : *
170 : * @param value
171 : * The new raw input value.
172 : * @return The new filtered output value.
173 : */
174 12 : float filter(float value) {
175 12 : filtered += (value - filtered) * alpha;
176 12 : return filtered;
177 : }
178 :
179 : /// @copydoc filter(float)
180 12 : float operator()(float value) { return filter(value); }
181 :
182 : private:
183 : float alpha;
184 : float filtered = 0;
185 : };
186 :
187 : END_AH_NAMESPACE
|