Go Down

Topic: Changing Arduino Zero PWM Frequency (Read 72208 times) previous topic - next topic

MartinL

#195
May 11, 2019, 10:17 am Last Edit: May 11, 2019, 10:20 am by MartinL
Hi manuelx10,

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

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

manuelx10

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 ?

Code: [Select]
while (TCC0->SYNCBUSY.bit.CCB0);                // Wait for synchronization
  delay(1000);                                    // Wait for 1 second

MartinL

#197
May 12, 2019, 11:57 pm Last Edit: May 13, 2019, 12:16 am by MartinL
Quote
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.

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

embring

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... :(

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



embring

#199
May 25, 2019, 10:57 pm Last Edit: May 25, 2019, 11:05 pm by embring
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

evi7538

#200
May 29, 2019, 04:08 am Last Edit: May 29, 2019, 05:02 am by evi7538
Hi Martin,

I got the PWM routine working fine on Zero with your help (https://forum.arduino.cc/index.php?topic=346731.msg4034058#msg4034058 ). 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!

MartinL

#201
May 29, 2019, 10:20 am Last Edit: May 31, 2019, 09:41 am by MartinL
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:

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

evi7538

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?

MartinL

Hi evi7538,

Quote
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?


evi7538

#204
May 29, 2019, 03:03 pm Last Edit: May 29, 2019, 03:32 pm by evi7538
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.

MartinL

#205
May 29, 2019, 03:34 pm Last Edit: May 29, 2019, 03:49 pm by MartinL
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:

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

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

evi7538

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.

evi7538

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

MartinL

#208
May 31, 2019, 11:07 am Last Edit: May 31, 2019, 12:56 pm by MartinL
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):


MartinL

#209
May 31, 2019, 11:08 am Last Edit: May 31, 2019, 11:12 am by MartinL
Here's the code:

Code: [Select]
// 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() {}

Go Up