Syncing multiple DMAC channels on SAMD21

Thanks to help from Martinl in https://forum.arduino.cc/t/ideas-for-pwm-with-repeating-pattern/847923 I now have my push-pull sine wave PWM synthesizer more-or-less working. Two DMAC channels reading the same sine lookup table drive TCC0 and TCC1. By making the second half of the table all zeroes, I get 50% duty cycle of the waveform, and by manually tweaking to have TCC1 start behind TCC0 I am getting the desired 180 degree phase shift. See attached photo (sorry for quality; scope is too old to have a USB port).

There are just a couple of loose ends I’m looking for guidance about:

  1. Is there a way to force synchronization of the two DMAC/timer channels so that TCC1 is exactly 1/2 period behind TCC0? Right now there is enough delay between the first and second timer starting that I have to manually tune the delay to get 50/50. I’d like to make that sync exact.

  2. What is the best way to enable and disable both output pins simultaneously, hopefully with minimal glitches.

Thanks!
John

@n8ur Hi John

There are probably a number of ways to do this.

The example shown below uses TCC0’s channels CC0 and CC1 on D0 and D2 for the Adafruit Trinket M0.

It uses two DMAC channels (0 & 1) together with 4 descriptors. Each DMAC channel uses two descriptors, to alternately output the pulses followed by 0V. Using a single timer keeps everything in sync.

To gracefully turn off and on the signals, the DMAC channels are suspended at the end of each transfer and an interrupt service routine (ISR) is triggered. The ISR alters the value of the DMAC descriptors on the fly, to either output the signals or 0V depending on the value of an enable flag. Upon exiting the ISR, DMAC operation is resumed once more.

Here’s the code:

// Use DMAC to synchronise TCC0 outputs on D0 (PA08) and D2 (PA09) using the DMAC
volatile uint16_t sintable1[8] = { 3750, 11520, 3750, 11520, 3750, 11520, 3750, 11520 }; // Wave table
volatile uint16_t sintable2[8] = {};                                                     // All zeros

volatile boolean enable = true;

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
volatile 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
  DMAC->CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                      // Enable suspend channel interrupts on each channel
  DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) |                         // Set DMAC priority to level 0 (lowest)
                      DMAC_CHCTRLB_TRIGSRC(TCC0_DMAC_ID_OVF) |      // Trigger on timer TCC0 overflow
                      DMAC_CHCTRLB_TRIGACT_BEAT;                    // Trigger every beat
                      
  descriptor.descaddr = (uint32_t)&descriptor_section[10];                   // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&sintable1[0] + 8 * sizeof(uint16_t);    // Read the current value in the sine table
  descriptor.dstaddr = (uint32_t)&TCC0->CCB[0].reg;                            // Copy it into the TCC0 counter compare 0 register
  descriptor.btcnt = 8;                                                   // This takes the number of table entries = 8 beats
  descriptor.btctrl = DMAC_BTCTRL_BLOCKACT_SUSPEND |                // Suspend DMAC channel at end of block transfer
                      DMAC_BTCTRL_BEATSIZE_HWORD |                  // Set the beat size to 16-bits (Half Word)
                      DMAC_BTCTRL_SRCINC |                          // Increment the source address every beat
                      DMAC_BTCTRL_VALID;                            // Flag the descriptor as valid
  memcpy((void*)&descriptor_section[0], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 0 descriptor  
  
  descriptor.descaddr = (uint32_t)&descriptor_section[0];                   // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&sintable2[0] + 8 * sizeof(uint16_t);    // Read the current value in the sine table
  descriptor.dstaddr = (uint32_t)&TCC0->CCB[0].reg;                            // Copy it into the TCC0 counter compare 0 register
  descriptor.btcnt = 8;                                                   // This takes the number of table entries = 8 beats
  descriptor.btctrl = DMAC_BTCTRL_BLOCKACT_SUSPEND |                // Suspend DMAC channel at end of block transfer
                      DMAC_BTCTRL_BEATSIZE_HWORD |                  // Set the beat size to 16-bits (Half Word)
                      DMAC_BTCTRL_SRCINC |                          // Increment the source address every beat
                      DMAC_BTCTRL_VALID;                            // Flag the descriptor as valid
  memcpy((void*)&descriptor_section[10], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 10 descriptor  

  DMAC->CHID.reg = DMAC_CHID_ID(1);                                 // Select DMAC channel 1
  DMAC->CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                      // Enable suspend channel interrupts on each channel
  DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) |                         // Set DMAC priority to level 0 (lowest)
                      DMAC_CHCTRLB_TRIGSRC(TCC0_DMAC_ID_OVF) |      // Trigger on timer TCC0 overflow
                      DMAC_CHCTRLB_TRIGACT_BEAT;                    // Trigger every beat

  descriptor.descaddr = (uint32_t)&descriptor_section[11];                   // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&sintable2[0] + 8 * sizeof(uint16_t);    // Read the current value in the sine table
  descriptor.dstaddr = (uint32_t)&TCC0->CCB[1].reg;                            // Copy it into the TCC0 counter compare 1 register
  descriptor.btcnt = 8;                                                   // This takes the number of table entries = 8 beats
  descriptor.btctrl = DMAC_BTCTRL_BLOCKACT_SUSPEND |                // Suspend DMAC channel at end of block transfer
                      DMAC_BTCTRL_BEATSIZE_HWORD |                  // Set the beat size to 16-bits (Half Word)
                      DMAC_BTCTRL_SRCINC |                          // Increment the source address every beat
                      DMAC_BTCTRL_VALID;                            // Flag the descriptor as valid
  memcpy((void*)&descriptor_section[1], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 1 descriptor  
  
  descriptor.descaddr = (uint32_t)&descriptor_section[1];                   // Set up a circular descriptor
  descriptor.srcaddr = (uint32_t)&sintable1[0] + 8 * sizeof(uint16_t);    // Read the current value in the sine table
  descriptor.dstaddr = (uint32_t)&TCC0->CCB[1].reg;                            // Copy it into the TCC0 counter compare 1 register
  descriptor.btcnt = 8;                                                   // This takes the number of table entries = 8 beats
  descriptor.btctrl = DMAC_BTCTRL_BLOCKACT_SUSPEND |                // Suspend DMAC channel at end of block transfer
                      DMAC_BTCTRL_BEATSIZE_HWORD |                  // Set the beat size to 16-bits (Half Word)
                      DMAC_BTCTRL_SRCINC |                          // Increment the source address every beat
                      DMAC_BTCTRL_VALID;                            // Flag the descriptor as valid
  memcpy((void*)&descriptor_section[11], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel 11 descriptor
 
  NVIC_SetPriority(DMAC_IRQn, 0);           // Set the Nested Vector Interrupt Controller (NVIC) priority for the DMAC to 0 (highest) 
  NVIC_EnableIRQ(DMAC_IRQn);                // Connect the DMAC to the Nested Vector Interrupt Controller (NVIC)

  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz/1=MHz
                     GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4

  GCLK->GENCTRL.reg = 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

  GCLK->CLKCTRL.reg = 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

  // Enable the port multiplexer on digital pins D0 (PA08) and D2 (PA09)
  PORT->Group[PORTA].PINCFG[8].bit.PMUXEN = 1;
  PORT->Group[PORTA].PINCFG[9].bit.PMUXEN = 1;
  // Set-up the pins as TCC0 WO[0] and WO[1] outputs 
  PORT->Group[PORTA].PMUX[8 >> 1].reg = PORT_PMUX_PMUXO_E | PORT_PMUX_PMUXE_E;

  TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;          // Setup TCC0 in Normal PWM (NPWM) mode
  while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization
  
  TCC0->PER.reg = 14999;                           // Set the period: GCLK / (PRESCALER * (PER + 1)) = frequency
  while(TCC0->SYNCBUSY.bit.PER);                   // Wait for synchronization
 
  TCC0->CTRLA.reg = //TCC_CTRLA_PRESCSYNC_PRESC |   // Set timer to overflow on the prescaler rather than GCLK
                    TCC_CTRLA_PRESCALER_DIV1;     // Divide GCLK4 by 1
  
  TCC0->CTRLA.bit.ENABLE = 1;                      // Enable the TCC0 output
  while (TCC0->SYNCBUSY.bit.ENABLE);               // Wait for synchronization
 
  DMAC->CHID.reg = DMAC_CHID_ID(0);                // Select DMAC channel 
  DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;        // Enable DMAC channel 
  DMAC->CHID.reg = DMAC_CHID_ID(1);                // Select DMAC channel 
  DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;        // Enable DMAC channel
  delay(1000);
}

void loop() 
{
  enable = !enable;                                // Toggle the enable flag
  delay(1000);                                     // Wait 1 second
}

void DMAC_Handler()
{
  uint16_t channel = DMAC->INTPEND.bit.ID;                 // Find the DMAC channel generating the interrupt
  DMAC->CHID.reg = DMAC_CHID_ID(channel);                 
  DMAC->CHINTFLAG.bit.SUSP = 1;                           // Clear the DMAC channel suspend (SUSP) interrupt flag
  channel = channel == 1 ? 11 : 0;
  descriptor_section[channel].btctrl &= ~DMAC_BTCTRL_VALID;     // Disable the descriptor
  if (enable)
  {   
    descriptor_section[channel].srcaddr = (uint32_t)&sintable1[0] + 8 * sizeof(uint16_t);    // Read the current value in the sine table  
  }
  else
  {   
    descriptor_section[channel].srcaddr = (uint32_t)&sintable2[0] + 8 * sizeof(uint16_t);    // Read the current value in the sine table 
  }
  descriptor_section[channel].btctrl |= DMAC_BTCTRL_VALID;      // Enable the descriptor
  DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;           // Resume the DMAC channel
}

Note that the code uses the TCC0’s buffered CCB regsiters rather than the CC registers. Also, I’ve just used a simplifed wave table with the timer operated at an arbitary frequency.

Thanks, Martin! That is very, very slick. What’s great is that comparing this and the earlier code I can learn a lot more about the whole DMAC thing – it’s been really hard to find clear documentation.

Much, much appreciate all your help!

HI again @MartinL

I’ve gotten my program working with more or less this code that you provided. See attached image of the output with 128 steps. Thanks again!

Now I’m playing with a different approach. Instead of toggling between table1 with sine values and table2 that’s null, I’m building the tables twice the length with the sine values in either the first half of the array with the second half set to 0, or the opposite with the first half set to 0 and the second half containing the sine values. Then with two DMAC channels each running one of the tables, I should get the same alternating output on the two pins.

I’m having trouble modifying your code for what I think is this simpler case. I turned your earlier code into a function that takes table, sizeof(table), chid, descriptor_section_in, and descriptor_section_out as parameters (see below).

I would think I’d just need to load the table into e.g. section 0 and memcpy to section 10 as in your version,but that doesn’t seem to work.

Am I on the right track or is the syntax to set up continuously running output on two DMAC channels different than this?

Thanks again!

[ code ]
void dmac_load_table(volatile uint16_t table, uint16_t elements, uint8_t chid, uint8_t sect_in, uint8_t sect_out) {
dmacdescriptor descriptor attribute ((aligned (16))); // Place holder descriptor

DMAC->CHID.reg = DMAC_CHID_ID(chid);                               // Select DMAC channel 0
DMAC->CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                      // Enable suspend channel interrupts on each channel
DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(0) |                         // Set DMAC priority to level 0 (lowest)
                    DMAC_CHCTRLB_TRIGSRC(TCC0_DMAC_ID_OVF) |      // Trigger on timer TCC0 overflow
                    DMAC_CHCTRLB_TRIGACT_BEAT;                    // Trigger every beat   
descriptor.descaddr = (uint32_t)&descriptor_section[sect_in];  // Set up a circular descriptor
descriptor.srcaddr = (uint32_t)table + elements * sizeof(uint16_t);  // Read the current value in the sine table
descriptor.dstaddr = (uint32_t)&TCC0->CCB[chid].reg;               // Copy it into the TCC0 counter compare 0 register
descriptor.btcnt = elements;                                      // This takes the number of table entries = 8 beats
descriptor.btctrl = DMAC_BTCTRL_BLOCKACT_SUSPEND |                // Suspend DMAC channel at end of block transfer
                    DMAC_BTCTRL_BEATSIZE_HWORD |                  // Set the beat size to 16-bits (Half Word)
                    DMAC_BTCTRL_SRCINC |                          // Increment the source address every beat
                    DMAC_BTCTRL_VALID;                            // Flag the descriptor as valid
memcpy((void*)&descriptor_section[sect_out], &descriptor, sizeof(dmacdescriptor));  // Copy to the channel descriptor   

}
[ /code]

I think I figured it out. I commented out
DMAC_BTCTRL_BLOCKACT_SUSPEND

and then used selector 0 for both places in one function call, and selector 1 for the other call. Now I’m getting a nice half wave without having to run the interrupt handler.