Measuring pulse width, interrupts

Status
Not open for further replies.

BobQ

Member
I've been testing different ways to measure pulse width using Teensy 3.1 and 48 MHz clock. The signal is a narrow positive going pulse at a 10 msec rate.
First, pulseIn, 1 usec resolution.
Second, polling with digitalRead, about 0.5 usec resolution.
Third, polling with digitalReadFast, about 0.27 usec resolution.
Fourth, interrupt using ARM cycle clock, 0.021 usec resolution, 1/(48 MHz).

I noticed the interrupt-based code below doesn't work correctly for pulses less than about 2.4 usec wide. Does that sound right for interrupt response time?

I just read about flextimer but I didn't understand the details. Would it work for narrower pulses?

I may also just use digitalReadFast and 72 MHz.

Thanks.

Code:
/* tests for Pulsein vs counting loop to test resolution
  16 Sept 2016
  Teensy 3.1, Arduino 1.6.9 and Teensyduino 1.29
  tests variation in readings using ARM cycle clock
*/
// global variables
const int testPin = 7;  // 100 Hz input from pulse gen
unsigned long longCounter = 0; // counts passes thru loop
unsigned long pw = 0;
volatile unsigned long timeStamp = 0; //system clock captured in interrupt
unsigned long riseIntr = 0; //system clock at rising edge
int testStat; // value of testPin

void setup() {
  // From Teensy forum
  ARM_DEMCR |= ARM_DEMCR_TRCENA;  // debug exception monitor control register; enables trace and debug
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA; // enable cycle counter
  Serial.begin(115200); // PC serial monitor via USB for debugging
  pinMode(testPin, INPUT);
  longCounter = -1; // I first noticed an occasional bad first read when testing digitalReadFast; skip 1st reading
  delay(20000); // delay to start serial monitor
  Serial.println("Test usng ARM cycle counter");
} // end of setup------------------------------------------------

void loop() {
  longCounter++;
  delay(50);
  do { // wait for input to go low
    testStat = digitalReadFast(testPin);
  } while (testStat == 1);
  attachInterrupt(testPin, phase_ISR, CHANGE); // turn on interrupt just after input goes low
  timeStamp = 0; // use as a flag too
  while (timeStamp == 0) { // wait for input to go high signalled by timeStamp
  }
  riseIntr = timeStamp;  // system time at rising edge
  timeStamp = 0;
  while (timeStamp == 0) { // wait for the next edge, will be falling edge
  }
  detachInterrupt(testPin); // off so other msmnts not affected
  pw = timeStamp - riseIntr;

  Serial.print(longCounter);
  Serial.print("       ");
  Serial.println(pw);

} // end of loop

//-----------------------
void phase_ISR() {
  timeStamp = ARM_DWT_CYCCNT;
}
 
You can do much better by using an ISR for an entire IO port, instead of the slow default ISR that dispatches to all the different pins. This hacked version of your code can handle 0.52us width pulses, CPU running at 96MHz.

FASTRUN places the ISR in RAM - that's faster than running from flash.

(Pin 7 is on port D, not on port C like pin 12.)

Code:
/* tests for Pulsein vs counting loop to test resolution
  16 Sept 2016
  Teensy 3.1, Arduino 1.6.9 and Teensyduino 1.29
  tests variation in readings using ARM cycle clock
*/
// global variables
//const int testPin = 7;  // 100 Hz input from pulse gen
const int testPin = 12;  // 100 Hz input from pulse gen

unsigned long longCounter = 0; // counts passes thru loop
unsigned long pw = 0;
volatile uint32_t timeStamp = 0; //system clock captured in interrupt
uint32_t riseIntr = 0; //system clock at rising edge
int testStat; // value of testPin

void setup() {
  Serial.begin(115200); // PC serial monitor via USB for debugging
  pinMode(testPin, INPUT);
  longCounter = -1; // I first noticed an occasional bad first read when testing digitalReadFast; skip 1st reading
  delay(2000); // delay to start serial monitor

  pinMode(22, OUTPUT);
  analogWriteFrequency(22, 187500);
  analogWrite(22, 25);
  
  Serial.println("Test usng ARM cycle counter");
  attachInterrupt(testPin, nullptr, CHANGE); // just using this to setup the CHANGE interrupt trigger
  attachInterruptVector(IRQ_PORTC, port_ISR); // this is the ISR that will be run
  ARM_DEMCR |= ARM_DEMCR_TRCENA;  // debug exception monitor control register; enables trace and debug
  ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA; // enable cycle counter
} // end of setup------------------------------------------------


void loop() {
  longCounter++;
  delay(50);
  do { // wait for input to go low
    testStat = digitalReadFast(testPin);
  } while (testStat == 1);
  timeStamp = 0; // use as a flag too
  while (timeStamp == 0) { // wait for input to go high signalled by timeStamp
  }
  riseIntr = timeStamp;  // system time at rising edge
  while (timeStamp == riseIntr) { // wait for the next edge, will be falling edge
  }
  uint32_t end = timeStamp;
  pw = end - riseIntr;

  Serial.print(longCounter);
  Serial.print("       ");
  Serial.println(pw);

} // end of loop

FASTRUN void port_ISR() {
  uint32_t isfr = PORTC_ISFR;
  PORTC_ISFR = isfr;
  timeStamp = ARM_DWT_CYCCNT;
}

(Pin 22 is used as signal generator, to generate a narrow PWM pulse.)
 
Last edited:
Thanks for the info on FASTRUN and using an ISR for a full port. Anything under 1.5 usec is great for me. I'll check it out and test it in the real program when I get back in town. Sounds like I need to check into which pins go to which ports and maybe move my input pin.
 
You might also wish to increase the interrupt priority, so it will be immediately serviced if the pin changes while another lower priority interrupt is running.
 
Hello folks


Does anyone know if it would be possible to measure these pulse-widths using a hardware timer instead of interrupts?

So we could just read a register that would have the duty cycle or something related to it calculated outside the execution firmware?

I've looked briefly at the input compare sections of the flex timer but didnt see exactly what I was hoping for. Wondering if anyone else has tried this?

Cheers
Kyle
 
Does anyone know if it would be possible to measure these pulse-widths using a hardware timer instead of interrupts?

Yes, it's possible, as long as there's at least a few microseconds between each edge.

So we could just read a register that would have the duty cycle or something related to it calculated outside the execution firmware?

Oh, it's *far* more complicated than that.

I've looked briefly at the input compare sections of the flex timer but didnt see exactly what I was hoping for. Wondering if anyone else has tried this?

You want "capture". The "compare" modes are for signal output. Use capture for signal input.

FreqMeasure and PulsePosition are the libraries which use capture mode. Maybe there's some useful ideas in there for you?
 
Yes, it's possible, as long as there's at least a few microseconds between each edge.



Oh, it's *far* more complicated than that.



You want "capture". The "compare" modes are for signal output. Use capture for signal input.

FreqMeasure and PulsePosition are the libraries which use capture mode. Maybe there's some useful ideas in there for you?



Thanks Paul.

Indeed the capture/compare typo was a bad one. Sorry about that.

Excellent work as always. Nice use of the friend function too for the ISR - very slick. Yet another example of your coding genius that will make its way into our products.

I was hoping to essentially record the rising and falling time-stamps using the input compare -capture (done in the above appnote section 3.9), and then have the difference of those two registers automatically calculated so all the execution firmware needed to do was read the resulting register, eliminating a high-frequency isr from firing.

Doesnt look like we can get away from an ISR firing, which is ok; it will just perform a subtraction, save the result and get out. Shouldn't take long.

Essentially trying to keep the loop times down for really crisp position, speed and acceleration control. We're looking at PWM-output position sensors instead of analog potentiometers to try and clean up the noise in speed and accel and was hoping we could do this with as low overhead as a quadrature encoder (using the K20's hardware quadrature decoder function).
 
If you want to get into even more complex design, it's possible to have the capture events trigger a DMA channel, which can read the capture register into a buffer. It will not do the subtraction, but you can do that when you later read the data from the buffer. That would eliminate the interrupts for each edge of the signal. Instead you'd get one interrupt when the DMA has filled of half-filled the buffer with captures.

The really tricky part is figuring out which numbers were rising edges and which were falling edges, if you set it up to capture both. One idea might involve using 2 separate pins to the same timer with the same signal connected, and 2 DMA channels with 2 buffers. Then you have a buffer of rising edges and a buffer of falling edge captures. Or maybe having them interleaved in the same buffer can work, if you can find a way to always start at a known place in the waveform, or if the waveform has known duty cycle or other properties than can let your code sort out which pairs are the ones you subtract.

Of course, setting up the DMA isn't trivial. So far I'm not aware of any timer capture DMA examples. The audio library does have timer compare DMA for PWM output, so that's at least sort-of close.
 
If you want to get into even more complex design, it's possible to have the capture events trigger a DMA channel, which can read the capture register into a buffer. It will not do the subtraction, but you can do that when you later read the data from the buffer. That would eliminate the interrupts for each edge of the signal. Instead you'd get one interrupt when the DMA has filled of half-filled the buffer with captures.

The really tricky part is figuring out which numbers were rising edges and which were falling edges, if you set it up to capture both. One idea might involve using 2 separate pins to the same timer with the same signal connected, and 2 DMA channels with 2 buffers. Then you have a buffer of rising edges and a buffer of falling edge captures. Or maybe having them interleaved in the same buffer can work, if you can find a way to always start at a known place in the waveform, or if the waveform has known duty cycle or other properties than can let your code sort out which pairs are the ones you subtract.

Of course, setting up the DMA isn't trivial.
So far I'm not aware of any timer capture DMA examples. The audio library does have timer compare DMA for PWM output, so that's at least sort-of close.

The DMA setup scares me.

I think we're just looking at one interrupt, after the falling edge is captured.

Rising edge captured -> saves counter value to an even-numbered channel.
Falling edge captured -> saves counter value to an odd-numbered channel and raises interrupt request.
Firmware ISR saves the difference of the two channels and bob's your uncle.

Not general purpose, not super flexible and requires knowledge of the period to get the actual duty, but should be pretty fast.

DMA would be super powerful but I'm afraid of the dev time required and the nightmare that would be troubleshooting.
 
If you want to get into even more complex design, it's possible to have the capture events trigger a DMA channel, which can read the capture register into a buffer. It will not do the subtraction, but you can do that when you later read the data from the buffer. That would eliminate the interrupts for each edge of the signal. Instead you'd get one interrupt when the DMA has filled of half-filled the buffer with captures.

The really tricky part is figuring out which numbers were rising edges and which were falling edges, if you set it up to capture both. One idea might involve using 2 separate pins to the same timer with the same signal connected, and 2 DMA channels with 2 buffers. Then you have a buffer of rising edges and a buffer of falling edge captures. Or maybe having them interleaved in the same buffer can work, if you can find a way to always start at a known place in the waveform, or if the waveform has known duty cycle or other properties than can let your code sort out which pairs are the ones you subtract.
You see problems, where there are none. :)

The FTM timers have a dual edge capture mode. The input pin of the first member of an input channel pair, e.g. channel 0 from the pair [channel 0, channel 1] can be used as input for both. E.g. Teensy pin 3 can be used as input for FTM1 channel 0 and channel 1. Channel 0 can be configured to capture the rising edge; channel 1 can capture the falling edge.

Here is DMA code that captures pin 3
. It uses a DMA minor loop to read the captured value of FTM1 channel 0 and channel 1. Channel 1 (which captures the falling edge) triggers the DMA transfers.

For testing purposes, pin 21 is configured as output (test signal) and connected to pin 3.
 
@tni, i just tried to build your input_capture_dma.ino (ubuntu linux,IDE 1.8.1/1.35) for T3.2, and i get lots of compiler warnings and errors:
Code:
...
sketch_may06a: In function 'void {anonymous}::generateDoublePulse() [with unsigned int high_time = 20u; unsigned int low_time = 80u]':
sketch_may06a:36: error: inlining failed in call to always_inline 'void {anonymous}::generateNops() [with unsigned int N = 20u]': function not inlinable
 template<size_t N> __attribute__((always_inline)) FASTRUN void generateNops() {
                                                                ^
sketch_may06a:50: error: called from here
     generateNops<high_time>();
...

EDIT: seemed to compile if i took the inline attribute off of generateNops()
 
Last edited:
TD 1.35 has an ancient compiler, GCC 4.8. It compiles with TD 1.36 (GCC 5.4).

The inlining isn't that important, it just makes timing for the test code accurate. Otherwise, there can be a difference of a few clock cycles.
 
addendum T3.2, T3.6, usec conversion for fast pulse measurement

You can do much better by using an ISR for an entire IO port, instead of the slow default ISR that dispatches to all the different pins. This hacked version of your code can handle 0.52us width pulses, CPU running at 96MHz.

FASTRUN places the ISR in RAM - that's faster than running from flash.
...

I wanted to measure small frequency changes at relatively high frequencies (50 - 500 kHz). This code from tni easily does the trick. (Thanks.) I've added a few notes and calculated the transforms to display the pulse duration directly in microseconds for the T3.6 and the T3.2. It also works to *very* low frequencies. I confirmed it accurate to 0.05Hz.

Code:
/*  tests for Pulsein vs counting loop to determine resolution
    16 Sep 2016
    Teensy 3.1, Arduino 1.6.9 and Teensyduino 1.29
    tests variation in readings using ARM cycle clock

    8 Oct 2021  David adds:
    Using Arduino 1.8.12, Teensyduino loader 1.51
    Tested on T3.6; resolves better than 0.1us
    and reads down to about 0.5us
    Tested on T3.2; 
    resolves to about 0.2us
    and reads down to about 0.5us 
    Both readily measure frequencies to 1 MHz and below 0.05 Hz
    This code transforms counts to display directly in microseconds
*/
const int testPin = 12;         // freq in from signal generator

unsigned long longCounter = 0;  // counts passes thru loop
unsigned long pw = 0;
volatile uint32_t timeStamp = 0;//system clock captured in interrupt
uint32_t riseIntr = 0;          //system clock at rising edge
int testStat;                   // value of testPin

void setup() {
    Serial.begin(115200);       // Serial USB monitor for debugging
    pinMode(testPin, INPUT);
    longCounter = -1; // I first noticed an occasional bad first read when testing digitalReadFast; skip 1st reading
    delay(2000); // delay to start serial monitor

    pinMode(22, OUTPUT);
    analogWriteFrequency(22, 187500);
    analogWrite(22, 25);

    Serial.println("Test usng ARM cycle counter");
    Serial.println("  counts  t_per  t_rf");
    attachInterrupt(testPin, nullptr, CHANGE); 	// just using this to setup the CHANGE interrupt trigger
    attachInterruptVector(IRQ_PORTC, port_ISR); // this is the ISR that will be run
    ARM_DEMCR |= ARM_DEMCR_TRCENA;  			// debug exception monitor control register; enables trace and debug
    ARM_DWT_CTRL |= ARM_DWT_CTRL_CYCCNTENA; 	// enable cycle counter
} // end of setup------------------------------------------------

// conversion from counts to usec for T3.6 (180MHz)
// t(us) = 0.011*counts - 0.212

float t_per;                    // period (1/freq)
float t_rf ;                    // time of pulse (rise-to-fall edges)

void loop() {
    uint32_t end_t;
    longCounter++;
    delay(50);
    do {                        // wait for input to go low
        testStat = digitalReadFast(testPin);
    } while (testStat == 1);
    timeStamp = 0;              // use as a flag too
    while (timeStamp == 0) {    // wait for input to go high signalled by timeStamp
    }
    riseIntr = timeStamp;       // system time at rising edge
    while (timeStamp == riseIntr) { // wait for the next edge, will be falling edge
    }
    end_t = timeStamp;
    pw = end_t - riseIntr;      // pulse width
 
    // for T3.6 device --------------------
    // t_rf = 0.0553 * (float)pw - 0.1060;   
    // for T3.2 device (slower clock) -----
    t_rf = 0.0103 * (float)pw - 0.0286;  
      
    t_per = t_rf * 2.0;         // 2x for exact square wave input
    Serial.printf("  counts:%5u   t[per]:%5.2f   t[rf]:%5.2f\n", pw, t_per, t_rf);

} // end of loop

// interrupt service routine to read pulses
FASTRUN void port_ISR() {
    uint32_t isfr = PORTC_ISFR;
    PORTC_ISFR = isfr;
    timeStamp = ARM_DWT_CYCCNT;
}
 
Status
Not open for further replies.
Back
Top