In my last post I used a simple low pass filter to start generating sine waves on my Arduino Uno. The code I wrote for this project probably seems pretty clear since the program is so basic. However in my haste I took a number of shortcuts which will lead to the code becoming confusing and inefficient as I start adding complexity. Today I’m going to work on cleaning up this code by discussing some best practices. I know this isn’t as exciting as moving forward with the project but it is important. This cleanup will save me a lot of time in future by making sure I am starting with, and continuing to write, simple, efficient and readable code.
I’ll be going through these changes one by one in the body of this post. At the end of the post I’ll provide the new code in it’s entirety for your review.
Table Lookups
void loop() {
for (i = 0; i < N; i++){
duty_cycle = 127+127*sin(i*(2*3.14)/N);
analogWrite(PWM_out, duty_cycle);
delay(sample_time);
}
}
Above I’ve shown the original loop I wrote for this program. For each step of the sine wave this loop calculates the value of sine, writes that value to the PWM and delays for a given time. The issue here is that any calculations we are doing at runtime take time. That means every time we calculate our duty cycle there is a small delay. This extra delay will cause our frequency to become less accurate.
A better solution is to use a look-up table. By conducting all of our calculations before hand and storing the results we can remove the equation from the main loop. To do this I’ll first initialize a table and then calculate it’s values in our setup function:
int sine_table[50];
void setup() {
for (i = 0; i < N; i++){
sine_table[i] = 127+127*sin(i*(2*3.14)/N);
}
}
Once I’ve set up a table in this way I can simply set the PWM value based on the index of this table in our main loop:
void loop() {
for (i = 0; i < N; i++){
analogWrite(PWM_out, sine_table[i]);
delay(sample_time);
}
}
Function Extraction
void setup() {
for (i = 0; i < N; i++){
sine_table[i] = 127+127sin(i(2*3.14)/N);
}
}
Looking back at our setup function you may not see any issues. Currently it looks pretty clean. What happens though when I start doing more than just this one thing during startup? Imagine if we were initializing several wave tables, this would start to get pretty ugly pretty fast.
The technique we use here is called function extraction. Basically the idea is to create functions within your program to handle each specific task your program does. This allows you to move code specific to those tasks out of your main code. Doing this will not change the functionality of the code but makes the code significantly more clear and readable, especially as your program grows more complex.
void get_sine_table();
First we declare our new function at the top of the code (with the global variable definitions).
void get_sine_table(){
for (i = 0; i < N; i++){
sine_table[i] = 127+127*sin(i*(2*3.14)/N);
}
}
Next we can implement the function at the end of our code. Notice the contents of this function are the same as what we originally had in our setup.
void setup() {
get_sine_table();
}
Finally we update our setup to call this function. Notice how much clearer this is. By giving the function a descriptive name we can now tell exactly what is being done in the setup.
Variables vs Definitions
int PWM_out = 5; //PWM output at pin5
int sample_time = 2; //ms
int PWM_frequency = 980; //Hz
int N = 50;
int duty_cycle; //unitless
int sine_table[50];
Next I’d like to have a look at the variable definitions of this program. These variables can be roughly separated into two categories, those which change during runtime and those that do not. Those which change are best stored as global variables however those which don’t we can handle in a better way.
When a variable is defined a location in memory is set aside for it and every time it is accessed that memory location is read (when the variable is called) or written to (when the variable is changed). This means each variable we have in the code is taking up a small amount of our system memory and each time we access them it takes time (though an infinitesimally small amount).
Using the keyword #define we can bypass this process for static variables. Define is what is known as a preprocessor directive. That’s a fancy way of saying that it is not an instruction for your Arduino, it is an instruction for the compiler which builds your program and sends it to the Arduino. When you build your program the compiler will search the code for any instances of the name you defined and replace it with the value given. this means rather than having to store and access a variable in the program the value is directly written into the code. All the while maintaining the ease of use and readability that having the value named provides.
#define PWM_PIN 5 //PWM output at pin5
#define SAMPLE_TIME 2 //ms
#define PWM_FREQ 980 //Hz
#define N 50
int sine_table[50];
Above I have changed all variables that do not need to be modified at runtime to definitions. Notice I also removed the duty cycle variable. Since I’m setting the value of the PWM directly from the table it is not needed.
Putting it Together
//www.SamVsSound.com 11/08/2020
#define PWM_PIN 5 //PWM output at pin5
#define SAMPLE_TIME 2 //ms
#define PWM_FREQ 980 //Hz
#define N 50
int sine_table[50];
void get_sine_table();
void setup() {
get_sine_table();
}
void loop() {
for (int i = 0; i < N; i++){
analogWrite(PWM_PIN, sine_table[i]);
delay(SAMPLE_TIME);
}
}
void get_sine_table(){
for (int i = 0; i < N; i++){
sine_table[i] = 127+127*sin(i*(2*3.14)/N);
}
}
Here we have our new cleaner code. I know with a program this small these changes are not striking. Still, you can imagine the difference these will make as we add complexity going forward. I’m sorry this turned into more of a coding tutorial than an audio project but I’ll be back soon to start building this project up into something cool.