Go Down

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

MartinL

Hi czitro,

The PWM signal at on D7 is at 50Hz, but at 50% duty-cycle the 10ms pulse width is too wide to drive a servo. Normally, servos usually operate with a pulse width between 1 and 2ms repeated at 50Hz (20ms).

MartinL

Hi czitro,

The following code generates a 50Hz servo output on D7, just load the CCBUF0 regsiter with the required pulse width in microseconds: 1000 to move the servo to it's minimum position, 1500 to centre the servo, 2000 to move the servo to it's maximum position, (or anything in between):

Code: [Select]
// Adafruit Metro M4 Only: Set-up digital pin D7 to 50Hz servo output
void setup()
{
  // Set up the generic clock (GCLK7) to clock timer TCC0
  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;      // Select 48MHz DFLL clock source
                         //GCLK_GENCTRL_SRC_DPLL0;     // 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 perhipheral 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_DIV16 |        // Set prescaler to 16, 16MHz/16 = 1MHz
                    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 = 19999;                             // Set-up the PER (period) register 50Hz PWM
  while (TCC0->SYNCBUSY.bit.PER);                    // Wait for synchronization
 
  TCC0->CC[0].reg = 1500;                            // Centre the servo
  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()

  TCC0->CCBUF[0].reg = 1000;                         // Set servo to minimum
  delay(1000);                                       // Wait for 1 second
  TCC0->CCBUF[0].reg = 2000;                         // Set servo to maximum
  delay(1000);                                       // Wait for 1 second
}

signal64

For the IRQ example, is there anyway to have it trigger on falling edge?

I'm using the PWM to clock an external device that reads data on the high pulse and wanted to use the IRQ to switch data on the Low. 


MartinL

Hi signal64,

Quote
For the IRQ example, is there anyway to have it trigger on falling edge?
By default in Normal PWM mode the output signal goes high at the start of the timer cycle and low when the timer's COUNT (counter) matches the value in its CCx (counter compare) register.

If you enable the MCx (match compare) interrupt for a given channel, then the interrupt service routine (TCCx_Handler() function) will be called on the waveform's fall edge.

signal64

I'm struggling with getting the match compare interrupt to work.
Overflow works fine.

The IRQ setup for TCC1 and CC[1] should be this right?:
Code: [Select]
NVIC_SetPriority(TCC1_0_IRQn, 0);   
NVIC_EnableIRQ(TCC1_0_IRQn);       
TCC1->INTENSET.reg |= TCC_INTENSET_MC1;

And the Handler:
Code: [Select]
void TCC1_0_Handler() {
   if (TCC1->INTENSET.bit.OVF && TCC1->INTFLAG.bit.OVF)
  {
     // my code

      TCC1->INTFLAG.bit.OVF = 1;
   }
   
   if (TCC1->INTENSET.bit.MC1 && TCC1->INTFLAG.bit.MC1) 
  {
     // my code
       
     TCC1->INTFLAG.bit.MC1 = 1;                           
  }
}

Also a bit confused on the change to use TCC1_0_IRQn and TCC1_0_Handler().
Without the _0_ these are undefined.
Also would think the _0_ should be _1_ but that doesn't work.
Not sure what the _0_ is in reference to.

I added debug lines to see if the handler and if conditions are getting hit.
With the OVF IRQ enable it works.
It's not calling the handler when I use the MC1 (or MC0) IRQ enable.

MartinL

Hi signal64,

My apologies, I was thinking of the SAMD21 rather than the SAMD51, in my previous post.

On the SAMD21 each TCCx timer has only one associated handler function TCCx_Handler(). The SAMD51 TCCx timers by contrast each have a number of interrupt handler functions TCCx_y_Handler(), where x is the timer number and y is the handler number.

The TCC1_0_Handler() function is called for all timer interrupt flags including overflow (OVF), except for the Match Compare (MCx) interrupts. The Match Compare (MCx) interrupts have their own interrupt service routine. MC0 calls TCC1_1_Handler(), MC1 calls TCC1_2_Handler() and so on... If you're using MC1 for example, you'll need to change your code for TCC1_2_Handler().

The key to which interrupt flag calls which handler function is in section 10.2.2 (NVIC) Interrupt Line Mapping table in the SAMD51 datasheet. The peripheral handler functions themselves are defined in the microcontroller's CMSIS (Cortex Microcontroller Software Interface Standard) "samd51j19a.h" file.

The CMSIS files can be found under (on my windows machine at least) at: C:\User\Computer\AppData\Local\Arduino15\packages\arduino\tools\CMSIS-Atmel\1.2.0\CMSIS\Atmel\samd51\include\... These directories also contain all the register definitions for the SAMD51 microcontroller. 

The fact that each MCx interrupt has its own dedicated handler function means that it isn't necessary to test interrupt flags, in order to test which interrupt called the routine. This provides the SAMD51 with a small speed optimisation over the SAMD21 microcontroller. (Although the interrupt flag still needs to be manually cleared by writing a one to it within the handler).

cyborg5

I'm the author of IRLib2 which is used to receive, decode, and send infrared signals. I need to be able to set up PWM output in a range anywhere from about 34 kHz up to 58 kHz. At one point I had support for Adafruit Metro M4 working but somewhere along the way the variant.cpp values changed and now it's no longer working. I barely understand how any of this stuff works so it's kind of hard to figure out what's wrong. I've modeled the code after digitalWrite in wiring_analog.c.there's something missing. I think my old code always used clock generator zero and assumed that it was pre-initialized to a usable value. I think that's what's missing in my code is that that's no longer true. I would like the code to be flexible enough that I can use any PWM capable pin. Also I want to avoid conflicts with other software so I was hoping I could find a clock that I didn't have to mess with for fear it would conflict with some other library. On the M0 boards I use clock zero with no reconfiguration. I just attach it to the proper TCC as is. I would like to be able to do the same thing on M4. Could you please take a look at this code and see where I missing. The pin number is defined as IR_SEND_PWM_PIN and for testing purposes I'm using pin 7 but I would like to be free to use any PWM capable pin. One of the problems with my code right now is that it only works for PWM TCC and not TC timers. I will worry about that later. I'm currently using a Metro M4 Grand Central pin 7 which is PD21 and uses PIN_ATTR_PWM_F and TCC1_CH1.

Code: [Select]
void initializeSAMD51PWM(uint16_t khz) {
  PinDescription pinDesc = g_APinDescription[IR_SEND_PWM_PIN];
  uint32_t attr = pinDesc.ulPinAttribute;
  //If PWM unsupported then do nothing and exit
  if( !(attr & (PIN_ATTR_PWM_E|PIN_ATTR_PWM_F|PIN_ATTR_PWM_G))){
    return;
  }
  uint32_t tcNum = GetTCNumber(pinDesc.ulPWMChannel);
  uint8_t tcChannel = GetTCChannelNumber(pinDesc.ulPWMChannel);

  if(attr & PIN_ATTR_PWM_E)
    pinPeripheral(IR_SEND_PWM_PIN, PIO_TIMER);
  else if(attr & PIN_ATTR_PWM_F)
    pinPeripheral(IR_SEND_PWM_PIN, PIO_TIMER_ALT);
  else if(attr & PIN_ATTR_PWM_G)
    pinPeripheral(IR_SEND_PWM_PIN, PIO_TCC_PDEC);

  GCLK->PCHCTRL[GCLK_CLKCTRL_IDs[tcNum]].reg =
 GCLK_PCHCTRL_GEN_GCLK0_Val | (1 << GCLK_PCHCTRL_CHEN_Pos); //use clock generator 0
  
  // Normal (single slope) PWM operation: timers countinuously count up to PER
  // register value and then is reset to 0
  //Configure TCC
  IR_TCCx = (Tcc*) GetTC(pinDesc.ulPWMChannel);
  //Reset
  IR_TCCx->CTRLA.bit.SWRST = 1;
  while (IR_TCCx->SYNCBUSY.bit.SWRST);
  // Disable TCCx
  IR_TCCx->CTRLA.bit.ENABLE = 0;
  while (IR_TCCx->SYNCBUSY.bit.ENABLE);
  // Sent pre-scaler to 1
  IR_TCCx->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV11 | TCC_CTRLA_PRESCSYNC_GCLK;
  //Set TCCx as normal PWM
  IR_TCCx->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;
  while (IR_TCCx->SYNCBUSY.bit.WAVE);
  while (IR_TCCx->SYNCBUSY.bit.CC0 || IR_TCCx->SYNCBUSY.bit.CC1);

  // 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/(khz*1000) - 1;
  // The CCx register value corresponds to the pulsewidth in microseconds (us)
  // Set the duty cycle of the PWM on TCCx to 33%
  IR_TCCx->CC[tcChannel].reg = (uint32_t) cc/3;      
  while (IR_TCCx->SYNCBUSY.bit.CC0 || IR_TCCx->SYNCBUSY.bit.CC1);

  IR_TCCx->PER.reg = cc;      // Set the frequency of the PWM on IR_TCCx
  while(IR_TCCx->SYNCBUSY.bit.PER);

  IR_TCCx->CTRLA.bit.ENABLE = 0;            //initially off will turn on later
  while (IR_TCCx->SYNCBUSY.bit.ENABLE);
}


Any idea what's going wrong here?

MartinL

Hi cyborg5,

I've had a look at your code, perhaps I've missed something, but everything seems OK provided that you've enabled the timer after intialisation.

Are you able to generate PWM output on D7 (PD21) on the Metro M4 Grand Central using the analogWrite() function? This would at least confirm that the analogWrite() portion of your code is functioning correctly.

Jimbee

Hi MartinL,

I have been trying to start and stop, enable and disable TCC1 timer using the commands from you but nothing seams to work for me. The output D10 always continues to pulse.  I can not seam to stop or disable the timer output.  I am doing the following;

void setup() {
TCC1StartTimer();

}

void loop() {

 delay(500);
TCC1->CTRLBSET.reg = TCC_CTRLBSET_CMD_STOP;  // stop TCC1
while (TCC1->SYNCBUSY.bit.CTRLB);
//TCC1->CTRLA.bit.ENABLE = 0;  // disable TCC1
//while (TCC1->SYNCBUSY.bit.ENABLE);

}

Thanks,
Jim

MartinL

Hi Jim,

Here's some example code that sets up TCC1/WO[2] on D10 (PA18) at 50Hz PWM, 50% duty-cycle. It then starts and stops the timer at 1 second intervals using the STOP and RETRIGGER commands:

Code: [Select]
// Adafruit Metro M4 Only: Set-up digital pin D10 to output 50Hz, single slope PWM with a 50% duty cycle
// Timer stop and started at 1 second intervals
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_DPLL0;     // 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 0
  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->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 = 119999;                            // Set-up the PER (period) register 50Hz PWM
  while (TCC1->SYNCBUSY.bit.PER);                    // Wait for synchronization
 
  TCC1->CC[2].reg = 59999;                           // Set-up the CC (counter compare), channel 2 register for 50% duty-cycle
  while (TCC1->SYNCBUSY.bit.CC0);                    // 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_STOP;        // Stop TCC1
  while (TCC1->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization
  delay(1000);                                       // Wait for 1 second
  TCC1->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER;   // Retrigger (start) TCC1
  while (TCC1->SYNCBUSY.bit.CTRLB);                  // Wait for synchronization
  delay(1000);                                       // Wait for 1 second
}

cyborg5

It turns out that the code I posted earlier does work. I had a combination of software and hardware problems and when I solved the software problem it didn't work because I still had a hardware issue. I still have to make it work with TC timers instead of TCC in some cases for some pins but I think I can get that on my own. If I can't, I'll be back. Thanks for taking a look at this.

Jimbee

Hi MartinL,
Thank you for your reply.  That works great... I was doing something stupid.... my bad. 

I have a question for you I am basically trying to create a controlled one shot pulse low for a variable pulse time low (typically 1500us variable down to 600us.  Basic steps are below.  Any thoughts on the best way to do this.

Step 1: Output high
Step 2: Trigger one shot
Step 3: Output goes low for 1500us then output goes high
Step 4: Wait for trigger


Thanks,
Jim

MartinL

Hi Jim,

Essentially the SAMD51's one shot mode works in exactly the same way as standard PWM, in as much as you set up the period (PER) and counter compare (CCx) registers in the same way. The only difference is that the output pulse is generated by a software trigger and the timer automatically stops at the end of the cycle (overflow), rather than being retriggered every cycle.

To set up TCC timer in one shot mode:

Code: [Select]
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)

Note that NRE stands for Non Recoverable State Output Enable.

To trigger a one shot pulse:

Code: [Select]
TCC1->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER;         // Retrigger the timer's One/Multi-Shot pulses
while (TCC1->SYNCBUSY.bit.CTRLB);                        // Wait for synchronization

To invert the output for an active low pulse:

Code: [Select]
TCC1->DRVCTRL.reg |= TCC_DRVCTRL_INVEN2;        // Invert the output to generate an active low pulse on TCC1/WO[2]

Jimbee

Hi MartinL,

Thanks for your help.  One or maybe three questions.

1) Do I add those lines inside the timer setup or external?  I know the RETRIGGER is external but not sure on the others.

2) If they are to be added inside the timer does the order of placement matter?

3) I am using PER reg to set time low but what should I set the CC[2] reg too?


Thanks,
Jim

Jimbee

Hi MartinL,

I did some playing it is working great but I am seeing some issues with the one shot;

1) the issue is timer output initially starts out low.
2) If I retrigger in setup() to run just once the output starts out low and then goes high.  I never get the high, low, high transistion.
3) If I retrigger in loop() I get a very small positive going pulse at the beginning then all is well after that.

I have attached my code.  Is there a way to set the output high to start I tried a digitalWrite but that did not work.  Your input would be greatly appreciated.

Thanks,
Jim

Go Up