Go Down

Topic: Metro M4 Express ATSAMD51 PWM Frequency and Resolution (Read 10616 times) previous topic - next topic

MartinL

Hi Jim,

Here's some example code that sets up the TCC1/WO[2] timer on digital pin D10 for one shot operation. The output signal by default is high and upon triggering generates a 1500us low pulse. The process repeats every 4 milliseconds:

Code: [Select]
// Adafruit Metro M4 Only: Set-up digital pin D10 to output a one shot pulse with 1500us pulse width
// at an interval of 4 milliseconds
void setup()
{
  // Set up the generic clock (GCLK7) to clock timer TCC1
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(1) |       // Divide the 48MHz clock source by divisor 1: 48MHz/1 = 48MHz
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         GCLK_GENCTRL_SRC_DFLL;      // Select 48MHz DFLL clock source
                         //GCLK_GENCTRL_SRC_DPLL1;     // Select 100MHz DPLL clock source
                         //GCLK_GENCTRL_SRC_DPLL0;     // Select 120MHz DPLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization 

  GCLK->PCHCTRL[TCC1_GCLK_ID].reg = GCLK_PCHCTRL_CHEN |        // Enable the TCC1 perhipheral channel
                                    GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to TCC1

  // Enable the peripheral multiplexer on pin D10
  PORT->Group[g_APinDescription[10].ulPort].PINCFG[g_APinDescription[10].ulPin].bit.PMUXEN = 1;
 
  // Set the D10 (PORT_PA18) peripheral multiplexer to peripheral (even port number) F(5): TCC1, Channel 2
  PORT->Group[g_APinDescription[10].ulPort].PMUX[g_APinDescription[10].ulPin >> 1].reg |= PORT_PMUX_PMUXE(5);
 
  TCC1->CTRLA.reg = TC_CTRLA_PRESCALER_DIV8 |        // Set prescaler to 8, 48MHz/8 = 6MHz
                    TC_CTRLA_PRESCSYNC_PRESC;        // Set the reset/reload to trigger on prescaler clock                 

  TCC1->CTRLBSET.reg = TCC_CTRLBSET_ONESHOT;         // Enable one shot
  while (TCC1->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization
 
  TCC1->DRVCTRL.reg |= TCC_DRVCTRL_NRE2;             // Continue to drive the output on TCC1/WO[2] when timer has stopped (rather than becoming tri-state) 
  TCC1->DRVCTRL.reg |= TCC_DRVCTRL_INVEN2;           // Invert the output to generate an active low pulse on TCC1/WO[2]
 
  TCC1->WAVE.reg = TC_WAVE_WAVEGEN_NPWM;             // Set-up TCC1 timer for Normal (single slope) PWM mode (NPWM)
  while (TCC1->SYNCBUSY.bit.WAVE)                    // Wait for synchronization

  TCC1->PER.reg = 8999;                              // Set-up the PER (period) register for 1500us pulse period
  while (TCC1->SYNCBUSY.bit.PER);                    // Wait for synchronization
 
  TCC1->CC[2].reg = 8999;                            // Set-up the CC (counter compare), channel 2 register for 1500us pulse width
  while (TCC1->SYNCBUSY.bit.CC2);                    // Wait for synchronization

  TCC1->CTRLA.bit.ENABLE = 1;                        // Enable timer TCC1
  while (TCC1->SYNCBUSY.bit.ENABLE);                 // Wait for synchronization
}

void loop()
{         
  TCC1->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER;   // Retrigger a one shot pulse
  while (TCC1->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization
  delay(4);                                          // Wait for 4 milliseconds
}

If you need to reduce the pulse width to 600us then it's necessary to change the CC[2]/CCBUF[2] register to 3599.

Here's the output, 1500us (1.5ms) active low one shot pulse triggered every 4 milliseconds:


Jimbee

Hi MartinL,

Thanks for the code and your reply.  I still see the same issue with the timer output initially starting low.  The first one shot trigger is a very short positive pulse then goes low for 1.5ms and is then fine after seeing the high-low-high transition.  Is there a way to have the timer start with the output defaulting state high so I can use the first shot.  I have attached a photo of what is being captured.

Best Regards,
Jim

Jimbee

Hi MartinL,

I am trying to setup an interrupt when the low pulse goes high and I am getting some errors compiling.  I have attached the code.  I must be doing something stupid.

Thanks,
Jim


Arduino: 1.8.9 (Mac OS X), Board: "Adafruit Metro M4 (SAMD51), Enabled, 120 MHz (standard), Small (-Os) (standard), 50 MHz (standard), Arduino, Off"

/Users/JimD/Documents/Arduino/TCC1_Timer_Test_Jim/TCC1_Timer_Test_Jim.ino: In function 'void setup()':
TCC1_Timer_Test_Jim:16:20: error: 'TCC1_IRQn' was not declared in this scope
   NVIC_SetPriority(TCC1_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC1 to 0 (highest)
                    ^
/Users/JimD/Documents/Arduino/TCC1_Timer_Test_Jim/TCC1_Timer_Test_Jim.ino: In function 'void TCC1_Timer()':
TCC1_Timer_Test_Jim:77:20: error: 'TCC1_IRQn' was not declared in this scope
   NVIC_SetPriority(TCC1_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC1 to 0 (highest)
                    ^
exit status 1
'TCC1_IRQn' was not declared in this scope

This report would have more information with
"Show verbose output during compilation"
option enabled in File -> Preferences.

MartinL

Hi Jim,

Regarding the timer glitch, it appears as though enabling the timer starts a oneshot trigger, as the output goes inactive high 1.5ms later.

The workaround is to leave the counter compare CC[2] register at its default of 0, enable the timer and then set CC[2] to 8999 afterwards:

Code: [Select]
// Remove setting the CC[2] register here...
//TCC1->CC[2].reg = 8999;                            // Set-up the CC (counter compare), channel 2 register for 1500us pulse width
//while (TCC1->SYNCBUSY.bit.CC2);                    // Wait for synchronization
 
TCC1->CTRLA.bit.ENABLE = 1;                        // Enable timer TCC1
while (TCC1->SYNCBUSY.bit.ENABLE);                 // Wait for synchronization
 
TCC1->CC[2].reg = 8999;                            // Set-up the CC (counter compare), channel 2 register for 1500us pulse width
while (TCC1->SYNCBUSY.bit.CC2);                    // Wait for synchronization

MartinL

In the case of the interrupts on the SAMD51:

TCC1 overflow (OVF) interrupt:

Code: [Select]
NVIC_SetPriority(TCC1_0_IRQn, 0);      // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC1 to 0 (highest)
NVIC_EnableIRQ(TCC1_0_IRQn);           // Connect the TCC1 timer to the Nested Vector Interrupt Controller (NVIC)

TCC1 match compare channel 2 (MC2) interrupt:

Code: [Select]
NVIC_SetPriority(TCC1_3_IRQn, 0);      // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC1 to 0 (highest)
NVIC_EnableIRQ(TCC1_3_IRQn);           // Connect the TCC1 timer to the Nested Vector Interrupt Controller (NVIC)

Jimbee

Hi MartinL,

Thanks for your help this works great if I leave CC.reg at 0.  If I try to set it back to 8999 then the pulse is still there on the first one shot.  Not sure?

I have added in an OVF interrupt this also works great.  The interrupt triggers on the falling edge of the one shot.  One question is it possible to have the interrupt trigger on the rising edge of the one shot pulse.


Thanks,
Jim

MartinL

Hi Jim,

Quote
If I try to set it back to 8999 then the pulse is still there on the first one shot.  Not sure?
The glitch pulse is most likely being generated, because the one shot pulse is being triggered immediately after enabling the timer and setting the CC[2] register. If you add a delay of say 4 milliseconds at the end of the setup():

Code: [Select]
delay(4);
The glitch pulse will extend out to 4ms:



Quote
One question is it possible to have the interrupt trigger on the rising edge of the one shot pulse.
Yes, if you use the Match Compare on channel 2 (MC2) interrupt, together with TCC1_3_Irq, this will call the TCC1_3_Handler() interrupt service routine function on the rising edge of the one shot pulse.

MartinL

Hi Jim,

Unlike the SAMD21, the SAMD51 uses separate TCC1 timer Handler functions for its overflow (OVF) and each of its match compare channels (MCx). This means that the SAMD51 doesn't need to test the interrupt flags in Handler function itself, (in order to determine which interrupt has been called), thereby providing a small efficiency saving.

Which Nested Vector Interrupt Controller (NVIC) interrupt (e.g. TCC1_0_Irq) is connected to what TCC1 interrupt flag, is detailed in section 10.2 NVIC Interrupt Line Mapping in the SAMD51 datasheet.

Jimbee

#53
Sep 16, 2019, 01:43 am Last Edit: Sep 16, 2019, 01:51 am by Jimbee Reason: missed attachments
Hi MartinL,

I tried the 4ms delay - it delays things but does not get rid of the initial pulse.  I tried setting up match for a interrupt on the rising edge but it did not change, I did however get another pulse between each one shot.  I must be doing something wrong.  I have attached a capture and my code.

On a side note, how do you like your Rigol scope.


Thanks,
Jim

Jimbee

Hi MartinL,

Everything is working fine I forgot to invert my pulse.... my bad.  Thanks for all your help.

Jim

MartinL

Hi Jim,

Glad to hear that everything's working fine.

The Rigol scope for the price is excellent. I bought the basic, 4 channel, DS1054Z, it works up to about 50MHz or so, which is good enough for 99% of what I want to do.

Shame that most the extra features that were available as a trial, have long since expired. These extra features such as increased bandwidth and serial decoding are quite expensive, with each one costing almost half the price of the scope itself. So I continue to use the scope without all the fancy enhancements, it's still really very good though.

cyborg5

Martin,

First of all thanks for all you are doing to help people in this thread including me. As a reminder I'm working on infrared library IRLib2. I need to generate PWM with a 33% duty cycle and a range of frequencies from about 36-58 kHz. Because I'm a glutton for punishment, I don't just want to get it working on one or two pins because I know as soon as I do someone is going to say "Why don't you support pin number (whatever)?" So I'm trying to write code that will work on any PWM pin. I got it working on pins that use TCC but it's not working on TC timers. I'm using as a model the code in wiring_analog.c for the function digitalWrite(). I don't particularly like the fact that fact that code uses COUNT8 but I can't get it to work anyway let alone switch to COUNT16.

I'm calculating the values using 120000000/(khz*1000) which for a sample value 30 kHz gives me 4000 which is obviously too big for an 8-bit counter. So I put a 1/16 divisor in thinking that would get me down into the 8-bit range. When I run the code, it seems to lock up. The serial monitor becomes unresponsive and I have to reboot the board to upload again. Here is my code. If you can get the eight bit version working that would be good enough. This doesn't need to be super accurate. But if you can show me how to do 16-bit that would be great.

Code: [Select]
  GCLK->PCHCTRL[GCLK_CLKCTRL_IDs[tcNum]].reg =
GCLK_PCHCTRL_GEN_GCLK0_Val | (1 << GCLK_PCHCTRL_CHEN_Pos); //use clock generator 0
  if (tcNum >= TCC_INST_NUM) {
// -- Configure TC
Tc* IR_PWM_TCx = (Tc*) GetTC(pinDesc.ulPWMChannel);
//reset
    IR_PWM_TCx->COUNT8.CTRLA.bit.SWRST = 1;
    while (IR_PWM_TCx->COUNT8.SYNCBUSY.bit.SWRST);
    // Disable TCx
    IR_PWM_TCx->COUNT8.CTRLA.bit.ENABLE = 0;
    while (IR_PWM_TCx->COUNT8.SYNCBUSY.bit.ENABLE);
    // Set Timer counter Mode to 8 bits, normal PWM, prescaler 1/16
IR_PWM_TCx->COUNT8.CTRLA.reg = TC_CTRLA_MODE_COUNT8 | TC_CTRLA_PRESCALER_DIV16;
    IR_PWM_TCx->COUNT8.WAVE.reg = TC_WAVE_WAVEGEN_NPWM;
    while (IR_PWM_TCx->COUNT8.SYNCBUSY.bit.CC0);
    // Set the initial value
    // Each timer counts up to a maximum or TOP value set by the PER register,
    // this determines the frequency of the PWM operation.
    uint32_t cc = 120000000UL/16/(khz*1000) - 1;
    // The CCx register value corresponds to the pulsewidth in microseconds (us)
    // Set the duty cycle of the PWM on TCx to 33%
    IR_PWM_TCx->COUNT8.CC[tcChannel].reg = (uint8_t) cc/3;
    while (IR_PWM_TCx->COUNT8.SYNCBUSY.bit.CC0);
    IR_PWM_TCx->COUNT8.PER.reg = cc;
    while (IR_PWM_TCx->COUNT8.SYNCBUSY.bit.PER);
    IR_PWM_TCx->COUNT8.CTRLA.bit.ENABLE = 0;      //temporarily disable, will enable later
    while (IR_PWM_TCx->COUNT8.SYNCBUSY.bit.ENABLE);
  } else {
// Use TCC timers. This code works so I won't post it.
}

Jimbee

Hi MartinL,

Thanks for the feedback on the scope.  I had a look awhile back, they seem to be very good value - price performance.  Much less than Techtronics scopes.

I have another question.  I have setup a second oneshot timer TCC4 which works fine... thanks.  I am trying to trigger the TCC4 one shot inside the TCC1 interrupt but I can not get it working.  I have tried wrapping the retrigger commands in noInterrupt() and interrupt() but no luck.  Is there a way to trigger the 2nd one shot after the 1st one shots pulse goes high.

Thanks,
Jim

MartinL

Hi cyborg5,

The TC timers have less features than the TCC timers with the main issue being the lack of frequency control in both 16-bit and 32-bit modes. To the control timer's frquency requires the use of either of 8-bit mode or 16-bit/32-bit in Match PWM (MPWM) mode. However, MPWM mode sacrifices the timer's CC0 channel to achieve this.

Looking at your code, it could be a synchronization problem, if you're using CC1 register but synchronizing on the CC0?

The other thing is setting the prescaler and counter synchronization in the CTRLA register. This determines whether the timer should wrap around/reset on the next generic clock or prescaler clock. If you're using the prescaler (DIV16) then it's better to select prescaler clock synchronization (TC_CTRLA_PRESCSYNC_PRESC):

Code: [Select]
IR_PWM_TCx->COUNT8.CTRLA.reg = TC_CTRLA_MODE_COUNT8 |
                               TC_CTRLA_PRESCALER_DIV16 |
                               TC_CTRLA_PRESCSYNC_PRESC;

MartinL

Hi Jim,

I didn't have much luck trying to retrigger TCC4 from within TCC1's interrupt service routine either.

Perhaps a more efficient way is to use the SAMD51's event system. The event system is a 32 channel peripheral-to-peripheral highway that allows them to communicate without CPU intervention.

It's possible to get TCC1 to generate a match compare event on the signal's rising edge on channel 2 and set up TCC4 to retrigger each time it receives this event.

Here I've set up TCC1 and TCC4 in one shot mode and to generate a 1.5ms pulse. TCC1 retriggers TCC4 on its rising edge, causing TCC4 to generate a similar pulse on its channel:

Code: [Select]
// Adafruit Metro M4 Only: Set-up digital pins D10 (TCC1) to output a one shot pulse with 1500us pulse width
// at an interval of 4 milliseconds
// Use the event system to retrigger the same 1500us output pulse on D5 (TCC4) on the rising edge of D10 (TCC1)
void setup()
{
  MCLK->APBBMASK.reg |= MCLK_APBBMASK_EVSYS;         // Switch on the event system peripheral
 
  // Set up the generic clock (GCLK7) to clock timer TCC1
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(1) |       // Divide the 48MHz clock source by divisor 1: 48MHz/1 = 48MHz
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         GCLK_GENCTRL_SRC_DFLL;      // Select 48MHz DFLL clock source
                         //GCLK_GENCTRL_SRC_DPLL1;     // Select 100MHz DPLL clock source
                         //GCLK_GENCTRL_SRC_DPLL0;     // Select 120MHz DPLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization 
 
  // TCC1 /////////////////////////////////////////////////////////////////////////////////////////////////
 
  GCLK->PCHCTRL[TCC1_GCLK_ID].reg = GCLK_PCHCTRL_CHEN |        // Enable the TCC1 perhipheral channel
                                    GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to TCC1
 
  // Enable the peripheral multiplexer on pin D10
  PORT->Group[g_APinDescription[10].ulPort].PINCFG[g_APinDescription[10].ulPin].bit.PMUXEN = 1;
 
  // Set the D10 (PORT_PA18) peripheral multiplexer to peripheral (even port number) F(5): TCC1, Channel 2
  PORT->Group[g_APinDescription[10].ulPort].PMUX[g_APinDescription[10].ulPin >> 1].reg |= PORT_PMUX_PMUXE(5);
 
  TCC1->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV8 |       // Set prescaler to 8, 48MHz/8 = 6MHz
                    TCC_CTRLA_PRESCSYNC_PRESC;       // Set the reset/reload to trigger on prescaler clock                 

  TCC1->CTRLBSET.reg = TCC_CTRLBSET_ONESHOT;         // Enable one shot
  while (TCC1->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization
 
  TCC1->DRVCTRL.reg |= TCC_DRVCTRL_NRE2;             // Continue to drive the output on TCC1/WO[2] when timer has stopped (rather than becoming tri-state) 
  TCC1->DRVCTRL.reg |= TCC_DRVCTRL_INVEN2;           // Invert the output to generate an active low pulse on TCC1/WO[2]
 
  TCC1->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;            // Set-up TCC1 timer for Normal (single slope) PWM mode (NPWM)
  while (TCC1->SYNCBUSY.bit.WAVE)                    // Wait for synchronization

  TCC1->PER.reg = 8999;                              // Set-up the PER (period) register for 1500us pulse period
  while (TCC1->SYNCBUSY.bit.PER);                    // Wait for synchronization

  // TCC4 /////////////////////////////////////////////////////////////////////////////////////////////////

  GCLK->PCHCTRL[TCC4_GCLK_ID].reg = GCLK_PCHCTRL_CHEN |        // Enable the TCC1 perhipheral channel
                                    GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to TCC4
 
  // Enable the peripheral multiplexer on pin D10
  PORT->Group[g_APinDescription[5].ulPort].PINCFG[g_APinDescription[5].ulPin].bit.PMUXEN = 1;
 
  // Set the D5 (PORT_PB14) peripheral multiplexer to peripheral (even port number) F(5): TCC4, Channel 0
  PORT->Group[g_APinDescription[5].ulPort].PMUX[g_APinDescription[5].ulPin >> 1].reg |= PORT_PMUX_PMUXE(5);
 
  TCC4->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV8 |       // Set prescaler to 8, 48MHz/8 = 6MHz
                    TCC_CTRLA_PRESCSYNC_PRESC;       // Set the reset/reload to trigger on prescaler clock                 

  TCC4->CTRLBSET.reg = TCC_CTRLBSET_ONESHOT;         // Enable one shot
  while (TCC4->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization
 
  TCC4->DRVCTRL.reg |= TCC_DRVCTRL_NRE0;             // Continue to drive the output on TCC4/WO[0] when timer has stopped (rather than becoming tri-state) 
  TCC4->DRVCTRL.reg |= TCC_DRVCTRL_INVEN0;           // Invert the output to generate an active low pulse on TCC4/WO[0]
 
  TCC4->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;            // Set-up TCC4 timer for Normal (single slope) PWM mode (NPWM)
  while (TCC4->SYNCBUSY.bit.WAVE)                    // Wait for synchronization

  TCC4->PER.reg = 8999;                              // Set-up the PER (period) register for 1500us pulse period
  while (TCC4->SYNCBUSY.bit.PER);                    // Wait for synchronization

  // Event System /////////////////////////////////////////////////////////////////////////////////////////////

  // Select the event system user on channel 0 (USER number = channel number + 1)
  EVSYS->USER[EVSYS_ID_USER_TCC4_EV_0].reg = EVSYS_USER_CHANNEL(1);                 // Set the event user (receiver) as timer TCC4 event action 0

  // Select the event system generator on channel 0
  EVSYS->Channel[0].CHANNEL.reg = 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_TCC1_MCX_2);     // Set event generator (sender) as TCC1 Match Capture channel 2
 
  TCC1->EVCTRL.reg |= TCC_EVCTRL_MCEO2;              // Output event enable on TCC1 Match Capture channel 2 (MC2)

  TCC4->EVCTRL.reg |= TCC_EVCTRL_TCEI0 |             // Input event enable 0 on TCC4
                      TCC_EVCTRL_EVACT0_RETRIGGER;   // Retrigger timer TCC4 upon receiving the event

  // Start TCC1 and TCC4 //////////////////////////////////////////////////////////////////////////////////////////
 
  TCC1->CTRLA.bit.ENABLE = 1;                        // Enable timer TCC1
  while (TCC1->SYNCBUSY.bit.ENABLE);                 // Wait for synchronization
 
  TCC1->CC[2].reg = 8999;                            // Set-up the CC (counter compare), channel 2 register for 1500us pulse width
  while (TCC1->SYNCBUSY.bit.CC2);                    // Wait for synchronization
 
  TCC4->CTRLA.bit.ENABLE = 1;                        // Enable timer TCC4
  while (TCC4->SYNCBUSY.bit.ENABLE);                 // Wait for synchronization
 
  TCC4->CC[0].reg = 8999;                            // Set-up the CC (counter compare), channel 0 register for 1500us pulse width
  while (TCC4->SYNCBUSY.bit.CC0);                    // Wait for synchronization
}

void loop()
{         
  TCC1->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER;   // Retrigger a one shot pulse
  while (TCC1->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization
  delay(8);                                          // Wait for 8 milliseconds
}

Here's the output, pulses spaced at 8ms intervals:


Go Up