SAMD21 Fault Blanking RAMP to "mix" two square waves

Humour me. If I wanted to

  • "mix" two 50% duty square waves of different frequencies, effectively XORing them,
  • avoid using interrupt handlers
  • and outputting the resultant waveform on a single pin

...could I use a TCC with fault blanking in RAMP1 operation (maybe with inverted polarity)? As per Figure 31-22 (attached) on p727 of the SAMD21 datasheet.

The main/primary wave would be controlled by the main TCCx in RAMP1 operation, with it's period set to generate the overall frequency and CC0 set at half way to TOP. Then a second TC or TCC would be set as an event generator and the event would feed TCCxMC0 as the event user (needs an async channel).

The reason for this is a basic audio fx application. So the update rate wouldn't be that often (fastest envisaged changes to the frequency of either wave would be 10Hz). Would need to adjust the fault blanking time and prescaler to match roughly half the second waveform's period but if the higher of any two "tones" were assigned to the Fault blanking, event generating TCC, it should be doable, right?

I'm open to any other ideas about generating roughly square waves of differing frequencies from 100 Hz - 15000 Hz out outputting the XORed result on a single pin, without relying on interrupt handlers. I know this seems obtuse. Thanks for reading this far!

RAMP SAMD21.PNG

Hi smerrett79,

The simplest way to interleave two PWM signals on to a single channel output is to use both the TCC timer's duty-cycle and period circular buffers.

In this mode both the Counter Compare: CCx and buffered equivalent: CCBx are loaded with the required duty-cycles. The same goes for the period registers: PER and PERB. At the end of each timer cycle the timer switches or cycles between them effectively interleaving two channels on to a single output.

Here's an example that sets up circular buffers for CC[0]/CCB[0] and PER/PERB on timer TCC0, output on D2 (Arduino Zero) or port pin PA14. The two interleaved channels output 50% duty-cycle at 100Hz and 200Hz frequencies:

// Use the TCC0 timer with circular CC and PER buffers to interleave two PWM signals on to a single channel (channel 0)
void setup()
{
  // Feed GCLK0 to TCC0 and TCC1
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK0
                      GCLK_CLKCTRL_GEN_GCLK0 |     // Select GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  // Enable the port multiplexer for the PWM channel TCC0 WO[0] on D2
  PORT->Group[g_APinDescription[2].ulPort].PINCFG[g_APinDescription[2].ulPin].bit.PMUXEN = 1;
 
  // Connect the TCC0 timer to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC0
  PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg |= PORT_PMUX_PMUXE_F;

  // Interleave TCC0 two PWM signals on to a single channel using the CC and PER circular buffers
  TCC0->WAVE.reg = TCC_WAVE_CICCEN0 |              // Enable the circular counter compare CC0 <--> CCB0 buffer 
                   TCC_WAVE_CIPEREN |              // Enable the circular period PER <--> PERB buffer
                   TCC_WAVE_WAVEGEN_NPWM;          // Set the PWM output to normal (single slope) PWM mode
  while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

  TCC0->PER.reg = 239999;                          // Set the frequency of the PWM on PER to 200Hz on D2
  while (TCC0->SYNCBUSY.bit.PER);                  // Wait for synchronization
  TCC0->PERB.reg = 479999;                         // Set the frequency of the PWM on PERB to 100Hz on D2
  while (TCC0->SYNCBUSY.bit.PERB);                 // Wait for synchronization
  
  TCC0->CC[0].reg = 120000;                        // Set the duty-cycle to 50%
  while (TCC0->SYNCBUSY.bit.CC0);                  // Wait for synchronization
  TCC0->CCB[0].reg = 240000;                       // Set the duty-cycle to 50%
  while (TCC0->SYNCBUSY.bit.CCB0);                 // Wait for synchronization

  TCC0->CTRLA.bit.ENABLE = 1;                      // Enable the TCC0 counter
  while (TCC0->SYNCBUSY.bit.ENABLE);               // Wait for synchronization
}

void loop() {}

Here's the output on the scope:

InterleavingPWM .png

To change the period or duty-cycle over set number of cycles without disrupting the output, it's possible use the event system to get a second timer to count TCC0 cycles and get this second timer to call an interrupt service routine, in order to change the CC[0]/CCB[0] and/or PER/PER[0] registers.

InterleavingPWM .png

A perhaps more flexible alternative, is to use the TCC0 timer in conjunction with the Direct Memory Access Controller (DMAC), in order to change the signal's duty-cycle and period without resorting to the CC0/CCB0 and PER/PERB circular buffers.

The example code below produces the same output above on D2, but this time using the TCC0 timer in conjunction with the DMAC. Two DMAC channels: 0 and 1 load the respective CCB0 and PERB registers with values from the pattern array each time the TCC0 timer overflows:

// Use the TCC0 timer in conjuction with the DMAC, to interleave two PWM signals of differing duty-cycle and period on to a single channel (channel 0)
// Buffers loaded from DMAC on each TCC0 timer cycle

// DMAC pattern destination register addresses                      
uint32_t pattern[2][2] =  { { 120000, 240000 },     // CCB0 pattern                                                                       
                            { 239999, 479999 } };   // PERB pattern
RwReg* tccDstAddr[] = { &TCC0->CCB[0].reg, &TCC0->PERB.reg };

typedef struct                                      // DMAC Descriptr sturcture, held in SRAM
{
    uint16_t btctrl;
    uint16_t btcnt;
    uint32_t srcaddr;
    uint32_t dstaddr;
    uint32_t descaddr;
} dmacdescriptor ;

volatile dmacdescriptor wrb[DMAC_CH_NUM] __attribute__ ((aligned (16)));
dmacdescriptor descriptor_section[DMAC_CH_NUM] __attribute__ ((aligned (16)));
dmacdescriptor descriptor __attribute__ ((aligned (16)));

void setup()
{
  // Direct Memory Access Controller (DMAC) /////////////////////////////////////////////////
  
  DMAC->BASEADDR.reg = (uint32_t)descriptor_section;
  DMAC->WRBADDR.reg = (uint32_t)wrb;
  DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);                // Enable the DMAC
  for (uint8_t i = 0; i < 2; i++)                                             // Iterate through the CCB0 and PERB DMAC channels
  {
    DMAC->CHID.reg = DMAC_CHID_ID(i);                                         // Select the DMAC channel
    DMAC->CHCTRLB.reg = DMAC_CHCTRLB_TRIGSRC(TCC0_DMAC_ID_OVF) |              // Set up the DMAC to trigger on a TCC0 overflow (OVF)
                        DMAC_CHCTRLB_TRIGACT_BEAT;
    descriptor.descaddr = (uint32_t)&descriptor_section[i];                   // Set up a circular descriptor;
    descriptor.srcaddr = (uint32_t)&pattern[i][0] + 2 * sizeof(uint32_t);     // Set the source address as either the CC or PER pattern
    descriptor.dstaddr = (uint32_t)tccDstAddr[i];                             // Set the destination
    descriptor.btcnt = 2;                                                     // Transfer two word beats
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_WORD |                           // Set beat size to WORD (32-bits)
                        DMAC_BTCTRL_SRCINC |                                  // Increment the source address
                        DMAC_BTCTRL_VALID;                                    // Indicate that the DMAC descriptor is valid
    memcpy(&descriptor_section[i], &descriptor, sizeof(descriptor));          // Copy the values into the DMAC descriptor array
  }  

  // Timer Counter for Control TCC0 /////////////////////////////////////////////////////////
  
  // Feed GCLK0 to TCC0 and TCC1
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK0
                      GCLK_CLKCTRL_GEN_GCLK0 |     // Select GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  // Enable the port multiplexer for the PWM channel TCC0 WO[0] on D2
  PORT->Group[g_APinDescription[2].ulPort].PINCFG[g_APinDescription[2].ulPin].bit.PMUXEN = 1;
 
  // Connect the TCC0 timer to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC0
  PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg |= PORT_PMUX_PMUXE_F;

  TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;          // Set the PWM output to normal (single slope) PWM mode
  while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

  TCC0->CTRLA.bit.ENABLE = 1;                      // Enable the TCC0 counter
  while (TCC0->SYNCBUSY.bit.ENABLE);               // Wait for synchronization

  for (uint8_t i = 0; i < 2; i++)                  // Enable the CCB0 and PERB DMAC channels
  {
    DMAC->CHID.reg = DMAC_CHID_ID(i);
    DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;
  }
}

void loop() {}

Hi Martin, thanks for both your replies and I'm sorry it took so long for me to see them. I thought I had notification emails set for this thread.

I will try both your methods but I can already see how they would work and how I can use them. Thanks again!

Hi Martin,

I've tried the code samples from this thread and it did not work on my M0 PRO until
I changed PORT_PMUX_PMUXE**_F** to PORT_PMUX_PMUXE**_E**

I have a really hard time with the multiplexer part but from what I can read on datasheet
page 21, pin 2 is PA08 associated to TCC0 WO[0] wich a E peripheral.

I guessed you've tried your code so I'd like to know if you have an idea why this change was needed ?

Hi ronin101,

That's because (confusingly) the D2 and D4 digital pins are reversed on the Arduino (.cc's) Zero and Arduino (.org's) M0 Pro. If possible just switch to D4 instead and change the code to:

// Enable the port multiplexer for the PWM channel TCC0 WO[0] on D4
  PORT->Group[g_APinDescription[4].ulPort].PINCFG[g_APinDescription[4].ulPin].bit.PMUXEN = 1;
 
  // Connect the TCC0 timer to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC0
  PORT->Group[g_APinDescription[4].ulPort].PMUX[g_APinDescription[4].ulPin >> 1].reg |= PORT_PMUX_PMUXE_F;

The above example isn't using RAMP2, it's just activating the circular buffers to switch the duty-cycle and period of a single PWM channel. To enable RAMP2 requires WAVE register's RAMP bitfield to be set:

// Interleave TCC0 two PWM signals on to a single channel using the CC and PER circular buffers
TCC0->WAVE.reg = TCC_WAVE_CICCEN0 |              // Enable the circular counter compare CC0 <--> CCB0 buffer
                 TCC_WAVE_CIPEREN |              // Enable the circular period PER <--> PERB buffer
                 TCC_WAVE_RAMP_RAMP2 |           // Enable RAMP2 operation                     
                 TCC_WAVE_WAVEGEN_NPWM;          // Set the PWM output to normal (single slope) PWM mode
while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

MartinL:
That's because (confusingly) the D2 and D4 digital pins are reversed on the Arduino (.cc's) Zero and Arduino (.org's) M0 Pro. If possible just switch to D4 instead and change the code to:

:o ... Indeed I have a signal with D4/PORT_PMUX_PMUXE_F. Strangely enough it has not the same characteristics than the D2/PORT_PMUX_PMUXE_E signal ?!! Anyway I will not investigate further since I have other coding problems right now.

Thanks for the RAMP2 code sample, I will try working with it.