Go Down

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

MartinL

Hi Jim,

The reason why some of your timers aren't working, is because on the SAMD51 some of the TCCx timers share the same generic clock:

PHCTRL[25] = TCC0/TCC1
PHCTRL[29] = TCC2/TCC3
PHCTRL[38] = TCC4

For example if you've set up generic clock 7 (GCLK7) for TCC0, then TCC1 is also automatically connected to GCLK7 as well. Same goes for TCC2 and TCC3.

On TCC4, (that doesn't share a generic clock), it's not working because PB15 is on channel 1, you just need to change the counter compare register from CC[0] to CC[1].

Jimbee

Hi MartinL,

Thanks for the info. things are getting a little clearer.  So just to clarify;

For example;

PA16 - TC2/WO[0], TCC1/WO[0], TCC0/WO[4]
PA17 - TC2/WO[1], TCC1/WO[1], TCC0/WO[5]

I would change in the TCC1 timer counter compare code;

From PA16 - channel 0, Even(5): Output PWM on PA16;
TCC1->CC[0].reg = 99;
while (TCC1->SYNCBUSY.bit.CC0);

To PA17 - channel 1, Odd(5); Output PWM on PA17;
TCC1->CC[1].reg = 99;
while(TCC1->SYNCBUSY.bit.CC1);

So to change the channel CC[WO#].

Thanks,
Jim

Would this now output PWM on pin PA17.


MartinL

Hi Jim,

That's right.

If you're using a WO[4] to WO[7] on TCC1, this will also map to CC[0] to CC[3] respectively.

Casey10110

Super helpful stuff Martin. Thank you for everything, this thread rocks and seems to get better and better.

Jimbee

Hi MartinL,

Thanks for all your help.  I need your help with an example of TCC timer using;

1) an interrupt with handler.
2) best way to stop and start the TCC timer.

Thanks,
Jim

MartinL

Hi Jim,

To use the TCCx interrupt handler it's first necessary to connect the peripherial to the ARM core through its NVIC (Nested Vector Interrupt Controller), then enable the required timer interrupt.

The SAMD51 offers a number of interrupt possibilities, but the main ones are OVF (overflow) that calls the interrupt handler at the end of each time cycle and MCx (Match Compare) that calls the interrupt handler each time the COUNT (Counter) register matches the correspoinding CCx (Counter Compare) value.

For example, to connect the TCC0 timer to the ARM core and enable overflow interrupts you'd add the following to the setup() portion of your sketch:

Code: [Select]
NVIC_SetPriority(TCC0_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC0 to 0 (highest)
NVIC_EnableIRQ(TCC0_IRQn);         // Connect TCC0 to Nested Vector Interrupt Controller (NVIC)
TCC0->INTENSET.reg |= /*TCC_INTENSET_MC1 | TCC_INTENSET_MC0 |*/ TCC_INTENSET_OVF;  // Enable overflow interrupts

Inside the handler function itself, the corresponding interrupt flag can be tested to discover which interrupt has occured and your own application code added. It's also necessary to clear the interrupt flag by writing a 1 to it. (The name of the interrupt handler itself is reserved and should not be changed).

For instance, here's the interrupt handler for TCC0:

Code: [Select]
void TCC0_Handler()                                       // Interrupt handler for TCC0
{
  if (TCC0->INTENSET.bit.OVF && TCC0->INTFLAG.bit.OVF)    // Test if the OVF (Overflow) interrupt has occured
  {
    // ...
    // Add your application code here...
    // ... 
    TCC0->INTFLAG.bit.OVF = 1;                            // Clear the OVF interrupt flag
  }
 
  if (TCC0->INTENSET.bit.MC0 && TCC0->INTFLAG.bit.MC0)    // Test if the MC0 (Match Compare Channel 0) interrupt has occured
  {
    // ...
    // Add your application code here...
    // ... 
    TCC0->INTFLAG.bit.MC0 = 1;                            // Clear the MC0 interrupt flag
  }

  if (TCC0->INTENSET.bit.MC1 && TCC0->INTFLAG.bit.MC1)    // Test if the MC1 (Match Compare Channel 1) interrupt has occured
  {
    // ...
    // Add your application code here...
    // ... 
    TCC0->INTFLAG.bit.MC1 = 1;                            // Clear the MC1 interrupt flag
  }
  // Add other channels as required...
}

To enable the TCC0 timer:

Code: [Select]
TCC0->CTRLA.bit.ENABLE = 1;           // Enable timer TCC0
while (TCC0->SYNCBUSY.bit.ENABLE);    // Wait for synchronization

To disable the TCC0 timer:

Code: [Select]
TCC0->CTRLA.bit.ENABLE = 0;           // Disable timer TCC0
while (TCC0->SYNCBUSY.bit.ENABLE);    // Wait for synchronization

To stop the TCC0 timer:

Code: [Select]
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_STOP;   // Stop timer TCC0
while (TCC0->SYNCBUSY.bit.CTRLB);             // Wait for synchronization

To restart the TCC0 timer:

Code: [Select]
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER;   // Start timer TCC0
while (TCC0->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization

To read the value of the TCC0 COUNT register:

Code: [Select]
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_READSYNC;    // Initiate read synchronization of the COUNT register
while (TCC0->SYNCBUSY.bit.CTRLB);                  // Wait for CTRLB register write synchronization
while (TCC0->SYNCBUSY.bit.COUNT);                  // Wait for COUNT register read synchronization
Serial.println(TCC0->COUNT.reg);                   // Read the COUNT register

Kind regards,
Martin

Jimbee

Hi MartinL,

Sorry for the delay in my reply, just got back from holidays.  Thank you again for all your help.

Best Regards,
Jim

sajattack

Hi MartinL,

I'm trying to do on-the-fly duty cycle adjustments using TC2 and PA16/D13, could you give me a hand?

MartinL

Hi sajattack,

Quote
I'm trying to do on-the-fly duty cycle adjustments using TC2 and PA16/D13, could you give me a hand?
Do you intend to use have control over both the output's frquency and duty-cycle?

If that's the case it will be necessary to output on TC2's channel 1 (TC2/WO[1]), such as port PA17 on D12, using the timer's Match PWM (MPWM) mode.

Normal PWM (NPWM) mode on the TCx timers offers two channel operation on channels 0 an 1, but sacrifices frequency control.

MartinL

Here's an example of using the TC2 timer to output PWM at 50Hz at 25% and 75% alternating duty-cycles every second on D12/PA17 (TC2/WO[1]):

Code: [Select]
// Output 50Hz PWM on Metro M4 pin D12/PA17 using the TC2 timer
void setup()
{
  MCLK->APBBMASK.reg |= MCLK_APBBMASK_TC2;           // Activate timer TC2

  // Set up the generic clock (GCLK7) used to clock timers
  GCLK->GENCTRL[7].reg = GCLK_GENCTRL_DIV(3) |       // Divide the 48MHz clock source by divisor 3: 48MHz/3 = 16MHz
                         GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                         GCLK_GENCTRL_GENEN |        // Enable GCLK7
                         GCLK_GENCTRL_SRC_DFLL;      // Generate from 48MHz DFLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL7);               // Wait for synchronization

  GCLK->PCHCTRL[26].reg = GCLK_PCHCTRL_CHEN |         // Enable perhipheral channel
                          GCLK_PCHCTRL_GEN_GCLK7;     // Connect generic clock 7 to TC2 at 16MHz

  // Enable the peripheral multiplexer on digital pin 12
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;
  // Set the peripheral multiplexer for D12 to peripheral E(4): TC2, Channel 0
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXO(4);
 
  TC2->COUNT16.CTRLA.reg = TC_CTRLA_PRESCALER_DIV16 |        // Set prescaler to 64, 16MHz/16 = 1MHz
                           TC_CTRLA_PRESCSYNC_PRESC |        // Set the reset/reload to trigger on prescaler clock
                           TC_CTRLA_MODE_COUNT16;            // Set the counter to 16-bit mode
  TC2->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MPWM;      // Set-up TC2 timer for Match PWM mode (MPWM)
  TC2->COUNT16.CC[0].reg = 19999;                    // Use CC0 register as TOP value, set for 50Hz PWM 
  while (TC2->COUNT16.SYNCBUSY.bit.CC0);             // Wait for synchronization
  TC2->COUNT16.CC[1].reg = 9999;                     // Set the duty cycle to 50% (CC1 half of CC0)
  while (TC2->COUNT16.SYNCBUSY.bit.CC1);             // Wait for synchronization
  TC2->COUNT16.CTRLA.bit.ENABLE = 1;                 // Enable timer TC2
  while (TC2->COUNT16.SYNCBUSY.bit.ENABLE);          // Wait for synchronization
}

void loop()
{
  // Using the TC2's buffered CCBUF registers for "on the fly" operation
  TC2->COUNT16.CCBUF[1].reg = 4999;                  // Set the duty cycle to 25% (CC1 half of CC0)
  delay(1000);                                       // Wait 1 second
  TC2->COUNT16.CCBUF[1].reg = 14999;                 // Set the duty cycle to 75% (CC1 half of CC0)
  delay(1000);                                       // Wait 1 second
}

sajattack

Oh, so I must've read the datasheet wrong. I thought WO[0] was the output. Thanks.

Jimbee

Hi MartinL,

I am trying to use a digital pot to adjust the frequency and duty cycle of the timer TCC1_CH2.  I am getting errors compiling and not sure what I am missing.  I have attached the errors and code.

Arduino: 1.8.9 (Mac OS X), Board: "Adafruit Grand Central M4 (SAMD51), Enabled"

/Users/JimD/Documents/Arduino/TCC1_Timer_Test/TCC1_Timer_Test.ino: In function 'void loop()':
TCC1_Timer_Test:22:15: error: no match for 'operator[]' (operand types are 'volatile TCC_PERBUF_Type' and 'int')
   TCC1->PERBUF[2].reg = PotValue-1;                     // Set the frequency of the PWM on TCC1 Channel 2
               ^
TCC1_Timer_Test:23:31: error: 'volatile struct TCC_SYNCBUSY_Type::<anonymous>' has no member named 'PERBUF2'
     while (TCC1->SYNCBUSY.bit.PERBUF2);
                               ^
TCC1_Timer_Test:25:31: error: 'volatile struct TCC_SYNCBUSY_Type::<anonymous>' has no member named 'CCBUF2'
     while (TCC1->SYNCBUSY.bit.CCBUF2);
                               ^
exit status 1
no match for 'operator[]' (operand types are 'volatile TCC_PERBUF_Type' and 'int')

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

Sorry I fixed the file it has a typo: TCC4 to TCC1 on the SYNCBUSY's.

Best Regards,
Jim


MartinL

Hi Jim,

The TCC timers only have a single PER and PERBUF register per timer, therefore the square array brackets [] are not required:

Code: [Select]
TCC1->PERBUF.reg = PotValue-1;                     // Set the frequency of the PWM on TCC1
However, this means that any change to the PER or PERBUF registers will affect all the TCC1 timer channels.

Curiously the SAMD51 datasheet states that the CCBUFx and PERBUF registers are read and write synchronized, but (unlike the SAMD21) offers no corresponding CCBUFx or PERBUF synchronization bits in the SYNCBUSY register. The bits don't appear in the SAMD51's CMSIS register definitions either.

In the first edition of the SAMD51 datasheet the TCC timers' CCx registers let alone the buffered registers weren't documented, so perhaps they haven't got round to documenting them fully?

In practical terms and in the absence of SYNCBUSY synchronization bits, I just forgo the synchronization check and delete the while() loops. Synchronization happens anyway irrespective of whether you check for it or not. It only really becomes an issue when accessing the peripheral's register two times in quick succession, before synchronization has had a chance to complete. This only normally takes a handful of peripheral generic and APB clock cycles:

5×PGCLK + 2×PAPB < D < 6×PGCLK + 3×PAPB

Jimbee

Hi MartinL,

Thanks, removing the brackets [ ] and the "while()" loops fixed the compile errors.


Thanks Again,
Jim

czitro

Hello there MartinL,

I tried to use your code for the timer and it compiles and uploads to the board but nothing actually happens then I can't get the servo connected to pin 7.


Did I miss something I should also write in the code additionally?
Otherwise I don't know what I'm doing wrong :/

Code: [Select]
// Adafruit Metro M4 Only: Set-up digital pin D7 to output 50Hz, single slope PWM with a 50% duty cycle
void setup()
{
 // Set up the generic clock (GCLK7) to clock timer TCC0
 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[25].reg = GCLK_PCHCTRL_CHEN |        // Enable the TCC0 peripheral channel
                         GCLK_PCHCTRL_GEN_GCLK7;    // Connect generic clock 7 to TCC0

 // Enable the peripheral multiplexer on pin D7
 PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
 
 // Set the D7 (PORT_PB12) peripheral multiplexer to peripheral (even port number) E(6): TCC0, Channel 0
 PORT->Group[g_APinDescription[7].ulPort].PMUX[g_APinDescription[7].ulPin >> 1].reg |= PORT_PMUX_PMUXE(6);
 
 TCC0->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                

 TCC0->WAVE.reg = TC_WAVE_WAVEGEN_NPWM;             // Set-up TCC0 timer for Normal (single slope) PWM mode (NPWM)
 while (TCC0->SYNCBUSY.bit.WAVE)                    // Wait for synchronization

 TCC0->PER.reg = 119999;                            // Set-up the PER (period) register 50Hz PWM
 while (TCC0->SYNCBUSY.bit.PER);                    // Wait for synchronization
 
 TCC0->CC[0].reg = 59999;                           // Set-up the CC (counter compare), channel 0 register for 50% duty-cycle
 while (TCC0->SYNCBUSY.bit.CC0);                    // Wait for synchronization

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

void loop() {}

Go Up