Go Down

Topic: Complementary PWM channels Output (Read 196 times) previous topic - next topic

jonnygainz

I'm using the following code to synchronize three channels and output a pair of complementary outputs per channel.

Code: [Select]
void setup() {

  PMC->PMC_PCER1 |= PMC_PCER1_PID36; //Enable PWM (Power On)
 
  PWM->PWM_DIS = PWM_DIS_CHID0;       //Disable PWM on Channel 0
  //PWM->PWM_DIS = PWM_DIS_CHID1;

  PIOC->PIO_PDR |= PIO_PDR_P3 | PIO_PDR_P5 | PIO_PDR_P7; // Setting pins 3,5,7 (DUE Pins 35, 37, 39) to PWM Peripheral, not GPIO
  PIOC->PIO_ABSR |= PIO_PC3B_PWMH0 | PIO_PC5B_PWMH1 | PIO_PC7B_PWMH2; // Setting pins to Peripheral B

  PIOC->PIO_PDR |= PIO_PDR_P2 | PIO_PDR_P4 | PIO_PDR_P6; // Setting pins 2,4,6 (DUE Pins 34, 36, 38) to PWM Peripheral, not GPIO
  PIOC->PIO_ABSR |= PIO_PC2B_PWML0 | PIO_PC4B_PWML1 | PIO_PC6B_PWML2; // Setting pins to Peripheral B

  PWM->PWM_CLK = PWM_CLK_PREA(0) | PWM_CLK_DIVA(42); //Set PWM clock rate to 2MHz (84MHz/42)
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA; // Period is left aligned,clock source is CLKA on Channel 0

  REG_PWM_SCM |= PWM_SCM_SYNC0 | PWM_SCM_SYNC1 | PWM_SCM_SYNC2; // Synchronizing of Channels 0, 1 and 2

  //PWM->PWM_CLK = PWM_CLK_PREA(1) | PWM_CLK_DIVA(42);
  //PWM->PWM_CH_NUM[1].PWM_CMR = PWM_CMR_CPRE_CLKA;

  REG_PWM_CPRD0 = 1000000; //Channel 0 Period f = 2MHz/(2*CPRD)
  REG_PWM_CDTY0 = 200000; //Channel 0 Duty Cycle x% = (CDTY/ CPRD)*100%
  REG_PWM_CPRD1 = 1000000;
  REG_PWM_CDTY1 = 1000000;
  REG_PWM_CPRD2 = 1000000;
  REG_PWM_CDTY2 = 0;
 
  PWM->PWM_ENA = PWM_ENA_CHID0; // Enable PWM on Channel 0
  //PWM->PWM_ENA = PWM_ENA_CHID1;

  //NVIC_EnableIRQ(TC0_IRQn); // enable TC0 interrupts
}

void loop() {
  // put your main code here, to run repeatedly:

}


When the duty cycle is set to 0%, 20%, 40%, 60%, 80% and 100% (0, 200000, 400000, 600000, 800000, 1000000) it outputs correctly. However any other value just gives me the output for 100% duty cycle. Can anyone explain this to me and offer some guidance on how to rectify this problem? 

MartinL

#1
Jan 12, 2019, 06:04 pm Last Edit: Jan 12, 2019, 07:56 pm by MartinL
Hi jonnygainz,

To generate synchronous, complementary PWM using method 1 (mode0): manual duty-cycle and trigger update, it's necessary to set the update unlock bit (PWM_SCUC_UPDULOCK) in the Sync Channels Update Control register (SCUC), to trigger once the duty-cycle registers have been changed:

Code: [Select]
PWM->PWM_SCUC = PWM_SCUC_UPDULOCK;      // Set the update unlock bit to trigger an update at the end of the next PWM period
Here's an example of using 3 synchronous, complementary channels on channel 0 (D34, D35), 1 (D36, D37) and 2 (D38, D39). The PWM is at 50Hz and switches between 25% and 75% duty-cycle every 1/2 second:

Code: [Select]
// Enable synchronous, complementary output, single-slope PWM at 50Hz on 3 channels (0 to 2), at 50% duty cycle
void setup() {
  // Set-up 6 PWM outputs on 3 complementary channels on on pins D34, D35, D36, D37, D38, and D39 for channels 0 through to 2 respectively
  PMC->PMC_PCER1 |= PMC_PCER1_PID36;                                               // Enable PWM   
  PIOC->PIO_ABSR |= PIO_ABSR_P7 | PIO_ABSR_P6 | PIO_ABSR_P5 | PIO_ABSR_P4 |        // Set the port C PWM pins to peripheral type B
                    PIO_ABSR_P3 | PIO_ABSR_P2;
  PIOC->PIO_PDR |= PIO_PDR_P7 | PIO_PDR_P6 | PIO_PDR_P5 | PIO_PDR_P4 |             // Set the port C PWM pins to outputs
                   PIO_PDR_P3 | PIO_PDR_P2;
  PWM->PWM_CLK = PWM_CLK_PREA(0) | PWM_CLK_DIVA(84);                               // Set the PWM clock A rate to 1MHz (84MHz/84)
  PWM->PWM_SCM |= PWM_SCM_SYNC2 | PWM_SCM_SYNC1 | PWM_SCM_SYNC0;                   // Set the PWM channels as synchronous                 
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA;      // Enable single slope PWM and set the clock source as CLKA for all synchronous channels
  PWM->PWM_CH_NUM[0].PWM_CPRD = 19999;                 // Set the PWM frequency 1MHz/(19999 + 1) = 50Hz for all synchronous channels
  for (uint8_t i = 0; i < 3; i++)                      // Loop for each PWM channel (8 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTY = 9999;                // Set the PWM duty cycle to 50%
  }
  PWM->PWM_ENA = PWM_ENA_CHID0;                        // Enable all synchronous PWM channels
  PWM->PWM_SCUC = PWM_SCUC_UPDULOCK;      // Set the update unlock bit to trigger an update at the end of the next PWM period
}

void loop()
{
  for (uint8_t i = 0; i < 3; i++)                      // Loop for each PWM channel (3 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTYUPD = 4999;             // Set the PWM duty cycle to 25%
  }
  PWM->PWM_SCUC = PWM_SCUC_UPDULOCK;      // Set the update unlock bit to trigger an update at the end of the next PWM period
  delay(500);
  for (uint8_t i = 0; i < 3; i++)                      // Loop for each PWM channel (3 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTYUPD = 14999;            // Set the PWM duty cycle to 75%
  }
  PWM->PWM_SCUC = PWM_SCUC_UPDULOCK;      // Set the update unlock bit to trigger an update at the end of the next PWM period
  delay(500);
}

Also, the PWM controller's timers and duty-cycle registers (PWM_CDTY and PWM_CDTYUPD) are essentially only 16-bit, therefore it's not possible to load them with values over 65535 (2^16 - 1).

jonnygainz

Thank you very much for the help, I'll let you know if I get through with what I'm doing.

jonnygainz

#3
Jan 16, 2019, 12:56 pm Last Edit: Jan 16, 2019, 01:07 pm by jonnygainz
Is there a way to set the update unlock bit at the end of every period without using the delay function?  I'm working on a three phase inverter with variable frequency and voltage and the period i will be working with is too small for the delay function. I stayed away from mode 2 because i was having problems understanding how to configure the PDC DMA, can you explain to me how does it work? I'm reading through the datasheet and I'm having some difficulty understanding how it works.

MartinL

#4
Jan 16, 2019, 01:18 pm Last Edit: Jan 16, 2019, 05:27 pm by MartinL
To automatically update the duty-cycle at the end of a timer period without having to set the update unlock bit, it's possible to use method 2 (mode1): manual duty-cycle update with automatic trigger.

The mode 1 bitfield is set in the Sync Channels Mode register:

Code: [Select]
PWM->PWM_SCM |= PWM_SCM_UPDM_MODE1 |                                             // Automatically update the duty-cycle register each timer cycle                   
                PWM_SCM_SYNC2 | PWM_SCM_SYNC1 | PWM_SCM_SYNC0;                   // Set the PWM channels as synchronous

Here's a similar example to the one above, but using mode 1 (and without the complementary outputs):

Code: [Select]
// Enable synchronous, complementary output, single-slope PWM at 50Hz on 3 channels (0 to 2), at 50% duty cycle
void setup() {
  // Set-up 6 PWM outputs on 3 complementary channels on on pins D34, D35, D36, D37, D38, and D39 for channels 0 through to 2 respectively
  PMC->PMC_PCER1 |= PMC_PCER1_PID36;                                               // Enable PWM   
  PIOC->PIO_ABSR |= PIO_ABSR_P6 | PIO_ABSR_P4 | PIO_ABSR_P2;                       // Set the port C PWM pins to peripheral type B               
  PIOC->PIO_PDR |= PIO_PDR_P6 | PIO_PDR_P4 | PIO_PDR_P2;                           // Set the port C PWM pins to outputs                 
  PWM->PWM_CLK = PWM_CLK_PREA(0) | PWM_CLK_DIVA(84);                               // Set the PWM clock A rate to 1MHz (84MHz/84)
  PWM->PWM_SCM |= PWM_SCM_UPDM_MODE1 |                                             // Automatically update the duty-cycle register each timer cycle                   
                  PWM_SCM_SYNC2 | PWM_SCM_SYNC1 | PWM_SCM_SYNC0;                   // Set the PWM channels as synchronous                 
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA;      // Enable single slope PWM and set the clock source as CLKA for all synchronous channels
  PWM->PWM_CH_NUM[0].PWM_CPRD = 19999;                 // Set the PWM frequency 1MHz/(19999 + 1) = 50Hz for all synchronous channels
  for (uint8_t i = 0; i < 3; i++)                      // Loop for each PWM channel (8 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTY = 9999;                // Set the PWM duty cycle to 50%
  }
  PWM->PWM_ENA = PWM_ENA_CHID0;                        // Enable all synchronous PWM channels
}

void loop()
{
  for (uint8_t i = 0; i < 3; i++)                      // Loop for each PWM channel (3 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTYUPD = 4999;             // Set the PWM duty cycle to 25%
  }
  delay(500);
  for (uint8_t i = 0; i < 3; i++)                      // Loop for each PWM channel (3 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTYUPD = 14999;            // Set the PWM duty cycle to 75%
  }
  delay(500);
}

jonnygainz

I'm trying to update the duty cycle without using the delay function because in an inverter the time in which the duty cycle must be updated is very small and the delay function will not be able to accommodate that time. Also the frequency will have to be adjustable by an keypad or pot and hence cannot be a fixed value. Will the mode 2 help with this?

MartinL

#6
Jan 16, 2019, 01:30 pm Last Edit: Jan 16, 2019, 01:38 pm by MartinL
Regarding mode 2 operation using the PDC: automatic duty-cycle update and trigger, this mode is geared towards updating the duty-cycle each period (or for a specified number of periods) with a set of predefined duty-cycles held in memory.

Here's an example of mode 2 in operation, (based on ard_newbie's example: https://forum.arduino.cc/index.php?topic=530217.0):

Code: [Select]
// Enable synchronous, complementary output, single-slope PWM at 50Hz on 3 channels (0 to 2), at 50% duty cycle
uint16_t data[] = { 0, 4999, 14999, 4999, 4999, 14999, 9999, 4999, 14999, 14999, 4999, 14999, 19999, 4999, 14999 };

void setup() {
  // Set-up 6 PWM outputs on 3 complementary channels on on pins D34, D35, D36, D37, D38, and D39 for channels 0 through to 2 respectively
  PMC->PMC_PCER1 |= PMC_PCER1_PID36;                                               // Enable PWM   
  PIOC->PIO_ABSR |= PIO_ABSR_P6 | PIO_ABSR_P4 | PIO_ABSR_P2;                       // Set the port C PWM pins to peripheral type B               
  PIOC->PIO_PDR |= PIO_PDR_P6 | PIO_PDR_P4 | PIO_PDR_P2;                           // Set the port C PWM pins to outputs                 
  PWM->PWM_CLK = PWM_CLK_PREA(0) | PWM_CLK_DIVA(84);                               // Set the PWM clock A rate to 1MHz (84MHz/84)
  PWM->PWM_SCM |= PWM_SCM_UPDM_MODE2 |                                             // Automatically load the duty-cycle register with the PDC each timer cycle                   
                  PWM_SCM_SYNC2 | PWM_SCM_SYNC1 | PWM_SCM_SYNC0;                   // Set the PWM channels as synchronous                 
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA;      // Enable single slope PWM and set the clock source as CLKA for all synchronous channels
  PWM->PWM_CH_NUM[0].PWM_CPRD = 19999;                 // Set the PWM frequency 1MHz/(19999 + 1) = 50Hz for all synchronous channels
  for (uint8_t i = 0; i < 3; i++)                      // Loop for each PWM channel (8 in total)
  {
    PWM->PWM_CH_NUM[i].PWM_CDTY = 9999;                // Set the PWM duty cycle to 50%
  }
  NVIC_SetPriority(PWM_IRQn, 0);                       // Set the Nested Vector Interrupt Controller (NVIC) priority for the PWM controller to 0 (highest)
  NVIC_EnableIRQ(PWM_IRQn);                            // Connect PWM Controller to Nested Vector Interrupt Controller (NVIC)
  PWM->PWM_IER2 = PWM_IER2_WRDY;                       // Enable interrupt when Write Ready (WRDY) is set
 
  PWM->PWM_TPR = (uint32_t)data;                       // Set the address of the transmit data pointer
  PWM->PWM_TCR = 15;                                   // Set the length of the transmit data
  PWM->PWM_TNPR = (uint32_t)data;                      // Set the next transmit data pointer
  PWM->PWM_TNCR = 15;                                  // Set the next transmit counter
  PWM->PWM_PTCR |= PWM_PTCR_TXTEN;                     // Enable the Peripheral DMA Controller
  PWM->PWM_ENA = PWM_ENA_CHID0;                        // Enable all synchronous PWM channels
}

void loop() {}

void PWM_Handler()
{
  PWM->PWM_TNPR = (uint32_t)data;                      // Set the next transmit data pointer
  PWM->PWM_TNCR = 15;                                  // Set the next transmit counter
  PWM->PWM_ISR2;                                       // Clear the interrupt status register 2
}

The duty-cycles are held in the data array and are stored { channel0, channel1, channel2, channel 0, channel 1, channel2, channel0, etc...

In this example the PWM interrupt handler simply reloads the next transmit data pointer (TNPR) and counter (TNCR) so that the duty-cycle pattern repeats indefinitely.

jonnygainz

Could you explain what this is in a little more detail for me? If it's not too much trouble?

Code: [Select]
  PWM->PWM_TPR = (uint32_t)data;                       // Set the address of the transmit data pointer
  PWM->PWM_TCR = 15;                                   // Set the length of the transmit data
  PWM->PWM_TNPR = (uint32_t)data;                      // Set the next transmit data pointer
  PWM->PWM_TNCR = 15;                                  // Set the next transmit counter
  PWM->PWM_PTCR |= PWM_PTCR_TXTEN;                     // Enable the Peripheral DMA Controller
  PWM->PWM_ENA = PWM_ENA_CHID0;                        // Enable all synchronous PWM channels
}

void loop() {}

void PWM_Handler()
{
  PWM->PWM_TNPR = (uint32_t)data;                      // Set the next transmit data pointer
  PWM->PWM_TNCR = 15;                                  // Set the next transmit counter
  PWM->PWM_ISR2;                                       // Clear the interrupt status register 2
}

MartinL

It's possible to change the period during operation using the period update register (PWM_CPRDUPD), followed by setting set the update unlock bit (PWM_SCUC_UPDULOCK) in the Sync Channels Update Control register (SCUC) :

Code: [Select]
PWM->PWM_CH_NUM[0].PWM_CPRDUPD = 19999;
PWM->PWM_SCUC = PWM_SCUC_UPDULOCK;

If all the channels are synchronised, this will change the period for all the channels.

Quote
I'm trying to update the duty cycle without using the delay function because in an inverter the time in which the duty cycle must be updated is very small and the delay function will not be able to accommodate that time. Also the frequency will have to be adjustable by an keypad or pot and hence cannot be a fixed value. Will the mode 2 help with this?
Might I ask what you mean by using the delay() function?

jonnygainz

#9
Jan 16, 2019, 02:01 pm Last Edit: Jan 16, 2019, 02:03 pm by jonnygainz
See in #4 how you used the delay(500) to update the duty cycle I can't use that because after a while it will not be able to accommodate the times. I am making a three phase space vector modulated inverter and every period i will have to calculate new duty cycles to load into each channel. Depending on the supply frequency you want the period in which these duty cycle calues must be calculated will be extremely small. If you want to find out more detail about how this works you can take a look at this page:

https://hvdc.ca/webhelp/Master_Library_Models/HVDC_and_FACTS/Space_Vector_Modulation/SVM_Theory.htm

MartinL

#10
Jan 16, 2019, 02:09 pm Last Edit: Jan 16, 2019, 02:15 pm by MartinL
Quote
Could you explain what this is in a little more detail for me? If it's not too much trouble?
The PDC has a transmit data pointer (TDP), this is a register that contains the address or location of the first data item in the data array, in this example it's the address of the data array: data or &data[0]. The PDC also has a transmit counter (TCR), a 16-bit counter that initially contains the number of data items in the array, in our case 15.

Each PWM timer period, the PRC copies the data from the array into PWM timer's PWM_CDTYUPD register and decrements the counter by 1. When the counter reaches zero the PDC checks the next transmit data pointer (TNPR) and (TNCR) registers. If the TNCR is zero the transfer is terminated. If however the TNPR and TNCR contain new data, it's copied to the TCP and TCR registers and the TNPR and TNCR are set to zero.

To keep the PWM timer cycling around the array indefinitely, (should this be a requirement), it's necessary to use the PWM Controller's interrupt service routine (ISR): PWM_Handler() function, to reload the PDC's next registers oncemore.

MartinL

It's possible to just remove the delay() functions and call on the PWM timer to output whatever duty-cycle and period you require, whenever you need it. The PWM timer will output the new waveform at the beginning of the next cycle.

jonnygainz

#12
Jan 16, 2019, 02:18 pm Last Edit: Jan 16, 2019, 02:20 pm by jonnygainz
I should say that there are two things I'm working on the first is generating three phase quasi-square waves using an inverter circuit and then implementing Space Vector Modulation to produce three phase sinusoidal waves from the quasi-square waves. For the quasi-square waves, only 6 circuit configurations are required in a specific order for a fixed period for duty cycles of either 0% or 100%. Space Vector Modulation however is much more complex than that.

Programming has never been my strong point, and working with this Due is a little difficult for me, but it is much more capable to do what i need it to do than the rest of the boards offered by arduino.

Go Up