Go Down

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

VincentVega

Hi everyone,

thank you all (and especially MartinL) for your work here and helping me understand this topic. It was a lot to read and understand, and my results tell me I am not even there...

My situation:
I am working on a Zero and need to create a 20kHz pwm signal to pins D3 (PA09) and D5 (PA15).
After a long study on the SAMD21 data sheet I thought I could take the TCCO CC1 (which should address WO1 for D3/PA09 and WO5 for D5/PA15) to do so.
The test results tell me I was wrong. But I can not figure it out...

Here is my code:

Code: [Select]
void setup()
{

  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz
                    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 port multiplexer
 PORT->Group[g_APinDescription[3].ulPort].PINCFG[g_APinDescription[3].ulPin].bit.PMUXEN = 1;  // D3(PA09)
 PORT->Group[g_APinDescription[5].ulPort].PINCFG[g_APinDescription[5].ulPin].bit.PMUXEN = 1;  // D5(PA15)
 
  // 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[5].ulPort].PMUX[g_APinDescription[5].ulPin>>1].reg = PORT_PMUX_PMUXO_E | PORT_PMUX_PMUXE_E; // D5
 
  // Feed GCLK4 to TCC0 and TCC1         ??? Is there any way to feed GCLK4 only to TCC0?
  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

  // Single slope PWM operation

   REG_TCC0_WAVE |= TCC_WAVE_WAVEGEN_DSBOTTOM;

   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:
  //
  REG_TCC0_PER = 1200;      // Set the frequency of the PWM on TCC1 to
  while(TCC0->SYNCBUSY.bit.PER);

  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  REG_TCC0_CCB1 = 600;       // TCC0 CCB0 - 50% duty cycle on D2
  while(TCC0->SYNCBUSY.bit.CCB1);


  // Divide the 48MHz signal by 1 giving 48MHz
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC1 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}


I would be happy about any kind of help, thank you in advance!

VincentVega

Sometimes some fresh air helps...
After a short break I found some copy paste errors and got it up working.
To be able to control the pwm on both pins separately, I changed D3 to TCC1 CBB1 and now I am able to do what I wanted.

Here is my now working code:
Code: [Select]
void setup()
{
  pinMode(13,INPUT);

  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz clock source by divisor 1: 48MHz
                    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 port multiplexer
 PORT->Group[g_APinDescription[3].ulPort].PINCFG[g_APinDescription[3].ulPin].bit.PMUXEN = 1;  // D3(PA09)
 PORT->Group[g_APinDescription[5].ulPort].PINCFG[g_APinDescription[5].ulPin].bit.PMUXEN = 1;  // D5(PA15)
 
  // 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[3].ulPort].PMUX[g_APinDescription[3].ulPin >> 1].reg |= PORT_PMUX_PMUXO_F; // D3
  PORT->Group[g_APinDescription[5].ulPort].PMUX[g_APinDescription[5].ulPin >> 1].reg |= PORT_PMUX_PMUXO_F; // D5
 
  // Feed GCLK4 to TCC0 and TCC1         ??? Is there any way to feed GCLK4 only to TCC0?
  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

  // Single slope PWM operation

   REG_TCC0_WAVE |= TCC_WAVE_WAVEGEN_NPWM;
   while (TCC0->SYNCBUSY.bit.WAVE);               // Wait for synchronization
   REG_TCC1_WAVE |= TCC_WAVE_WAVEGEN_NPWM;
   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:
  //
  REG_TCC0_PER = 2400;      // Set the frequency of the PWM on TCC0 to
  while(TCC0->SYNCBUSY.bit.PER);

  REG_TCC1_PER = 2400;      // Set the frequency of the PWM on TCC1 to
  while(TCC0->SYNCBUSY.bit.PER);

  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  REG_TCC0_CCB1 = 1600;       // TCC0 CCB0 - 50% duty cycle on D2
  while(TCC0->SYNCBUSY.bit.CCB1);

  REG_TCC1_CCB1 = 800;       // TCC0 CCB0 - 50% duty cycle on D2
  while(TCC0->SYNCBUSY.bit.CCB1);


  // Divide the 48MHz signal by 1 giving 48MHz
  REG_TCC0_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC1 output
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
  REG_TCC1_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC1 output
  while (TCC1->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}


Thank you all again for your provided knowledge! I would not have been able to do this without this topic.

farlane

Having a bit of a problem using the TCC0 timer on the SAMD21G18 which is on the Arduino Tian. I use the TCC0 in dual slope mode, using only buffered registers to generate waveforms on two pins to avoid corrupting the waveforms. Generating the waveforms is not a problem, but i see some strange effects regarding the PER/PERB register.

According to the datasheet the PERB register is copied to the PER register on an UPDATE event (BOTTOM for dual slope mode). This should be the same for the other buffered registers CCB[X].

What i am seeing is that the CC registers (when read) actually do reflect the actual value of the compare values written using the CCB registers.

The PER register however always reads back as 0xFFFFFF, its reset and also maximum value. (Directly writing to the PER register works like one would expect and reads back correctly.)

Any of you guys have a clue as to what is going on there?

MartinL

Hi farlane,

I ran a small test. It looks like a read of the PER register returns the last value written to it, but like you mention it's not updated directly from the PERB register during double buffering, as described in the SAMD21 datasheet.

This suggests to me that perhaps internally the period double buffering isn't actually using the PER register.

Looks like another "undocumented feature" in the SAMD21 datasheet.

farlane

Hi farlane,

I ran a small test. It looks like a read of the PER register returns the last value written to it, but like you mention it's not updated directly from the PERB register during double buffering, as described in the SAMD21 datasheet.

This suggests to me that perhaps internally the period double buffering isn't actually using the PER register.

Looks like another "undocumented feature" in the SAMD21 datasheet.
Hi Martin, thanks for your time and troubles double checking this. A very strange behavior indeed given that the CC[X] registers are in fact updated with their buffered counterparts ...

Cudos & thanks again :)

farlane

This suggests to me that perhaps internally the period double buffering isn't actually using the PER register.
I just got confirmation from the Microchip support desk, and i quote:

Quote
Actually the PER value is loaded from PERB register in UPDATE condition. If you check the WO pin, you can observe that.

Unfortunatley there is some issue with reading the PER register due to some synchronisation issue.

So the read-back values of the PER and PERB registers are not reliable. When you read the PER/PERB register, it may not give the actual/updated value.

johnyrobot

Dear Forum,

I am having a problem configuring the PWM frequency in ARDUINO ZERO.

The ports I use are D8 and D111  , I want to make the PWM with more than 30Khz .

Can someone help me out with a small snippet of code ? I would really appreciate your help , because I am really confused with the timers in ATSAMD21


Kind regards
Johny

MartinL

#172
Oct 12, 2018, 06:06 pm Last Edit: Oct 12, 2018, 06:06 pm by MartinL
Hi johnyrobot,

Here's the code that outputs 30kHz PWM, 50% duty-cycle on D8 and D11:

Code: [Select]
// Output 30kHz PWM on timer TCC1/W0[0] (D8) and TCC0/WO[6] (D11) (10-bit resolution)
void setup()
{
  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
                      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 PWM channel on pin D8 and D11
  PORT->Group[g_APinDescription[8].ulPort].PINCFG[g_APinDescription[8].ulPin].bit.PMUXEN = 1;
  PORT->Group[g_APinDescription[11].ulPort].PINCFG[g_APinDescription[11].ulPin].bit.PMUXEN = 1;
 
  // Connect the TCC1 timer to the port outputs - port pins are paired odd PMUO and even PMUXE
  // F & E peripherals specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[8].ulPort].PMUX[g_APinDescription[8].ulPin >> 1].reg |= PORT_PMUX_PMUXE_E;
  PORT->Group[g_APinDescription[11].ulPort].PMUX[g_APinDescription[11].ulPin >> 1].reg |= PORT_PMUX_PMUXE_F;

  // Normal (single slope) PWM operation: timers countinuously 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
  TCC0->WAVE.reg |= TCC_WAVE_WAVEGEN_NPWM;         // Setup single slope PWM on TCC1
  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: 1600 = 30kHz
  TCC1->PER.reg = 1599;                            // Set the frequency of the PWM on TCC1 to 30kHz
  while (TCC1->SYNCBUSY.bit.PER);                  // Wait for synchronization
  TCC0->PER.reg = 1599;                            // Set the frequency of the PWM on TCC0 to 30kHz
  while (TCC0->SYNCBUSY.bit.PER);                  // Wait for synchronization

  // The CCx register value corresponds to the pulsewidth in microseconds (us)
  TCC1->CC[0].reg = 799;                           // Set the duty cycle of the PWM on TCC1 to 50%
  while (TCC1->SYNCBUSY.bit.CC0);                  // Wait for synchronization
  TCC0->CC[2].reg = 799;                           // Set the duty cycle of the PWM on TCC1 to 50%
  while (TCC0->SYNCBUSY.bit.CC0);                  // Wait for synchronization
             
  TCC1->CTRLA.bit.ENABLE = 1;                     // Enable TCC1 timer
  while (TCC1->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
  TCC0->CTRLA.bit.ENABLE = 1;                     // Enable TCC0 timer
  while (TCC0->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop() { }

johnyrobot

Wuooo thank you Martin!

I will try this and let you know .

When I change these settings to the timers TCC1 and TCC0 will this affects any other devices? Like for example SPI?

Thank you very much!

Warm Regards

Johnyrobot

MartinL

Hi Johnyrobot,

Quote
When I change these settings to the timers TCC1 and TCC0 will this affects any other devices? Like for example SPI?
The TCC timer counters are separate peripherals and independent from the SPI, or for that matter I2C or Serial.

It also won't affect the timing functions delay(), millis() or micros(), as they use the systick timer on the SAMD21.

Kind regards,
Martin

johnyrobot

Hi Martin,

Thank you very much , your codes works perfect!!!

Can you please explain me the maths behind these :

TCC1->CC[0].reg = 799;    // What is the math behind it?

TCC1->PER.reg = 1599;   // What is the math behind it?


Kind Regards
Johnyrobot


MartinL

#176
Oct 14, 2018, 06:26 pm Last Edit: Oct 14, 2018, 06:41 pm by MartinL
Hi Johnyrobot,

The timers TCC0 and TCC1 are being clocked by generic clock 4 (GCLK4) at 48MHz.

The formula for calculating the PWM frequency (in the SAMD21 datasheet) is:

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

where:
GCLK frequency => generic clock frequency fed to the timer, (in our case: 48MHz)
N => timer prescaler (in our case: 1)
PER => the value of the timer's period (PER) register

Rearranging the formula:

PER = GCLK frequency / (N * PWM frequency) - 1

therefore:

PER = 48000000 / (1 * 30000) - 1 = 1599

The timers count up from 0 up to 1599, before being reset back to 0, then from 0 to 1599 oncemore and so on. There's a single PER register for each timer.

The counter compare or CC registers determine the PWM waveform's duty-cycle. There is one CC register for each timer channel. Timer TCC0 has 4 channels: (0..3) and TCC1 has 2: (0..1).

The duty-cycle can be set between 0 (0%) and the value in the PER register (100%).

Therefore, for a 50% duty-cycle the CC register is simply set to half the PER register, in our case 1599/2 = 799.

By the way, if you want to change the duty-cycle or period during operation, then it's best to use the buffered PERB and CCBx registers. The buffered registers are loaded into their PER and CC counterparts only at the beginning of the timer cycle (know as an update), thereby preventing glitches from appearing on your outputs.

Code: [Select]
TCC1->CCB[0].reg = 399;                           // Set the duty cycle of the PWM on TCC1 to 25%
while (TCC1->SYNCBUSY.bit.CCB0);                  // Wait for synchronization

Kind regards,
Martin

RicoW

#177
Oct 16, 2018, 11:20 am Last Edit: Oct 16, 2018, 11:32 am by RicoW
Hello MartinL,

love your work on the Zero, and I need your help as well.
I want to send a simple clock signal over port 2 with 8 MHz. The code I modified from yours is this:
Code: [Select]
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(6) | GCLK_GENDIV_ID(4);
while (GCLK->STATUS.bit.SYNCBUSY);
REG_GCLK_GENCTRL = GCLK_GENCTRL_OE | GCLK_GENCTRL_IDC | GCLK_GENCTRL_GENEN | GCLK_GENCTRL_SRC_DFLL48M | GCLK_GENCTRL_ID(4);
while (GCLK->STATUS.bit.SYNCBUSY);
PORT->Group[g_APinDescription[2].ulPort].PINCFG[g_APinDescription[2].ulPin].bit.PMUXEN = 1;
PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg |= PORT_PMUX_PMUXO_H;


I need the signal for communication with an IC, but it does not respond. I want to eliminate all possible error sources. Can you tell me what I need to change here?

Greetings, Rico

johnyrobot

Hello Martin!

Thank you very much for everything !! Your example works like a charm!!

Mucho gracias!!

Johnyrobot

MartinL

#179
Oct 16, 2018, 04:55 pm Last Edit: Oct 16, 2018, 04:59 pm by MartinL
Hi Rico,

All you need to do is just change:

Code: [Select]
PORT_PMUX_PMUXO_H
to...

Code: [Select]
PORT_PMUX_PMUXE_H
The MKRZero has digital pin 2 on port PA10. This is an even port (rather than an odd one), hence the E instead of the O.

I tested your code on the my Arduino Zero and it works, giving an 8MHz output.

Kind regards,
Martin

Go Up