Alright… We’ve put the work in, lets bring this one home. We’ve got a fairly reliable (if a little bit low res) sine wave generator, but we can make our Arduino do a lot more. Today I’d like to modify my Arduino based sine wave generator to output a variety of predefined waveforms.
To accomplish this I need two things. First I need to program a button to let me select a waveform. Then I’ll need to create tables for these additional waveforms and implement logic to pull values from the correct table.
The Circuit
The circuit for this one is essentially the same as the circuit from my last post. The only change I’ve made is the addition of a push button connected between pin 2 on the Arduino and ground. In the code I will set this pin to “pull up” meaning the pin is kept high until the button is pressed. When the button is pressed the pin is connected to ground which will force it low.
Polling Vs Interrupts
When setting up the button press I have two choices. We can either use polling to check for a button press or set up an interrupt. In polling we would add a check in the main loop so that each time the Arduino cycles through the loop it will check the status of the button. This option is very easy to implement but is not terribly efficient.
The analogy I often hear is to imagine you are waiting for a phone call. Polling would be the equivalent of disabling your ringer and checking the phone every 30 seconds to see if the call is coming in. There are a few obvious issues with this approach. What if the call comes in in between the times when you check? you would miss it. Further what if you have a homework assignment your trying to get done at the same time? You wouldn’t make much progress if you stopped every 30 seconds to check your phone.
A better option would be to turn your ringer on and set your phone to the side while you work on other things. This is what happens when we set an interrupt. We tell the Arduino to let us know when an event happens. When something does happen the Arduino takes note of what it’s working on, stops, and proceeds to take care of the instructions we left it for the event.
Setting up an Interrupt
There are a few steps to set up an interrupt for the button press. First I define the pin and create a variable for the pins state. Notice I use the keyword volatile to communicate to the Arduino that this value may change at any time.
#define SWITCH_PIN 2 //Push button to switch waveform
volatile int switch_state = HIGH;
Next comes the setup function for the Arduino. Here I need to set the pinMode. I set this pin as an INPUT_PULLUP so that the pin is held high until the button is pressed.
Also in the setup we will call the attachInterrupt function. This tells the Arduino how to handle this pin. This function takes three arguments. The first of these sets the pin we are attaching the interrupt to. The second gives the name of the function to be called when the interrupt happens. The third identifies the event at the pin which will trigger the interrupt, I have used FALLING here meaning the Arduino will trigger the event when the pin goes from high to low.
void setup() {
pinMode(SWITCH_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switch_pin_ISR, FALLING);
get_sine_table();
}
Finally I need to implement the function mentioned in the setup (switch_pin_ISR). My goal here is to use a new variable (table) to track what table is being used. When the button is pressed I want this variable to increment by one which will tell the main loop to start pulling from the next waveform’s table (more on this later). Additionally I don’t want this variable to ever go beyond the number of tables I have so I will use the modulo operator. This ensures that if the variable reaches beyond the number of tables I have defined it will reset to zero.
int table = 0;
/* Identifies current waveform table
* 0 - Sine
* 1 - Right Saw
* 2 - Left Saw
* 3 - Triangle
*/
void switch_pin_ISR(){
table = (table + 1) % 4;
}
Tables Tables Tables
Now that I’ve built up the logic to handle the button control I’d better set up some tables to create the different waveforms. At the time of writing I have 4 waveforms I’m using. These are the sine we originally created, right and left biased saw waves and a triangle. I generate each of these during the set-up function.
int sine_table[100];
int rsaw_table[100];
int lsaw_table[100];
int triangle_table[100];
void setup() {
pinMode(SWITCH_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switch_pin_ISR, FALLING);
get_sine_table();
get_rsaw_table();
get_lsaw_table();
get_triangle_table();
}
For each wave I have created a function to create the table. The logic behind these waveforms is fairly straightforward. For the right sine wave I created loop from 0 to N setting each value to i*(255/N). The left sine is identical except that I subtract the result of that equation from 255 to invert the result. The triangle is slightly more interesting, I used an if statement to separate the values before N/2 from those after. Those before I set equal to i*(255/(N/2)). Those after I set equal to 255- i*(255/(N/2)).
void get_sine_table(){
for (int i = 0; i < N; i++){
sine_table[i] = 127+127*sin(i*(2*3.14)/N);
}
}
void get_rsaw_table(){
for (int i = 0; i < N; i++){
rsaw_table[i] = i*(255/N);
}
}
void get_lsaw_table(){
for (int i = 0; i < N; i++){
lsaw_table[i] = 255 - i*(255/N);
}
}
void get_triangle_table(){
for (int i = 0; i < N; i++){
if (i<(N/2)){
triangle_table[i] = i*(255/(N/2));
}
else{
triangle_table[i] = 255 - (i-N/2)*(255/(N/2));
}
}
}
If the logic of these functions seems unclear don’t worry. These kind of algorithms are not necessarily intuitive at first. My best advice is to work through them, take an N value of 5 and calculate the result for each i less than N. Remember that a value of 0 equates to 0V output and 255 equates to 5V output. Your results should mimic the desired waveform.
The Switch
The final piece of this puzzle comes in the main loop. We have tables for each waveform, a variable to track which table we’re using and an interrupt to modify that variable. Now we just have to read that variable and pull our PWM values from the correct table. Here I’ve used a switch statement. If you’ve programmed mostly in Python or Java you may not have encountered switches before. A switch is essentially a clean way to write a very ugly if else statement. We define a variable to evaluate and then give a list of “cases” for different values of that variable. Each case describes the actions to take if the variable equals that value. If you’d like to learn more about switch statements Geeks for Geeks have a great article about them.
void loop() {
for (int i = 0; i < N; i++){
sample_time = analogRead(ANALOG_0);
sample_time = (sample_time/16)+2;
switch (table) {
case 0:
analogWrite(PWM_PIN, sine_table[i]);
break;
case 1:
analogWrite(PWM_PIN, rsaw_table[i]);
break;
case 2:
analogWrite(PWM_PIN, lsaw_table[i]);
break;
case 3:
analogWrite(PWM_PIN, triangle_table[i]);
break;
}
delay(sample_time);
}
}
All The Code
With that we’ve done it! We’ve turned our Sine Wave Generator into a proper function generator. There’s no reason to stop here though, I’ve only scratched the surface of the available waveforms. Additionally there are better filters and better algorithms begging to be explored. Now that I have this up and running I’d also like to spend some time in a future post exploring how to interface it with other oscillators to use it as an LFO. For today I’ll finish up by providing my code in full, Take care. I’ll see you all soon.
/*
* www.SamVsSound.com
* Arduino PWM Function Generator
* 11/14/2020
*/
#define PWM_PIN 5 //PWM output at pin5
#define ANALOG_0 A0 //Input for sample_time
#define SWITCH_PIN 2 //Push button to switch waveform
#define PWM_FREQ 980 //Hz
#define N 100
int sine_table[100];
int rsaw_table[100];
int lsaw_table[100];
int triangle_table[100];
volatile int switch_state = HIGH;
int table = 0;
/*
* 0 - Sine
* 1 - Right Saw
* 2 - Left Saw
* 3 - Triangle
*/
int sample_time;
void get_sine_table();
void get_rsaw_table();
void get_lsaw_table();
void get_triangle_table();
void setup() {
pinMode(SWITCH_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switch_pin_ISR, FALLING);
get_sine_table();
get_rsaw_table();
get_lsaw_table();
get_triangle_table();
}
void loop() {
for (int i = 0; i < N; i++){
sample_time = analogRead(ANALOG_0);
sample_time = (sample_time/16)+2;
switch (table) {
case 0:
analogWrite(PWM_PIN, sine_table[i]);
break;
case 1:
analogWrite(PWM_PIN, rsaw_table[i]);
break;
case 2:
analogWrite(PWM_PIN, lsaw_table[i]);
break;
case 3:
analogWrite(PWM_PIN, triangle_table[i]);
break;
}
delay(sample_time);
}
}
void switch_pin_ISR(){
table = (table + 1)%4;
}
void get_sine_table(){
for (int i = 0; i < N; i++){
sine_table[i] = 127+127*sin(i*(2*3.14)/N);
}
}
void get_rsaw_table(){
for (int i = 0; i < N; i++){
rsaw_table[i] = i*(255/N);
}
}
void get_lsaw_table(){
for (int i = 0; i < N; i++){
lsaw_table[i] = 255 - i*(255/N);
}
}
void get_triangle_table(){
for (int i = 0; i < N; i++){
if (i<(N/2)){
triangle_table[i] = i*(255/(N/2));
}
else{
triangle_table[i] = 255 - (i-N/2)*(255/(N/2));
}
}
}