Maximum interrupt frequency

Hi Martin,

I don't think that this is possible in my project because I don't know which signal should be inverted, it is an information that is given during the execution of the program and it can be changed, so I need that during the execution of the loop function, an interrupt change the indexes of the sine wave lookup table. Using the DMAC, as you said, it is not possible because using external circuits, the signals inverted and not inverted are always the same.

With an external DAC, don't you think that there is a way to avoid the bottleneck of the i2c speed? I'm using 400kHz for the i2c clock till now, but if I add new instructions in the loop, it does not output a signal with frequency of 100Hz, do you know a way to solve this problem? Should I change board?

Hi birdm3n,

I think I’ve found a solution.

It’s possible to change the polarity of the signals during operation, using the channel polarity bits in the TCC0 WAVE register. I was previous using the buffered WAVEB register, which doesn’t work.

This code allows you to change the phase by 180° by setting and clearing the polarity bits in the loop() section, however this provides no control over where in the sine wave period the switch occurs:

// 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

  for (uint8_t i = 0; i < 4; i++)
  {
    DMAC->CHID.reg = DMAC_CHID_ID(i);                                 // 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[i];                 // 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[i].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[i], &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 D2, D5, D6 and D7
  PORT->Group[g_APinDescription[2].ulPort].PINCFG[g_APinDescription[2].ulPin].bit.PMUXEN = 1;   // Arduino Zero uncomment 
  //PORT->Group[g_APinDescription[4].ulPort].PINCFG[g_APinDescription[4].ulPin].bit.PMUXEN = 1;   // Arduino M0/M0 PRO uncomment
  PORT->Group[g_APinDescription[5].ulPort].PINCFG[g_APinDescription[5].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[6].ulPort].PINCFG[g_APinDescription[6].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
 
  // Connect the TCC0 timer to the port outputs D2, D5, D6 and D7 - port pins are paired odd PMUXO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[5].ulPort].PMUX[g_APinDescription[5].ulPin >> 1].reg |= PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
  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

  for (uint8_t i = 0; i < 4; i++)
  {
    DMAC->CHID.reg = DMAC_CHID_ID(i);               // Select DMAC channel 
    DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;       // Enable DMAC channel 
  }
}

void loop() 
{
  REG_TCC0_WAVE &= ~TCC_WAVE_POL0;                 // Toggle the channels' polarity every second
  REG_TCC0_WAVE |= TCC_WAVE_POL1;
  REG_TCC0_WAVE &= ~TCC_WAVE_POL2;
  REG_TCC0_WAVE |= TCC_WAVE_POL3;
  delay(1000);
  REG_TCC0_WAVE |= TCC_WAVE_POL0;
  REG_TCC0_WAVE &= ~TCC_WAVE_POL1;
  REG_TCC0_WAVE |= TCC_WAVE_POL2;
  REG_TCC0_WAVE &= ~TCC_WAVE_POL3;
  delay(1000);  
}

If you’re using an M0 or M0 Pro you’ll need to include the pin multiplexer line for D4 and comment out the one for D2. This is because on the Arduino Zero pins D2 and D4 are swapped.

I've now rescaled your sine table from 0 - 1023 to 0 - 1919, this increases the amplitude of the sine wave outputs from 1.8Vp-p to around 2.5Vp-p:

// Sine table
volatile const uint16_t sintable[256] = {
  960,982,1007,1029,1054,1076,1101,1123,1146,1170,1193,1215,1238,1260,1283,1305,1326,1348,1369,1390,1412,
  1433,1451,1472,1493,1511,1530,1549,1568,1586,1603,1620,1637,1654,1669,1686,1701,1716,1729,1744,1757,1770,
  1782,1795,1806,1815,1827,1836,1845,1855,1862,1870,1877,1883,1890,1896,1900,1903,1907,1911,1915,1917,1917,
  1919,1919,1919,1917,1917,1915,1911,1907,1903,1900,1896,1890,1883,1877,1870,1862,1855,1845,1836,1827,1815,
  1806,1795,1782,1770,1757,1744,1729,1716,1701,1686,1669,1654,1637,1620,1603,1586,1568,1549,1530,1511,1493,
  1472,1451,1433,1412,1390,1369,1348,1326,1305,1283,1260,1238,1215,1193,1170,1146,1123,1101,1076,1054,1029,
  1007,982,960,936,911,889,864,842,817,795,772,748,725,703,680,658,635,613,592,570,549,528,506,485,467,446,
  425,407,388,369,350,332,315,298,281,264,249,232,217,202,189,174,161,148,136,123,112,103,91,82,73,63,56,48,
  41,35,28,22,18,15,11,7,3,1,1,0,0,0,1,1,3,7,11,15,18,22,28,35,41,48,56,63,73,82,91,103,112,123,136,148,161,
  174,189,202,217,232,249,264,281,298,315,332,350,369,388,407,425,446,467,485,506,528,549,570,592,613,635,658,
  680,703,725,748,772,795,817,842,864,889,911,936 };

The image shows 2 of the 4 sine wave outputs, with a simultaneous phase reversal on both channels:

Perfect! But in case I would like to reduce dinamically the amplitude of the sine waves, I can't multiply a reduce factor to the sine wave because the lookup table is passed directly from memory to the pin thanks to the DMA, right? But I need to modify the amplitude of the sine wave in the loop function.

But I need to modify the amplitude of the sine wave in the loop function.

So how would you propose to implement that using PWM and the sine table? (Irrespective of whether or not the DMAC is used).

In order to reduce the amplitude using the DAC or also passing the lookup table value to the output pin, I could multiply the lookup table values with a reduce factor that is a value between 0 and 1. In this way I could modify the amplitude of the signal, do you think that it is possible to use this approach in the last version of the program?
Is there a way to access the content of the register that points to the lookup table in the loop function?

In order to reduce the amplitude using the DAC or also passing the lookup table value to the output pin, I could multiply the lookup table values with a reduce factor that is a value between 0 and 1. In this way I could modify the amplitude of the signal, do you think that it is possible to use this approach in the last version of the program?
Is there a way to access the content of the register that points to the lookup table in the loop function?

How would you synchronise any changes made to the sine table with the waveform output during operation?

The idea could be to stop the DMAC and then make it restarts with the values of the lookup table multiplied by the reduce factor, do you think that it can be possible?

The idea could be to stop the DMAC and then make it restarts with the values of the lookup table multiplied by the reduce factor, do you think that it can be possible?

The problem is that if you stop the DMAC, it'll kill the waveform output.

I don't think that there's a simple way to adjust the waveform's amplitude. The 25kHz PWM waveform is simply too high for the SAMD21 micro-controller to service and do other things besides.

So you are suggesting to reduce the number of the samples in the lookup table and as consequence the reduction of the frequency, passing from 25kHz to another lower frequency?

So you are suggesting to reduce the number of the samples in the lookup table and as consequence the reduction of the frequency, passing from 25kHz to another lower frequency?

I just don't think that PWM and sine table this is the right method, if you require an adjustable amplitude.

Hi birdm3n,

After having given it some thought, there's possibly a way to synchronise the code to the 100Hz sine wave. It involves using the SAMD21's event system, that can be used for peripheral to peripheral communciation.

The TCC0 timer is configured to generate an event each time it overflows at 25kHz and another TC timer in 8-bit mode counts them. As you've got 256 entries in your sine table, when the TC timer overflows you know that you've reached the end of the table. The TC timer then generates an overflow interrupt, which occurs 100 times a second, giving you a window of opportunity to change the values in your sine table.

I'll give it a go.

Ok, I implemented the code using TC4 to synchronise to the 100Hz sine wave. I then used the TC4 interrupt that occurs at the end of the sine wave cycle to change the sine lookup table, thereby changing the amplitude.

I set it up to change the amplitude every cycle, here's the results (on two channels):

The code is attached as a zip file below.

DirectDigitalSynthesis5.zip (3.83 KB)

This method has the additional benefit, that if you change the signal's polarity in the TC timer's overflow interrupt, it will also synchronise any changes to the start of the next cycle.

Furthermore, if you use the TC's counter compare register (CC) and it's associated interrupt, it's possible to synchronise interrupt code to be run at any point along the waveform.

You are a genius! Just one last thing, I don't understand the code to attach the event of the sine wave's end to the interrupt. Could you explain me those lines in a few words? Thanks! You have been very helpful!

Hi birdm3n,

The event system is a 12 channel highway on the SAMD21 that allows peripheral to peripheral communication without CPU intervention.

An event is generated or sent by a peripheral on a specified event channel, in response to a defined trigger. In our case trigger is the TCC0 overflow.

The following code sets up a TCC0 overflow on event channel 0. It defines the event as asynchronous with no event signal edge detection:

REG_EVSYS_CHANNEL = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT |                // No event edge detection
                    EVSYS_CHANNEL_PATH_ASYNCHRONOUS |                   // Set event path as asynchronous
                    EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_TCC0_OVF) |        // Set event generator (sender) as TCC0 overflow
                    EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0

We also have to enable the TCC0 event output:

REG_TCC0_EVCTRL |= TC_EVCTRL_OVFEO;              // Enable an event output on overflow of the TCC0 timer

The event is received by what is known as a user, in our case the TC4 timer:

REG_EVSYS_USER = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                 EVSYS_USER_USER(EVSYS_ID_USER_TC4_EVU);                // Set the event user (receiver) as timer TC4

Confusingly, the user channel number is defined as (n + 1), where n is the event channel number.

We also have to enable the TC4 event input and set it up to count the events as they arrive:

REG_TC4_EVCTRL |= TC_EVCTRL_TCEI |              // Enable asynchronous input events on the TC timer
                  TC_EVCTRL_EVACT_COUNT;        // Increment the TC timer each time an event is received

Essentially, each time the TCC0 timer overflows at a frequency of 25kHz, the four DMAC channels load the next sine table entry into the four corresponding TCC0 counter compare registers (CCx) . Each overflow also causes the TCC0 timer to generate an event on event channel 0 that's received and counted by timer TC4 in 8-bit mode.

In 8-bit mode TC4 can count up to 256 (0-255) that exactly matches our 256 (0-255) sine table entries. So each time the TC4 overflows, or in other words reaches 256 (at 100Hz), we know we've reached the end of the sine table. At this point we can generate a TC4 overflow interrupt to run code in its interrupt service rountine (ISR). In our case the ISR changes the sine table entries for the next cycle, in order to alter the amplitude.

As the DMAC channel descriptors are circular they automatically return back to the start of the sine table and repeat the sine wave output cycle continously.

Hi Martin,

Thanks for your help! Where can I find the .h files with all the instructions to design the behaviour of the dmac?
The problem is that I can't find the code to set the registers of the DMAC and for the Timer Overflow Interrupt, is there an API with all the possible options with which I can set the various registers?

Hi birdm3n,

The register and bitfield definitions are located (on my Windows machine at least) at:

C:\Users\Computer\AppData\Local\Arduino15\packages\arduino\tools\CMSIS\atmel-4.0.0\Device\ATMEL\SAMD21\include...

In the "include" directory there are two subdirectories, namely "instance" and "component". Instance contains the register definitions for each peripheral instance, for example "tcc0.h", "tcc1.h", etc..., while component contains the definitions for the registers' bitfields for each peripheral type, for instance "tcc.h", "tc.h" etc...

Generally there are two ways of accessing the micro-controller's registers, either by using the register definitions in the instance directory:

REG_TCC0_CC0 = 0;
while(TCC0->SYNCBUSY.bit.CC0);

or by using an offset from the peripheral's base address:

TCC0->CC[0].reg = 0;
while(TCC0->SYNCBUSY.bit.CC0);

Both are valid, but the offset method has the slight advantage that it can be called iteratively within a for or while loop if necessary.

Also note that you can use "reg" to access the whole register and "bit" to access just the bitfield you're interested in.

Regarding the DMAC, the descriptor definitions are not defined in these files, as they are stored in RAM and are usually declared in your sketch.

Ok thanks! In the screenshot of the oscilloscope that you updloaded in the post #32 the signal is already filtered with an RC filter with R = 10kOhm and C = 100nF? When I visualize the signal on the oscilloscope without the filter I can only see the PWM signal with a frequency of 24kHz and I can’t get the sine wave.

In the screenshot of the oscilloscope that you updloaded in the post #32 the signal is already filtered with an RC filter with R = 10kOhm and C = 100nF?

Yes, that's right.

I just used a standard leaded 10kOhm resistor and a 100nF ceramic (decoupling) capacitor on a bread board.

These are configured as a simple, first order low pass filter. The resistor and capacitor values set the 3dB point to a frequency around 160Hz. This filters out the 24kHz PWM signal, leaving you with a sine wave output.