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:
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.
What is the best way to enable and disable both output pins simultaneously, hopefully with minimal glitches.
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.
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?
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
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.