Arduino Due PWM channels

For the Arduino Due, is it possible to set up one of the PWM channels (say PWM4) to run at a particular frequency (say ~= 4 kHz, using MCK/1024, and then again that divided by DIVA = 20)), and other PWM channels to give PWM outputs at higher frequencies?

I want to use one of the channels as a sort of state-machine/system tick clock (not the inbuilt SysTick) and have that run from the turn on of the system, so that I can measure how long the processor takes to perform operations defined by me.

I would have used the Timer Counter to do this, but I need this clock to be at a frequency of 5-10 kHz.

From this section of the ATMEL datasheet, it seems that we can indeed do this,

If anyone could help me out with the code for an example of this (say any 2 channels programmed differently) that would be great.
Thank you!

Be advised that the clock generator outputs are all MCK divided by powers of 2 only. After that, you only have two "linear dividers" to provide non power of 2 divisors. Maybe you already know that much...

A PWM channel is not an appropriate choice for a tick timer. What is wrong with using SysTick?

Also you have 3 Timer Counters. Surely one of them is free?

Enquiring minds would also be wondering, why you would use a low frequency like 5-10 kHz to time software events? It would be a great sacrifice in precision compared with timing near the CPU clock frequency which is definitely possible.

Why would you want to use the PWM for that? The Due also has an RTC peripheral (clocked at 32kHz) for measuring low speed stuff), and DWT->CYCCNT (32bit counter clocked at the CPU rate) for high speed stuff.

(Also, it has separate "TimerCounter" peripheral AND a "PWM Peripheral" (Arduino uses both to implement analogWrite()) IIRC, the TCs are cascadable to more than 32bits.)

Thank you for replying. I chose the PWM channel over Timer because I could go down to 5-10 kHz with PWM, and I don't think that's possible with the Timer. I need that low a frequency because I need to be able to set up a sort of state machine like process - where processes are like read and writing to an ADC, reading 2 ADC values and generating 2 PWMs, or multiplying 2 ADC channels, etc. I want to be able to see these signals on the oscilloscope and calculate how long each process took and what idle time is left.

It is to sort of have a master clock to keep other events in sync with, instead of using constantly polling the timer count to decide what to do, I want to use interrupts.

Thank you for replying. I want to use one of the PWM channels to set up a master clock for my whole system, which will be the synchronizing clock for all the processes on my board. So it should be slower than all the other PWM processes to be run. The Timer counter has fixed 84 MHz/2^n clocks only which goes to +656 kHz which is too high for my use. That's why I went to the PWM channel. The inbuilt CYCCNT is too fast as well I think.

Why? The common function 'millis()' "goes down to" 1 kHz, and it's Timer driven... Any hardware timer can produce periodic interrupts in the kHz range. Hundreds of libraries use that already.

I'm pretty sure you just don't understand how to configure a Timer...

Also, I ask again, what is so wrong with using SysTick? You can decrease the effective frequency of whatever tasks you have, by subdividing the calls, i.e. implement a counter and only act when the counter overflows.

Can you tell us more about your " sort of state-machine/system"? Have you written anything yet?

So you're looking for a hardware time (which excludes millis()) that has a resolution of about 0.25ms (4kHz) and an overflow time of "long" (which excludes CYCCNT and Systick and most of the TC timers.) Do you need an output pin?

Except for the output pin, it sounds like a job for the RTT...

Hi @smanna

The following code configures the Due's TC6 timer (TC2 Channel 0) to output 50% duty-cycle, 4kHz PWM on pins D4 (TIOB6) and D5 (TIOA6). It also incorporates an interrupt service routine triggered at 0.25ms (4kHz):

// Output 50% duty-cycle PWM at 4kHz on digital pins D4 and D5 using timer TC6
void setup() 
{
  PMC->PMC_PCER1 |= PMC_PCER1_PID33;                      // Enable peripheral TC6 (TC2 Channel 0)
  PIOC->PIO_ABSR |= PIO_ABSR_P26 | PIO_ABSR_P25;          // Switch the multiplexer to peripheral B for TIOA6 and TIOB6
  PIOC->PIO_PDR |= PIO_PDR_P26 | PIO_PDR_P25;             // Disable the GPIO on the corresponding pins

  TC2->TC_CHANNEL[0].TC_CMR = TC_CMR_BCPC_SET |           // Set TIOB on counter match with RC0
                              TC_CMR_ACPC_SET |           // Set TIOA on counter match with RC0
                              TC_CMR_BCPB_CLEAR |         // Clear TIOB on counter match with RB0
                              TC_CMR_ACPA_CLEAR |         // Clear TIOA on counter match with RA0
                              TC_CMR_WAVE |               // Enable wave mode
                              TC_CMR_WAVSEL_UP_RC |       // Count up with automatic trigger on RC compare
                              TC_CMR_EEVT_XC0 |           // Set event selection to XC0 to make TIOB an output
                              TC_CMR_TCCLKS_TIMER_CLOCK1; // Set the timer clock to TCLK1 (MCK/2 = 84MHz/2 = 42MHz)

  TC2->TC_CHANNEL[0].TC_RA = 5250;                        // Load the RA0 register
  TC2->TC_CHANNEL[0].TC_RB = 5250;                        // Load the RB0 register
  TC2->TC_CHANNEL[0].TC_RC = 10500;                       // Load the RC0 register

  NVIC_SetPriority(TC6_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TC6 to 0 (highest) 
  NVIC_EnableIRQ(TC6_IRQn);         // Connect TC6 to Nested Vector Interrupt Controller (NVIC)

  TC2->TC_CHANNEL[0].TC_IER |= TC_IER_CPCS;                // Enable interrupts for TC6 (TC2 Channel 0)
  TC2->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Enable the timer TC6
}

void loop() {}

void TC6_Handler()                                         // ISR TC6 (TC2 Channel 0)
{
  if (TC2->TC_CHANNEL[0].TC_SR & TC_SR_CPCS)               // Check for RC compare condition
  {
    // Add you code here...
  }
}

I'll look into the RTT - but what did you mean by "Except for the output pin"? Thank you.

Thank you for your reply and code! I'm fairly new to this- could you please tell me how I would see the interrupts occurring at 4 kHz on an oscilloscope? Or in other words, if I could probe a pin corresponding to this or make an LED glow, etc?

You certainly can. But scopes are all different. Which one do you have? To make the signal, all you need to do is designate a test pin and drive it from inside the ISR

1 Like

Hi @smanna

If you hook your scope probe up to either digital pin D4 or D5 you'll see a 4kHz PWM signal with 50% duty cycle. These ouputs come directly from the TC6 timer.

If you change the RA and RB values between 0 and the value in the RC register, you'll see the duty-cycle of the corresponding PWM output change between 0% and 100%.

The TC6 timer's frequency is determined by the value in the RC register given by the formula:

PWM_FREQUENCY = TIMER_CLOCK / RC

or alternatively:

RC = TIMER_CLOCK / PWM_FREQUENCY

Since TIMER_CLOCK is set to CLOCK1, the timer's clock frequency is MCLK / 2 = 42MHz, so in this case:

RC = 42MHz / 4kHz = 10500

To get a 50% duty-cycle RA and RB are set to half of this value (5250).

There are various ways to test the frequency of the ISR. Personally, if I can see a 4kHz output from the timer on my scope (and I'm sure I've selected the correct timer mode) then that's good enough.

To be 100% sure though, one way is to manually turn on and off (know as bit banging) a digital pin with the ISR itself, then look at the resulting output on a scope.

To do this, set up a pin to be a digital output in the setup() portion of the sketch, say digital pin D2:

pinMode(2, OUTPUT);   // Set digital pin D2 to an OUTPUT

...then within the ISR itself add this code:

void TC6_Handler()                                         // ISR TC6 (TC2 Channel 0)
{
  if (TC2->TC_CHANNEL[0].TC_SR & TC_SR_CPCS)               // Check for RC compare condition
  {
    // Add you code here...
    static uint8_t state = LOW;  // Define an output state variable
    digitalWrite(2, state);      // Output the current state
    state = !state;              // Change the output state LOW to HIGH or HIGH to LOW
  }
}

After uploading these changes you should be able to see an 2kHz square wave output on D2. The toggling of the output each time the ISR runs, means that the output frequency of the waveform is half the number of times the ISR is called per second.

1 Like

Thank you, @MartinL , this clarifies things up quite a bit. I had a question on the ISR part - I did see in the datasheet that there are a few different kinds of conditions for comparison that can be set up. So to set up any ISR I would need to do an "if(compare condition)" statement, correct? Now if this were to run in the background like a master clock (not 84 MHz built-in) and I wanted all other events using the Due (say an ADC calculation, LED toggling, reading from a digital pin etc) to be synchronized with this 4kHz clock, say on the rising edge of this PWM all events have the "Go" ahead to execute, then I would have to write all those codes under the ISR, right?

Also, in that case, if I wanted to start a particular task at say x microseconds delayed from the rising 4kHz edge, then I can adjust the number of times the ISR is called per second by changing the compare condition?

Hi @smanna

I just thought I'd mention that what you're explaining here, sounds remarkably like a real-time operating system (RTOS). A RTOS can be used to schedule tasks to run at pedefined intervals using interrupt driven software timers. The system defaults to an idle task whenever CPU is free.

Anyway, in answer to your questions:

Yes, on the SAM3X8E microcontroller it's necessary to read TC timer's status register, in order to clear the interrupt flag.

In addition to the TC_SR_CPCS update interrupt flag that occurs at the beginning of every timer cycle, there's also TC_SR_CPAS and TC_SR_CPBS that occur whenever the timer's COUNT matches the values in the RA and RB registers respectively.

In principle yes, but it depends upon how much code you need to execute within the interrupt service routine itself.

It's generally considered good practice to keep the ISR routine as short as possible. Sometimes it's better to simply set a boolean flag in the interrupt and execute the interrupt code in the main loop() function:

volatile bool interruptFlag = false;

void loop() {
  if (interruptFlag == true)
  {
    interruptFlag = false;   // Clear the interrupt flag
    // Execute interrupt code here...
  }
}

void TC6_Handler()                                         // ISR TC6 (TC2 Channel 0)
{
  if (TC2->TC_CHANNEL[0].TC_SR & TC_SR_CPCS)               // Check for RC compare condition
  {
    interruptFlag = true;   // Set the interrupt flag
  }
}

The issue here is that at 4kHz the timer's period is only 0.25ms.

There are many ways to achieve this, depending on your specific requirements. You could count the number of 0.25ms timer periods within the ISR and execute whenever the following condition is met:

0.25ms * count >= delay_ms

Or alternatively, you could kick-off a second TC timer with its period set to the required delay, but this starts to add increasing layers of complexity.

1 Like

@MartinL So I took your code and made a few modifications so as to synchronize the ADC (Channel 7 = A0) with this 4 kHz clock. I want to read an input using the ADC, and write that result into a PWM by simply doing an analogWrite(pin, result). Is there anything wrong in this code?
I see that the waveforms of the 4 kHz and the LED toggling at 2 kHz also change (in 2 steps) when I change the pot value. What am I doing wrong?

// Output 50% duty-cycle PWM at 4kHz on digital pins D4 and D5 (check on DUE pinout to find pin) using timer TC6
int result;
void setup() 
{ pinMode(2, OUTPUT); 
  pinMode(3, OUTPUT);
  adc_setup();
  PMC->PMC_PCER1 |= PMC_PCER1_PID33;                      // Enable peripheral TC6 (TC2 Channel 0)
  PIOC->PIO_ABSR |= PIO_ABSR_P26 | PIO_ABSR_P25;          // Switch the multiplexer to peripheral B for TIOA6 and TIOB6
  PIOC->PIO_PDR |= PIO_PDR_P26 | PIO_PDR_P25;             // Disable the GPIO on the corresponding pins

  TC2->TC_CHANNEL[0].TC_CMR = TC_CMR_BCPC_SET |           // Set TIOB on counter match with RC0
                              TC_CMR_ACPC_SET |           // Set TIOA on counter match with RC0
                              TC_CMR_BCPB_CLEAR |         // Clear TIOB on counter match with RB0
                              TC_CMR_ACPA_CLEAR |         // Clear TIOA on counter match with RA0
                              TC_CMR_WAVE |               // Enable wave mode
                              TC_CMR_WAVSEL_UP_RC |       // Count up with automatic trigger on RC compare
                              TC_CMR_EEVT_XC0 |           // Set event selection to XC0 to make TIOB an output
                              TC_CMR_TCCLKS_TIMER_CLOCK1; // Set the timer clock to TCLK1 (MCK/2 = 84MHz/2 = 42MHz)

  TC2->TC_CHANNEL[0].TC_RA = 5250;                        // Load the RA0 register
  TC2->TC_CHANNEL[0].TC_RB = 5250;                        // Load the RB0 register
  TC2->TC_CHANNEL[0].TC_RC = 10500;                       // Load the RC0 register
  //RA and RB can be loaded with a value between 0 and RC value for different duty cycle 0 to 100%. TC6 timer frequency is determined 
  //by PWM_FREQUENCY = TIMER_CLOCK / RC. 

  NVIC_SetPriority(TC6_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TC6 to 0 (highest) 
  NVIC_EnableIRQ(TC6_IRQn);         // Connect TC6 to Nested Vector Interrupt Controller (NVIC)

  TC2->TC_CHANNEL[0].TC_IER |= TC_IER_CPCS;                // Enable interrupts for TC6 (TC2 Channel 0)
  TC2->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Enable the timer TC6
}

void adc_setup() {

  PMC->PMC_PCER1 |= PMC_PCER1_PID37;                    // ADC power on
  ADC->ADC_CR = ADC_CR_SWRST;                           // Reset ADC
  ADC->ADC_MR =  ADC_MR_TRGEN_EN                       // Hardware trigger select
                  | ADC_MR_TRGSEL_ADC_TRIG3            // Trigger by TIOA output of Timer Counter Ch2
                  | ADC_MR_PRESCAL(10);   //ADC clock = MCK/4 
                  
  ADC->ADC_IER = ADC_IER_EOC7;  //Interrupt Enable Register End Of Conversion interrupt enable for channel 7
  NVIC_EnableIRQ(ADC_IRQn);     // Enable ADC interrupt
  ADC->ADC_CHER = ADC_CHER_CH7; // Channel Enable Register, Enable Channel 7 = A0

}

void ADC_Handler() {
  static uint32_t Count;
  ADC->ADC_CDR[7];  // Read and clear status register
  if ((adc_get_status(ADC) & ADC_ISR_DRDY) == ADC_ISR_DRDY)
       {
       //Get latest digital data value from ADC and can be used by application
           result = adc_get_latest_value(ADC); 
       }
       analogWrite(3,result);  //Writing a PWM output to pin 3
}


void TC6_Handler()                                         // ISR TC6 (TC2 Channel 0)
{
  if (TC2->TC_CHANNEL[0].TC_SR & TC_SR_CPCS)               // Check for RC compare condition
  {
    ////************Interrupt Task 1 *************************
    //Next 3 Lines Toggles an LED connected to digital Pin D2
         static uint8_t state = LOW;  // Define an output state variable
         digitalWrite(2, state);      // Output the current state
         state = !state;  
         
    /////***********************************///////////////////
    // Next Lines of code allows ADC to read potentiometer input and see the time it takes to do the same
    ADC_Handler();
  }
}
void loop() {}

Hi @smanna

I see that you're internally synchronising the ADC to a TC timer by setting the trigger select (TRGSEL) in the ADC's Mode Register (ADC_MR).

Looking at your code, the TC6_Handler() function shouldn't be strictly necessary, since the TC output should automatically trigger the ADC, to start a conversion. The ADC_Handler() function being triggered by the ADC each time the results are ready. The ADC_Handler() ISR will also be called at 4kHz, albeit slightly delayed with respect to the TC timer, due to the ADC's conversion time.

Note that since the ADC_Handler() function is triggered by the ADC, it isn't necessary to call or run it yourself in your sketch.

Also, Arduino have already set up the Due's ADC in preparation for using the analogRead() function, prior to running your sketch. Rather than calling a software reset of the ADC, which will wipe all of its registers clean, it might be better to instead use this to your advantage, by checking what they've set up and just tweak the register bitfields that your require.

This can be achieved by running a small test sketch to print out the ADC's registers. For example to check contents of the ADC's mode register in binary:

Serial.println(ADC->ADC_MR, BIN);

or (if you prefer your registers displayed in nibbles rather than bits), in hexadecimal:

Serial.println(ADC->ADC_MR, HEX);

I also noticed that the Due's ADC's resolution can be either 10-bits (0 to 1023), or 12-bits (0 to 4095). Either way this requires the use of the Arduino "map" function before passing it to the analogWrite(), whose output PWM resolution can only be it 8-bit (0 to 255).

1 Like

Hi @smanna

I had a go at getting this to work on my Arduino Due. The SAM3X8E datasheet rather glibly states that the ADC triggers off the TC timers TIOA output on channels 0 to 2.

What it doesn't say however, is that this only works for TC timer TC0's channels 0 to 2. Therefore it's necessary to switch over to TC0, in order use it to trigger the ADC.

Here's the code:

  • Analog input on A0
  • TC0 channel 0 (TIOA0) output at 4kHz is on D2
  • ADC ISR output toggle signal moved over to D3
  • PWM output on D7
// Trigger the ADC from timer TC0 at 4kHz sample rate and output results as PWM duty-cycle
void setup() {
  pinMode(3, OUTPUT);                                     // Set digital pin D3 to an OUTPUT
  
  PMC->PMC_PCER0 |= PMC_PCER0_PID27;                      // Enable peripheral TC0 (TC0 Channel 0)
  PIOB->PIO_ABSR |= PIO_ABSR_P25;                         // Switch the multiplexer to peripheral B for TIOA0
  PIOB->PIO_PDR |= PIO_PDR_P25;                           // Disable the GPIO on the corresponding pins

  TC0->TC_CHANNEL[0].TC_CMR = TC_CMR_BCPC_SET |           // Set TIOB on counter match with RC0
                              TC_CMR_ACPC_SET |           // Set TIOA on counter match with RC0
                              TC_CMR_BCPB_CLEAR |         // Clear TIOB on counter match with RB0
                              TC_CMR_ACPA_CLEAR |         // Clear TIOA on counter match with RA0
                              TC_CMR_WAVE |               // Enable wave mode
                              TC_CMR_WAVSEL_UP_RC |       // Count up with automatic trigger on RC compare
                              TC_CMR_EEVT_XC0 |           // Set event selection to XC0 to make TIOB an output
                              TC_CMR_TCCLKS_TIMER_CLOCK1; // Set the timer clock to TCLK1 (MCK/2 = 84MHz/2 = 42MHz)

  TC0->TC_CHANNEL[0].TC_RA = 5250;                        // Load the RA0 register
  TC0->TC_CHANNEL[0].TC_RB = 5250;                        // Load the RB0 register
  TC0->TC_CHANNEL[0].TC_RC = 10500;                       // Load the RC0 register

  NVIC_SetPriority(TC0_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for TC0 to 0 (highest) 
  NVIC_EnableIRQ(TC0_IRQn);         // Connect TC0 to Nested Vector Interrupt Controller (NVIC)

  TC0->TC_CHANNEL[0].TC_IER = TC_IER_CPCS;                // Enable interrupts for TC0 (TC0 Channel 0)
  TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_SWTRG | TC_CCR_CLKEN; // Enable the timer TC0

  NVIC_SetPriority(ADC_IRQn, 0);                          // Set the Nested Vector Interrupt Controller (NVIC) priority for the PWM controller to 0 (highest) 
  NVIC_EnableIRQ(ADC_IRQn);                               // Connect ADC Controller to Nested Vector Interrupt Controller (NVIC)
  ADC->ADC_IER = ADC_IER_EOC7;                            // Enable interrupts on channel 7 on A0
  ADC->ADC_CHER = ADC_CHER_CH7;                           // Enable ADC channel 7 on A0
  ADC->ADC_MR |= ADC_MR_TRGEN_EN |                        // Enable TC0 channel 0 timer as the start trigger for the ADC
                 ADC_MR_TRGSEL_ADC_TRIG1;                                
}

void loop() {}

void TC0_Handler()                                        // ISR TC0 (TC0 Channel 0)
{
  if (TC0->TC_CHANNEL[0].TC_SR & TC_SR_CPCS)              // Check for RC compare condition
  {
    // Add you code here...
  }
}

void ADC_Handler() {
  if (ADC->ADC_ISR & ADC_ISR_EOC7)                        // If the data is ready
  {
    uint16_t result = ADC->ADC_CDR[7];                    // Read the result from the ADC
    result = map(result, 0, 4095, 0, 255);                // Map from 12-bits to 8-bits
    analogWrite(7, result);                               // Output the result as PWM

    static uint8_t state = HIGH;                          // Define an output state variable
    digitalWrite(3, state);                               // Output the current state, should toggle output at 2kHz
    state = !state;                                       // Change the next output state
  }
}
1 Like