This shows you the differences between two versions of the page.

moving_average_on_avrs [2016/03/21 14:32] Traumflug [Practical Code] Note binary size of the 8-value version. |
moving_average_on_avrs [2018/05/27 16:10] |
||
---|---|---|---|

Line 1: | Line 1: | ||

- | ====== Moving Average on AVRs ====== | ||

- | One problem with the controlling strategy on the [[ISTAtrol]] is thermistor readings jittering by about 1%, which is quite a lot if we want to keep the reading ( = target temperature) in a similarly tight range. As a result of this, the radiator valve is often opened or closed a bit because such a jittered value comes in, just to be moved back on the next reading. Not good, mechanics is subject to wear and we want to preserve our mechanics. | ||

- | |||

- | There are solutions. Building better hardware is neither trivial nor free of cost, so the typical approach is to implement a **moving average algorithm**. | ||

- | |||

- | ===== Algorithm Principle ===== | ||

- | |||

- | The mathematical principle of a moving average is simple: collect a number of measurements, then calculate the average: | ||

- | |||

- | x = (x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8) / 8 | ||

- | | ||

- | With each incoming value, the oldest one is removed and the new one added. Easy. A typical application for a FIFO buffering strategy, see your favorite programming algorithms textbook. | ||

- | |||

- | ===== More Practical Algorithm ===== | ||

- | |||

- | Trivial to see for a mathematician, this algorithm can be simplified. One can see that the sum of all the individual factors is the same as the average multiplied by the number of factors: | ||

- | |||

- | x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 == x * 8 | ||

- | | ||

- | This observation leads to a similar (but not identical) algorithm, which avoids summing up all the values each time. One doesn't remove the oldest value each time, but the average value. A new value ''x_new'' is counted in like this: | ||

- | |||

- | x = (x * 8 - x + x_new) / 8 | ||

- | |||

- | Tadaa, no longer a need for implementing a FIFO buffer! | ||

- | |||

- | Difference to the previous algorithm is that changes slip in slower. In a theoretical case one reads a value of 1000 all the time, then suddenly the reading changes to 1200. With the algorithm above it takes exactly 8 new values until the average is exactly on the new value. With the algorithm here, it takes about 30 new values until the moving average is close to the new value and it never gets there entirely. That's why this variant is also called //exponential moving average//. Exercise for the reader: prove that in a spreadsheet. | ||

- | |||

- | ===== Practical Code ===== | ||

- | |||

- | Perhaps you've wondered why all the above investigations always used 8 as a factor. Looking as a mathematician it works just as fine with any other number: 5, 10, 100, 468, whatever you like. In the eye of a code writer, using exponents of 2 (2, 4, 8, 16, ...) has a noticeable advantage: multiplications as well as divisions can be done by bit-shifting. That's why 8 was used. No need to write such bit-shifts directly into the code, any decent compiler will recognize this and use shifting machine instructions instead of a full blown multiplication/division. | ||

- | |||

- | Another one is that we can likely spare the few bytes to store the multiplied sum. No need to calculate it newly each time. | ||

- | |||

- | Third point is that the sum of all values has to fit into the variable. On the developer's ISTAtrol, typical thermistor readings are around 5000..6500, so they fit into 12\_bits. Variables have 16\_bits, so we can multiply by 8 without overflowing. | ||

- | |||

- | Enough talk, here's the code, including a fallback for larger readings: | ||

- | |||

- | <code c> | ||

- | // Global variables. | ||

- | static uint16_t temp_c = 0; // Reading used for controlling. | ||

- | static uint16_t temp_temp = 0; // Reading directly from ADC. | ||

- | #if TARGET_TEMPERATURE < 7000 | ||

- | // We can expect thermistor readings to be always below 8192, so it always | ||

- | // fits into 12 bits and we can always keep a multiplication by 8. | ||

- | // Initialize to a reasonable value to avoid underflows on the first steps. | ||

- | static uint16_t temp_temp_eight = TARGET_TEMPERATURE * 8; | ||

- | #endif | ||

- | | ||

- | // ... other code ... | ||

- | |||

- | // Accept a new ADC reading. | ||

- | #if TARGET_TEMPERATURE < 7000 | ||

- | // Use a moving average with 8 values. New readings count in at about 12%. | ||

- | temp_temp_eight -= temp_c; | ||

- | temp_temp_eight += temp_temp; | ||

- | temp_c = (temp_temp_eight + 4) / 8; // '+ 4' for rounding | ||

- | #else | ||

- | // Use a two-point moving average, which allows readings up to 32767. | ||

- | temp_c = (temp_temp + temp_c + 1) / 2; | ||

- | #endif | ||

- | </code> | ||

- | |||

- | Code size on the ISTAtrol/ATtiny2313 firmware: 58\_bytes Flash and 2\_bytes RAM for the 8-value version, 26\_bytes Flash and 2\_bytes RAM for the 2-value version. |

moving_average_on_avrs.txt ยท Last modified: 2018/05/27 16:10 (external edit)