[SOLVED] Non-blocking PWM duty cycle capture using Timer Counter

May 25, 2013 - See my later post for working code

I am attempting to use one of the Due's timer/counters to measure the duty cycle of a PWM signal from an RC receiver. Using the TC's external trigger I want the counter to reset at every rising edge (start of duty cycle) and then record the timer value at the falling edge (end of duty cycle) to one of the TC's registers. What I was expecting to see as output was the number of counts that the signal was high, however all I get is 0.

I have also attached an interrupt such that every time the register is loaded by the TC (i.e., every falling edge) it is read and a variable that counts the number of pulses is incremented. The number of pulses increases as expected. See the code and example output below.

Does anyone know why the counter doesn't appear to be actually counting?

RX_example.ino

#define TC_WPMR_WPKEY_VALUE TC_WPMR_WPKEY((uint32_t)0x54494D)

volatile uint32_t duty_value = 0;
volatile uint32_t pulses_captured = 0;

long start_time, previous_time;

// timer interrupt handle
void TC0_Handler() {
  TC_GetStatus(TC0, 0);
  
  pulses_captured++;
  duty_value = TC0->TC_CHANNEL[0].TC_RA;
  
}



void setup() {
  Serial.begin(57600);
  Serial.print("Initializing...");
  
  // enable timer clock
  pmc_enable_periph_clk(ID_TC0);
  
  // configure the PIO pin as peripheral
  PIO_Configure(
    g_APinDescription[2].pPort,
    g_APinDescription[2].ulPinType,
    g_APinDescription[2].ulPin,
    g_APinDescription[2].ulPinConfiguration);
  
  // disable TC register write protection
  TC0->TC_WPMR = TC_WPMR_WPKEY_VALUE;
  
  // configure the timer
  TC_Configure(TC0, 0,
    TC_CMR_TCCLKS_TIMER_CLOCK4
    | TC_CMR_ETRGEDG_RISING
    | TC_CMR_ABETRG
    | TC_CMR_LDRA_FALLING
  );

  // start counter
  TC0->TC_CHANNEL[0].TC_CCR = TC_CCR_CLKEN | TC_CCR_SWTRG;
  
  // enable interrupts on loading of Register A
  TC0->TC_CHANNEL[0].TC_IER=TC_IER_LDRAS;
  TC0->TC_CHANNEL[0].TC_IDR=~TC_IER_LDRAS;
  
  // enable the NVIC (Nested Vector Interrupt Controller)
  NVIC_EnableIRQ(TC0_IRQn);
  
  Serial.println("ready!");
  
  previous_time = micros();
}


void loop() {
  start_time = micros();
  
  if(start_time - previous_time > 50000) {
    Serial.print(pulses_captured); Serial.print(",");
    Serial.println(duty_value);
    
    previous_time = start_time;
  }
}

output.txt

Initializing...ready!
5,0
11,0
17,0
23,0
27,0
33,0
39,0
45,0
50,0
55,0
61,0
67,0
73,0
77,0
83,0
89,0
96,0
102,0
107,0
113,0
119,0
125,0
129,0
136,0
143,0
150,0
155,0
...

Hi I have the same problem did you find a solution ?

I haven't figured it out yet, but I'm still working on it. There is example code included in the Atmel Software Framework (tc_capture_waveform_example.c) that should do exactly what I'm attempting to do but it seems that a lot of the driver functions they use aren't included in the Arduino IDE.

I will post some code as soon as I get this sorted out.

Shouldn't you be reading the counter value register rather than the compare register? TC_CVx

Registers TC_RA and TC_RB are actually capture registers, at least when the TC is put into capture mode. Setting TC_CMR_LDRA and TC_CMR_LDRB dictate when the registers capture the counter value based on the TIOA signal behaviour. In my original example I use TC_CMR_LDRA_FALLING, so every time the TIOA signal falls the current counter value is stored in register TC_RA. This way you never need to actually read the counter value directly.

I seem to have a working sketch now. It incorporates some code from the Atmel Software Framework, mainly the direct register manipulation.

The basic working principle is that a timer counter (TC) is put into capture mode. The TC is told to load capture register A (TC_RA) on the rising edge of a PWM signal and capture register B (TC_RB) on the falling edge. The number of counts that the signal is active for is equal to TC_RB - TC_RA. An interrupt routine is run every time TC_RB is loaded and saves the register values to variables. The clock is also reset on the falling edge. The PWM frequency, duty cycle and time active can be calculated because the TC clock frequency, and therefore the duration of each count, is known.

I have tested this using all 5 available TIOAx channels individually and they all seem to work. I have not yet tested multiple channels running at the same time. The sketch also has some issues when the PWM signal is noisy, occasionally giving very incorrect values.

TL;DR The timer counter calculates PWM frequency and duty cycle while using almost no CPU time (i.e., it is non-blocking)

/* 
 * PWM Capture using Arduino Due Timer Counters
 * 
 * Available channels:
 *   TC    Chan   NVIC irq   Handler       PMC ID   Arduino Pin
 *   TC0   0      TC0_IRQn   TC0_Handler   ID_TC0   D2     (TIOA0)
 *   TC0   1      TC1_IRQn   TC1_Handler   ID_TC1   D61/A7 (TIOA1)
 *   TC2   0      TC6_IRQn   TC6_Handler   ID_TC6   D5     (TIOA6)
 *   TC2   1      TC7_IRQn   TC7_Handler   ID_TC7   D3     (TIOA7)
 *   TC2   2      TC8_IRQn   TC8_Handler   ID_TC8   D11    (TIOA8)
 * 
 * Change the following defines to use different channels as input.
 * 
 */

#define CAPTURE_TC TC0
#define CAPTURE_CHANNEL 0
#define CAPTURE_IRQn TC0_IRQn
#define CAPTURE_Handler TC0_Handler
#define CAPTURE_ID ID_TC0
#define CAPTURE_PIN 2
#define CAPTURE_CLOCK_SELECTION TC_CMR_TCCLKS_TIMER_CLOCK3

// clock divisors corresponding to CAPTURE_CLOCK_SELECTION
static const uint32_t divisors[5] = { 2, 8, 32, 128, 0};


volatile uint32_t captured_pulses = 0;
volatile uint32_t captured_ra = 0;
volatile uint32_t captured_rb = 0;
uint32_t frequency, duty_cycle, active_time;


// timer interrupt handle
void CAPTURE_Handler() {
  if ((TC_GetStatus(CAPTURE_TC, CAPTURE_CHANNEL) & TC_SR_LDRBS) == TC_SR_LDRBS) {
    captured_pulses++;
    captured_ra = CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_RA;
    captured_rb = CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_RB;
  }
}


void setup() {
  Serial.begin(57600);
  Serial.print("Initializing...");
  
  // configure the PIO pin as peripheral
  const PinDescription *config = &g_APinDescription[CAPTURE_PIN];
	PIO_Configure(
	  config->pPort,
	  config->ulPinType,
	  config->ulPin,
	  config->ulPinConfiguration
	);
    
  // enable timer peripheral clock
  pmc_enable_periph_clk(CAPTURE_ID);
  
  // configure the timer
  TC_Configure(CAPTURE_TC, CAPTURE_CHANNEL,
    CAPTURE_CLOCK_SELECTION /* Clock Selection */
    | TC_CMR_LDRA_RISING /* RA Loading: rising edge of TIOA */
    | TC_CMR_LDRB_FALLING /* RB Loading: falling edge of TIOA */
    | TC_CMR_ABETRG /* External Trigger: TIOA */
    | TC_CMR_ETRGEDG_FALLING /* External Trigger Edge: Falling edge */
  );
  
  // configure TC interrupts
  NVIC_DisableIRQ(CAPTURE_IRQn);
  NVIC_ClearPendingIRQ(CAPTURE_IRQn);
  NVIC_SetPriority(CAPTURE_IRQn, 0);
  NVIC_EnableIRQ(CAPTURE_IRQn);
  
  // enable interrupts
  CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_IER = TC_IER_LDRBS;
  
  // start timer counter
  CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_CCR = TC_CCR_CLKEN | TC_CCR_SWTRG;
  
  Serial.println("ready!");
  
}


void loop() {
  // measurement is interrupt based so delay doesn't really block it
  delay(1000);
  
  Serial.print("Captured "); Serial.print(captured_pulses);
  Serial.println(" pulses from TC since last read");
  captured_pulses = 0; 
  
  // frequency in Hz
  frequency
    = (F_CPU / divisors[CAPTURE_CLOCK_SELECTION]) / captured_rb;
  
  // duty cycle in percent
  duty_cycle
    = (captured_rb - captured_ra) * 100 / captured_rb;
  
  // time active in microseconds
  active_time
    = ((captured_rb - captured_ra) * 1000) / 
      ((F_CPU / divisors[CAPTURE_CLOCK_SELECTION]) / 1000);
  
  Serial.print("Captured wave frequency = "); Serial.print(frequency);
  Serial.print(" Hz, Duty cycle = "); Serial.print(duty_cycle);
  Serial.print(" %, Pulse width = "); Serial.print(active_time);
  Serial.println(" us");
  
}

output:

Initializing...ready!
Captured 52 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1292 us
Captured 53 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1292 us
Captured 53 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1290 us
Captured 52 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1292 us

@drm0hr: Just wanted to say thank you for posting your code, this is exactly what I needed to get timing information out the Due -- I was having a tough time wrapping my head around the TC on the Due, even with the datasheet. :~

I'm trying to modify working code (on the Uno and Duemilanove, at least) that decodes a PPM signal from the Trainer Port of my Spektrum DX6i. And, I'm not alone in trying to decode PPM signals read ppm unknown voltage - Arduino Due - Arduino Forum

@NielsJL: You are very welcome, I'm glad you found it useful.

I don't think it would be too hard to modify the code to capture a PPM signal instead of just PWM. Maybe if you used an array to store the duration of all 6? 8? pulses, loading new values on every falling edge. The only challenge would be to know which pulse corresponds to which channel and detecting the end of each PPM frame.

Hi drmOhr,

First, thank you so much for writing this code, it does exactly what I needed it to.

I'm wondering though if you ever tested it with multiple inputs at the same time? I tried setting up two inputs on D2 and D3, but can't seem to get it to work.

The issue I'm having seems to be where the PIO pin is configured as a peripheral. I tried just changing variable names to duplicate your code and have two running at the same time, but obviously there's more to it than that. Unfortunately, I'm not at all familiar with utilizing the datasheets to directly manipulate registers on the MCU.

Here's the part that is giving me trouble -

const PinDescription2 *config = &g_APinDescription2[CAPTURE2_PIN];
	PIO_Configure(
	  config->pPort,
	  config->ulPinType,
	  config->ulPin,
	  config->ulPinConfiguration
	);

And here's the error I'm getting out -

Arduino: 1.5.4 (Windows 7), Board: "Arduino Due (Programming Port)"

Due_Datalogger_DualFreqInput2.ino: In function 'void setup()':
Due_Datalogger_DualFreqInput2:152: error: expected initializer before '*' token

  This report would have more information with
  "Show verbose output during compilation"
  enabled in File > Preferences.

It looks to me like it's getting hung up on the "PinDescription2" title that I've given it, but as far as I can tell, that should just be a variable name, so I'm not sure why it cares. The rest of the code is identical to what you have posted here, I've just got two versions, one being titled "CAPTURE_....." and the other being "CAPTURE2_......". Renaming everything to "CAPTURE2_....." and running a single version of the code works fine, so I know it's not an issue with the naming of the capture variables.

Any help would be greatly appreciated. Thanks!

-Tyler

drm0hr:
Registers TC_RA and TC_RB are actually capture registers, at least when the TC is put into capture mode. Setting TC_CMR_LDRA and TC_CMR_LDRB dictate when the registers capture the counter value based on the TIOA signal behaviour. In my original example I use TC_CMR_LDRA_FALLING, so every time the TIOA signal falls the current counter value is stored in register TC_RA. This way you never need to actually read the counter value directly.

I seem to have a working sketch now. It incorporates some code from the Atmel Software Framework, mainly the direct register manipulation.

The basic working principle is that a timer counter (TC) is put into capture mode. The TC is told to load capture register A (TC_RA) on the rising edge of a PWM signal and capture register B (TC_RB) on the falling edge. The number of counts that the signal is active for is equal to TC_RB - TC_RA. An interrupt routine is run every time TC_RB is loaded and saves the register values to variables. The clock is also reset on the falling edge. The PWM frequency, duty cycle and time active can be calculated because the TC clock frequency, and therefore the duration of each count, is known.

I have tested this using all 5 available TIOAx channels individually and they all seem to work. I have not yet tested multiple channels running at the same time. The sketch also has some issues when the PWM signal is noisy, occasionally giving very incorrect values.

TL;DR The timer counter calculates PWM frequency and duty cycle while using almost no CPU time (i.e., it is non-blocking)

/* 
  • PWM Capture using Arduino Due Timer Counters
  • Available channels:
    *  TC    Chan  NVIC irq  Handler      PMC ID  Arduino Pin
    *  TC0  0      TC0_IRQn  TC0_Handler  ID_TC0  D2    (TIOA0)
    *  TC0  1      TC1_IRQn  TC1_Handler  ID_TC1  D61/A7 (TIOA1)
    *  TC2  0      TC6_IRQn  TC6_Handler  ID_TC6  D5    (TIOA6)
    *  TC2  1      TC7_IRQn  TC7_Handler  ID_TC7  D3    (TIOA7)
    *  TC2  2      TC8_IRQn  TC8_Handler  ID_TC8  D11    (TIOA8)
  • Change the following defines to use different channels as input.

*/

#define CAPTURE_TC TC0
#define CAPTURE_CHANNEL 0
#define CAPTURE_IRQn TC0_IRQn
#define CAPTURE_Handler TC0_Handler
#define CAPTURE_ID ID_TC0
#define CAPTURE_PIN 2
#define CAPTURE_CLOCK_SELECTION TC_CMR_TCCLKS_TIMER_CLOCK3

// clock divisors corresponding to CAPTURE_CLOCK_SELECTION
static const uint32_t divisors[5] = { 2, 8, 32, 128, 0};

volatile uint32_t captured_pulses = 0;
volatile uint32_t captured_ra = 0;
volatile uint32_t captured_rb = 0;
uint32_t frequency, duty_cycle, active_time;

// timer interrupt handle
void CAPTURE_Handler() {
  if ((TC_GetStatus(CAPTURE_TC, CAPTURE_CHANNEL) & TC_SR_LDRBS) == TC_SR_LDRBS) {
    captured_pulses++;
    captured_ra = CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_RA;
    captured_rb = CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_RB;
  }
}

void setup() {
  Serial.begin(57600);
  Serial.print("Initializing...");
 
  // configure the PIO pin as peripheral
  const PinDescription config = &g_APinDescription[CAPTURE_PIN];
PIO_Configure(
  config->pPort,
  config->ulPinType,
  config->ulPin,
  config->ulPinConfiguration
);
   
  // enable timer peripheral clock
  pmc_enable_periph_clk(CAPTURE_ID);
 
  // configure the timer
  TC_Configure(CAPTURE_TC, CAPTURE_CHANNEL,
    CAPTURE_CLOCK_SELECTION /
Clock Selection /
    | TC_CMR_LDRA_RISING /
RA Loading: rising edge of TIOA /
    | TC_CMR_LDRB_FALLING /
RB Loading: falling edge of TIOA /
    | TC_CMR_ABETRG /
External Trigger: TIOA /
    | TC_CMR_ETRGEDG_FALLING /
External Trigger Edge: Falling edge */
  );
 
  // configure TC interrupts
  NVIC_DisableIRQ(CAPTURE_IRQn);
  NVIC_ClearPendingIRQ(CAPTURE_IRQn);
  NVIC_SetPriority(CAPTURE_IRQn, 0);
  NVIC_EnableIRQ(CAPTURE_IRQn);
 
  // enable interrupts
  CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_IER = TC_IER_LDRBS;
 
  // start timer counter
  CAPTURE_TC->TC_CHANNEL[CAPTURE_CHANNEL].TC_CCR = TC_CCR_CLKEN | TC_CCR_SWTRG;
 
  Serial.println("ready!");
 
}

void loop() {
  // measurement is interrupt based so delay doesn't really block it
  delay(1000);
 
  Serial.print("Captured "); Serial.print(captured_pulses);
  Serial.println(" pulses from TC since last read");
  captured_pulses = 0;
 
  // frequency in Hz
  frequency
    = (F_CPU / divisors[CAPTURE_CLOCK_SELECTION]) / captured_rb;
 
  // duty cycle in percent
  duty_cycle
    = (captured_rb - captured_ra) * 100 / captured_rb;
 
  // time active in microseconds
  active_time
    = ((captured_rb - captured_ra) * 1000) /
      ((F_CPU / divisors[CAPTURE_CLOCK_SELECTION]) / 1000);
 
  Serial.print("Captured wave frequency = "); Serial.print(frequency);
  Serial.print(" Hz, Duty cycle = "); Serial.print(duty_cycle);
  Serial.print(" %, Pulse width = "); Serial.print(active_time);
  Serial.println(" us");
 
}




output:


Initializing...ready!
Captured 52 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1292 us
Captured 53 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1292 us
Captured 53 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1290 us
Captured 52 pulses from TC since last read
Captured wave frequency = 52 Hz, Duty cycle = 6 %, Pulse width = 1292 us

when i compile this code got an error

'TC0' was not declared in this scope

what could be the reason

No problem at compile time with this code.

Which board did you select in Menu>Tools> Board ?

i have selected arduino/genunino board in Menu>Tools> .The main application is to detect two freq. signals one is signal between 10Khz to 15Khz and if it is above or below the predefined range a LED should start blinking.other one is PWM signal and i need to check its duty cycle ranging from 5% to 100%

vip1:
i have selected arduino/genunino board in Menu>Tools> .

Wrong selection !

the code is for Due, is there any way to run it on uno

thnx

vip1:
is there any way to run it on uno

No