Changing Arduino Zero PWM Frequency

Hi,

I am looking to migrate my project from Arduino MEGA2560 to MKR ZERO.
My sketch controls 3 motor driver ICs and does it via 3 separate pins per driver IC, and sets up the pins to 20KHz via timers, and then directly modify the duty cycle in the program to adjust the speed.

Right now i have this for my MEGA2560:

  // Timer 4 (TCCR4) configuration controls pins 6, 7, and 8. 
  // PWM frequency calculation: [ 16MHz / 1 (prescaler) / 2 (phase-correct) / 400 (top) = 20kHz ]
  int eightOnes = 255;          // Equivalent to 11111111 in binary 
  TCCR3A &= ~eightOnes;         // Set the eight bits in register to 0
  TCCR3B &= ~eightOnes;         // Set the eight bits in register to 0
  ICR3 = (F_CPU/20000)/2;       // Top = 400 = (16Mhz/20Khz)/2
  TCCR3A = _BV(COM3A1)          // pin 6 - non-inverted PWM output    
         | _BV(COM3B1)          // pin 7 - non-inverted PWM output
         | _BV(COM3C1)          // pin 8 - non-inverted PWM output
         | _BV(WGM31);          // mode 10: phase correct PWM with ICR4 as Top
  TCCR3B = _BV(WGM33) 
         | _BV(CS30);           // Prescaler = 1

and control the duty cycle by updating OCR3A, OCR3B, OCR3C:

// speed = range between -400 to 400 
OCR3A = speed
OCR3B = speed

Anyone could help with a solution or could point me the right direction? thanks!

Hi manuelx10,

Here's some example code for the MKR Zero that outputs 20kHz PWM on digital pins 2, 3 and 4:

// MKR Zero: Output 20kHz PWM on timer TCC0 (8-bit resolution) on 3 channels on D4 (0), D2 (2) and D3 (3)
void setup()
{
  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(3) |          // Divide the 48MHz clock source by divisor 3: 48MHz/3=16MHz
                     GCLK_GENDIV_ID(4);            // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |         // Enable GCLK4
                      GCLK_GENCTRL_SRC_DFLL48M |   // Set the 48MHz clock source
                      GCLK_GENCTRL_ID(4);          // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization
  
  // Enable the port multiplexer for the 3 PWM channels: timer TCC0 outputs on D4, D2 and D3
  PORT->Group[g_APinDescription[2].ulPort].PINCFG[g_APinDescription[2].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[3].ulPort].PINCFG[g_APinDescription[3].ulPin].bit.PMUXEN = 1;
  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
  // Peripheral F specifies the timer: TCC0
  PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
  PORT->Group[g_APinDescription[4].ulPort].PMUX[g_APinDescription[4].ulPin >> 1].reg |= PORT_PMUX_PMUXE_F;

  // Feed GCLK4 to TCC0 and TCC1
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                      GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  // Dual slope PWM operation: timers countinuously count up to PER register value then down 0
  TCC0->WAVE.reg = TCC_WAVE_POL(0xF) |            // Reverse the output polarity on all TCC0 outputs
                   TCC_WAVE_WAVEGEN_DSBOTTOM;     // Set up dual slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);                // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation: 16MHz / (1 * (399 + 1) * 2) = 20kHz
  TCC0->PER.reg = 399;                            // Set the frequency of the PWM on TCC0 to 20kHz
  while (TCC0->SYNCBUSY.bit.PER);                 // Wait for synchronization

  // Set the CC[x] registers for 50% duty-cycle
  TCC0->CC[0].reg = 199;                          // TCC0 CC0 - 50% duty cycle on D4
  while (TCC0->SYNCBUSY.bit.CC0);                 // Wait for synchronization
  TCC0->CC[2].reg = 199;                          // TCC0 CC1 - 50% duty cycle on D2
  while (TCC0->SYNCBUSY.bit.CC2);                 // Wait for synchronization
  TCC0->CC[3].reg = 199;                          // TCC0 CC2 - 50% duty cycle on D3
  while (TCC0->SYNCBUSY.bit.CC3);                 // Wait for synchronization

  // Divide the 16MHz signal by prescaler 1 giving 16MHz (62.5ns) TCC0 timer tick
  TCC0->CTRLA.reg |= TCC_CTRLA_PRESCALER_DIV1;    // Divide GCLK4 by 1
  
  TCC0->CTRLA.bit.ENABLE = 1;                     // Enable the TCC0 timer
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop()   // Change TCC0 timer channel 0 between 25% and 75% duty-cycle using buffered CCB[0] register
{ 
  TCC0->CCB[0].reg = 99;                          // TCC0 CCB0 - 25% duty cycle on D4
  while (TCC0->SYNCBUSY.bit.CCB0);                // Wait for synchronization
  delay(1000);                                    // Wait for 1 second
  TCC0->CCB[0].reg = 299;                         // TCC0 CCB0 - 75% duty cycle on D4
  while (TCC0->SYNCBUSY.bit.CCB0);                // Wait for synchronization
  delay(1000);                                    // Wait for 1 second
}

I've set up the generic clock 4 (GCLK4) to generate 16MHz to timer TCC0, thereby providing the same resolution as the Mega, (although the MKR Zero is capable of much higher resolution if required). It also uses dual slope PWM, the same as phase correct PWM on the AVR microcontrollers.

Hi MartinL,

Thanks for the quick reply! and the sample sketch, great starting point.

What is the difference between using the CC.reg and CCB.reg?

Also, is the delay and while loop for wait for sync necessary? will this affect simultaneous / instant control of the 3 pwm ?

while (TCC0->SYNCBUSY.bit.CCB0);                // Wait for synchronization
  delay(1000);                                    // Wait for 1 second

What is the difference between using the CC.reg and CCB.reg?

The CCBx.reg are buffered counter compare registers that automatically update the CCx only at the beginning of a new timer cycle (period). This allows the PWM duty-cycle to be changed during operation without causing glitches to appear on the PWM output. Changes by writing to CCx registers directly by contrast take effect (almost) immediately at the output.

Also, is the delay and while loop for wait for sync necessary?

The delay isn't necessary, as it's only in this example for demonstration purposes.

The register synchronisation is sometimes required, because CPU and its corresponding AHB/APB bus system is driven by a clock signal that is asynchronous respect the generic clock that drives the given peripheral. In this case the CPU clock domain is asynchronous with respect to GCLK4 that drives the TCC0 timer peripheral.

After writing to a TCC0 register that requires write synchronisation, the CPU can wait for the corresponding CCBx SYNCBUSY bit to clear before proceeding. This ensures that the written data has been successfully clocked into the TCC0 register.

However, register synchronisation happens anyway, whether you poll the corresponding SYNCBUSY bit or not, therefore waiting for the SYNCBUSY bit to clear in a while loop is only really necessary if there is a chance TCC0 register could be written two times in quick succession.

Thanks for this topic !! And all the excellent information from Martin L!

I try to drive two motors with two Motor driver cards Adafruit MD 8871, to be able to drive more powerful electric motors. 3,5 A each. I need 4 separate channels with PWM to drive both motors forward and backwards.
I use a small Adafruit ItsyBitsy M0 connected via I2C to have a more flexible motor driver.
I have almost got everything to work, but it seems that the channels are connected in some way, and only two channels work.. I think its the connection pins between the M0 chip and the pins that I really don't can figure out.. I get perfect 20k PWM signals from your code but I don't get 4 separate channels.

Any one has any clue? (Part of the code I use from this topic)
I have attached a Screendump width the pin configuration on the Adafruit ItsyBitsy M0.. And looked in the ATSAMD21G18 32-bit Cortex M0+ data sheet but I still don't get it... :frowning:

REG_GCLK_GENDIV = GCLK_GENDIV_DIV(3) | // Divide the 48MHz clock source by divisor 3: 48MHz/3=16MHz
GCLK_GENDIV_ID(4); // Select Generic Clock (GCLK) 4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // Enable GCLK4
GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz clock source
GCLK_GENCTRL_ID(4); // Select GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

// Enable the port multiplexer for the 4 PWM channels: timer TCC0 outputs
const uint8_t CHANNELS = 4;
const uint8_t pwmPins[] = { 11, 13, 10, 12 }; // CCB0 = 11, CCB13 = 5, CCB2 = 10, CCB3 = 12 ????
for (uint8_t i = 0; i < CHANNELS; i++)
{
PORT->Group[g_APinDescription[pwmPins].ulPort].PINCFG[g_APinDescription[pwmPins*].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 TCC2*
* PORT->Group[g_APinDescription[11].ulPort].PMUX[g_APinDescription[11].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
PORT->Group[g_APinDescription[10].ulPort].PMUX[g_APinDescription[10].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;*

* // Feed GCLK4 to TCC0 and TCC1*
* REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable GCLK4 to TCC0 and TCC1*
* GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4*
* GCLK_CLKCTRL_ID_TCC0_TCC1; // Feed GCLK4 to TCC0 and TCC1*
* while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization*
* // Dual slope PWM operation: timers countinuously count up to PER register value then down 0*
* REG_TCC0_WAVE |= TCC_WAVE_POL(0xF) | // Reverse the output polarity on all TCC0 outputs*
* TCC_WAVE_WAVEGEN_DSBOTTOM; // Setup dual slope PWM on TCC0*
* while (TCC0->SYNCBUSY.bit.WAVE); // Wait for synchronization*
* // Each timer counts up to a maximum or TOP value set by the PER register,*
* // this determines the frequency of the PWM operation:*
* // 400 = 20kHz*
* REG_TCC0_PER = 400; // Set the frequency of the PWM on TCC0 to 20kHz*
* while(TCC0->SYNCBUSY.bit.PER);*
* // The CCBx register value corresponds to the pulsewidth in microseconds (us)*
* REG_TCC0_CCB0 = 0; // TCC0 CCB0 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB0);_
REG_TCC0_CCB1 = 0; // TCC0 CCB1 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB1);_
REG_TCC0_CCB2 = 0; // TCC0 CCB2 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB2);_
REG_TCC0_CCB3 = 0; // TCC0 CCB3 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB3);_
_
// Divide the 16MHz signal by 1 giving 16MHz (62.5ns) TCC0 timer tick and enable the outputs*_
* REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 | // Divide GCLK4 by 1*
* TCC_CTRLA_ENABLE; // Enable the TCC0 output*
* while (TCC0->SYNCBUSY.bit.ENABLE); // Wait for synchronization*

I found the problem! (some hours later... ) Now the code is working!

When I changed pins so they corresponded to the ItsyBitsy M0 layout (PAXX to Digital pin) and the M0 data sheet and the E or F column in the Table 6-1. PORT Function Multiplexing in the ATMEL SAMD21G
data sheet. Previous pairs (11,13 ) and (10,12) of pins had the same (W O numbers)

TCC0 (W O 0) E PA08 = 4
TCC0 (W O 1) E PA09 = 3
TCC0 (W O 2) F PA18 = 10
TCC0 (W O 3) F PA19 = 12

CODE:

// Output 20kHz PWM on timer TCC0 (8-bit resolution)
void setup()
{
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(3) | // Divide the 48MHz clock source by divisor 3: 48MHz/3=16MHz
GCLK_GENDIV_ID(4); // Select Generic Clock (GCLK) 4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // Enable GCLK4
GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz clock source
GCLK_GENCTRL_ID(4); // Select GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

// Enable the port multiplexer for the 4 PWM channels: timer TCC0 outputs
const uint8_t CHANNELS = 4;
const uint8_t pwmPins[] = { 4, 3, 10, 12 }; // TCC0 (W O 0) E PA08 = 4, TCC0 (W O 1) E PA09 = 3, TCC0 (W O 2) F PA18 = 10, TCC0 (W O 3) F PA19 = 12
for (uint8_t i = 0; i < CHANNELS; i++)
{
PORT->Group[g_APinDescription[pwmPins].ulPort].PINCFG[g_APinDescription[pwmPins*].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 TCC2*
* PORT->Group[g_APinDescription[4].ulPort].PMUX[g_APinDescription[4].ulPin >> 1].reg = PORT_PMUX_PMUXO_E | PORT_PMUX_PMUXE_E;
PORT->Group[g_APinDescription[10].ulPort].PMUX[g_APinDescription[10].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;*

* // Feed GCLK4 to TCC0 and TCC1*
* REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable GCLK4 to TCC0 and TCC1*
* GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4*
* GCLK_CLKCTRL_ID_TCC0_TCC1; // Feed GCLK4 to TCC0 and TCC1*
* while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization*
* // Dual slope PWM operation: timers countinuously count up to PER register value then down 0*
* REG_TCC0_WAVE |= TCC_WAVE_POL(0xF) | // Reverse the output polarity on all TCC0 outputs*
* TCC_WAVE_WAVEGEN_DSBOTTOM; // Setup dual slope PWM on TCC0*
* while (TCC0->SYNCBUSY.bit.WAVE); // Wait for synchronization*
* // Each timer counts up to a maximum or TOP value set by the PER register,*
* // this determines the frequency of the PWM operation:*
* // 400 = 20kHz*
* REG_TCC0_PER = 400; // Set the frequency of the PWM on TCC0 to 20kHz*
* while(TCC0->SYNCBUSY.bit.PER);*
* // The CCBx register value corresponds to the pulsewidth in microseconds (us)*
* REG_TCC0_CCB0 = 0; // TCC0 CCB0 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB0);_
REG_TCC0_CCB1 = 0; // TCC0 CCB1 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB1);_
REG_TCC0_CCB2 = 0; // TCC0 CCB2 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB2);_
REG_TCC0_CCB3 = 0; // TCC0 CCB3 - 200 = 50% duty cycle on D2 (0-400)
_
while(TCC0->SYNCBUSY.bit.CCB3);_
_
// Divide the 16MHz signal by 1 giving 16MHz (62.5ns) TCC0 timer tick and enable the outputs*_
* REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 | // Divide GCLK4 by 1*
* TCC_CTRLA_ENABLE; // Enable the TCC0 output*
* while (TCC0->SYNCBUSY.bit.ENABLE); // Wait for synchronization*

Hi Martin,

I got the PWM routine working fine on Zero with your help (Changing Arduino Zero PWM Frequency - #193 by MartinL - Arduino Zero - Arduino Forum ). But I now need to synchronize my ADC measurements with PWM signal. As a reminder, I have the PWM directed to the PA08:

PORT->Group[PORTA].PINCFG[8].bit.PMUXEN = 1;
PORT->Group[PORTA].PMUX[8 >> 1].reg |= PORT_PMUX_PMUXE_F;

For synchronization I'm trying to do direct read of the bit 08 of the PA register, something like:
PwmOut = REG_PORT_OUT0 & (1 << 8 );

But I'm getting only constant values, 0 or 1 depending on how I set the port values in the setup(). The PWM routine does not seem to affect the state of the REG_PORT_OUT0 register, although I can clearly see the PWM squarewave signal on the PA08 pin, so I guess the MUX connects the PA8 to the timer output instead of the port register?

So how do I read the state of the PA08 pin when it's driven by timer in the PWM mode?

I wanted to try the simplest method first (which is just polling the PA08 register bit in a while loop) but I know that there are other ways to do that which are a bit more complicated and have other consequences:

  • I can connect PA08 electrically to some other pin and do digital read, but for that I will need to modify the board.
  • I can trigger ADC read by an interrupt from the counter overflow, this will slightly increase the execution time (every us counts in my application)

Your help is much appreciated!

Hi evi7538,

The easiest way to trigger an ADC conversion from a PWM output is to use the SAMD21's event system. The event system is a 12-channel highway that can be used to allow peripheral-to-peripheral communication without CPU intervention.

It's possible to set up a peripheral to be the event sender (generator) and any number of peripherals to be the event receiver (user), on a given event channel (0 to 11). The peripherals can be configured to send and receive events on a number of peripheral specific criteria.

Here's some example code that sets up timer TCC1 to output a 1Hz PWM signal on port PA08. The TCC1 is configured so that it triggers an event each time it overflows (every second) on event channel 0. The ADC timer is initialised to start a conversion on analog pin A0 each time it receives this event from the TCC1 timer (on event channel 0). Once the ADC conversion is complete, the it generates an interrupt to output the result to the console:

// Use event system to trigger ADC conversion every second from a 1Hz PWM signal
void setup()
{
  SerialUSB.begin(115200);
  while(!SerialUSB);
  
  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;     // Switch on the event system peripheral

  // Intialise the ADC - generic clock already routed to ADC
  ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV512 |    // Divide Clock ADC GCLK by 512 (48MHz/512 = 93.7kHz)
                   ADC_CTRLB_RESSEL_12BIT;         // Set ADC resolution to 12 bits
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization
  ADC->SAMPCTRL.reg = 0x00;                        // Set max Sampling Time Length to half divided ADC clock pulse (5.33us)
  ADC->INPUTCTRL.bit.MUXPOS = 0x0;                 // Set the analog input to A0
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization

  NVIC_SetPriority(ADC_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for ADC to 0 (highest) 
  NVIC_EnableIRQ(ADC_IRQn);         // Connect ADC to Nested Vector Interrupt Controller (NVIC)

  ADC->INTENSET.reg = ADC_INTENSET_RESRDY;        // Set ADC to generate an interrupt when the result is ready
  
  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(3) |         // Divide the 48MHz clock source by divisor 1: 48MHz/3=16MHz
                     GCLK_GENDIV_ID(4);           // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |        // Enable GCLK4
                      GCLK_GENCTRL_SRC_DFLL48M |  // Set the 48MHz clock source
                      GCLK_GENCTRL_ID(4);         // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Feed GCLK4 to TCC0 and TCC1
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                      GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                      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 TCC1 PWM channel 0 on port pin PA08 
  PORT->Group[PORTA].PINCFG[8].bit.PMUXEN = 1;
  
  // Connect the TCC1 timer to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[PORTA].PMUX[8 >> 1].reg |= PORT_PMUX_PMUXE_F;// | PORT_PMUX_PMUXO_F;

  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_ADC_START);              // Set the event user (receiver) as ADC

  ADC->EVCTRL.reg |= ADC_EVCTRL_STARTEI;                                   // Enable the ADC start conversion on event input

  EVSYS->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_OVF) |        // Set event generator (sender) as timer TCC1 overflow
                       EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0
  
  TCC1->EVCTRL.reg |= TCC_EVCTRL_OVFEO;                                    // Enable the TCC1 overflow event output
                      
  // Normal (single slope) PWM operation: timer countinuouslys count up to PER register value and then is reset to 0
  TCC1->WAVE.reg |= TCC_WAVE_WAVEGEN_NPWM;         // Setup single slope PWM on TCC1
  while (TCC1->SYNCBUSY.bit.WAVE);                 // Wait for synchronization
  
  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  TCC1->PER.reg = 15999999;                         // Set the frequency of the PWM on TCC1 to 1Hz
  while (TCC1->SYNCBUSY.bit.PER);

  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  TCC1->CC[0].reg = 7999999;                        // TCC1 CC0 - 50% duty cycle
  while (TCC1->SYNCBUSY.bit.CC0);
  
  // Divide the 16MHz signal by 1 giving 16MHz (62.5ns) TCC1 timer tick and enable the outputs
  TCC1->CTRLA.reg |= TCC_CTRLA_PRESCALER_DIV1;      // Divide GCLK4 by 1

  ADC->CTRLA.bit.ENABLE = 0x01;                    // Enable the ADC
  while(ADC->STATUS.bit.SYNCBUSY);                 // Wait for synchronization

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

void loop() {}

void ADC_Handler()
{
  while (ADC->STATUS.bit.SYNCBUSY);                 // Read synchronization
  SerialUSB.println(ADC->RESULT.reg);               // Output the result
  ADC->INTFLAG.bit.RESRDY = 1;                      // Clear the RESRDY interrupt flag
}

Martin, thank you for good suggestion but sorry, I forgot to mention, I'm using an external 20-bit ADC through SPI. But I guess may be I can still use your idea (without initializing the ADC ) but somehow redirect the overflow event to a different event channel that I can just read directly in the interrupt handler function? I still use the internal ADC for other purposes so I don't want to disturb it with PWM.

Another thing: can I just poll that specific event bit from the channel without using interrupts?

Hi evi7538,

I'm using an external 20-bit ADC through SPI.

No problem, is the ADC conversion started through an SPI command, or with an external sync pulse from the microcontroller?

MartinL:
Hi evi7538,

No problem, is the ADC conversion started through an SPI command, or with an external sync pulse from the microcontroller?

The ADC starts and it is controlled entirely through SPI

I guess another possibility (if I still have to use interrupt handler anyway to clear the overflow) is just to trigger the interrupt by TCC1 overflow and run the ADC conversion in the interrupt handler without using the event system.

In that case, the simplest method is to call the TCC1 timer's Interrupt Service Routine (ISR) each time it overflows.

Just add this code before you enable TCC1 timer, in the setup() portion of your code:

NVIC_SetPriority(TCC1_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC1 to 0 (highest)
NVIC_EnableIRQ(TCC1_IRQn);         // Connect TCC1 to Nested Vector Interrupt Controller (NVIC)

TCC1->INTENSET.reg = TC_INTENSET_OVF;          // Enable TCC1 overflow interrupt

And this function to implement the ISR itself:

void TCC1_Handler()                  // Interrupt Service Routine (ISR) for timer TCC1
{     
  // Put your timer overflow (OVF) code here:     
  // ...  
  TCC1->INTFLAG.bit.OVF = 1;         // Clear the OVF interrupt flag
}

In this instance it isn't necessary to use the event system, as sending the SPI command requires CPU intervention in the ISR.

This should work well for low to medium PWM frequencies. If however you intend to use a high PWM frequency, the rapid calling of ISR can overwhelm the processor. In this scenario, it's necessary to route the TCC1's overflow event (via the event system) to the Direct Memory Access Controller (DMAC) and get the DMAC to automatically send out the SPI command to the ADC.

Using the event system and DMAC together allows the construction of completely autonomous peripheral systems that operate with a minimum of CPU intervention.

Thank you, Martin, I will give it a try. My PWM period is 16us so the ISR should not bee too overwhelming for the CPU I think.

I got it all working, thanks a lot for your help, Martin!

Hi evi7538,

Glad to hear you got it working.

Out of interest I decided to have a go at getting the PWM to generate a synchronised 2 byte SPI command output, in order to simulate sending an SPI conversion signal (including register sub-address and command bytes) to an external device. This all occurs without CPU intervention (the loop() function is empty) and runs like a clockwork machine.

The code uses timer TCC0 and all of its 4 channels to generate the necessary timing. Channel 2 generates a 62.5kHz PWM output and channel 3 the SPI chip select output. Channels 0 and 1 are set up to route their compare match events through the event system to DMA channels 0 and 1. The DMA channels 0 and 1 simply output the first (sub-address) and second (command) bytes on the SPI port respectively.

(Unfortunately, it's not possible to use a single DMA channel to send two SPI bytes, as the SAMD21 doesn't support DMA burst mode).

On my Arduino Zero, the code outputs a 62.5kHz PWM signal on digtial pin D6 (yellow), the SPI chip select on D7 (light blue), SPI's SCK (pink) and MOSI (dark blue):

Sync_PWM_To_SPI.jpg

Sync_PWM_To_SPI.jpg

Here's the code:

// Use event system to use 62.5kHz PWM signal to trigger DMAC to send SPI ADC conversion
#include <SPI.h>

typedef struct                              // DMAC descriptor structure
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint32_t srcaddr;
  uint32_t dstaddr;
  uint32_t descaddr;
} dmacdescriptor ;

// DMAC_CH_NUM = 12 channels on SAMD21
volatile dmacdescriptor wrb[DMAC_CH_NUM] __attribute__ ((aligned (16)));        // Write-back DMAC descriptors
dmacdescriptor descriptor_section[DMAC_CH_NUM] __attribute__ ((aligned (16)));  // DMAC channel descriptors
dmacdescriptor descriptor __attribute__ ((aligned (16)));                       // Place holder descriptor

uint8_t data[2] = { 0x68, 0x77 };            // Dummy SPI command: 0x68 = Register address, 0x77 = Command

void setup()
{
  SPI.begin();
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));   // SPI at 10MHz
  
  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;     // Switch on the event system peripheral

  DMAC->BASEADDR.reg = (uint32_t)descriptor_section;                // Set the descriptor section base address
  DMAC->WRBADDR.reg = (uint32_t)wrb;                                // Set the write-back descriptor base adddress
  DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf);      // Enable the DMAC and priority levels
 
  DMAC->CHID.reg = DMAC_CHID_ID(0);                                 // Select DMAC channel 0
  // Set DMAC channel 0 to priority level 3 (highest), to trigger on TCC0 overflow and to trigger every beat
  DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(3) |                    
                      DMAC_CHCTRLB_TRIGACT_BEAT |
                      DMAC_CHCTRLB_EVIE |
                      DMAC_CHCTRLB_EVACT_TRIG;

  DMAC->CHID.reg = DMAC_CHID_ID(1);                                 // Select DMAC channel 1
  // Set DMAC channel 1 to priority level 3 (highest), to trigger on TCC0 overflow and to trigger every beat
  DMAC->CHCTRLB.reg = DMAC_CHCTRLB_LVL(3) | 
                      DMAC_CHCTRLB_TRIGACT_BEAT |
                      DMAC_CHCTRLB_EVIE |
                      DMAC_CHCTRLB_EVACT_TRIG;

  descriptor.descaddr = (uint32_t)&descriptor_section[0];          // Circular descriptor
  descriptor.srcaddr = (uint32_t)&data[0] + 1;
  descriptor.dstaddr = (uint32_t)&SERCOM4->SPI.DATA.reg;
  descriptor.btcnt = 1;
  descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_BYTE | DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_VALID;
  memcpy(&descriptor_section[0], &descriptor, sizeof(descriptor));

  descriptor.descaddr = (uint32_t)&descriptor_section[1];          // Circular descriptor
  descriptor.srcaddr = (uint32_t)&data[1] + 1;
  descriptor.dstaddr = (uint32_t)&SERCOM4->SPI.DATA.reg;
  descriptor.btcnt = 1;
  descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_BYTE | DMAC_BTCTRL_SRCINC | DMAC_BTCTRL_VALID;
  memcpy(&descriptor_section[1], &descriptor, sizeof(descriptor));
  
  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(1) |         // Divide the 48MHz clock source by divisor 1: 48MHz/1=48MHz
                     GCLK_GENDIV_ID(4);           // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |        // Enable GCLK4
                      GCLK_GENCTRL_SRC_DFLL48M |  // Set the 48MHz clock source
                      GCLK_GENCTRL_ID(4);         // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Feed GCLK4 to TCC0 and TCC1
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC0 and TCC1
                      GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization

  PORT->Group[g_APinDescription[6].ulPort].PINCFG[g_APinDescription[6].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[7].ulPort].PINCFG[g_APinDescription[7].ulPin].bit.PMUXEN = 1;
  
  // Connect the TCC timers to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[6].ulPort].PMUX[g_APinDescription[6].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;
  
  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_DMAC_CH_0);              // Set the event user (receiver) as DMAC channel 0

  EVSYS->USER.reg = EVSYS_USER_CHANNEL(2) |                                // Attach the event user (receiver) to channel 1 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_DMAC_CH_1);              // Set the event user (receiver) as DMAC channel 0

  EVSYS->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_TCC0_MCX_0) |      // Set event generator (sender) as timer TCC0 match compare channel 0
                       EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0

  EVSYS->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_TCC0_MCX_1) |      // Set event generator (sender) as timer TCC0 match compare channel 1
                       EVSYS_CHANNEL_CHANNEL(1);                           // Attach the generator (sender) to channel 0
  
  TCC0->EVCTRL.reg |= TCC_EVCTRL_MCEO0 |                                   // Enable the TCC0 match compare channel 0 output
                      TCC_EVCTRL_MCEO1;                                    // Enable the TCC0 match compare channel 1 output
                                           
  // Normal (single slope) PWM operation: timer countinuouslys count up to PER register value and then is reset to 0
  TCC0->WAVE.reg |= TCC_WAVE_WAVEGEN_NPWM;         // Setup single slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization
  
  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation:
  TCC0->PER.reg = 767;                          // Set the frequency of the PWM on TCC0 to 62.5kHz
  while (TCC0->SYNCBUSY.bit.PER);               // Wait for synchronization
  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  TCC0->CC[2].reg = 384;                        // TCC0 CC2 - 50% duty cycle
  while (TCC0->SYNCBUSY.bit.CC2);               // Wait for synchronization
  TCC0->CC[3].reg = 129;                        // TCC0 CC3 - set duty cycle
  while (TCC0->SYNCBUSY.bit.CC3);               // Wait for synchronization  
  TCC0->DRVCTRL.reg |= TCC_DRVCTRL_INVEN7;      // Invert CC3 = WO[7]
  TCC0->CC[0].reg = 1;                         // TCC0 CC0 - set duty cycle
  while (TCC0->SYNCBUSY.bit.CC0);               // Wait for synchronization
  TCC0->CC[1].reg = 49;                         // TCC0 CC1 - set duty cycle
  while (TCC0->SYNCBUSY.bit.CC1);               // Wait for synchronization
  
  // Divide the 16MHz signal by 1 giving 16MHz (62.5ns) TCC0 timer tick and enable the outputs
  TCC0->CTRLA.reg |= TCC_CTRLA_PRESCALER_DIV1;      // Divide GCLK4 by 1

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

  DMAC->CHID.reg = DMAC_CHID_ID(0);                 // Select DMAC channel 0
  DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;         // Enable DMAC channel 0
  DMAC->CHID.reg = DMAC_CHID_ID(1);                 // Select DMAC channel 1
  DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;         // Enable DMAC channel 1
}

void loop() {}

Dear MartinL,

Great work with this topic. I have learnt quite a lot reading through. I have some troubles understanding a few things.

I am using an Adafruit M0 Feather board and I am trying to get a 100kHz waveform on D10, D12. I am having trouble understanding the section of the code where you connect TCC timer to the ports. I am not sure how to go about doing that for any board(with a custom mapping) for any particular pin. Will you please help explain that or point to where in the Atsamd21 datasheet, I should be reading for clarification.

I am attaching the variant.cpp file for pin Mapping for Adafruit M0 if that helps.

variant.cpp (8 KB)

Hi dev_000,

Looking at the Adafruit Feather M0 schematic: https://cdn-learn.adafruit.com/assets/assets/000/040/553/original/arduino_schem.png?1490994398, it can be seen that digital pin D10 is connected to SAMD21's port pin PA18 and D12 to PA19.

By default the microcontroller's pins are set to General Purpose Input Output (GPIO), however the pins can also be switched over to act as peripheral inputs and outputs. Switching over from GPIO to peripheral IO is performed by setting the PMUXEN (Peripheral Multiplexer Enable) bit in a given pin's CONFIG (Configuration) register:

This can be set either for an Arduino style digtial pin:

// Set PMUXEN for digital pin D10
PORT->Group[g_APinDescription[10].ulPort].PINCFG[g_APinDescription[10].ulPin].bit.PMUXEN = 1;

...or using the SAMD21's port pin:

PORT->Group[PORTA].PINCFG[18].bit.PMUXEN = 1;    // Set PMUXEN for port pin PA18

The SAMD21's datasheet contains a table, called "Table 6-1 PORT Function Multiplexing". It lists each of the microcontroller's pins against peripherals labelled A to H. Going down the table we can see that port pin PA18 (D10) has a timer TCC0/WO[2] on peripheral F. WO[2] indicates that this pin uses TCC0 channel 2. Port pin PA19 (D12) has timer TCC0/WO[3], also on peripheral F.

On the SAMD21 the port pins a paired as odd and even, starting PA00 and PA01, PA02 and PA03 and so on... Port pin PA18 on D10, happens to be paired with PA19 on D12. Using the pins' PMUX register we can activate both peripheral outputs. Each PMUX register is 8-bits wide with the upper nibble (4-bits) determining the ODD port peripheral IO: A through to H and the lower nibble determining the EVEN port peripheral IO, again: A through to H.

Therefore to activate the even PA18 (D10) and the odd PA19 (D12) as PWM outputs, it's necessary to set both the even and odd nibbles to peripheral F.

Again, this can be set either for an Arduino style digtial pins:

PORT->Group[g_APinDescription[10].ulPort].PMUX[g_APinDescription[10].ulPin >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;

...or using the SAMD21's port pins:

PORT->Group[PORTA].PMUX[18 >> 1].reg = PORT_PMUX_PMUXO_F | PORT_PMUX_PMUXE_F;

Each port has 32 pins (PA00-PA31) (although not all port pins are available on some SAMD21 variants), this means there are 32 port pin CONFIG registers (one for each pin), but only 16 PMUX registers (one for each port pair), the logical shift right ">>" essentially divides the value by to 2, to account for half the number of PMUX registers.

Thanks a lot for taking the time to explain that beautifully. Got it working fine at 100kHz.

Just a small correction. In the newer datasheet, its Table 7-1. I had searched for Table 6-1 and couldn't find it. For anyone searching for it in the future, search for "PORT Function Multiplexing" in the datasheet.

Hi MartinL,
your post has been super useful to me to understand how PWM on Arduino Zero works.

I'm trying to make a LED (pin 8 ) blink with PWM, changing the frequency to value visible to the eyes. Is it possible in some ways?