Arduino Due PWM Interrupt

Hello

For a project I need to output 2 pins, A and B. These pins are correlated to a variable PWM signal.
PWM signal = 0 -> (AB) = (01)
PWM signal = 1 -> (AB) = (10)
Transistions should always go via (00). eg (01) -> (00) -> (10)

The PWM signal has a sample frequency of 15kHz and presents after an LPF a sinewave of 1Hz - 65Hz. This is done using a lookup table.

Now I want to have an interrupt handler for when the overflow (?) value of the pwm happens. So I can change the outputs of A and B. Reading online and in the datasheet, I can enable the interrupt by setting bits in the pwm registers, but I can't figure it out.

NVIC_EnableIRQ(PWM_IRQn);
REG_PWM_IER1 |= PWM_IER1_CHID0;

void PWM_Handler() {
REG_PWM_ISR1;
toggle_pin(&(PIN_PORT -> PIO_ODSR), PIN_V);
}

Gives me a square wave of 7.5kHz and duty cycle 50%, not what I'm looking for.

Can someone point me to the right direction? I've attached my code file. Thanks

EDIT: just now I saw the arduino due subforum, maybe a mod can place this in the proper spot

pwm.cpp (5.03 KB)

Some thoughts about your code(although I'm not sure to fully understand what you are doing):

Once you have set a frequency and a duty cycle for a PWM channel in setup(), with PWM_CPRD and PWM_CDTY, and enable a PWM channel, you can't change the frequency and/or the duty cycle anymore in loop() with these same registers. Once in loop(), (once a PWM channel has been enabled) you will use PWM_CPRDUPD and PWM_CDTYUPD to UPDate frequency and/or duty cycle.

Cut your program into several more simple ones, each easier to debugg, e.g.:
-Variable duty cycle (to output a sine wave thru a LPF) and fixed frequency
-Idem + PWM_Handler() to set and clear A and B accordingly,

  • etc...

Hi, I've added more comments to my file, trying to explain what I do. The frequency of the sinewave gets handled in the TC0_handler. The pwm frequency is fixed at 15kHz. LPF is extern with a resistor and capacitor.
I set the current duty cycle value of the lut in PWM -> PWM_CH_NUM[0].PWM_CDTY. This is done in the TCO_handler. The faster timer0, the faster it goes through the lut, so I get a higher frequency output.

This is my first time programming for a Due. I have experience with the Uno but this is a whole other level. Same with C++ since I started with C. Tomorrow I'll try to make my own class for a pwm signal with functions for setting the correct output. I hope I'll figure out more how the system works. Now it's with a lot of trial and error.

pwm.cpp (5.86 KB)

As you don't modify PWM_CPRDUPD, and you need to constantly vary PWM_CDTYUPD, I suggest that you use the automatic trigger feature of PWM synchro channels, but only with PWM channel 0.

Here is an example sketch to show how this feature works with a PDC DMA (Use Serial Plotter in your IDE window and select 9600 bauds to see the sine wave):

/*******************************************************************************************/
/*                Synchro channel PWML0 ONLY with PDC DMA trigger on Compare               */
/*******************************************************************************************/

#define sinsize  (256)                // Sample number (a power of 2 is better)
#define PERIOD_VALUE (31250)//   (15625)        // For a 1 Hz sinwave
#define NbCh      (1)                 // Only channel 0 ---> Number of channels = 1

#define DUTY_BUFFER_LENGTH      (sinsize * NbCh) // Half words

uint16_t Duty_Buffer[DUTY_BUFFER_LENGTH];

#define UpdatePeriod_Msk (0b1111)
#define UpdatePeriod    (UpdatePeriod_Msk & 0b0000) //Defines the time between each duty cycle update of the synchronous channels
//This time equals to (UpdatePeriod + 1) periods of the Reference channel 0

uint16_t Sin_Duty[sinsize];

void setup () {

  Serial.begin(9600);

  PMC->PMC_PCER1 |= PMC_PCER1_PID36;       // PWM controller power ON
  PMC->PMC_PCER0 |= PMC_PCER0_PID13;       // PIOC power ON

  // PWML0 on PC2, peripheral type B
  PIOC->PIO_PDR |= PIO_PDR_P2;
  PIOC->PIO_ABSR |= PIO_PC2B_PWML0;

  // Set synchro channels list : Channel 0
  PWM->PWM_DIS = PWM_DIS_CHID0;

  PWM->PWM_SCM  = PWM_SCM_SYNC0          // Add SYNCx accordingly, at least SYNC0
                  | PWM_SCM_UPDM_MODE2;  //Automatic write of duty-cycle update registers by the PDC DMA

  // Set duty cycle update period
  PWM->PWM_SCUP = PWM_SCUP_UPR(UpdatePeriod);

  // Set the PWM Reference channel 0 i.e. : Clock/Frequency/Alignment
  PWM->PWM_CLK = PWM_CLK_PREA(0b0000) | PWM_CLK_DIVA(3);       // Set the PWM clock rate for 84 MHz/1
  PWM->PWM_CH_NUM[0].PWM_CMR = PWM_CMR_CPRE_CLKA;               // The period is left aligned, clock source as CLKA on channel 0
  PWM->PWM_CH_NUM[0].PWM_CPRD = PERIOD_VALUE;                   // Set the PWM frequency (84MHz/1)/PERIOD_VALUE Hz

  /****  Final frequency = MCK/DIVA/PRES/CPRD/(UPR + 1)/sinsize = 7 Hz    ****/

  // Set Interrupt events
  PWM->PWM_IER2 = PWM_IER2_WRDY;   //Write Ready for Synchronous Channels Update Interrupt Enable
  //synchro with ENDTX End of TX Buffer Interrupt Enable

  // Fill duty cycle buffer for channels 0, x, y ...
  // Duty_Buffer is a buffer of Half Words(H_W) composed of N lines which structure model for each duty cycle update is :
  // [ H_W: First synchro channel 0 duty cycle **Mandatory** ]/[ H_W: Second synchro channel duty cycle ] ... and so on

  for (int i = 0; i < sinsize; i++) {
    Sin_Duty[i] = 1 + (2047 * (sinf( i * 2 * PI / sinsize ) + 1));
  }

  for (uint32_t i = 0; i < sinsize; i++) {
    Duty_Buffer[i * NbCh + 0] = Sin_Duty[i];
    // ............................................
    // Duty_Buffer[i * NbCh + (NbCh - 1)] = Sin_Duty[i];
  }

  PWM->PWM_ENA = PWM_ENA_CHID0;                  // Enable PWM for all channels, channel 0 Enable is sufficient

  PWM->PWM_TPR  = (uint32_t)Duty_Buffer;        // FIRST DMA buffer
  PWM->PWM_TCR  = DUTY_BUFFER_LENGTH;           // Number of Half words
  PWM->PWM_TNPR = (uint32_t)Duty_Buffer;        // Next DMA buffer
  PWM->PWM_TNCR = DUTY_BUFFER_LENGTH;
  PWM->PWM_PTCR = PWM_PTCR_TXTEN;               // Enable PDC Transmit channel request

  NVIC_EnableIRQ(PWM_IRQn);

}

void loop() {

  // For debugging only :
  // Check correct updates of CDTY via CDTYUPD by DMA
  // Menu > Tools > Serial Tracer -- 9600 baud

  Serial.println(PWM->PWM_CH_NUM[0].PWM_CDTY);

  // Here you can modify PWM->PWM_SCUPUPD()
  // to vary frequency of sine output

}

void PWM_Handler() {  // move PDC DMA pointers to next buffer

  PWM->PWM_ISR2;      // Clear status register

  PWM->PWM_TNPR = (uint32_t)Duty_Buffer;
  PWM->PWM_TNCR = DUTY_BUFFER_LENGTH;

}

And a few remarks on your code:
1/
To toggle Pin V:
PIOC->PIO_ODSR ^= PIN_V;
2/
in void init_pins(), you don't power ON PIOC :
void init_pins() {
//PIOC power ON
PMC->PMC_PCER0 |= PMC_PCER0_PID13; //PIOC power ON
3/
in init_pwm(), you initialize CDTY with 0, is that what you want ?
4/
in TC0_Handler(), replace PWM_CDTY by PWM_CDTYUPD because TC0_Handler is called once PWM is enabled.

Hi, I thank you for your reply. Yesterday I modified my code with classes. It's much clearer now. I've looked at this Timer library GitHub - ivanseidel/DueTimer: ⏳ Timer Library fully implemented for Arduino DUE and started with my own. I also have a PWM class so that it should be easier to test stuff out.

I've tested your code. It helped me understand the PWM better but it's not what I was looking for. The sine on a oscilloscope doesn't look good. Some things are quite yet clear though.

What does this register do?
// Set duty cycle update period
PWM->PWM_SCUP = PWM_SCUP_UPR(UpdatePeriod);

If I have it correctly, this is mode 2 of the PWM. Without overhead of the cpu
PWM->PWM_TPR = (uint32_t)Duty_Buffer; // FIRST DMA buffer
PWM->PWM_TCR = DUTY_BUFFER_LENGTH; // Number of Half words
PWM->PWM_TNPR = (uint32_t)Duty_Buffer; // Next DMA buffer
PWM->PWM_TNCR = DUTY_BUFFER_LENGTH;
PWM->PWM_PTCR = PWM_PTCR_TXTEN; // Enable PDC Transmit channel request

These are a pointer and a counter. When goes the pointer up and when the counter? TPR and TNPR point to the same memory address no?

Hi

I did some more testing today, and still haven't found what I'm looking for. I've attached my files. I know the classes are not that good and a lot of stuff can be updated, but I'm using this to keep it simple and I can always add more configuration later.

I can create a timer interrupt, I can create a PWM signal with variable duty cycle but I can't create a pwm interrupt.

I've created a PWM signal with method 2 (mode 1) and enabled the compare interrupt in IER2 but the PWM_handler doesn't get called.

In this simple program I have a PWM signal with frequency 15kHz and duty cycle 80%. What I want is an interrupt on the rising and falling edge of the PWM signal (atm I'm flipping a pin to check my code with an oscilloscope, in the end 2 pins should switch to their correct output). But PWM_handler doesn't get called.

What am I doing wrong? Thanks

DuePin.cpp (854 Bytes)

DuePin.h (385 Bytes)

DuePWM.cpp (2.93 KB)

DuePWM.h (445 Bytes)

DueTimer.cpp (2.12 KB)

DueTimer.h (395 Bytes)

main.cpp (633 Bytes)

Your code is over complicated.

Here is an example sketch to generate a PWM pulse on TIOA0 (arduino pin 2), frequency = 15 KHz, Duty cycle = 80%, Interruptions on Rising and Falling edges of TIOA0:

/**************************************************************************************/
/*       PWM pulses on TIOA0 (arduino pin 2) Frequency = 15 KHz, Duty cycle = 80%     */
/*       Interruptions on TIOA0 rising and falling edges                              */                
/**************************************************************************************/

void setup() {

  /*************  Timer Counter 0 Channel 0 to generate PWM pulses thru TIOA0  ************/
  PMC->PMC_PCER0 |= PMC_PCER0_PID27;       // TC0 power ON - Timer Counter 0 channel 0 IS TC0
  
  PIOB->PIO_PDR |= PIO_PDR_P25;           // PB25 is no more driven by the GPIO
  PIOB->PIO_ABSR |= PIO_PB25B_TIOA0;

  TC0->TC_CHANNEL[0].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1  // MCK/2, clk on rising edge
                              | TC_CMR_WAVE               // Waveform mode
                              | TC_CMR_WAVSEL_UP_RC        // UP mode with automatic trigger on RC Compare
                              | TC_CMR_ACPA_CLEAR          // Clear TIOA0 on RA compare match
                              | TC_CMR_ACPC_SET;           // Set TIOA0 on RC compare match


  TC0->TC_CHANNEL[0].TC_RC = 2800;  //<*********************  Frequency = (Mck/2)/TC_RC  Hz = 15 KHz
  TC0->TC_CHANNEL[0].TC_RA = 2240;  //<********************   Duty cycle = (TC_RA/TC_RC) * 100  %  = 80 %

  TC0->TC_CHANNEL[0].TC_IER = TC_IER_CPCS       // Interrupt on RC compare match
                              | TC_IER_CPAS;    // Interrupt on RA compare match
                              
  NVIC_EnableIRQ(TC0_IRQn);                     // Interrupt enable
                           
  TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Software trigger TC0 counter and enable
}
void TC0_Handler()
{
 uint32_t status;
 status = TC0->TC_CHANNEL[0].TC_SR;  // Read and clear TC0 status register

 if(status & TC_SR_CPAS) {
  // Toggle pin 1
 }
 else  // if(status & TC_SR_CPCS)
  {
  // Toggle pin 2
 }
}
void loop() {
 
}

Thanks for this. Using RA and RC is smart and gives me possibilty to do what I want without PWM handlers.

I've succesfully programmed TC0, TC1 and TC2 to simulate the sinewave. In their interrupt handler I change 2 outputs of their state.

I've programmed TC3 to loop through the lookup table and set the correct RA values.

However, I'm trying to output TC1 and TC2 the same way as TC0 through pin2.
This Timer Pins routed to the Arduino Due Board · Issue #11 · ivanseidel/DueTimer · GitHub shows TIOA1 is on AnalogIn 7.

So:

  PIOB -> PIO_PDR |= PIO_PDR_P25;        //PB25 no more driven by gpio
  PIOB -> PIO_ABSR |= PIO_PB25B_TIOA0;

  PIOA -> PIO_PDR |= (1ul << 2);
  PIOA -> PIO_ABSR |= (1ul << 2);        //where can one see the correct macros for these pins?

  PMC -> PMC_PCER0 |= PMC_PCER0_PID27 | PMC_PCER0_PID28 | PMC_PCER0_PID29 | PMC_PCER0_PID30;  //TC0 power on

  uint8_t i = 0;
  for (; i < 3; ++i) {

    TC0->TC_CHANNEL[i].TC_CMR = TC_CMR_TCCLKS_TIMER_CLOCK1  // MCK/2, clk on rising edge
                              | TC_CMR_WAVE               // Waveform mode
                              | TC_CMR_WAVSEL_UP_RC        // UP mode with automatic trigger on RC Compare
                              | TC_CMR_ACPA_CLEAR          // Clear TIOA0 on RA compare match
                              | TC_CMR_ACPC_SET;           // Set TIOA0 on RC compare match

    TC0->TC_CHANNEL[i].TC_RC = 2800;      //<*********************  Frequency = (Mck/2)/TC_RC  Hz = 15 KHz
    TC0->TC_CHANNEL[i].TC_RA = 2200;      //<********************   Duty cycle = (TC_RA/TC_RC) * 100  %  = 80 %

    TC0->TC_CHANNEL[i].TC_IER = TC_IER_CPCS       // Interrupt on RC compare match
                                | TC_IER_CPAS;    // Interrupt on RA compare match
                              
                             
    TC0->TC_CHANNEL[i].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Software trigger TC0 counter and enable

  }

But this isn't working. Only TC0 has output. So I guess my question is, how can I attach the pin to the timer? So it gets flip because these 2 bits are set: TC_CMR_ACPA_CLEAR and TC_CMR_ACPC_SET

In Timer Counter section of Sam3x datasheet, page 858.

TIOA0 is on PB25 peripheral type B
TIOA1 is on PA2 peripheral type A
TIOA2 is on PA5 peripheral type A

Yeah well, I have no output on PA2 (or AI7)

I also have a feeling that the pwm's generated by using the PWM module are a lot cleaner than with using Timers. There's more noise I think.

DuePin.cpp (1.04 KB)

DuePin.h (439 Bytes)

main.cpp (5.82 KB)

Post your code between code tags.

Speklap:
Yeah well, I have no output on PA2 (or AI7)

Of course, you don't set correctly the pin to the peripheral.

For TIOA1:

void setup() {
PIOA -> PIO_PDR |= PIO_PDR_P2;        //PA2 no more driven by gpio
PIOA -> PIO_ABSR &= ~PIO_PA2A_TIOA1;  // Peripheral type A
}
void loop() {
  
}

Speklap:
I also have a feeling that the pwm's generated by using the PWM module are a lot cleaner than with using Timers. There's more noise I think.

I don't see why there should be more noise using the Timer Counter.

Thanks again. But where's the logic in this

  PIOB -> PIO_PDR |= PIO_PDR_P25;        //PB25 no more driven by gpio
  PIOB -> PIO_ABSR |= PIO_PB25B_TIOA0;

  PIOA -> PIO_PDR |= PIO_PDR_P2;        //PA2 no more driven by gpio
  PIOA -> PIO_ABSR &= ~PIO_PA2A_TIOA1;  // Peripheral type A

What will it be for TIOA2? Where can I find this in the datasheet or somewhere else? I've looked everywhere but apperently not enough

Datasheet page 656 : PIO_ABSR

And page 858. This is so stupid. Why didn't I find this table before. So I guess for TIOA2 it would be:

PIOA -> PIO_PDR |= PIO_PDR_P5;
PIOA -> PIO_ABSR &= ~PIO_PA5A_TIOA2;

Thank you ard_newbie for your PDC/PWM sine wave code above.

I finally got round to learning how to use the Peripheral DMA Controller with PWM on the Due. Your excellent example made it much easier to understand.

IMO, PWM PDC DMA (or PWM AHB DMA) is one of the most interesting features of the PWM peripheral. :slight_smile:

IMO, PWM PDC DMA (or PWM AHB DMA) is one of the most interesting features of the PWM peripheral. :slight_smile:

Thanks mentioning the AHB DMA. Looks like I've got another chapter in the datasheet to read, in order to fully understand the SAM3X8E's peripheral to peripheral DMA transfers.