Howto use timer capture mode? Teensy 3.5

Status
Not open for further replies.

cebersp

Well-known member
Hi,
According to the data sheet it must be possible to create the following:
Set up a timer to capture mode at rising and falling edges of an input. (Goal is to measure pulse length.) And to have an interrupt routine to calculate and store the last pulse length. Is there some code as a template, where I could find some information? While I am quite sure, that this must be possible, I am a bit overwhelmed by all the possibilities of the timers....
I only need about 30ms of resolution, but an edge must not be totally missed.
Thanks for some hints
 
The timers are indeed very powerful but complex.

Before jumping into the details, you might consider whether a much simpler approach with attachInterrupt() and micros() will meet your needs. The simplest way gives about 2 to 3 us accuracy, and it's usually not hard to get to 1 us accuracy by just raising one of the port change interrupt priorities. Since you said "only need about 30ms of resolution", this way might be worthwhile to try first. Just use attachInterrupt() to run a function when the pin changes, and in that function record the micros() time to a variable, and set a flag. Like with all interrupts, the variables need to be "volatile" and care is needed when reading them from the main program.

However, that approach is sensitive to interrupt latency. If other code temporarily disable interrupts, or a higher priority interrupt is running when the pulse edge is detected, your code gets delayed until your interrupt can run. So you get the micros() timestamp somewhat later. Usually this sort of error is only a few microseconds unless you're running something bad like Adafruit_Neopixel which blocks interrupts for long times. While these errors are undesirable, if all you need is 30 ms, then a couple microseconds is almost nothing.

The timers can do the capture all in hardware, so your interrupt can read a number from the precise moment the edge was detected. This gives you accuracy to within 1 cycle of F_BUS, which defaults to 60 MHz on Teensy 3.5.

As far as example code, the FreqMeasure and PulsePosition libraries are the best place to start. Both of these use input capture.

The full documentation can be found in the reference manual, at this page:

https://www.pjrc.com/teensy/datasheets.html

The FTM timer chapter is huge and loaded with very complex details. But the vast majority is the many special PWM features, so the first step is to ignore all that complicated stuff in the latter part of that chapter.

Using input capture usually involves 3 steps.

1: First you need to configure the timer to increment continuously from 0 to 65535 and roll over automatically. If you use a prescaler of 1 it increments at 60 MHz, which means it rolls over every 1.09 ms. To measure longer times, the simplest approach is to use a slower prescaler, but then you get less resolution and you might be only as good as using attachInterrupt.

2: Next you configure one of the 8 channels (or 2 channels if using FTM1 or FTM2) to capture the counter. This is where you use the big table on page 964. While this looks complex, the thing to remember is each channel is only 2 registers. One for config and one for the actual captured count. All you need to do is write the correct config and the timer will automatically start capturing the rapidly incrementing counter when those edges happen.

This question was about Teensy 3.5... but if using Teensy LC (as in, someone else later finding this message by search), the very similar "TPM" timers have a quirk where you have to set the config to zero and either wait a brief delay or repeatedly read it back until it's really zero, and only then can you successfully change the channel config. The TPM timers have the ability to run from very different clocks (much faster or slower than the CPU) so they have this extra step. The FTM timers on Teensy 3.x don't have this limitation, but they are limited to only running from F_BUS which is always an integer division of the main F_CPU clock.

3: Finally, you need to handle the interrupts. You want to get that counter value read ASAP. Then figuring out what to actually do with it is the challenge. Usually you would subtract it from the prior reading. If using 16 bit unsigned math, this automatically handles the roll over and gives you an elapsed time at a 16 bit number. Usually you'd store this to a volatile variable, so some sort of volatile flag so your program can know it's new data, and get out of the interrupt as quickly as you can. The longer you remain running the interrupt, the longer you block other interrupts and your ability to get the next edge.

One drawback is you only get 16 bits of resolution, which spans only 1.09 ms if the timer counts at 60 MHz. It's possible to also respond to overflow interrupts and increment an extended 16 bits, then combine it with the 16 bit count to give you 32 bits of resolution. This is very tricky to get right in the case where these 2 interrupts happen very close in time, so I highly recommend you look at the FreqMeasure code. It's been well tested, so use it if you need more than 16 bits resolution.

But again, while you can get 16.7 ns precision by using the timer input capture, if you only need 30 ms you might do well to save yourself a lot of trouble and just go with attachInterrupt() and micros(). The timers have incredible capability, but it is tricky to use properly.
 
Thank you very much, Paul and manitou, for your inputs! I will try the attachinterrupt and micros() approach first and see if this works good enough for this project.
Christof
 
Panic!
I have to use hardware input capture because the rest of the code has other interrupts and long stretches of no interrupt code. I use ICP1 on the Arduino Micro. ICP1 exists on other Arduinos so must be useful to other people.
I thought I could use A12 and FTM1 to get the same, even better results on the Teensy 3.2 (after a lot of reading!!).
So I connected my servo tester to A12 on the back of the teensy and set
PORTA_PCR12 |= 3<<8;
But I couldn't get it to work. The FTM1 timer is working correctly as I can read FTM1_CNT correctly, but always 0 from C0V and C1V.
Then I noticed that A12 is analog only, then panic that it is 3.3V only and my tester is 5V.
Have I killed the Teensy, or maybe hopefully only the A12?
How else can I get an ICP1 equivalent?
George Shering.
 
Last edited:
Thank you Manitou. That solved my problem. The basic problem was that I confused connection A12 on the back of the Teensy 3.2 with chip PA12. A look at the Teensy 3.2 schematic showed that pad A12 goes to ADC0_DP3. I hope it survived my 5V ministrations! In fact PA12 is connected to Teensy digital pin 3, much more convenient anyway. I had chosen FTM1 to save FTM0 with its 8 registers for a later phase of the project.
Once re-soldered and with your input I soon had the equivalent of ICP1 on the Arduino working fine, thank you.
I then went on to try dual edge capture, and got it working, I found the reference:
https://www.nxp.com/docs/en/application-note/AN5142.pdf
helpful, particularly Figure 7. Dual-edge capture mode. The thing that stuck me there was the need to set the MSA bit for continuous operation, not mentioned in their code example, and not required for ICP1 emulation.
Thanks again, George Shering.
 
// TeensyDualFTM1 - input capture test
// Captures length of positive pulse in RxPulseWidth
// Pulse input is on Teensy digital pin 3, microprocessor PA12

unsigned short RxPulseWidth; // width last measured

#define ledPin 13
#include <LiquidCrystal.h>
// initialize the library by associating any needed LCD interface pin
// with the pin number it is connected to
const int rs = 7, en = 8, d4 = 9, d5 = 10, d6 = 11, d7 = 12;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

void setup() {
// set up the LCD's number of columns and rows:
lcd.begin(20, 4);
pinMode(ledPin, OUTPUT);

// Set up FTM1 to capture the RX pulse
FTM1_SC = 0;
FTM1_CNT = 0;
FTM1_CNTIN=0x00;
FTM1_MOD = 0xFFFF;
FTM1_MODE=0x05; //set FTMEN bit to unprotect FTM1_COMBINE register, FTM mode
FTM1_SC = (FTM_SC_CLKS(1) | FTM_SC_PS(5)); // system clock/32, 2.25MHz
FTM1_C0SC=0b00010100; //Capture on Rising Edge Only
FTM1_C1SC=0b01011000; //Capture on Falling Edge, interrupt
FTM1_COMBINE=0x04; //enable dual capture mode
FTM1_C1SC|=0x40; //enable CH1 interrupt
FTM1_SC|=0x08;
FTM1_COMBINE|=0x08; //setting DECAP0 bit to launch the dual capture process
PORTA_PCR12 = 3<<8; // set Teensy pin 3, PA12, as FTM1 trigger for CH0
NVIC_ENABLE_IRQ(IRQ_FTM1);
NVIC_SET_PRIORITY(IRQ_FTM1, 64);
}

// Interrupt service routine for dual edge capture on FTM1
void ftm1_isr()
{
unsigned short capture0, capture1;
digitalWrite(ledPin,HIGH);
capture0=FTM1_C0V;
capture1=FTM1_C1V;
RxPulseWidth=capture1-capture0;
FTM1_STATUS&=0xFD;
digitalWrite(ledPin,LOW);
}

void loop() {
lcd.setCursor(0, 0); lcd.print("RxPulseWidth "); lcd.print(RxPulseWidth);
lcd.setCursor(0, 1); lcd.print("FTM1_CNT "); lcd.print(FTM1_CNT);
delay(20);
}
 
Note: The above is for Teensy 3.2. I only have two of them. The small format is better for airborne applications. But I wish space could be found for a memory chip.
 
Teensy 3.2 dual edge input capture on FTM1

Can you post your code for dual edge capture?
Below is the code, George Shering

// TeensyDualFTM1 - input capture test for Teensy 3.2
// Captures length of positive pulse in RxPulseWidth (unsigned short)
// Pulse input is on Teensy digital pin 3, microprocessor PA12

unsigned short RxPulseWidth; // width last measured, best convert to int before use

#define ledPin 13
#include <LiquidCrystal.h>
// initialize the library by associating any needed LCD interface pin
// with the pin number it is connected to
const int rs = 7, en = 8, d4 = 9, d5 = 10, d6 = 11, d7 = 12;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

void setup() {
// set up the LCD's number of columns and rows:
lcd.begin(20, 4);
pinMode(ledPin, OUTPUT);

// Set up FTM1 to capture the RX pulse
FTM1_SC = 0;
FTM1_CNT = 0;
FTM1_CNTIN=0x00;
FTM1_MOD = 0xFFFF;
FTM1_MODE=0x05; //set FTMEN bit to unprotect FTM1_COMBINE register, FTM mode
FTM1_SC = (FTM_SC_CLKS(1) | FTM_SC_PS(5)); // system clock/32, 2.25MHz
FTM1_C0SC=0b00010100; //Capture on Rising Edge Only
FTM1_C1SC=0b01011000; //Capture on Falling Edge, interrupt
FTM1_COMBINE=0x04; //enable dual capture mode
FTM1_C1SC|=0x40; //enable CH1 interrupt
FTM1_SC|=0x08;
FTM1_COMBINE|=0x08; //setting DECAP0 bit to launch the dual capture process
PORTA_PCR12 = 3<<8; // set Teensy pin 3, PA12, as FTM1 trigger for CH0
NVIC_ENABLE_IRQ(IRQ_FTM1);
NVIC_SET_PRIORITY(IRQ_FTM1, 64);
}

// Interrupt service routine for dual edge capture on FTM1
void ftm1_isr()
{
unsigned short capture0, capture1;
digitalWrite(ledPin,HIGH);
capture0=FTM1_C0V;
capture1=FTM1_C1V;
RxPulseWidth=capture1-capture0;
FTM1_STATUS&=0xFD;
digitalWrite(ledPin,LOW);
}

void loop() {
lcd.setCursor(0, 0); lcd.print("RxPulseWidth "); lcd.print(RxPulseWidth);
lcd.setCursor(0, 1); lcd.print("FTM1_CNT "); lcd.print(FTM1_CNT);
delay(20);
}
 
Best practice warning. When setting a variable in an ISR that is referenced elsewhere, you need to declare the variable volatile so the compiler can handle asynchronous updates.
volatile unsigned short RxPulseWidth;
 
Thanks Manitou. I would also suggest removing the line
FTM1_SC|=0x08;
As it has no function.
The reason there are two copies is that the first two posts were sent in the morning. When I checked in the afternoon they had disappeared! So I sent the third, then later the first two reappeared?
 
Status
Not open for further replies.
Back
Top