Go Down

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

R0BERT0

Hello!!!! i have a question.
I try it but i can change pwm frequency.

Can use this code in arduino mkr1000 board?
Thanks
http://domotica-arduino.es

MartinL

#106
Sep 07, 2016, 04:17 pm Last Edit: Sep 07, 2016, 07:34 pm by MartinL
Hi ROBERTO

Quote
Can use this code in arduino mkr1000 board?
Yes you can. However, while the port pin mapping (for example PA00, PA01, etc...) of the Zero and MKR1000 are the same, (as they share the same microcontroller), the Arduino mapping (D0, D1, etc...) for both boards is different.

The other issue is that you've got less pins to chose from, as the MKR1000 has fewer pins broken out than the Zero and some of these might also need to be configured as the default I2C or SPI port.

When selecting the timer pins you can use table 6.1 PORT Function Multiplexing from the SAMD21 datasheet for the SAMD21G. The conversion from port pin to Arduino pin can be found either in the MKR1000's "variant.cpp" file, or alternatively in the board's schematic. A PDF version of the MKR1000's schematic can be found here: https://www.arduino.cc/en/uploads/Main/MKR1000-schematic.pdf.

Anyway, here's a list of the MKR1000's pins than can be defined as TCC timer pins and their associated channel (WO[X]), unless stated otherwise the TCC timers are on peripheral F:

A0 - PA02 - None
A1 - PB02 - None
A2 - PB03 - None
A3 - PA04 - TCC0/WO[0] (same channel as TCC0/WO[4])
A4 - PA05 - TCC0/WO[1] (same channel as TCC0/WO[5])
A5 - PA06 - TCC1/WO[0]
A6 - PA07 - TCC1/WO[1]
D0 - PA22 - TCC0/WO[4] (same channel as TCC0/WO[0])
D1 - PA23 - TCC0/WO[5] (same channel as TCC0/WO[1])
D2 - PA10 - TCC1/WO[0]
D3 - PA11 - TCC1/WO[1]
D4 - PB10 - TCC0/WO[4] (same channel as TCC0/WO[0])
D5 - PB11 - TCC0/WO[5] (same channel as TCC0/WO[1])
D6 - PA20 - TCC0/WO[6] (same channel as TCC0/WO[2])
D7 - PA21 - TCC0/WO[7] (same channel as TCC0/WO[3])
D8 - PA16 - TCC0/WO[6] (same channel as TCC0/WO[2]) on peripheral F, TCC2/WO[0] on peripheral E
D9 - PA17 - TCC0/WO[7] (same channel as TCC0/WO[3]) on peripheral F, TCC2/WO[1] on peripheral E
D10 - PA19 - TCCO/WO[3] (same channel as TCC0/WO[7])
D11 - PA08 - TCC1/WO[2] (same channel as TCC1/WO[0]) on peripheral F, TCC0/WO[0] on peripheral E
D12 - PA09 - TCC1/WO[3] (same channel as TCC1/WO[1]) on peripheral F, TCC0/WO[1] on peripheral E
D13 - PB22 - None
D14 - PB23 - None

Note the timer TCC0 has only 4 channels (0-3 and 4-7 are the same), while TCC1 and TCC2 each have 2, giving you 8 channels in total.

SSPW

#107
Sep 18, 2016, 03:15 am Last Edit: Sep 19, 2016, 03:22 am by SSPW
MartinL

Thanks for this tutorial. I have to admit, it a had to read it many times to get the details.

If anybody is interested, i have got 100Hz PWM with 0.16us resolution running on 4 pins on the Adafruit Feather M0 boards.

https://cdn-learn.adafruit.com/assets/assets/000/030/921/original/adafruit_products_2772_pinout_v1_0.png?1457305814

D11/D13 runs on TCC2
D10/D12 runs on TCC0

GCLK4 running at 48MHz
Both timers are using prescaler of 4 and PER=60000

Sketch sets D10 at 1000us, D11 at 2000us, D11/13 at 1500us pulse widths.

Runs fine in 1.6.11 IDE

rjastap

Hi MartinL.
Would you help to set this up?
I need 50khz on D9 pin, in my board this pin is mapped to PA07. What value I need to modify to alter duty cycle after making the configuration?
Thanks in advance.

MartinL

#109
Sep 24, 2016, 10:11 am Last Edit: Sep 24, 2016, 10:35 am by MartinL
Hi rjastap,

The following code sets-up 50kHz, at 50% duty cycle on D9:

Code: [Select]
// Output 50kHz PWM on timer TCC1 (9-bit resolution)
void setup()
{
  REG_GCLK_GENDIV = 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

  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 PWM channel on pin D9 
  PORT->Group[g_APinDescription[9].ulPort].PINCFG[g_APinDescription[9].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[9].ulPort].PMUX[g_APinDescription[9].ulPin >> 1].reg |= PORT_PMUX_PMUXO_E;

  // 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

  // Normal (single slope) PWM operation: timers countinuously count up to PER register value and then is reset to 0
  REG_TCC1_WAVE |= 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: 959 = 50kHz
  REG_TCC1_PER = 959;      // Set the frequency of the PWM on TCC1 to 50kHz
  while(TCC1->SYNCBUSY.bit.PER);

  // The CCBx register value corresponds to the pulsewidth in microseconds (us)
  REG_TCC1_CC1 = 480;      // Set the duty cycle of the PWM on TCC0 to 50%
  while(TCC1->SYNCBUSY.bit.CC1);
 
  // Enable TCC1 timer
  REG_TCC1_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 |    // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;             // Enable the TCC0 output
  while (TCC1->SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop() { }

Digital pin 9, or PA07 uses timer TCC1 on perhiperal E. The timer is clocked a 48MHz, as the generic clock divisor and the timer prescaler are set to 1. I've also set-up the timer work in normal PWM mode, rather than dual slope, as at 50kHz this will increase your resolution by 1-bit.

The formula for working out the frequency (in normal PWM mode) is:

timer frequency = generic clock frequency / (N * (PER + 1))

where:
N = timer prescaler (in this case 1)
PER = value in the period (PER) register

We can therefore rearrange this formula to find the PER value for a given timer frequency:

PER = 48MHz / 50kHz - 1 = 959

This will give you a 50kHz PWM output.

Loading the counter compare 1 register (REG_TCC1_CC1) with a value half that of the PER gives a 50% duty cycle.

To change the duty cycle during operation however, it's better to load the buffered counter compare register, in this case REG_TCC1_CCB1. Note the 'B'. For example, to set the duty cycle to 25%:

Code: [Select]
REG_TCC1_CCB1 = 240;                      // Load the CCB1 register with a new value
while(TCC1->SYNCBUSY.bit.CCB1);                  // Wait for synchronization

The buffered register loads the new value into the unbuffered CC1 register at the beginning of each timer cycle. Loading the CC1 register directly (not using the buffered register) may cause glitches on you output PWM waveform when changing the duty cycle.

The value of the CCB1 register can range from 0 = 0% duty cycle (0V), through to the value in the PER register, in this case 959 = 100% duty cycle (3.3V).

This configuration will give out an output resolution of 9-bits.

rjastap

Hi MartinL.

Thank you so much for taking your time to write that beautiful explanation.


I was in a hurry , so I kept trying to configure it and I got to understand the whole concepts exactly as you explained.

Finally I made an aproximation to 20kHz that sastisfy my requirements.

This is the code I've got.

Code: [Select]

void pwmconfig()
{
  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[9].ulPort].PINCFG[g_APinDescription[9].ulPin].bit.PMUXEN = 1;  // D9(PA07)
  
  // 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[g_APinDescription[8].ulPort].PMUX[g_APinDescription[8].ulPin>>1].reg = PORT_PMUX_PMUXO_E | PORT_PMUX_PMUXE_E; // D8
  
  // Feed GCLK4 to TCC0 and TCC1         ??? Is there any way to feed GCLK4? only to 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

  // Single slope PWM operation

   REG_TCC1_WAVE |= TCC_WAVE_WAVEGEN_NPWM;

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

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


  // Divide the 48MHz signal by 1 giving 48MHz
  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
}


And I made a function to replace analogWrite(i,9);

Code: [Select]

void set_pwm(int i){
  REG_TCC1_CCB1 = i;       // TCC0 CCB0 - 50% duty cycle on D2
  while(TCC1->SYNCBUSY.bit.CCB1);

}

DR49

Hi MartinL,

I am interested in your code since I need to generate a number (N) of pulses at a given frequency (20 to 45 KHz more or less), on a given PIN of my M0PRO board.

I tried to compile your example (dual slope PWM over PIN 7) to start with.

I unfortunately got the following error message.

Could you help ?

Many thanks,

Daniel

Arduino : 1.7.9 (Windows XP), Carte : "Arduino M0 Pro (Programming Port)"

Gene_frequence_1.ino: In function 'void setup()':

Gene_frequence_1.ino:24:22: error: 'GCLK_CLKCTRL_ID_TCC0_TCC1' was not declared in this scope

Erreur lors de la compilation.

 

MartinL

#112
Oct 08, 2016, 07:47 pm Last Edit: Oct 08, 2016, 08:25 pm by MartinL
 Hi Daniel,

The reason why the code doesn't compile is because you're using Arduino.org's IDE and M0 Pro board, rather than Arduino.cc's IDE and Arduino/Genuino Zero.

Arduino split into two companies, Arduino.org and Arduino.cc (and are currently seeking an amicable resolution), but the Zero and M0 Pro never fully escaped the consequences. The main differences between the boards being that the digital pins D2 and D4 are reversed and that the M0 Pro doesn't have the additional ATN pin. In addition, the two boards use a different bootloader.

Unfotunately, it also appears that Arduino.org software doesn't use some of the Atmel register definitions for the SAMD21 microcontroller found on both boards and that's what's causing the error.

Personally, I think the easiest solution is to download the Arduino.cc IDE and take the following steps below to burn the Arduino.cc bootloader on to your M0 Pro. However, this will have the effect that that the digital pins: D2 and D4 will be reversed. So, calling D2 in your sketch will effect the D4 pin and vice-versa. Following this you should be able to compile and upload the above sketch without any errors. It also has the added benefit that you'll be using the same software as most of the other Zero/M0 Pro users on this forum.

To burn the Arduino.cc bootloader on to your Arduino M0 Pro:

1. Download the Arduino.cc IDE (currently version 1.6.12)
2. In the menu go to Tools->Board: ...->Boards Manager
3. Install the "Arduino SAMD Boards (32-bit ARM Cortex M0+)" package (currently version 1.6.9)
4. Close the Boards Manager
5. Select Tools->Board: ..., then select "Arduino/Genuino Zero (Programming Port)
6. Plug your USB into your M0 Pro's programming port and the other end into your PC
7. Select Tools->Port and select your M0 Pro's COM port
8. Select Tools->Programmer..., and select "Atmel EDBG"
9. Select Tools->Burn Bootloader

Maverick123

#113
Oct 30, 2016, 08:29 pm Last Edit: Oct 30, 2016, 08:36 pm by Maverick123
Hi MartinL

I have been trying to read up on how to implement PWM on an M0. I have learnt heaps up to this point from this thread. I have included some of the code posted to create my own code to drive 7 PWM pins of an Adafruit Feather M0: PA15, PA20, PA07, PA18, PA16, PA19 and PA17 if I am to believe the following figure:
Adafruit M0 proto

However I get the following errors when verifying my code (see attachment). Could you point me out what I am doing wrong, and more importantly, point out how to fix this error?


Arduino: 1.6.8 (Mac OS X), Board: "Adafruit Feather M0 (Native USB Port)"

error: 'REG_TCC0_CC5' was not declared in this scope
  REG_TCC0_CC5 = 600;                             // TCC0 CC5 - on D05
error: 'REG_TCC0_CC6' was not declared in this scope
 REG_TCC0_CC6 = 600;                             // TCC0 CC6 - on D06
error: 'volatile struct TCC_SYNCBUSY_Type::<anonymous>' has no member named 'CC6'
 while (TCC0->SYNCBUSY.bit.CC6);             // Wait for synchronization  

exit status 1
Error compiling for board Adafruit Feather M0 (Native USB Port).


MartinL

#114
Oct 30, 2016, 10:59 pm Last Edit: Oct 30, 2016, 11:34 pm by MartinL
Hi Maverick123,

The reason why you're getting the errors, is because timer TCC0 only has 4 channels for the waveform outputs: WO[0] to WO[3]. These 4 channels are then repeated for waveform outputs: WO[4] to WO[7]. So counter compare registers CC5 and CC6 registers don't exist, they instead correspond to CC1 and CC2.

This means that digtal pin D05 uses REG_TCC0_CC1 and D06 the register REG_TCC0_CC2.

Note that each CCx register requires write synchronization, for example:

Code: [Select]
while (TCC0->SYNCBUSY.bit.CC1);
Also in your loop() section, it's possible to use the buffered CCBx registers, as these allow the waveform's duty cycle to be changed at the beginning of each waveform period and therefore prevents glitches from appearing on your output. Note that these buffered registers also have to be write synchronized.


Maverick123

#115
Oct 30, 2016, 11:37 pm Last Edit: Oct 31, 2016, 01:14 am by Maverick123
Hi MartinL,

thank you for your quick response. Changing CC5 and CC6 to CC1 and CC2 works like a charm.

I do still have some questions about your second remark:
do you mean that in the setup I should use
Code: [Select]
while (TCCx->SYNCBUSY.bit.CCy)
for every pin?

Should I, in the loop() section, use
Code: [Select]
REG_TCCx_CCBy in stead of REG_TCCx_CCy
since this will write to the same buffer as was initialized during setup?
and should I then also use
Code: [Select]
while(TCCx->SYNCBUSY.bit.CCy)
after each change in PWM duty cycle?

P.S. now that I take a better look at the code, I see that both D6 (PA20) and D10 (PA18) now use TCC0_CC2. Isn't this a problem? Could I use PA04: TCC0 CC0 and PA10: TCC1 CC0 instead of PA20 to create 8 independent PWM signals?

MartinL

#116
Oct 31, 2016, 09:56 am Last Edit: Nov 01, 2016, 10:26 am by MartinL
Hi Maverick123,

On the SAMD21 each of the peripherals, such as SERCOM modules or the TCC/TC timers are internally connected to the processor through a single digital interface, on the what is know as the APB bus, (Advanced Peripheral Bus). This bus is clocked using a synchronous clock.

The peripherals themselves are also clocked by a core clock supplied by the generic clock controller. In order to read and write to some of the peripheral registers, these clocks need to be synchronized.

After writing to a peripheral register that requires write synchronization, it's necessary to check that clocks have synchronized. This can be achieved in the case of the TCC timer, either by polling the register's SYNCBUSY bit or generating an associated interrupt. If you do not check that synchronization has occured, then in the case of the TCC timer it risks another write to the same register being discarded. In other peripherals such as the TC timer it risks stalling the bus interface if you try to immediately read or write to the peripheral again.

The TCCx timers' CCx and CCB registers require write synchonization. So after each write I poll the register's SYNCBUSY bit with the line:

Code: [Select]
while (TCCx->SYNCBUSY.bit.CCy);
Quote
P.S. now that I take a better look at the code, I see that both D6 (PA20) and D10 (PA18) now use TCC0_CC2. Isn't this a problem?
Yes, then it's necessary to reassign either D6 (PA20) or D10 (PA18) to another pin.

Quote
Could I use PA04: TCC0 CC0 and PA10: TCC1 CC0 instead of PA20 to create 8 independent PWM signals?
Yes, using either PA04 or PA10 as an output for TCC0/WO[0] would work.

In the loop() section to use the buffered registers, just add a 'B' to the CC in the register name and poll the corresponding CCBx SYNCBUSY bit. So just as you mentioned it's:

Code: [Select]
REG_TCCx_CCBy = 600;
while (TCCx->SYNCBUSY.bit.CCBy);

Maverick123

#117
Oct 31, 2016, 12:00 pm Last Edit: Oct 31, 2016, 12:17 pm by Maverick123
Hi MartinL,

thanks again for the quick response and the elaborate answer. If it is possible I would like to use 8 pins for PWM.

If I want to use:
  • PA04 TCC0 CC0
  • PA10 TCC1 CC0


Would the following code work?

Code: [Select]
PORT->Group[PORTA].PMUX[04 >> 1].reg = PORT_PMUX_PMUXE_E; //E PA04 (TCC0/ WO[0])
PORT->Group[PORTA].PMUX[10 >> 1].reg = PORT_PMUX_PMUXE_E; //E PA10 (TCC1/ WO[0])


Since PA04 and PA10 are not of the kind D6 (PA20), what would be the equivalent description using:

Code: [Select]
PORT->Group[g_APinDescription[z].ulPort].PMUX[g_APinDescription[z].ulPin >> 1].reg = PORT_PMUX_PMUXE_E;

or should z in these cases be:
  • PA04: z=17
  • PA10: z=1

MartinL

#118
Oct 31, 2016, 02:21 pm Last Edit: Oct 31, 2016, 02:22 pm by MartinL
Hi Maverick123,

Quote
Would the following code work?
Yes.

Quote
Since PA04 and PA10 are not of the kind D6 (PA20), what would be the equivalent description using:
As PA04 is the A3 pin and PA10 is D1, it's also possible to use:

Code: [Select]
PORT->Group[g_APinDescription[A3].ulPort].PMUX[g_APinDescription[A3].ulPin >> 1].reg |= PORT_PMUX_PMUXE_E;
PORT->Group[g_APinDescription[1].ulPort].PMUX[g_APinDescription[1].ulPin >> 1].reg |= PORT_PMUX_PMUXE_E;



Maverick123

Hi MartinL,

Thank you for your response. It has been really helpful. I do have a question about setting up the Feather M0 as an SPI slave, but I will post that question in a more appropriate thread.

Go Up