Timer Input Capture

Status
Not open for further replies.

slomobile

Well-known member
I need to decode the pulse stream from a Radio Control receiver using a Teensy3. Information about the RC hardware and signal is here http://www.frank-zhao.com/cache/vex_rx.php Below is working code for Teensy++ 2.0. Obviously those registers are not the same for Teensy3. I would like some guidance on the best way to get similar functionality. It must measure the pulsewidth of 1 to 8 pulses every 20mS without dramatically affecting the main loop. Each 20ms there is a single 7ms synch pulse and the remaining pulses are from 750uS to 2250uS (servo). I am aware that the Teensy3 ISR routines are still a work in progress. I would like this project to be one of the first to use them.
Code:
#ifndef ServoDecode_h
#define ServoDecode_h
//Attribution: ServoDecode library from mem at http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1228137503/all
//adapted for Arduino 1.0.1 by Dustin Maki
//Intended for Vex, but should work with any receiver giving access to the complete pulse train on a single pin
#include "Arduino.h"
// Teensy 2.0 ICP_PIN 22, Teensy++ 2.0 ICP_PIN 4
#define ICP_PIN 4
#define TICKS_PER_uS	2	    // number of timer ticks per microsecond
typedef enum {NULL_state=-1, NOT_SYNCHED_state, ACQUIRING_state, READY_state, FAILSAFE_state } decodeState_t;
String stateStrings[] = { "NOT_SYNCHED", "ACQUIRING", "READY", "in Failsafe"};
#define MAX_CHANNELS    8	   // maximum number of channels we can store, don't increase this above 8
#define MIN_IN_PULSE_WIDTH (750 * TICKS_PER_uS) //a valid pulse must be at least 750us (note clock counts in 0.5 us ticks)
#define MAX_IN_PULSE_WIDTH (2250 * TICKS_PER_uS) //a valid pulse must be less than  2250us
#define SYNC_GAP_LEN	(3000 * TICKS_PER_uS) // we assume a space at least 3000us is sync (note clock counts in 0.5 us ticks)
//#define FAILSAFE_PIN   13  // if defined, this will set the given pin high when invalid data is received
#define PULSE_START_ON_RISING_EDGE  0
#define PULSE_START_ON_FALLING_EDGE (1<<ICES1)
#define ACQUISITION_COUNT  8  // must have this many consecutive valid frames to transition to the ready state.
volatile uint8_t pulseEnd = PULSE_START_ON_RISING_EDGE ; // default value
static volatile uint16_t Pulses[ MAX_CHANNELS + 1]; // array holding channel pulses width value in microseconds
static volatile uint16_t Failsafe[MAX_CHANNELS + 1]; // array holding channel fail safe values
static volatile uint8_t Channel;	// number of channels detected so far in the frame (first channel is 1)
static volatile uint8_t NbrChannels; // the total number of channels detected in a complete frame
static volatile decodeState_t State;	   // this will be one of the following states:
static volatile uint8_t stateCount;	   // counts the number of times this state has been repeated

static void processSync()
{
// Sync was detected so reset the channel to 1 and update the system state
   Pulses[0] = ICR1 / TICKS_PER_uS;  // save the sync pulse duration for debugging
   if(State == READY_state) 
   {
	   if( Channel != NbrChannels)
           {  // if the number of channels is unstable, go into failsafe
		   State = FAILSAFE_state;
	   }
   }
   else
   {
     if(State == NOT_SYNCHED_state)
     {
	 State = ACQUIRING_state;	  // this is the first sync pulse, we need one more to fill the channel data array
	 stateCount = 0;
     }
	 else if( State == ACQUIRING_state)	
         {
	    if(++stateCount > ACQUISITION_COUNT) 
            {
		State = READY_state;	     // this is the second sync and all channel data is ok so flag that channel data is valid
	        NbrChannels = Channel;       // save the number of channels detected
	    }
         }
	 else if( State == FAILSAFE_state)	
         {
		 if(Channel == NbrChannels)
                 {  // did we get good pulses on all channels
			State = READY_state;
		 }
	 }
   }
   Channel = 0;	 // reset the channel counter
}

// required interrupt service routines
ISR(TIMER1_OVF_vect)
{
  if(State == READY_state)
  {
    State = FAILSAFE_state;  // use fail safe values if signal lost
    Channel = 0; // reset the channel count
  }
}

ISR(TIMER1_CAPT_vect)
{
  // we want to measure the time to the end of the pulse
  if( (_SFR_BYTE(TCCR1B) & (1<<ICES1)) == pulseEnd )
  {
    TCNT1 = 0;	 // reset the counter
    if(ICR1 >= SYNC_GAP_LEN)
    {   // is the space between pulses big enough to be the SYNC
	processSync();
    }
    else if(Channel < MAX_CHANNELS) 
    {  // check if its a valid channel pulse and save it
	if( (ICR1 >= MIN_IN_PULSE_WIDTH)  && (ICR1 <= MAX_IN_PULSE_WIDTH) )
        { // check for valid channel data
	  Pulses[++Channel] = ICR1 / TICKS_PER_uS;  // store pulse length as microsoeconds
	}
	else if(State == READY_state)
        {
	  State = FAILSAFE_state;  // use fail safe values if input data invalid
	  Channel = 0; // reset the channel count
	}
    }
  }
}


class ServoDecodeClass //ServoDecodeClass begin
{
  public:
    ServoDecodeClass(){}
    
    void begin()
    {
      pinMode(ICP_PIN,INPUT);
      Channel = 0;
      State = NOT_SYNCHED_state;
      TCCR1A = 0x00;	   // COM1A1=0, COM1A0=0 => Disconnect Pin OC1 from Timer/Counter 1 -- PWM11=0,PWM10=0 => PWM Operation disabled
      TCCR1B = 0x02;	   // 16MHz clock with prescaler means TCNT1 increments every .5 uS (cs11 bit set
      TIMSK1 = _BV(ICIE1)|_BV (TOIE1);   // enable input capture and overflow interrupts for timer 1
      for(uint8_t chan = 1; chan <=  MAX_CHANNELS; chan++)
    	  Failsafe[chan] = Pulses[chan]= 1500; // set midpoint as default values for pulse and failsafe
    }
    
    decodeState_t getState()
    {
      return State;
    }
    
    uint8_t getChanCount()
    {
      return NbrChannels;
    }
    
    void  setFailsafe(uint8_t chan, int16_t value)
    {
    // pulse width to use if invalid data, value of 0 uses last valid data
      if( (chan > 0) && (chan <=  MAX_CHANNELS)  ) 
      {
    	 Failsafe[chan] = value;
      }
    }
    void  setFailsafe()
    {
    // setFailsafe with no arguments sets failsafe for all channels to their current values
    // usefull to capture current tx settings as failsafe values
      if(State == READY_state)
        for(uint8_t chan = 1; chan <=  MAX_CHANNELS; chan++) 
        {
    	  Failsafe[chan] = Pulses[chan];
        }
    }
    
    int16_t GetChannelPulseWidth( uint8_t channel)
    {
      // this is the access function for channel data
      int16_t result = 0; // default value
      if( channel <=  MAX_CHANNELS)  
      {
         if( (State == FAILSAFE_state)&& (Failsafe[channel] > 0 ) )
    	   result = Failsafe[channel]; // return the channels failsafe value if set and State is Failsafe
    	 else if( (State == READY_state) || (State == FAILSAFE_state) )
         {
           bitClear(TIMSK1, _BV(ICIE1)|_BV (TOIE1));//disable Timer1 input capture and overflow interrupts 
    	   //cli();	 //disable interrupts
    	   result =  Pulses[channel] ;  // return the last valid pulse width for this channel
    	   //sei(); // enable interrupts
           bitSet(TIMSK1, _BV(ICIE1)|_BV (TOIE1));//enable Timer1 input capture and overflow interrupts
    	 }
      }
      return result;
    }
    
    
};//ServoDecodeClass end


// make one instance for the user
extern ServoDecodeClass ServoDecode = ServoDecodeClass();

void printRC()//used for debugging, testing, and capturing typical values for setting up constants
{
	// snipped this code to fit forum character limit
}

void parseRCbuttons(bool& L_UP_pushed, bool& L_DWN_pushed, bool& R_UP_pushed, bool& R_DWN_pushed, Servo& L_servo, Servo& R_servo)
{
	// note: if servo is at end of range, *_pushed will be false even when the button is actually pushed
	// this behavior can be changed if desired by removing parentheses
    int servoTmp = 90;

    if(L_UP_pushed = (ServoDecode.GetChannelPulseWidth(5) < 1500-200) &&
	 (servoTmp = L_servo.read()) < 180)// L_servo commanded and not already at maximum
	{L_servo.write(servoTmp+1);}
	if(L_DWN_pushed = (ServoDecode.GetChannelPulseWidth(5) > 1500+200) &&
	 (servoTmp = L_servo.read()) > 0)// L_servo commanded and not already at minimum
	{L_servo.write(servoTmp-1);}

	if(R_UP_pushed = (ServoDecode.GetChannelPulseWidth(6) < 1500-200) &&
	 (servoTmp = R_servo.read()) < 180)// R_servo commanded and not already at maximum
	{R_servo.write(servoTmp+1);}
	if(R_DWN_pushed = (ServoDecode.GetChannelPulseWidth(6) > 1500+200) &&
	 (servoTmp = R_servo.read()) > 0)// R_servo commanded and not already at minimum
	{R_servo.write(servoTmp-1);}  
	
	//illuminate LED on any button push
	if(L_UP_pushed || L_DWN_pushed || R_UP_pushed || R_DWN_pushed)
	{digitalWrite(6, HIGH);}
	else
	{digitalWrite(6, LOW);} 
}      

#endif
 
Last edited:
I'm starting to understand that I'll need to configure an FTM FlexiTimer 0 or 1 in channel mode, and combine 2 channels for dual edge trigger. Something similar to the rough outline below. I'll need to change that code to read the pulsewidths from the channel registers instead of the counter directly. What I do not know is how to translate say FTM0 Channel 6 to a pin number. Is there a figure or chart anywhere that maps teensy3 pins to K20 channel registers?
Also, how can I tell if the interrupt was triggered by a rising or falling edge?
EDIT- I think I know the answer to the above question. Channel n will record the count value on rising interrupts and channel n+1 will record the count value on falling interrupts. By combining the channels, they read thre same pin. Is that correct? Again, how are they associated with a particular pin?

Code:
//	  Mode Configuration
//	16bit up counting dual edge input capture timer on FTM0
FTM0_MODE[WPDIS] = 1;
FTM0_MODE[FTMEN] = 1;
DECAPEN = 1;
COMBINE = 1;
CPWMS = 0;

MSnA:MSnB = 0;
ELSnB = 1;
ELSnA = 1;

// global
static volatile uint8_t currentChannel;
static volatile uint8_t pulsewidth[NUM_CHANNELS+2];//pulsewidth[0] holds the sync threshold, pulsewidth[NUM_CHANNELS+1] holds the synch pulsewidth
pulsewidth[0] = 7000*PULSES_PER_uS;

ISR // any edge triggered
{	
	if(risingEdge)
	{
		lastRisingEdge = FTM0_CNT;	
		currentChannel	++;
	}
	else
	{
		lastFallingEdge = FTM0_CNT;
		if((pulsewidth[currentChannel] = lastFallingEdge-lastRisingEdge) > pulsewidth[0])
		{
			currentChannel = 0; // end of synch pulse received
			FTM0_CNT = 0; // count = 0; reset timer 
		}
		else // end of regular channel pulse received
		{
			// anything to do here?
			// pulsewidth[currentChannel] already stored
		}
	}
}
 
Last edited:
Hi
To be honest I had already started with AVR similar projects, to read PWM signals from a RX receiver. Since I switched to Teensy 3 (because I need the more powerful processor) I rearranged my old projects using attachInterrupt() function and it works nicely.
Now I wanted to rewrite my code and using instead the Input Capture feature of the micro. I find that feature very nice and it could be really solve the problems with my code. Just for starting I plugged one pin of T3 to one signal wire of my receiver and start setting the Input Capture function with FlexTimer0 to measure each lenght of each pulses coming from the same channel (I will write the code for more channels, when I m ok with one channel). I figure out the feature Input Capture as the possibility for the micro to "sense" the raising edge of a pulse and start a timer (through one interrupt). The timer keep running until the falling edge of the pulse is detected. So is the timer again read and the difference between both edges says to me how long the pulse was. Ok....keepign this in mind I wrote a program, which set the FTM0 as a timer starting with the raising edge of the incoming pulse.

Code:
void setupFTM0( void ) {

    /*****************************************************
    * The Port Control Module sets Teensy to use the ALT 4
    * so the following Channels are configurable:
    *
    *     Teensy Port Name  CPU Pin  Signal
    *
    *     22          PTC1      44   FTM0_CH0
    *     23          PTC2      45   FTM0_CH1
    *      9          PTC3      46   FTM0_CH2
    *     10          PTC4      49   FTM0_CH3
    *      6          PTD4      61   FTM0_CH4
    *     20          PTD5      62   FTM0_CH5
    *     21          PTD6      63   FTM0_CH6
    *      5          PTD7      64   FTM0_CH7
    *
    *****************************************************/
    const uint8_t throttlePulsePin = 22;

    pinMode( throttlePulsePin, INPUT );

    // Input filter to help prevent glitches from triggering the capture
    // only for CH0 (throttle)
    FTM0_FILTER = 0x07;

    // define MODE of operation for FMT0
    // At first the Write Protection Disable must be disabled
    // WPDIS = 1 => 0b0100 
    FTM0_MODE = 0x04;  // now can one write value in the bitfield

    // FAULTIE = 0, FAULTM = 0, CAPTEST = 0, PWMSYNC = 0, WPDIS = 1, INIT = 0, FTMEN = 1;
    // => 0b00000101
    FTM0_MODE = 0x05;

    // Flex Timer 0 configurations bits
    // The Clock source is Fixed Frequency Clock Running @ 31.25 kHz
    // FLL Clock Input = MCGFFCLK
    // Dividing that by 2 would have the counter roll over about every 4 seconds

    // SC: Status and Control
    // Overflow status flag and control bits to use the interrupt, clock source and prescaler
    // TOF = 0, TOIE = 0, CPWMS = 0, CLKS = 0b10, PS = 0b001 
    FTM0_SC = 0x11;
    FTM0_CNT = 0x0000;
    FTM0_MOD = 0xFFFF; // max modulus = 65535

    // C0SC: Channel n Status and Control
    // configure the timer for raising AND falling edge 
    // CHF = 0, CHIE = 1, MSB = 0, MSA = 0, ELS0B:ELS0A = 0b11, DMA = 0
    FTM0_C0SC = 0b01001100;

    // Enable FMT0 interrupt inside NVIC
    NVIC_ENABLE_IRQ( IRQ_FTM0 );

    // WPDIS = 0 write protecton enabled
    FTM0_FMS = 0x40;

    // PIN configuration alternative function 4 on pin 44 (Teensy 22)
    // MUX = 0b100 
    PORTC_PCR1 = 0x400;
}

and the interrupt rotutine reads the first value of the counter starting the timer (if the corresponding pin is found HIGH).

Code:
/* ISR procedures */
extern "C" void ftm0_isr( void ) {


    if( ( GPIOC_PDIR & 0b0000000000000010 ) && ( FTM0_C0SC & 0x80 ) ) {
        
        // clear channel interrupt flag (CHF)
        FTM0_C0SC &= ~( 0x80 );

        // reset counter to zero
        FTM0_CNT = 0x00;

    } else {

        dataServo.throttlePulse = FTM0_C0V;
        
        // ONLY FOR DEBUG INFORMATION!!!!
        Serial.println( (dataServo.throttlePulse * 1000000) / (31250 / 2) );

        // clear channel interrupt flag (CHF)
        FTM0_C0SC &= ~( 0x80 );
   
    }
}

So I keep the ISR as easy as possible. Nevertheless I coudn't found a better way to calculate the length of the pulse with the "speed" of the processor (in my case 48 MHz).
The above operation
Code:
(dataServo.throttlePulse * 1000000) / (31250 / 2)
is from the code form this site: http://www.digitalmisery.com/2013/06/timer-input-capture-on-teensy-3-0/ but never explain how to calculate it.

I must say the above code works pretty well in my case, so now I m going to expand the rest for the other signals.
But just for my understanding, when for istance in my code instead of:
Code:
(dataServo.throttlePulse * 1000000) / (31250 / 2)
I write:
Code:
        // ONLY FOR DEBUG INFORMATION!!!!
        Serial.println( FTM0_C0V );
I read steadly the value: 22

How can I scale it to milliseconds?

Thank in advance
 
Read the stuff from the link you posted again. It does explain where the "31250 / 2" comes from (clock/clock divider setup). FTM0_C0V is the captured value, it's relative to the timer module input clock.
 
Status
Not open for further replies.
Back
Top