Arduino R2R DAC: Frequency

Modified R2R DAC With Added Analog Input

As promised I’m back with another update for this project. Today I’ve been playing around with my Arduino trying to get frequency control working. It took a bit of experimentation due to my use of timer interrupts but I got there. Additionally I’ve made some slight modifications to the other code to clean things up a bit.

The Big Idea

I played around with a few different approaches to adjust the frequency. After some exploration I settled on modifying the overflow counter (OCR1A) value. If you remember from my article on Timer Interrupts this is the value which the timer counts up to before outputting an interrupt. By manipulating this value using a potentiometer we can adjust the frequency in real time.

Some Math

To get started we need to figure out what values we’ll need to set the OCR1A register to. My goal is to get a frequency range of approximately 50Hz – 1kHz. Luckily the calculation to find the OCR value given a frequency is fairly straight forward.

By inputting the known values for clock frequency (16MHz on Arduino), prescaler (8) and N (50) I can solve for my upper and lower frequency bounds.

From here I know that the minimum OCR value I want to use is going to be 40. Additionally, the range of values between the lowest and highest value on the potentiometer should be 760. To set the lower limit we can add 40 to whatever our reading is on the potentiometer. For the upper limit we have to do a little more fancy footwork. Since the analog inputs on the Arduino output a value between 0 and 1023, I need to normalize this value. To achieve this I multiply the reading by 760.0 and then divide by 1023. Note the .0 is important as it forces the division to be handled as a float (maintains decimal points). Once the calculation is complete I can type cast it back to an int to be used by the program.

The final implementation of this algorithm is as follows. Note FREQ is the name I defined for the analog input at the start of my code.

ARR_check = int(((analogRead(FREQ)*760.0)/1023)+40);

Dealing With Ripple

You may notice in the code above I do not write the value directly to the OCR. I ran into problems as the oscillations in the circuit caused a fair amount of noise on my ground and voltage lines. This issue was compounded when connected to a speaker. This caused the analogRead value to bounce around and the OCR to be constantly updating with different values. The constant updates broke the flow of my waveform and left the output frequency bouncing around even when the pot wasn’t being touched.

By adding a set of decoupling capacitors I was able to limit this noise but not completely remove it. I used a 0.1uF capacitor and a 100uF capacitor between the ground and power rails.

My final solution in the program itself was to set the value of the analog read (after normalizing it to the desired range) to a variable (ARR_check). I then took the absolute value of this check minus the current value of the OCR. With this I could set the OCR to update only if the difference reached a certain margin. In my experimentation I found 25 was a large enough margin to stabilize things. You can see the resulting main loop below.

void loop(){
  ARR_check = int(((analogRead(FREQ)*760.0)/1023)+40);
  if (abs(ARR_check - OCR1A) > 25){
    OCR1A = ARR_check;  
    }
  
  delay(50);
  }

Note that this is not an ideal situation. The use of this buffer means I will not have a smooth range of frequencies as I turn the potentiometer. The output is more like a step tone generator with the value of the margin defining the step size. One interesting experiment might be to add a second potentiometer to adjust the margin value which would produce something similar to an Atari Punk Console. Only this APC would be capable of outputting sine and triangle waves. I’ll be looking into better ways to isolate this circuit and minimize the effect of this noise in future posts. For now though this solution still gets the variance I was after.

Some Optimization

One of my experiments while I was trying to get the frequency adjustment working involved increasing the size of the tables and adjusting how quickly I iterated through them. Attempting to do this however I ran into some problems with the Arduino Uno’s memory constraints. This led me to make some changes to the code to optimize the memory use.

uint8_t wave_table[N];

The first thing I did was do away with the 4 separate table arrays and replace them with a single array. I additionally changed the type of this array from int (32-bits) to uint8_t (8-bits). Since the DAC’s values range from 0-15 the uint8 is plenty to handle them. In fact even if I move up to 8-bits the uint8 will still be sufficient.

void get_sine_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(7.5+7.5*sin(i*(2*3.14)/N));
  }
}

void get_rsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(i*(15.0/N));
  }
}

void get_lsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(15 - i*(15.0/N));
  }
}

void get_triangle_table(){
  for (int i = 0; i < N; i++){
    if (i<(N/2)){
      wave_table[i] = int(i*(15.0/(N/2)));
      }
    else{
      wave_table[i] = int(15 - (i-N/2)*(15.0/(N/2)));
      }
  }
}

Next I modified the get table functions to write directly to this single wave table rather than their respective tables.

void switch_pin_ISR(){
  curr_table = (curr_table + 1)%4;
    switch(curr_table){
    case 0:
      get_sine_table();
      break;
    
    case 1:
      get_rsaw_table();
      break;
    
    case 2:
      get_lsaw_table();
      break;

    case 3:
      get_triangle_table();
      break;
    }
  }

I then removed the calls to each get table function from set-up. To replace them I reated a switch statement in the button interrupt. This switch causes the wave_table array to be overwritten with a new table each time the button is pressed. The curr_table variable still tracks which table is currently selected and governs which get table function is called after each press.

ISR(TIMER1_COMPA_vect){
  v_level = wave_table[table_index];
  PORTB = v_level;         
  table_index = (table_index+1)%N;
  
}

Finally I removed the switch statement in the timer interrupt and set v_level to be updated directly from the wave_table array.

That wraps it up for today. I’m going to be doing some work to stabilize the circuit and hopefully generate a cleaner frequency sweep. As always I’ll provide any updates as I find them.

#define PIN_8 8
#define PIN_9 9
#define PIN_10 10
#define PIN_11 11

#define FREQ A0
#define SWITCH_PIN 2         //Push button to switch waveform

#define N 50            //Samples per cycle

volatile int switch_state = HIGH;

int table_index = 0;
uint8_t v_level;

uint16_t ARR_check = 35;

//Lookup tables for waveforms
uint8_t wave_table[N];

uint8_t curr_table = 1;

void timer1_init();

//Function definitions to generate wave tables
void get_sine_table();
void get_rsaw_table();
void get_lsaw_table();
void get_triangle_table();

void setup() {
  DDRB = B1111;      //Set Pins 8-11 as Inputs
  
  timer1_init();    //Initiate timer

  //initiate button interrupt to change waveform
  pinMode(SWITCH_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switch_pin_ISR, FALLING);

  //Initialize wave tables
  get_sine_table();
}

void loop(){
  ARR_check = int(((analogRead(FREQ)*760.0)/1023)+40);
  if (abs(ARR_check - OCR1A) > 25){
    OCR1A = ARR_check;  
    }
  
  delay(50);
  }

void timer1_init(){
  noInterrupts();        
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  OCR1A = 40;               //Compare Value
  TCCR1B |= (1 << WGM12);    // CTC mode
  TCCR1B |= (1<<CS11);       // 8 prescaler 
  TIMSK1 |= (1 << OCIE1A);   // enable timer compare interrupt

  interrupts();     
}

ISR(TIMER1_COMPA_vect){
  v_level = wave_table[table_index];
  PORTB = v_level;         
  table_index = (table_index+1)%N;
  
}

void switch_pin_ISR(){
  curr_table = (curr_table + 1)%4;
    switch(curr_table){
    case 0:
      get_sine_table();
      break;
    
    case 1:
      get_rsaw_table();
      break;
    
    case 2:
      get_lsaw_table();
      break;

    case 3:
      get_triangle_table();
      break;
    }
  }

void get_sine_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(7.5+7.5*sin(i*(2*3.14)/N));
  }
}

void get_rsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(i*(15.0/N));
  }
}

void get_lsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(15 - i*(15.0/N));
  }
}

void get_triangle_table(){
  for (int i = 0; i < N; i++){
    if (i<(N/2)){
      wave_table[i] = int(i*(15.0/(N/2)));
      }
    else{
      wave_table[i] = int(15 - (i-N/2)*(15.0/(N/2)));
      }
  }
}