Increase MKRZero DAC Frequency

I am fairly new to the Arduino ecosystem and have started working with an MKRZero DAC to generate a sine wave from 0 to 3.3V. The code I am using does not have any delay in the 'loop' section of the code to control the frequency, and hence the default frequency of the sine wave obtained is 4 Hz. I want to increases this frequency to greater than 2000 Hz. How do I achieve the same?

Following is the Arduino code that I am using:

float x = 270; // Current degrees for sine wave (initially 270 so that
// the sine wave is initially zero).

//Setup Function
void setup() {
//Setup DAC/Analog Output
//pinMode(A0, OUTPUT); //Set A0 as a DAC
analogWriteResolution(10); //Change the DAC resolution to 10-bits
analogWrite(A0, 0); // Initialize Dac to Zero
}

//Loop Function
void loop() {
//Local Variables
int output; // Value to output to the DAC

/*

  • Calculate Sine Value
  • The sine wave should be a value between 0 and 1023 (i.e. a 10 bit
  • value). By scaling the output by 512 and offsetting by 512, the value
  • should be between 0 and 1024. Casting to an int appears to limit the
  • value to 1023. To convert degrees to radians, x is multiplied by
  • pi/180 = 0.017453.
    */
    output = (int) (512.0 * sin(0.017453 * x) + 512); // Sine Wave

//Output Analog Value
analogWrite(A0, output); // Write the analog output to A0

x += 0.35;
if(x>=360.0) x -= 360.0;
}

To begin with, floating point calculations are inherently slow on an MCU with no numeric co-processor. Try doing your calculations in integer. For the sine function, use a look up table.

The table values can be calculated once in setup() using float calculations. After that, the frequency will be controlled by the sampling rate, and the rate of change of the index pointer that you use to obtain values from the table (in your code, it's obtusely called 'x').

You haven't specified much about the output waveform requirements. "above 2000 Hz" is pretty vague. Range? Accuracy? Frequency resolution?

Hey, I redesigned the code to generate the sine wave function using a lookup table instead of a function. Here's the code:

//Global Variables

int sinfunc[121]={512, 539, 566, 592, 618, 645, 670, 695, 720, 744, 768, 791, 813, 834, 855, 874, 892, 910, 926, 941, 955, 968, 980, 990, 999, 1007, 1013, 1018, 1021, 1023, 1024, 1023, 1021, 1018, 1013, 1007, 999, 990, 980, 968, 955, 941, 926, 910, 893, 874, 855, 834, 813, 791, 768, 744, 720, 696, 670, 645, 618, 592, 566, 539, 512, 485, 459, 432, 406, 380, 354, 329, 304, 280, 256, 233, 211, 190, 169, 150, 132, 114, 98, 83, 69, 56, 44, 34, 25, 17, 11, 6, 3, 1, 0, 1, 3, 6, 11, 17, 25, 34, 44, 56, 69, 83, 98, 114, 131, 150, 169, 190, 211, 233, 256, 280, 304, 328, 354, 379, 405, 432, 458, 485, 512};
int i;
int output;

void setup() {
  //Setup DAC/Analog Output
  //pinMode(A0, OUTPUT);        //Set A0 as a DAC  
  Serial.begin(9600);
  analogWriteResolution(10);  //Change the DAC resolution to 10-bits
  analogWrite(A0, 0);         // Initialize Dac  to Zero
}


void loop() {
  
//  int output; // Value to output to the DAC 
  for(i=0; i<121; i++){
  analogWrite(A0, sinfunc[i]); // Write the analog output to A0
  delayMicroseconds(10000);
  }
}

This code works fine until the delayMicroseconds is above 10,000. If I start to reduce the delayMicroseconds time below 10000, the range of the sine wave starts reducing from 0-3.3 V i.e., the signal starts becoming flatter. When the delay period is reduced below 500 microsecs, the wave becomes completely flat and registers a constant voltage of 1.6V.

My desired sine wave has a range of 0-3.3V and a frequency of 1600 Hz. I don't have any strict requirements for accuracy and frequency resolution as of now. What improvements shall I make to the code to increase the frequency even further? I am guessing I might be overlooking some kind f preset for the sampling rate.

Thank you!

As far as I can see, it should work. How are you measuring the output? Scope?

By the way, can you please post your code in code tags, according to the forum guidelines?

Also, you have a discontinuity (bump) in your sin table because the first and last values are the same. You should delete the last value, 512.

Edit - oh wait - I think I see the problem. You have probably exceeded the allowable conversion rate of the DAC. That's because as you increase the frequency, you still use all 121 (and as I pointed out, it should be 120) elements of the LUT.

To increase the frequency, you have to work backwards from the maximum conversion rate and modify the step index increment so it is greater than 1. Changing the step increment 'i' will change the frequency because it changes the rate at which you sample the sine function.

Notice that if the increment is not an exact divisor of 121 (120 please...) then you have to keep the remainder and add it when you exceed 121 (120!) when reaching the end of the LUT. A simple 'for' loop won't do any more. You need to re-think that.

To reproduce 1600 Hz using the entire table, requires sampling at 1600x120 = 192,000 Hz. The Zero DAC probably can't do that.

I tried looking it up, but the product page lacks specific details about the DAC.

const int LUT_SIZE = 120;
// declare LUT here...

void loop() {
i += 7;
if (i >= LUT_SIZE)
  {
  i -= LUT_SIZE
  }
analogWrite(A0, sinfunc[i]); // Write the analog output to A0
delayMicroseconds(66); //hopefully a 15k samples/sec
}

Thanks for your prompt responses aarg! I am checking the output using a multimeter and if the range is correct, then a scope.

I modified my code as suggested by you:

//Global Variables

int sinfunc[120]={512, 539, 566, 592, 618, 645, 670, 695, 720, 744, 768, 791, 813, 834, 855, 874, 892, 910, 926, 941, 955, 968, 980, 990, 999, 1007, 1013, 1018, 1021, 1023, 1024, 1023, 1021, 1018, 1013, 1007, 999, 990, 980, 968, 955, 941, 926, 910, 893, 874, 855, 834, 813, 791, 768, 744, 720, 696, 670, 645, 618, 592, 566, 539, 512, 485, 459, 432, 406, 380, 354, 329, 304, 280, 256, 233, 211, 190, 169, 150, 132, 114, 98, 83, 69, 56, 44, 34, 25, 17, 11, 6, 3, 1, 0, 1, 3, 6, 11, 17, 25, 34, 44, 56, 69, 83, 98, 114, 131, 150, 169, 190, 211, 233, 256, 280, 304, 328, 354, 379, 405, 432, 458, 485};
int i;
const int LUT_SIZE = 120;
void setup() {
  //Setup DAC/Analog Output
  //pinMode(A0, OUTPUT);        //Set A0 as a DAC  
  Serial.begin(9600);
  analogWriteResolution(10);  //Change the DAC resolution to 10-bits
  analogWrite(A0, 0);         // Initialize Dac  to Zero
}


void loop() {
i += 1;
if (i >= LUT_SIZE)
  {
  i -= LUT_SIZE;
  }
analogWrite(A0, sinfunc[i]); // Write the analog output to A0
delayMicroseconds(750); //hopefully a 15k samples/sec
}

When I set the increment to 1, the voltage range is 1.5 to 2.25 V with 750 microseconds delay. however, when I increase the increment to 6 with the same delay, the voltage becomes constant at 1.5V. I even rechecked this output with a scope just to make sure it's not exceeding the sampling rate of my multimeter. The scope, too, shows a flat line at 1.5V. I wonder why this is happening.

Well, I have some thoughts... but really the cart is before the horse here. The proper way is to find out the exact specifications of the DAC and the core interface to it. I'm not motivated to do that myself because I don't own the board.

Are you sure you have the scope timebase set low enough to capture an entire cycle?

Also at 750 uS, and an increment of 1, the output frequency would be very low, in the range of 10Hz.

F = 1/(t * s) where F is the frequency, t is the sample interval, s is the number of samples per cycle.

Ignoring loop() overhead, the cycle time is 0.00075*120 = 0.09 thus the frequency is 11 Hz.
Along the same lines, if the increment is 6, then the frequency should be near 66 Hz.

I don't know why you picked 750, probably just throwing darts and hoping it will magically start working? Anyway, you would need much less delay than that, in order to maintain a practical sample rate for the higher frequencies you want to achieve.

@vedsoni1996 As aarg mentions, how you implement this depends on your requirements.

One issue is that your using the loop() to time your analogWrite() function sample rate. A better way to generate the sample rate might be to use one of the microcontroller's timers to generate a regular interrupt.

If you require a fixed frequency sine wave then it's possible use a timer to generate the sample rate and waveform look up table size that meets your requirements. A fixed frequency sine wave can also be generated by using the microcontroller's DMA to move wave table values from memory to the DAC without CPU intervention.

If you require a variable frequency waveform then it's possible to generate this using linear interpolation in conjuction with the wave table, again employing a timer to knock out a fixed sample rate.

Actually, a variable frequency can be generated also with the timer interrupt. You just move the phase pointer at different rates as I did in the loop() example where I interpolated by the demo value 7. Either the phase increment, or the sine values, or both, can be interpolated.

Hello @aarg and @Martin L! Yes, earlier I was setting the delay value just to see if the MKRZero DAC does produce sine waves with that high frequencies. However, since that approach didn't provide me the desired results, I looked up a sine wave generation code using timers on YouTube:

//This sketch generat es a sine wave on the Arduino Zero DAC based on user entered sample count and sample rate
//It was used in a tutorial video on the ForceTronics YouTube Channel. This code can be used and modified freely
//at the users own risk
volatile int sIndex; //Tracks sinewave points in array
int sampleCount = 100; // Number of samples to read in block
int *wavSamples; //array to store sinewave points
uint32_t sampleRate = 1000; //sample rate of the sine wave

void setup() {
  analogWriteResolution(10); //set the Arduino DAC for 10 bits of resolution (max)
  getSinParameters(); //get sinewave parameters from user on serial monitor
  
  /*Allocate the buffer where the samples are stored*/
  wavSamples = (int *) malloc(sampleCount * sizeof(int));
  genSin(sampleCount); //function generates sine wave
}

void loop() {
  sIndex = 0;   //Set to zero to start from beginning of waveform
  tcConfigure(sampleRate); //setup the timer counter based off of the user entered sample rate
  //loop until all the sine wave points have been played
  while (sIndex<sampleCount)
  { 
 //start timer, once timer is done interrupt will occur and DAC value will be updated
    tcStartCounter(); 
  }
  //disable and reset timer counter
  tcDisable();
  tcReset();
}

//This function generates a sine wave and stores it in the wavSamples array
//The input argument is the number of points the sine wave is made up of
void genSin(int sCount) {
 const float pi2 = 6.28; //2 x pi
 float in; 
 
 for(int i=0; i<sCount;i++) { //loop to build sine wave based on sample count
  in = pi2*(1/(float)sCount)*(float)i; //calculate value in radians for sin()
  wavSamples[i] = ((int)(sin(in)*511.5 + 511.5)); //Calculate sine wave value and offset based on DAC resolution 511.5 = 1023/2
 }
}

//This function handles getting and setting the sine wave parameters from 
//the serial monitor. It is important to use the Serial.end() function
//to ensure it doesn't mess up the Timer counter interrupts later
void getSinParameters() {
  Serial.begin(9600);
  //Serial.println("Enter number of points in sine wave (range 10 to 1000)");
  //sampleCount = readParameter();
  sampleCount = 100;
  //if (sampleCount < 10 || sampleCount > 1000) sampleCount = 100;
  //Serial.print("Sample count set to ");
  //Serial.println(sampleCount);
  //Serial.println("Enter sample rate or samples per second for DAC (range 1 to 100k)");
  sampleRate = 100000;
  //if (sampleRate < 1 || sampleRate > 100000) sampleRate = 10000;
  //Serial.print("Sample rate set to ");
  //Serial.println(sampleRate);
  //Serial.println("Generating sine wave........");
  Serial.end();
}

//waits for serial data and reads it in. This function reads in the parameters
// that are entered into the serial terminal
//int readParameter() {
// while(!Serial.available());
// return Serial.parseInt(); //get int that was entered on Serial monitor
//}

// Configures the TC to generate output events at the sample frequency.
//Configures the TC in Frequency Generation mode, with an event output once
//each time the audio sample frequency period expires.
 void tcConfigure(int sampleRate)
{
 // Enable GCLK for TCC2 and TC5 (timer counter input clock)
 GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCM_TC4_TC5)) ;
 while (GCLK->STATUS.bit.SYNCBUSY);

 tcReset(); //reset TC5

 // Set Timer counter Mode to 16 bits
 TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;
 // Set TC5 mode as match frequency
 TC5->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;
 //set prescaler and enable TC5
 TC5->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1 | TC_CTRLA_ENABLE;
 //set TC5 timer counter based off of the system clock and the user defined sample rate or waveform
 TC5->COUNT16.CC[0].reg = (uint16_t) (SystemCoreClock / sampleRate - 1);
 while (tcIsSyncing());
 
 // Configure interrupt request
 NVIC_DisableIRQ(TC5_IRQn);
 NVIC_ClearPendingIRQ(TC5_IRQn);
 NVIC_SetPriority(TC5_IRQn, 0);
 NVIC_EnableIRQ(TC5_IRQn);

 // Enable the TC5 interrupt request
 TC5->COUNT16.INTENSET.bit.MC0 = 1;
 while (tcIsSyncing()); //wait until TC5 is done syncing 
} 

//Function that is used to check if TC5 is done syncing
//returns true when it is done syncing
bool tcIsSyncing()
{
  return TC5->COUNT16.STATUS.reg & TC_STATUS_SYNCBUSY;
}

//This function enables TC5 and waits for it to be ready
void tcStartCounter()
{
  TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE; //set the CTRLA register
  while (tcIsSyncing()); //wait until snyc'd
}

//Reset TC5 
void tcReset()
{
  TC5->COUNT16.CTRLA.reg = TC_CTRLA_SWRST;
  while (tcIsSyncing());
  while (TC5->COUNT16.CTRLA.bit.SWRST);
}

//disable TC5
void tcDisable()
{
  TC5->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
  while (tcIsSyncing());
}

void TC5_Handler (void)
{
  analogWrite(A0, wavSamples[sIndex]);
  sIndex++;
  TC5->COUNT16.INTFLAG.bit.MC0 = 1;
}

I'm not too familiar with Arduino coding, so I have only done a few simple modifications to the code so as to remove the need for user Input and have directly fed the sine wave generation parameters in the code. In the video that I referenced, all the parameters have been set exactly the same, and they are able to generate a sine wave with 1000 Hz frequency on an Arduino Zero.

However, when I implemented this with my MKRZero, I still got a constant voltage of 1.6V and no sinusoidal behavior. Following is the image of the output waveform on the scope:

The timebase is set to 2 ms/div and voltage base to 1V/div. Since the wave frequncy is 1000 Hz, i.e. period is 1 ms, there should have been a full sine wave in the first division itself, but however, the scope shows a flat line. I'm going to repeat this procedure with another MKRZero board just to be sure. Also, my board is in perpetual bootloader mode currently as it was being rendered unrecognizable by an old erronous code.

Do you have any thoughts/suggestions to offer in this situation?

Well, that is some stuff to digest and I will... but I'm still curious what Arduino has to say about it in the documentation.

Also now there are two sketches and two boards, original/modified and Zero/MKRZero. It's getting a little confusing so can you clarify which combinations actually worked?

...and... walk before you run... have you tried writing a fixed value to the DAC and reading a fixed DC value?

There is a lot of overhead in the analogWrite()... I wonder if it limits the bandwidth...

void analogWrite(pin_size_t pin, int value)
{
  PinDescription pinDesc = g_APinDescription[pin];
  uint32_t attr = pinDesc.ulPinAttribute;

  if ((attr & PIN_ATTR_ANALOG) == PIN_ATTR_ANALOG)
  {
    // DAC handling code

    if ((pinDesc.ulADCChannelNumber != ADC_Channel0) && (pinDesc.ulADCChannelNumber != DAC_Channel0)) { // Only 1 DAC on AIN0 / PA02
      return;
    }

    value = mapResolution(value, _writeResolution, 10);

    syncDAC();
    DAC->DATA.reg = value & 0x3FF;  // DAC on 10 bits.
    syncDAC();
    DAC->CTRLA.bit.ENABLE = 0x01;     // Enable DAC
    syncDAC();
    return;
  }

@vedsoni1996 Using the Forcetronics code, I can also produce a 1kHz sine wave on my Arduino Zero.

Do you have any additional analog circuitry connected to your DAC0 output on you MKRZero? It's just that the DAC output on the SAMD21 has a minimum resistive load of 5kΩ.