- Blink without blocking
- Grouping logical routines together in functions
- A locally scoped variable that remembers its last value
- Windowed moving average filter
- Exponential moving average filter
Blink without blocking
The “Blink” sketch is fine if you only ever want your Arduino just to blink a light. But odds are good you want it to do some other things as well, like read a button, or run some other outputs.
The trick is to get rid of any long
delay() statement, because during
delay() the Arduino generally can’t read inputs or change outputs. (There’s an exception: a special thing called an interrupt can run even during a delay, but it’s a bit tricky to implement and not recommended for beginners.)
Grouping logical routines together in functions
Perhaps you have a variety of sections of your code.
There’s a section that checks whether a light should be on or off, based on a switch’s state:
And then a different section that changes a motor’s speed based on the temperature:
These two different small pieces of code could be grouped into functions for your own legibility and convenience. To do that, below the
loop(), you can define a new function and put anything you’d like into it, like so:
Once you’ve written the
adjustLight() function, you can call it at any time by simply putting the line
adjustLight(); into your code. The Arduino will run whatever instructions are inside the brackets of the function.
Just to complete the example, here is how a sketch could be made a bit more legible through the use of functions used to group routines together:
Did you notice that the functions you wrote resemble the two functions we’ve always included in our sketches, namely
void loop() and
void setup()? That’s because
setup() are actually just functions themselves—they have special “reserved” names which the software interprets to know when, and in what order, they should be run.
A locally scoped variable that remembers its last value
As C is a scoped language, variables only are accessible from inside the scope (which you can think of as brackets) in which they were created, or any sub-scope.
If you initialize a variable in the
loop(), then it’s going to keep getting reset to that initial value every time the
loop() runs. For instance, in:
You might think that
x is going to keep getting bigger as the loop runs, but it will never get bigger than 1. (It will start as 0, then increment to 1, and then get reset to 0 next time the loop restarts.)
There is a way around this, though: using the keyword
static. This will essentially initialize the variable in the scope you want only once, and from there on out it will remember its value. Returning to the boring example from above, we can still keep
x’s scope inside the
loop() and add the keyword when declaring the variable, like so:
x is going to keep incrementing over and over, reaching tragically for the stars. (Tragic because once it hits 32,767 on an Arduino Uno all its little dreams will be shattered.1)
Who cares? Well, it’s a very good habit to limit variables to the smallest possible scope.
for loops are most usually written like this:
i is being initialized inside of
for. You could also achieve the same result like this:
And it would probably work just fine. But! If some other math at some point touched that
i and changed its value, it could screw up your
for loop. To avoid that uncertainty, it’s best to make variables with the smallest possible scope.
Windowed moving average filter
Often you will wire up a sensor to your Arduino and find that the readings you get seem noisy. The individual values change from reading to reading, but they are all in the same ballpark. As you can see in the data below, there is a general trend, but the individual readings don’t follow that trend perfectly. In this case you might want to filter your data so that you can determine what the data’s general trend is.
One of the easiest filters is the windowed moving average filter. This filter simply remembers the last few sensor readings (the readings in the “window”) and averages them to get the filtered value.
The size of the window has a large effect on how the filter performs. The larger the window, the more readings will be averaged, and thus the smoother the filtered value will be. However, a larger window will also make the filtered value respond to changes in the data slower. You can see the results of a moving average filter on a dataset in the four images below (Red is the moving average, Blue is the raw data):
An important thing to keep in mind when using windowed moving average filters is that a sensor reading only affects the average while it is in the filters window. As it leaves the window, its influence drops from 1/numSamples to 0 immediately. If that sensor reading is a very high or low outlier, your average may suddenly change when that sensor reading drops out of the window.
Exponential moving average filter
Another way to implement a moving average filter is the exponential moving average. This filter is simpler to implement, but has slightly different behavior than the windowed moving average.
The exponential moving average is kind of like the average of the new sensor reading with the previously calculated average. The exact equation is:
x in the equation above is a weighting factor that is usually used to weight the previous average more than the sensor reading. The following graphs show raw data (blue) and the data filtered with an exponential moving average (red). The number in parentheses at the top of each graph is the value of
x for each of the filters. As you can see, a higher value for
x is somewhat like a larger window for the windowed moving average. The filtered value is smoother but responds slower to changes in the data.
While the exponential and windowed moving averages behave similarly, they have different characteristics. Importantly, in an exponential moving average, a sensor value will theoretically influence the average forever, with its influence getting smaller each loop. This is different from the windowed moving average where the influence of a sensor reading stays constant while it is in the window and then drops to 0.
One subtlety to keep in mind: you may want (or not want) to run the smoothing function at a different rate than the data acquisition happens. In the above code, each time a new data point is recorded by
cur_reading = (float)analogRead(SENSOR_PIN);, the smoothing runs once:
filtered_val = filtered_val * PREV_WEIGHT + cur_reading * CUR_WEIGHT;. This is because they’re both in the same functional loop, right on top of each other. But it would be possible to set the filtering function to run independently from the data acquisition, for instance by using the Blink without blocking paradigm to trigger one or the other action. If you filtered much faster than acquired data, then the filter would smooth much more, acting as if the incoming data was itself smooth (because it would keep reusing the last reported datum). On the other hand, if you let two data points in a row come in before running the filter, the older one would be totally ignored since this algorithm cares only about the most recent datum. In practice the easiest way to avoid either of these cases is to do data acquisition and filtering sequentially, as the example above does.
That number, 32,767, is the largest value of type
intthat an Arduino Uno is able to store. If you ask the Arduino Uno to solve the seemingly simple math problem 32767 + 1, it will answer –32768; this is because you’re causing it to “roll over” from the largest-possible
intto the smallest-possible one. This Wikipedia article explains what’s going on. ↩