Maximum interrupt frequency

Hi guys! I'm working on an Arduino m0 pro. I would like to know what is the maximum frequency for an interrupts. I would like to develop a system that, thanks to the interrupt overflow routine, outputs different values at each interrupt. Till now I followed this topic:

I want to do the same thing but with frequency of 25kHz, I tried to do it but in output I obtain just a constant signal. Is it possible to reach the frequency of 25kHz with an Arduino M0 pro?
In order to obtain an output every 39.06 microseconds (25kHz) what formula should I use?

This is my code:

int i = 0;
int sintable[256] PROGMEM={512,524,537,549,562,574,587,599,611,624,636,648,660,672,684,696,707,719,30,741,753,764,774,785,796,806,816,826,836,846,855,864,873,882,890,899,907,915,922,930,937,944,950,957,963,968,974,979,984,989,993,997,1001,1004,1008,1011,1013,1015,1017,1019,1021,1022,1022,1023,1023,1023,1022,1022,1021,1019,1017,1015,1013,1011,1008,1004,1001,997,993,989,984,979,974,968,963,957,950,944,937,930,922,915,907,899,890,882,873,864,855,846,836,826,816,806,796,785,774,764,753,741,730,719,707,696,684,672,660,648,636,624,611,599,587,574,562,549,537,524,512,499,486,474,461,449,436,424,412,399,387,375,363,351,339,327,316,304,293,282,270,259,249,238,227,217,207,197,187,177,168,159,150,141,133,124,116,108,101,93,86,79,73,66,60,55,49,44,39,34,30,26,22,19,15,12,10,8,6,4,2,1,1,0,0,0,1,1,2,4,6,8,10,12,15,19,22,26,30,34,39,44,49,55,60,66,73,79,86,93,101,108,116,124,133,141,150,159,168,177,187,197,207,217,227,238,249,259,270,282,293,304,316,327,339,351,363,375,387,399,412,424,436,449,461,474,486,499};

void setup() {
    analogWriteResolution(10);
    Serial.begin(9600);    // Set up the generic clock (GCLK4) used to clock timers
    REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                      GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
    while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

    REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                       GCLK_GENCTRL_GENEN |         // Enable GCLK4
                       GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
    GCLK_GENCTRL_ID(4);          // Select GCLK4
    while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Feed GCLK4 to TC4 and TC5
        REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TC4 and TC5
                           GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                           GCLK_CLKCTRL_ID_TC4_TC5;     // Feed the GCLK4 to TC4 and TC5
    while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

    REG_TC4_COUNT16_CC0 = 0xEA;                    // Set the TC4 CC0 register as the TOP value in match frequency mode
    while (TC4->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for synchronization

    NVIC_SetPriority(TC4_IRQn, 0);                  // Set the Nested Vector Interrupt Controller (NVIC) priority for TC4 to 0 (highest)
    NVIC_EnableIRQ(TC4_IRQn);                       // Connect TC4 to Nested Vector Interrupt Controller (NVIC)

    REG_TC4_INTFLAG |= TC_INTFLAG_OVF;              // Clear the interrupt flags
    REG_TC4_INTENSET = TC_INTENSET_OVF;             // Enable TC4 interrupts

    REG_TC4_CTRLA |= TC_CTRLA_PRESCALER_DIV8 |      // Set prescaler to 1024, 48MHz/1024 = 46.875kHz
                     TC_CTRLA_WAVEGEN_MFRQ |        // Put the timer TC4 into match frequency (MFRQ) mode 
                     TC_CTRLA_ENABLE;               // Enable TC4
    while (TC4->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for synchronization
}

void loop() {}

void TC4_Handler()                                // Interrupt Service 
Routine (ISR) for timer TC4
{     
  // Check for overflow (OVF) interrupt
  if (TC4->COUNT16.INTFLAG.bit.OVF && TC4->COUNT16.INTENSET.bit.OVF){ 
    Serial.print(sintable[i]);
    Serial.print("\t");
    Serial.print(i);
    Serial.print("\n");
    analogWrite(A0,sintable[i]);
    i++;
    if(i==256)
      i=0;
    REG_TC4_INTFLAG = TC_INTFLAG_OVF;         // Clear the OVF interrupt flag 
  }
}

Hi birdm3n,

The interrupts are capable of exceeding 25kHz.

The following code shows the use of timer TCC0, (rather than TC4), that outputs a 25kHz, 50% duty cycle on digital pin D7. The code also includes the TCC0 interrupt service routine that triggers on the timer's overflow:

// Output 25kHz single slope PWM with timer TCC0 on D7
void setup()
{
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Enable the port multiplexer for the digital pins D7
  PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
 
  // Connect the TCC0 timer to the port output D7 - port pins are paired odd PMUXO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[6].ulPort].PMUX[g_APinDescription[6].ulPin >> 1].reg |= PORT_PMUX_PMUXO_F; //| PORT_PMUX_PMUXE_F;

  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Normal (single slope) PWM operation: timers countinuously count up to PER register value and then is reset to 0
  REG_TCC0_WAVE |= TCC_WAVE_WAVEGEN_NPWM;         // Setup single slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);                // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  REG_TCC0_PER = 1919;                            // Set the frequency of the PWM on TCC0 to 25kHz
  while(TCC0->SYNCBUSY.bit.PER);                  // Wait for synchronization
 
  // Set the PWM signal to output 50% duty cycle on D7
  REG_TCC0_CCB3 = 959;                            // TCC0 CCB3 - on output on D7 50% duty cycle
  while(TCC0->SYNCBUSY.bit.CCB3);                 // Wait for synchronization

  NVIC_SetPriority(TCC0_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC0 to 0 (highest)
  NVIC_EnableIRQ(TCC0_IRQn);         // Connect TCC0 to Nested Vector Interrupt Controller (NVIC)

  REG_TCC0_INTENSET = TC_INTENSET_OVF;            // Enable TCC0 overflow interrupt

  // Divide the 48MHz signal by 1 giving 48MHz (20.83ns) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop() {}

// Ineterrupt handler called every time the TCC0 timer overflows
void TCC0_Handler()                              // Interrupt Service Routine (ISR) for timer TCC0
{     
  // Check for overflow (OVF) interrupt
  if (TCC0->INTFLAG.bit.OVF && TCC0->INTENSET.bit.OVF)             
  {
    // Put your timer overflow (OVF) code here:     
    // ...
    
    REG_TCC0_INTFLAG = TC_INTFLAG_OVF;         // Clear the OVF interrupt flag
  }
}

To change the duty cycle just load the CC3B register with a number between 0 and the value PER register.

In this case the equation for the PWM frequency is:

PWM frequency = 48MHz / (PER + 1)

It works, thanks! How many counters can I use simultaneously with Arduino M0 pro?

Hi birdm3n,

Are you trying to generate a 100Hz sine wave with a 40us resolution on the DAC output?

There's a simple waveform generator example for the Arduino Due, that I would imagine is equally valid for the Zero here: https://www.arduino.cc/en/Tutorial/DueSimpleWaveformGenerator.

There's also a Youtube video here: Arduino Zero DAC Overview and Waveform Generator Example - YouTube.

Going back to your original code, to trigger the TC4 timer at 25kHz requires setting it to Match Frequency (MFRQ) mode with the following code:

// Set timer TC4 to call the TC4_Handler every 40us (25kHz)
void setup() {
  // Set up the generic clock (GCLK4) used to clock timers
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Feed GCLK4 to TC4 and TC5
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TC4 and TC5
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TC4_TC5;     // Feed the GCLK4 to TC4 and TC5
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization
 
  REG_TC4_COUNT16_CC0 = 1919;                     // Set the TC4 CC0 register as the TOP value in match frequency mode
  while (TC4->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for synchronization

  NVIC_SetPriority(TC4_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TC4 to 0 (highest)
  NVIC_EnableIRQ(TC4_IRQn);         // Connect TC4 to Nested Vector Interrupt Controller (NVIC)

  REG_TC4_INTENSET = TC_INTENSET_OVF;             // Enable TC4 interrupts
 
  REG_TC4_CTRLA |= TC_CTRLA_PRESCALER_DIV1 |      // Set prescaler to 1, 48MHz/1 = 48MHz
                   TC_CTRLA_WAVEGEN_MFRQ |        // Put the timer TC4 into match frequency (MFRQ) mode 
                   TC_CTRLA_ENABLE;               // Enable TC4
  while (TC4->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for synchronization
}

void loop() {}

void TC4_Handler()                              // Interrupt Service Routine (ISR) for timer TC4
{     
  // Check for overflow (OVF) interrupt
  if (TC4->COUNT16.INTFLAG.bit.OVF && TC4->COUNT16.INTENSET.bit.OVF)             
  {
    // Put your timer overflow (OVF) code here:     
    // ...
   
    REG_TC4_INTFLAG = TC_INTFLAG_OVF;         // Clear the OVF interrupt flag
  }
}

This will call the TC4_Handler() ISR every 40us. Note, with this example they'll be no output on D7.

All of the SAMD21's timers can be used simultaneously. The millis(), micros() and delay() functions use the Systick timer, leaving timers: TCC0, TCC1, TCC2, TC3, TC4 and TC5 free.

Hi birdm3n,

Glad to hear you got it going.

If you're using timer TCC0 to trigger the interrupts, but don't want the 25kHz PWM output on D7 then just comment out the PMUX and PINCFG lines of code. This will turn D7 back to GPIO.

Thanks for the hints! By the way if I execute your last code I obtain a sine wave of 29Hz, how is it possible?

The first snippet of code you provided works also without using pin D7, just output the value of the DAC I can observe a clear sine wave. In that case if I want to change the frequency I just need to modify the PER value and the value of the duty cycle, right?

Hi birdm3n,

Thanks for the hints! By the way if I execute your last code I obtain a sine wave of 29Hz, how is it possible?

Because I made a small mistake in the code. I accidentally left "0x" in front of the 1919 value used to load the CC0 register, this causes the number to be hexidecimal rather than decimal.

If you just remove the "0x" from the line, it should function properly:

REG_TC4_COUNT16_CC0 = 1919;                   // Set the TC4 CC0 register as the TOP value in match frequency mode

Thanks for pointing this out, I've corrected it in the example.

In that case if I want to change the frequency I just need to modify the PER value and the value of the duty cycle, right?

If you're using the TCCx registers and want to change the frequency during operation, then it's usually best to use the buffered period register (PERB). This causes the change in frequency to occur at the end of the timer cycle:

REG_TCC0_PERB = 1919;                            // Set the frequency of the PWM on TCC0 to 25kHz
while(TCC0->SYNCBUSY.bit.PERB);                  // Wait for synchronization

Hi Martin, in case I would like to generate a sine wave of 100Hz using PWM what should I modify? I need to use the PWM because I need 4 sine waves that have the same frequency of 100Hz and the DAC which I used till now is too slow due to the i2c communication. I used the code that you posted here and added some other lines, can you help me to modify it in order to substitute the dac function with the PWM sine wave generation?

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_MCP4725.h>
#include <Adafruit_MMA8451.h>
// Output 25kHz single slope PWM with timer TCC0 on D7

volatile int flag = 0;
int ind = -1;

Adafruit_MCP4725 dac;

#define DAC_RESOLUTION (10)

volatile int sintable[256] PROGMEM = {
    512,524,537,549,562,574,587,599,611,624,636,648,660,672,684,696,707,719,730,741,753,764,774,785,796,806,816,826,836,846,855,864,873,882,890,899,907,915,922,930,
937,944,950,957,963,968,974,979,984,989,993,997,1001,1004,1008,1011,1013,1015,1017,1019,1021,1022,1022,1023,1023,1023,1022,1022,1021,1019,1017,1015,1013,1011,1008,1004,1001,997,993,989,
984,979,974,968,963,957,950,944,937,930,922,915,907,899,890,882,873,864,855,846,836,826,816,806,796,785,774,764,753,741,730,719,707,696,684,672,660,648,636,624,
611,599,587,574,562,549,537,524,512,499,486,474,461,449,436,424,412,399,387,375,363,351,339,327,316,304,293,282,270,259,249,238,227,217,207,197,187,177,168,159,
150,141,133,124,116,108,101,93,86,79,73,66,60,55,49,44,39,34,30,26,22,19,15,12,10,8,6,4,2,1,1,0,0,0,1,1,2,4,6,8,
10,12,15,19,22,26,30,34,39,44,49,55,60,66,73,79,86,93,101,108,116,124,133,141,150,159,168,177,187,197,207,217,227,238,249,259,270,282,293,304,
316,327,339,351,363,375,387,399,412,424,436,449,461,474,486,499
};


void setup()
{ 
  ind = i+128;
  Wire.begin();
  Wire.setClock(400000);
  Serial.begin(9600);
  dac.begin(0x62);
  
  Serial.println("MCP4725A found!");

  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Normal (single slope) PWM operation: timers countinuously count up to PER register value and then is reset to 0
  REG_TCC0_WAVE |= TCC_WAVE_WAVEGEN_NPWM;         // Setup single slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);                // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  REG_TCC0_PER = 1919;                            // Set the frequency of the PWM on TCC0 to 25kHz
  while(TCC0->SYNCBUSY.bit.PER);                  // Wait for synchronization
 
  // Set the PWM signal to output 50% duty cycle on D7
  REG_TCC0_CCB3 = 959;                            // TCC0 CCB3 - on output on D7 50% duty cycle
  while(TCC0->SYNCBUSY.bit.CCB3);                 // Wait for synchronization

  NVIC_SetPriority(TCC0_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC0 to 0 (highest)
  NVIC_EnableIRQ(TCC0_IRQn);         // Connect TCC0 to Nested Vector Interrupt Controller (NVIC)

  REG_TCC0_INTENSET = TC_INTENSET_OVF;            // Enable TCC0 overflow interrupt

  // Divide the 48MHz signal by 1 giving 48MHz (20.83ns) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization  
}

void loop() {
  if(flag){
    dac.setVoltage(pgm_read_word(&(sintable[ind])), false);
    flag = 0;
  }
}

// Ineterrupt handler called every time the TCC0 timer overflows
void TCC0_Handler()                              // Interrupt Service Routine (ISR) for timer TCC0
{     
  // Check for overflow (OVF) interrupt
  if (TCC0->INTFLAG.bit.OVF && TCC0->INTENSET.bit.OVF)             
  { 
    flag = 1;
    ind++;
    if(ind == 256)
      ind = 0;
    REG_TCC0_INTFLAG = TC_INTFLAG_OVF;         // Clear the OVF interrupt flag
  }
}

Hi birdm3n,

I'm working on a solution.

The issue is that the SAMD21 is so busy servicing the TCC0 interrupt service routine at 25kHz that it isn't able to run the loop() in a timely manner.

The solution is to use the TCC0 timer overflow to instead trigger the SAMD21's Direct Memory Access Controller (DMAC) and get the DMAC to load the sine table duty cycles into the timer's counter compare register. The DMAC's descriptor that describes the transfer can be set up to be circular, so that when it reaches the end of the table it will return back to the beginning and therefore operate continuously. With an RC analog low pass filter on the output, this should enable a sine wave output to be produced autonomously without any CPU intervention.

Thanks Martin! I tried to use this code but it does not work, the frequency of the sine waves is too low maybe because as you said the samd21 is not able to run everything correctly. This is the code:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_MCP4725.h>
#include <Adafruit_MMA8451.h>
// Output 25kHz single slope PWM with timer TCC0 on D7
volatile int i = 0;
uint16_t max_z = 0;
volatile float reduce = 1;
volatile const float max_value = 6000.00;
volatile float output;
volatile int flag = 0;
volatile int j = 127;

volatile int sintable[256] PROGMEM = {
    512,524,537,549,562,574,587,599,611,624,636,648,660,672,684,696,707,719,730,741,753,764,774,785,796,806,816,826,836,846,855,864,873,882,890,899,907,915,922,930,
937,944,950,957,963,968,974,979,984,989,993,997,1001,1004,1008,1011,1013,1015,1017,1019,1021,1022,1022,1023,1023,1023,1022,1022,1021,1019,1017,1015,1013,1011,1008,1004,1001,997,993,989,
984,979,974,968,963,957,950,944,937,930,922,915,907,899,890,882,873,864,855,846,836,826,816,806,796,785,774,764,753,741,730,719,707,696,684,672,660,648,636,624,
611,599,587,574,562,549,537,524,512,499,486,474,461,449,436,424,412,399,387,375,363,351,339,327,316,304,293,282,270,259,249,238,227,217,207,197,187,177,168,159,
150,141,133,124,116,108,101,93,86,79,73,66,60,55,49,44,39,34,30,26,22,19,15,12,10,8,6,4,2,1,1,0,0,0,1,1,2,4,6,8,
10,12,15,19,22,26,30,34,39,44,49,55,60,66,73,79,86,93,101,108,116,124,133,141,150,159,168,177,187,197,207,217,227,238,249,259,270,282,293,304,
316,327,339,351,363,375,387,399,412,424,436,449,461,474,486,499
};


void setup()
{ 
  pinMode(3, OUTPUT);
  analogWriteResolution(10);

  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  
  PORT->Group[g_APinDescription[3].ulPort].PINCFG[g_APinDescription[3].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[4].ulPort].PMUX[g_APinDescription[4].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F; 


  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Normal (single slope) PWM operation: timers countinuously count up to PER register value and then is reset to 0
  REG_TCC0_WAVE |= TCC_WAVE_WAVEGEN_NPWM;         // Setup single slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);                // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  REG_TCC0_PER = 1919;                            // Set the frequency of the PWM on TCC0 to 25kHz
  while(TCC0->SYNCBUSY.bit.PER);                  // Wait for synchronization
 
  // Set the PWM signal to output 50% duty cycle on D7
  REG_TCC0_CCB3 = 959;                            // TCC0 CCB3 - on output on D7 50% duty cycle
  while(TCC0->SYNCBUSY.bit.CCB3);                 // Wait for synchronization

  NVIC_SetPriority(TCC0_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC0 to 0 (highest)
  NVIC_EnableIRQ(TCC0_IRQn);         // Connect TCC0 to Nested Vector Interrupt Controller (NVIC)

  REG_TCC0_INTENSET = TC_INTENSET_OVF;            // Enable TCC0 overflow interrupt

  // Divide the 48MHz signal by 1 giving 48MHz (20.83ns) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization  
}

void loop() {
  analogWrite(3,sintable[j]);
  j++;
  if(j==256)
    j = 0;
}

// Ineterrupt handler called every time the TCC0 timer overflows
void TCC0_Handler()                              // Interrupt Service Routine (ISR) for timer TCC0
{     
  // Check for overflow (OVF) interrupt
  if (TCC0->INTFLAG.bit.OVF && TCC0->INTENSET.bit.OVF)             
  { 
    analogWrite(A0,sintable[i]);
    
    i++;
    j++;
    if(i == 256){
      i = 0;
    }
    if(j == 256){
      j = 0;
    }
    REG_TCC0_INTFLAG = TC_INTFLAG_OVF;         // Clear the OVF interrupt flag
  }
}

Hi birdm3n,

Ok, here's the code.

It configures timer TCC0 to output 25kHz PWM on digtial pin D7 as normal, but instead uses the DMAC to load the timer's counter compare (CC3) register with the current sine table duty cycle value each time it overflows. The DMAC then automatically increments the location of the next sine table value. The DMAC's descriptor is set-up to be cyclic so that when it reaches the end of the sine table it starts over from the beginning again. It does this without CPU intervention, in other words the loop() function is empty. This releases the SAMD21's CPU from the burden of having to service the TCC0 interrupt service routine at 25kHz.

The PWM output on D7 is then fed into a simple RC low pass filter with R=10kOhms and C=100nF (0.1uF), this generates a sine wave at 98Hz and an amplitude of about 1.8V peak to peak.

Is that the sort of signal you're expecting to see?

// Direct Digital Synthesis - generate 100Hz sine wave from 25kHz PWM signal plus RC LPF on output

// Sine table
volatile const uint16_t sintable[256] = {
  512,524,537,549,562,574,587,599,611,624,636,648,660,672,684,696,707,719,730,741,753,764,774,785,796,806,816,826,836,846,855,864,873,882,890,899,907,915,922,930,
937,944,950,957,963,968,974,979,984,989,993,997,1001,1004,1008,1011,1013,1015,1017,1019,1021,1022,1022,1023,1023,1023,1022,1022,1021,1019,1017,1015,1013,1011,1008,1004,1001,997,993,989,
984,979,974,968,963,957,950,944,937,930,922,915,907,899,890,882,873,864,855,846,836,826,816,806,796,785,774,764,753,741,730,719,707,696,684,672,660,648,636,624,
611,599,587,574,562,549,537,524,512,499,486,474,461,449,436,424,412,399,387,375,363,351,339,327,316,304,293,282,270,259,249,238,227,217,207,197,187,177,168,159,
150,141,133,124,116,108,101,93,86,79,73,66,60,55,49,44,39,34,30,26,22,19,15,12,10,8,6,4,2,1,1,0,0,0,1,1,2,4,6,8,
10,12,15,19,22,26,30,34,39,44,49,55,60,66,73,79,86,93,101,108,116,124,133,141,150,159,168,177,187,197,207,217,227,238,249,259,270,282,293,304,
316,327,339,351,363,375,387,399,412,424,436,449,461,474,486,499
};

typedef struct                                                                // DMAC descriptor structure
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint32_t srcaddr;
  uint32_t dstaddr;
  uint32_t descaddr;
} dmacdescriptor ;

volatile dmacdescriptor wrb[12] __attribute__ ((aligned (16)));               // Write-back DMAC descriptors
dmacdescriptor descriptor_section[12] __attribute__ ((aligned (16)));         // DMAC channel descriptors
dmacdescriptor descriptor __attribute__ ((aligned (16)));                     // Place holder descriptor

void setup()
{
  DMAC->BASEADDR.reg = (uint32_t)descriptor_section;                // Set the descriptor section base address
  DMAC->WRBADDR.reg = (uint32_t)wrb;                                // Set the write-back descriptor base adddress
  DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);      // Enable the DMAC and priority levels
 
  DMAC->CHID.reg = DMAC_CHID_ID(0);                                 // Select DMAC channel 0
  // Set DMAC channel 0 to priority level 0 (lowest), to trigger on TCC0 overflow and to trigger every beat
  DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) | DMAC_CHCTRLB_TRIGSRC(TCC0_DMAC_ID_OVF) | DMAC_CHCTRLB_TRIGACT_BEAT; 
  descriptor.descaddr = (uint32_t)&descriptor_section[0];                 // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&sintable[0] + 256 * sizeof(uint16_t);   // Read the current value in the sine table
  descriptor.dstaddr = (uint32_t)&TCC0->CC[3].reg;                        // Copy it into the TCC0 counter comapare 0 register
  descriptor.btcnt = 256;                                                 // This takes the number of sine table entries = 256 beats
  descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_VALID;   // Copy 16-bits (HWORD), increment the source and flag discriptor as valid
  memcpy(&descriptor_section[0], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 0 descriptor
  
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                    GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                     GCLK_GENCTRL_GENEN |         // Enable GCLK4
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                     GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Enable the port multiplexer for the digital pins D7
  PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
 
  // Connect the TCC0 timer to the port output D7 - port pins are paired odd PMUXO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[7].ulPort].PMUX[g_APinDescription[7].ulPin >> 1].reg |= PORT_PMUX_PMUXO_F; //| PORT_PMUX_PMUXE_F;

  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Normal (single slope) PWM operation: timers countinuously count up to PER register value and then is reset to 0
  REG_TCC0_WAVE |= TCC_WAVE_WAVEGEN_NPWM;         // Setup single slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);                // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  REG_TCC0_PER = 1919;                            // Set the frequency of the PWM on TCC0 to 25kHz
  while(TCC0->SYNCBUSY.bit.PER);                  // Wait for synchronization
 
  // Divide the 48MHz signal by 1 giving 48MHz (20.83ns) TCC0 timer tick and enable the outputs
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization

  DMAC->CHID.reg = DMAC_CHID_ID(0);               // Select DMAC channel 0
  DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;       // Enable DMAC channel 0
}

void loop() {}

I have to use this signal as input to different audio exciters. The signals pass through the audio amplifiers and then they go to the exciters. I think that 1.8V is enough also because I can increase the gain with the amplifier. In case I need more voltage, is it possible to increase with this configuration? Moreover if I need more than a signal, let’s suppose on other 3 pins, should I modify only the part in which the code attachs the signals to the pins? Thanks for your help! One last thing, is it possible to configure a shift in the phase? I need a main signal with the normal phase and then the other three with a phase shifted of 180°, is it possible?

In case I need more voltage, is it possible to increase with this configuration?

The voltage is determined by the external RC filter circuit.

Moreover if I need more than a signal, let’s suppose on other 3 pins, should I modify only the part in which the code attachs the signals to the pins?

Depends what you intend to do?

You can have the TCC0 output the same signal on all 4 of its channels by configuring the TCC0 timer’s output matrix, or you can have it output independent signals on all 4 of its channels, each being driven by 4 separate DMAC channels. Although it remains to be seen if the DMAC arbiter can switch fast enough between 4 channels at 25kHz, to reliably generate all the sine wave signals.

One last thing, is it possible to configure a shift in the phase? I need a main signal with the normal phase and then the other three with a phase shifted of 180°, is it possible?

If you just require just a 180° phase shift then it's possible to simply invert the TCC0 channels.

The SAMD21 also offers complementary high and low side PWM outputs, but only on certain pins.

Is there a tutorial or some guide that can introduce me in the use of DMAC? I have never seen so advanced code and so I don't even know what to modify in order to achieve what I need. Otherwise is there a simpler way to do it?

According to an input, I want to choose which signal should be the normal one and which should be shifted of 180°, is there some way to do it in the loop function?

By the way, for those that are interested the output sine wave frequency is determined by the PWM base frequency and the number of entries in the sine table:

In this instance the PWM base frequency is 25kHz and the number of sine table entries is 256. Therefore the output sine wave frequency is 25kHz / 256 = 97.65Hz, this corresponds to the 98Hz I measured in the above example.

Is there a tutorial or some guide that can introduce me in the use of DMAC?

Unfortunately, with running the PWM at 25kHz, I don't think there's any alternative.

Excellent examples of using the DMAC can be found here: https://github.com/manitou48/ZERO.

The only other source of information I've found is really the SAMD21 datasheet.

At first glance the DMAC looks pretty complicated, but actually it's quite easy to use. It allows you to move data from memory to memory, peripheral to memory, memory to peripheral and peripheral to peripheral, all independently of the CPU. It can be used to do all the heavy lifting in the background, while allowing the processor core to get on with more productive tasks. In certain situations the DMAC can be a powerful ally.

It's the DMAC descriptor that describes the source and destination addresses, if these addresses are incremented, the type of the data: BYTE, HWORD (16-bits) or WORD (32-bits), the number of bytes and the location of the next descriptor, if there is one. Unlike other registers the descriptors can be chained together and for this reason they're stored in the SAMD21's SRAM.

The DMAC's CTRLB register determines the source of the trigger, in this case the TCC0 overflow.

According to an input, I want to choose which signal should be the normal one and which should be shifted of 180°, is there some way to do it in the loop function?

I need to consider how change the signals' phase during operation, I'll get back to you.

And modifying the duty cycle of the PWM with the values of the lookup table during the execution of the loop is not a feasible solution? I'm trying to think to different solutions in order to make the code easier. Thank you for the patience.

Hi birdm3n,

I investigated reversing the phase of the sine wave signal by 180°. This is possible either by changing the channel polarity or by inverting the output driver during configuration. It's also possible to set-up four independent TCC0 timer channels driven by four separate DMAC channels. This proves that the DMAC can indeed cope with four independent channels.

Unfortunately however, I was unable change the polarity during operation. I believe the issue might be to do with the interaction between the DMAC and the timer. Attempts to change the polarity or the invert the output driver during operation either killed the signal or caused to micro-controller to crash.

I also looked into calling the DMAC's transfer complete interrupt, as means of synchronizing the polarity change to the beginning of the sine wave cycle. However, it turns out that as the DMAC descriptor is cyclic, (in order to continuously output the sine wave), the interrupt is never called. This means there's no way to get a handle on your current position in the sine wave period with the DMAC.

At the moment, the only way I can see foward is to produce a single sine wave output, or the sine wave and its inverse and manipulate the signals using external circuitry.

Oh, by the way the other thing I noticed was that the sine wave table only goes up to 1024, but the PER or period register on the TCC0 timer goes up to 1919.

If you expand the sine wave duty cycle table up to the PER register value this might increase the sine wave's amplitude.