Metro M4 Express ATSAMD51 PWM Frequency and Resolution

Hi,

Can anyone clarify what the default PWM frequency and resolution on the Adafruit Metro M4 is when using the arduino IDE and analogWrite? I've seen some places that it's 8 bit and 1.8 Khz, is that correct?

Furthermore, can anyone clarify how this resolution and frequency can be adjusted?

Lastly, if you can answer the above, so much thanks, and can you point me to where you got the info, so I can hopefully answer future questions for myself?

Thanks!

Hi jazzlw,

Yes, for analogWrite() the Adafruit M4 boards configure the timers for 8-bits, clock them at 120MHz and use a 256 prescaler:

120MHz / 256 / 2^8 = 1.83kHz

...where as Arduino Zero/MKR boards configure the timers for 16-bits, clock them at 48MHz and use no prescaler:

48MHz / 2^16 = 732Hz

The SAMD51J19A used on the Metro M4 has a huge amout of timers, there's five fully featured TCCx timers (TCC0 to TCC4), plus six standard TC timers (TC0 to TC5) and that's excluding the system tick timer and the RTC timer that are available as well. Two of the TCCx timers are 24-bit, the rest are 16-bit. All the TC timers are 16-bit, but can be chained together in pairs to create a 32-bit timer.

The timers need to be driven by a clock source via the generic clock system. On the Metro M4 boards Adafruit have set up the SAMD51's Digital Phase and Frequency Locked Loops to generate 48MHz, 100MHz and 120MHz clock sources. It's possible to configure the generic clock system to route any of these clock sources to the timer.

The timers themselves operate in a very similar manner to the SAMD21's, but unfortunately the generic clock set-up is slightly different.

In addition, it's necessary to also configure the microcontroller's IO pins, to select the timer output. This is achieved by selecting the pin's peripheral multiplexer and switching the multiplexer to the correct timer output. Which timer channel is connected to what pin is defined in the I/O Multiplexing and Considerations chapter in the SAMD51 datasheet.

Here's an example that outputs a 50Hz, 50% duty-cycle PWM signal with 16-bit resolution on D7:

// 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() {}

the adafruit zerotimer library is somewhat helpful for playing with the TC timers...

Thanks so much MartinL for the super detailed answer! It's very helpful, and points me in the right direction to learn more as well.

With the arduino UNO, several of the timers are used for things like the servo library already, and if you use those you can't also use the servo library, etc. What's the best way to check which timers for the M4 will be used for what things (like servos)? Should I just find the source to the libraries I want to use (which presumably have to be M4 / SAMD51 adapted), and then dig through the source to find which timers are used where, or is there some better way?

Thanks again!

On Arduino's SAMD21 boards at least, the servo library uses timer TC4. The main other user of timers is the analogWrite() function. Which timer it uses, depends upon which pin has been selected.

Unlike the AVR boards that use timer 0 for the delay(), millis() and micros() functions, the ARM based boards employ the systick (system tick) timer, this frees up the other TCC/TC timers to use as you wish.

Another point to note is that Adafruit don't enable TC timers TC0 to TC2 by default. To use these timers it's first necessary to activate them in Main Clock (MCLK) system:

MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0;           // Activate timer TC0

The PWM frequency can be calculated by the formula:

PWM frequency = GCLK frequency / (Prescaler * (PER + 1))

For the above example:

PWM frequency = 48MHz / (8 * (119999 + 1)) = 50Hz

The PWM resolution:

PWM resolution = log (PER + 1) / log(2)

Again for the above example:

PWM resolution = log (119999 + 1) / log(2) = 16.87 bits

Hi Martin,

Your code is super helpful to me. I noticed you posted this in another thread:

void setup()
{
  MCLK->APBAMASK.reg |= MCLK_APBAMASK_TC0;           // Activate timer TC0
 
  // 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[9].reg = GCLK_PCHCTRL_CHEN |         // Enable perhipheral channel
                         GCLK_PCHCTRL_GEN_GCLK7;     // Connect generic clock 7 to TC0

  // Enable the peripheral multiplexer on pin A1
  PORT->Group[g_APinDescription[A1].ulPort].PINCFG[g_APinDescription[A1].ulPin].bit.PMUXEN = 1;
 
  // Set A1 the peripheral multiplexer to peripheral E(4): TC0, Channel 1
  PORT->Group[g_APinDescription[A1].ulPort].PMUX[g_APinDescription[A1].ulPin >> 1].reg |= PORT_PMUX_PMUXO(4);
 
  TC0->COUNT16.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
                           TC_CTRLA_MODE_COUNT16;            // Set the counter to 16-bit mode

  TC0->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MPWM;      // Set-up TC0 timer for Match PWM mode (MPWM)

  TC0->COUNT16.CC[0].reg = 1999;                    // Use CC0 register as TOP value, set for 50Hz PWM
  while (TC0->COUNT16.SYNCBUSY.bit.CC0);             // Wait for synchronization

  TC0->COUNT16.CC[1].reg = 999;                     // Set the duty cycle to 50% (CC1 half of CC0)
  while (TC0->COUNT16.SYNCBUSY.bit.CC1);             // Wait for synchronization

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

void loop() {}

Works awesome on the Feather M4 Express. I was trying to get it to work on other pins (like A2), but this doesn't work:

  // Enable the peripheral multiplexer on pin A2
  PORT->Group[g_APinDescription[A2].ulPort].PINCFG[g_APinDescription[A2].ulPin].bit.PMUXEN = 1;
 
  // Set A2 the peripheral multiplexer to peripheral E(4): TC0, Channel 1
  PORT->Group[g_APinDescription[A2].ulPort].PMUX[g_APinDescription[A2].ulPin >> 1].reg |= PORT_PMUX_PMUXO(4);

What am I doing wrong?

UPDATE: Turns out the tone() function works fine on this board as well, so that should suffice for my purposes. Thank you for your awesome contribution, though!

Hi Casey10110,

The A2 pin on the Adafruit Feather M4 is only connected to timer TC4, (I/O Multiplexing and Considerations table in the SAMD51 datasheet). As you mention, the tone() function also happens to use timer TC4 as well.

By the way, if you need to change the duty-cycle or frequency during operation, then it's possible to use the SAMD51's buffered CCBUF and PERBUF registers. This updates the corresponding CC and PER registers at the beginning of the timer cycle, thereby preventing glitches from appearing on your PWM output. (Changes to the CC and PER registers occur immediately at the output, regardless of the current position in the timer cycle).

For example, to change the duty-cycle between 25% and 75% every second:

void loop() 
{
  TCC0->CCBUF[0].reg = 29999;    // Set-up the CCBUF (Counter Compare Buffered), channel 0 register for 25% duty-cycle          
  delay(1000);                   // Wait for 1 second
  TCC0->CCBUF[0].reg = 89999;    // Set-up the CCBUF (Counter Compare Buffered), channel 0 register for 75% duty-cycle
  delay(1000);                   // Wait for 1 second
}

Hi MartinL,

Do you have more timer examples for the SAMD51 (M4) both TCCx and TCx. I am trying to control 3 stepper motors and each having its own variable speed control. Do I need to use individual timers ie: TCC0, TCC1, TCC2, etc. or can I just use one timer and just change channels.

I have done some playing using your examples. TCC0 (PCHCTRL[25]) modified pin output to D9 (PA20) E(6) this timer works fine. I tried to create another timer TCC4 (PCHCTRL[38]) pin output D5 (PB14) E(5) does not work. I also tried the TC timers I was able to create TC5 (PCHCTRL[30]) pin output to D6 (PB15) O(4) this too works fine but any others for example TC4 (PCHCTRL[30]) pin output to D7 (PB12) E(4) does not work. Looking at your examples and the data sheets I thought I finally figured out setting up timers.... not even close? I have attached the code - I must be missing something, I hope you can help.

Best Regards,
Jim

timertest.ino (12.3 KB)

Hi Jim,

I had a look at your code, it just requires a couple of tiny modifications.

Firstly unlike the AVR Arduinos, on the ARM based boards it isn't necessary to use the pinMode() function set the peripheral or timer outputs. These lines can be deleted.

On TC4, you've set the output for channel 0, but in match PWM (MPWM) mode the counter compare (CC) channel is on channel 1.

TCC4 is only a 16-bit timer, therefore the PER/CC registers must be a value between 0 and 65535. Only TCC timers TCC0 and TCC1 are 24-bits.

Kind regards,
Martin

Hi MartinL,

Thank you for your reply.

TC4 MPWM/CC I am not sure how to change channels.

TCC4 you were correct as soon as I changed the registers to 16 bit this timer works fine.

Thanks,
Jim

Hi Jim,

A TC4 channel 1 pin can be found on the Metro M4 Express' digital pin 4, aka port pin PB13.

All that needs to be changed is the digital pin number for the pin configuration (PINCFG) and multiplexer (PMUX) registers from 7 to 4, and as we're now using an odd port pin number (PB13), the PMUX bitfield definition to odd:

PORT->Group[g_APinDescription[4].ulPort].PINCFG[g_APinDescription[4].ulPin].bit.PMUXEN = 1;
 
PORT->Group[g_APinDescription[4].ulPort].PMUX[g_APinDescription[4].ulPin >> 1].reg |= PORT_PMUX_PMUXO(4);

Here's a full example of the TC4 timer set-up to output 50Hz with a 50% duty-cycle on digtial pin D4:

//Set-up timer TC4 to output 50Hz, 50% duty-cycle on digital pin D4
void setup() {
  // Set up the generic clock (GCLK6) used to clock timers
  GCLK->GENCTRL[6].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 GCLK6
                         GCLK_GENCTRL_SRC_DFLL;      // Generate from 48MHz DFLL clock source
                         //GCLK_GENCTRL_SRC_DPLL1;     // Generate from 100MHz DPLL clock source
                         //GCLK_GENCTRL_SRC_DPLL0;     // Generate from 120MHz DPLL clock source
  while (GCLK->SYNCBUSY.bit.GENCTRL6);               // Wait for synchronization 

  GCLK->PCHCTRL[30].reg = GCLK_PCHCTRL_CHEN |        // Enable perhipheral channel
                         GCLK_PCHCTRL_GEN_GCLK6;     // Connect generic clock 6 to TC4, PCHCTRL[30]

  // Enable the peripheral multiplexer on pin D4
  PORT->Group[g_APinDescription[4].ulPort].PINCFG[g_APinDescription[4].ulPin].bit.PMUXEN = 1;
 
  // Set D4 (Port PB13) the peripheral multiplexer to peripheral (even) E(4): TC4, Channel 1 (peripheral A=0, B=1, C=2, D=3, E=4, etc)
  PORT->Group[g_APinDescription[4].ulPort].PMUX[g_APinDescription[4].ulPin >> 1].reg |= PORT_PMUX_PMUXO(4);
 
  TC4->COUNT16.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
                           TC_CTRLA_MODE_COUNT16;     // Set the counter to 16-bit mode

  TC4->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MPWM;       // Set-up TC4 timer for Match PWM mode (MPWM)
  
  TC4->COUNT16.CC[0].reg = 19999;                    // Use CC0 register as TOP value, set for 50Hz PWM
  while (TC4->COUNT16.SYNCBUSY.bit.CC0);             // Wait for synchronization

  TC4->COUNT16.CC[1].reg = 9999;                     // Set the duty cycle to 50% (CC1 half of CC0)
  while (TC4->COUNT16.SYNCBUSY.bit.CC1);             // Wait for synchronization

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

void loop() {}

Hi MartinL,

Ok thanks. I have a stupid question - so - why didn't PB12 (TCC3_CH0, TC4_CH0) work? Just trying to wrap my head around this and not sure what am I missing.

Thanks,
Jim

Hi Jim,

On the SAMD21 and SAMD51 microcontrollers the TC timers aren't as fully featured as their TCC counterparts. For this reason it's generally better to favour the TCC timers for PWM output and the TC timers for internal timing.

The TC timers can generate PWM output in either normal PWM (NPWM) or match PWM (MPWM) modes. Normal PWM mode allows independent output generation on both of its channels: CC0 and CC1, but offers no control of the waveforms' frequency. Match PWM on the other hand also uses both channels, but sacrifices CC0 to provide frequency control, (it determines the waveform's period), while CC1 determines the duty-cycle. The resulting waveform being output on CC1. This is the reason why it's necessary to use the CC1 channel output in match PWM mode.

The PB12 port pin (D7) can output frequency controlled PWM output, but requires the pin mutliplexer to be switched to a different timer output. PB12 can also be connected either to TCC3/WO[0] (channel 0) on switch F(5) or TCC0/WO[0] on switch G(6), as shown in the I/O Multiplexing and Considerations table in the SAMD51 datasheet.

Hi MartinL,

Thanks for your explanation, things are starting to make some sense.

So you recommend TCCx timers thanks. So they will have the same issue one TCCx timer output takes up two pins (odd / even).

I have few more questions I hope you can help me with;

  1. If the output pins are paired (odd/even) - one pin is PWM'd could the other pin be used reliably as GPIO or should the other pin be left unused?
  2. Timers that are on the same channel (pin) PA09 (TC0, TCC0, TCC1) for PA09 I pick one timer the others can not be used on PA09. Could I re-use these timers on a different channel (pin) each with independent frequency and control.
  3. The timer output with the WO pairing (0-1, 2-3, etc) should always be configured to output on the odd channel/pin?
  4. Within one timer TCC0 which has multiple channels - can each channel pair (odd/even) be a different frequency/dutycycle and controlled independent of the other channels? Or should I use separate timers TCC2, TCC3, TCC4 for example.

I have attached some TCCx code that TCC0 works but TCC1, TCC3, TCC4 do not work? Things for me just aren't clicking yet.

Thanks,
Jim

TCC_timer_test.ino (10.7 KB)

Hi Jim,

The TCCx timers each have a number of channels (table 6.9 of the SAMD51 datasheet):

  • TCC0 - 6 channels
  • TCC1 - 4 channels
  • TCC2 - 3 channels
  • TCC3 - 2 channels
  • TCC4 - 2 channels
    In the SAMD51 datasheet these channels are labelled: WO[0], WO[1], WO[2] and WO[3], WO[4] and WO[5], these correspond to channels 0, 1, 2, 3, 4 and 5. However, on timers TCC0 and TCC1 some of the channels are repeated (WO[6]/WO[7]) and can provide a complementary (inverted) output. The SAMD51 also offers dead-time insertion on timers TCC0 and TCC1.

For example on TCC1, WO[0] is repeated on WO[4], WO[1] on WO[5], WO[2] on WO[6] and finally WO[3] on WO[7]. Setting the duty-cycle with the counter compare register on channel 1 (CC[1]), outputs the PWM waveform on both WO[1] and WO[5].

Each timer has single period (PER) register, but a separate counter compare (CC(x)) register for each channel. This means that each timer can be configured to operate at one frequency (PER), but with different duty-cycles (CC(x)) on each channel.

The TCCx timers also offer buffered counter compare (CCBUF(x)) and period (PERB) registers. Changes to the buffered registers only occur at the start of the timer cycle, where as changes to CC(x) and the PER register appear immediately at the PWM output.

Any of the SAMD51's pins can be either GPIO or mutliplexed to any peripheral, irrespective of port pin pairs. What pin is connected to what peripheral is provided by the SAMD51 datasheet I/O Multiplexing and Considerations table.

Each port pin has it's own pin configuration (PINCONFIG) registers, but shares the port multiplexer (PMUX) register with its neighbouring pin. The concept of port pin pairing is simply because the chip designers decided to create a single PMUX register to set the multiplexer peripheral switch (A, B, C, D, etc...) for two pins rather than one.

For instance PMUX[0] sets the port pins PA00 and PA01, PMUX[1] sets PA02 and PA03 and so on. The port pairs can be set to different peripherals (ADC input, TC timer, etc...) and either of them may be set to GPIO.

Kind regards,
Martin

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].

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.

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.

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

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