Direct Digital Synthesizer (DDS) (Part 2)

Part 1234567

Now that we have the hardware ready, let’s go for the firmware! Firstly, we need a reference table containing wave data points. In our case, we will use a 256 steps table. In the context of this project, the content of this table will change depending on a wave type option: apart from the standard wave profiles (sine, square, triangle) and some more exotic ones (saw teeth, random), you will be able to create your own wave profiles. Custom data shall be recorded in the non volatile memory (EEPROM) using the read() and write() functions from the EEPROM library in order to avoid boring uploads.

// Generate wave data in global vector
void generateWaveData(int style){
  switch(style){
  case OPT_WAV_TYP_FLAT: // Flat
    for (int i=0; i < stepsPerCycle; i++)  vWaveData[i] = standByValue;
    break;
  case OPT_WAV_TYP_SINE: // Sine
    for (int i=0; i < stepsPerCycle; i++)  vWaveData[i] = (maxDACValue*(sin((i*6.2831)/stepsPerCycle)+1))/2;
    break;
  case OPT_WAV_TYP_SQUARE: // Square
    for (int i=0; i < stepsPerCycle/2; i++) vWaveData[i] = 0;
    for (int i=stepsPerCycle/2; i < stepsPerCycle; i++) vWaveData[i] = maxDACValue;
    break;
  case OPT_WAV_TYP_TRIANGLE: // Triangle
    for (int i=0; i < stepsPerCycle/2; i++) vWaveData[i] = (i*maxDACValue*2)/stepsPerCycle;
    for (int i=stepsPerCycle/2; i < stepsPerCycle; i++) vWaveData[i] = ((stepsPerCycle-i)*maxDACValue*2)/stepsPerCycle;
    break;
  case OPT_WAV_TYP_SAW_TOO_POS_SLP: // Saw tooth positive slope
    for (int i=0; i < stepsPerCycle; i++) vWaveData[i] = (i*maxDACValue)/stepsPerCycle;
    break;
  case OPT_WAV_TYP_SAW_TOO_NEG_SLP: // Saw tooth negative slope
    for (int i=0; i < stepsPerCycle; i++) vWaveData[i] = ((stepsPerCycle-i)*maxDACValue)/stepsPerCycle; 
    break;
  case OPT_WAV_TYP_RANDOM: // Random (Noise)
    // randomSeed(millis());
    for (int i=0; i < stepsPerCycle; i++) vWaveData[i] = random(0, maxDACValue); 
    break;
  case OPT_WAV_TYP_CUSTOM: // Custom wave
    for (int i=0; i < stepsPerCycle; i++) vWaveData[i] = EEPROM.read(i); 
    break;
  }
}

Note: OPT_WAV_TYP_xxx are global constants. Steps per cycle is 256 (table length) and max dac counts are 2^6 – 1 = 63, where 6 is the number of bits of the R2R ladder DAC.

Then we need a time base. It has to be fast and accurate. We will use timer/counter2 to do the job and perform conversions inside the Interrupt Service Routine (ISR). The number of instructions within the ISR shall be kept as short as possible in order to prevent time overlaps.

// Interrupt service run when Timer/Counter2 reaches OCR2A 
ISR(TIMER2_COMPA_vect){
  long lastPhaseAccumulator = phaseAccumulator;
  // Update phase accumulator counter, the phase accumulator will automatically overflow at 2^32
  phaseAccumulator += tuningWordM; 
  // Use the 8 MSBs from phase accumulator as frequency information
  byte phaseAccumulatorMSBs = (phaseAccumulator >> 24);
  // Set R2R DAC using lookup table (2^8 bits in length)
  PORTB = vWaveData[phaseAccumulatorMSBs];
  // For diag purpose: warning, execution of these lines take time!
  PORTD ^= (1 << PIND6);
  if (lastPhaseAccumulator >= phaseAccumulator) {
    PORTD ^= (1 << PIND5);
  }
}

Note: From the code, you may understand that the time base ticks can be monitored on PIND6, while cycle ticks can be monitored on PIND5. These lines can be commented in order to save a little extra time.

Here is the initialization routine for timer/counter2:

// Initialize Timer/Counter2 parameters 
void setTimer() {
  // Disable interrupts while setting registers
  cli();  
  // Reset control registers
  TCCR2A = 0;
  TCCR2B = 0;
  // Clear Timer on Compare Match (CTC) Mode 
  TCCR2A |= (1 << WGM21);
  // Prescaler x1 
  TCCR2B |= (1 << CS20);
  // Set compared value
  OCR2A = upperCountingValue;
  // Use system clock for Timer/Counter2 
  ASSR &= ~(1 << AS2); 
  // Reset Timer/Counter2 Interrupt Mask Register
  TIMSK2 = 0;
  // Enable Output Compare Match A Interrupt
  TIMSK2 |= (1 << OCIE2A);
  // Enable interrupts once registers have been updated
  sei();
}

Note: Timer/counters 0 and 1 are disabled at the se up level in order to prevent timer/counter2 unstability. Delays have been replaced by NOP (No OPeration) instructions.

  // Reset timer/counters 0 and 1 in order to reduce noise on timer/counter2 
  TIMSK0 = 0;
  TIMSK1 = 0;

The reference frequency is derived from the MCU frequency (16 Mhz in Arduino’s case) and from the timer/counter2 settings. For an upper counting value of 199, a prescaler of 1, the reference frequency is 80 KHz.

  const int upperCountingValue = 199; // Change value for setting different reference frequency. Minimum is 127, for no synch pins configuration
const double refFrequency = 16000000/(upperCountingValue+1); // CPU_frequency/upperCountingValue/prescaler

Setting the cycle frequency is as simple as computing the magic tuningWord!

  
// Set frequency; 
int setFrequency(int frequency){
  // Calculate tuning word value
  tuningWordM = (unsigned long) ((pow(2, 32) * frequency) / refFrequency); 
}

Part 3

6 Comments

  1. Ehsan MAsnavi says:

    Dear All;
    Dear Friends;

    I would like to produce different analog Waveforms (Sinewave, Triangle, Squarewave …) with different frequencies through a Arduino Duemilanove. I would like to have the waveforms on three different ports.

    I have used the “DIRECT DIGITAL SYNTHESIZER (DDS)” method.

    In order to have three different waveforms on three ports I need to use “Compare interrupts” of TIMER0, TIMER1 and TIMER2.

    To test the performance of this method, I first wrote a simple code which is similar to the code above. I have included my code below for your review. Now, Here is the problem:

    I can successfully generate signals with desired frequencies on the ports which are being updated by TIMER1 and TIMER2 Interrupts, but the frequnecy of the signal which is being generated by TIMER0 interrupt is 1/3 (One third) of the two other signals (generated by the TIMER1 and TIMER2 interrupts). For example when I have 250Hz signal on PortB and PortD, There is a 80Hz signal on PortC!!!

    The strange thing here is when I remove TIMER1 Interrupt from this code, the signal frequency on PortC which is being updated by TIMER0 becomes 250Hz!!!!

    Can anyone please tell me what is the probelm?

    ———————————————————-
    int vWaveData[256];
    long int i;
    unsigned long int tuningWordM;
    int upperCountingValue, desiredfreq;
    double refFrequency;
    long int phaseAccumulator0,phaseAccumulator1,phaseAccumulator2;
    void generateWaveData(); // Generate wave data in global vector

    void setup()
    {
    cli(); // Disable interrupts while setting registers
    TCCR2A = 0; // Reset control registers
    TCCR2B = 0; // Reset control registers
    TCCR2A |= (1 << WGM21); // Clear Timer2 on Compare Match (CTC) Mode
    TCCR2B |= (1 << CS20); // Prescaler x1 for Timer2
    OCR2A = 255; // Set compared value
    ASSR &= ~(1 << AS2); // Use system clock for Timer/Counter2
    TIMSK2 = 0; // Reset Timer/Counter2 Interrupt Mask Register
    TIMSK2 |= (1 << OCIE2A); // Enable Output Compare Match A Interrupt

    TCCR1A = 0; // Reset control registers
    TCCR1B = 0; // Reset control registers
    TCCR1B |= (1 << WGM12); // Clear Timer on Compare Match (CTC) Mode
    TCCR1B |= (1 << CS10); // Prescaler x1
    OCR1A = 255; // Set compared value
    TIMSK1 = 0; // Reset Timer/Counter2 Interrupt Mask Register
    TIMSK1 |= (1 << OCIE1A); // Enable Output Compare Match A Interrupt */

    TCCR0A = 0; // Reset control registers
    TCCR0B = 0; // Reset control registers
    TCCR0A |= (1 << WGM01); // Clear Timer on Compare Match (CTC) Mode
    TCCR0B |= (1 << CS00); // Prescaler x1
    OCR0A = 255; // Set compared value
    TIMSK0 = 0; // Reset Timer/Counter2 Interrupt Mask Register
    TIMSK0 |= (1 << OCIE0A); // Enable Output Compare Match A Interrupt

    generateWaveData(); // Create Sinewave look-up table
    upperCountingValue = 255;
    desiredfreq=250;
    refFrequency = 16000000/(upperCountingValue+1);
    tuningWordM = ((pow(2, 32) * desiredfreq) / refFrequency);

    DDRB = B11111111; //Set PORTB as output
    DDRC = B11111111; //Set PORTC as output
    DDRD = B11111111; //Set PORTD as output

    sei(); // Enable interrupts once registers have been update
    }

    void loop()
    {
    }

    void generateWaveData() //Generate a Sawtooth Wave
    {
    for (int i=0; i > 24);
    PORTB = vWaveData[phaseAccumulatorMSB2];
    }

    ISR(TIMER1_COMPA_vect) // Interrupt service run when Timer/Counter1 reaches OCR1A
    {

    phaseAccumulator1 += tuningWordM;
    byte phaseAccumulatorMSB1 = (phaseAccumulator1 >> 24);
    PORTD = vWaveData[phaseAccumulatorMSB1];

    }

    ISR(TIMER0_COMPA_vect) // Interrupt service run when Timer/Counter1 reaches OCR1A
    {
    phaseAccumulator0 += tuningWordM;
    byte phaseAccumulatorMSB0 = (phaseAccumulator0 >> 24);
    PORTC = vWaveData[phaseAccumulatorMSB0];
    }

  2. Didier says:

    Generally speaking, the only idea that crosses my mind is that the interrupt generated by timer0 has the lowest priority (Check Table 11-2. Reset and Interrupt Vectors in ATmega88) versus Timer 2 (8 th position) and Timer 1 (12 th position). May be are you experiencing an interrupts conflict, which is canceled when removing one timer with higher priority?

    Q1: The generateWaveData routine and its use look strange tome! Can you explain?

  3. Mark Jones says:

    I am new to the Arduino and do not understand what to do with your code. Do I put it all in one file and upload to the Arduino? Or, do I load part of it from Processing (assuming that you are using the Processing interface).

    Thanks for the project!!

    Mark

  4. Mark Jones says:

    Thanks, admin, but the link you provided appears to be to a basic overview of Arduino. I am familiar enough with Arduino to have several projects running. I’m just confused about what to do with the various code fragments above.

    Mark

    • admin says:

      OK, I am with you now (BTW admin==Didier)
      I am making public portions of code that I specifically worked on and which bring significant informationto programmers. However, I plan to open a download area to registered users…

Leave a Reply

You must be logged in to post a comment.