Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 7 of 7

Thread: New Pulse Position Modulation (PPM) Library

  1. #1
    Junior Member
    Join Date
    May 2014
    Posts
    12

    New Pulse Position Modulation (PPM) Library

    For the past few months, I've been working on a remote controlled gimbal mounted 3-axis live camera system for real race cars. After some huge learning curves, trial and error, I came across using a Teensy board to get better control over the cameras. In a race car, it's not possible to get the driver to turn on/off start/stop record the video system. So instead I came up with a way to do it all with remote control. But there was one problem I couldn't solve even by changing hardware. When the RC transmitter and receiver lock signals, the servo controlled relays would flutter and activate my camera's or start recording when I didn't want them to do this. I saw the Teensy board as a way to stop this because I can decode the PPM from the RC receiver and directly control relays and PWM devices (like the camera mount motors) with Teensy.

    But there was still one problem I needed to solve -- even using Teensy. I need four to six PWM channels, and if I use the original PulsePosition library to decode the PPM, then I only have four PWM channels remaining on pins 3, 4, 25, and 32. The PWM channels and PPM use the Flex Timer Module of Teeny's CPU. The PPM library modifies the FTM clock for better resolution, and that means you can't use any of those FTM's channels for PWM if it's already using PPM. I needed another solution. Searching a little further, I found that Teensy pins [3,4] are on FTM1, and pins [25,32] are on FTM2. So I decided to modify and somewhat rewrite the PPM library to work on all FTM capable pins of the Teensy.

    In the process, I rewrote the library somewhat and for the most part kept all of the algorithms intact (they work). But instead of creating an instance of PPM for every pin, I used a different approach that uses a single instance of the library. Also instead of registering a pointer to the object-specific ISR which gives access to the object-specific data structures, I placed all of the data structures in a common table, thus making the entire design table driven and sharing as much common code as possible. This also means there isn't a need to have multiple instances of the PPM library. Even though I haven't tested it, I'm pretty sure this design would work with multiple instances if somebody really wanted to use it that way.

    I ran tests for 24+ hours without a hitch. I benchmarked both libraries, and found mine slightly faster. Not being a C/C++ coder, I'm sure there's some things I could and should do differently. I'll look into those later. But for now, here's what I've got.

    Features:
    • Single instance to support all in/out PPM streams
    • Read/Write up to 12 simultaneous input/output PPM streams, each up to 16 channels apiece.
    • Each in/out stream is polarity selectable
    • 48 Mhz to 96 Mhz clock resolution


    Testing:
    I tested by inputting from my Taranis + DragonLink into one channel, then output to another channel, switching polarity and moving the data through the various channels. My loopback test uses seven different input/outputs and all three FTM's.

    Code:
     * 	Taranis Dragon-Link input         ==> INPUT:   PIN(05) FALLING	(FTM-00)
     *	INPUT:	PIN(05) FALLING (FTM-00)  ==> OUTPUT:  PIN(04) RISING	(FTM-01)
     *	OUTPUT:	PIN(04) RISING	(FTM-01)  ==> JUMPER:  PIN(25) RISING	(FTM-02)
     *	INPUT:	PIN(25) RISING	(FTM-02)  ==> OUTPUT:  PIN(23) FALLING	(FTM-00)
     *	OUTPUT:	PIN(23) FALLING	(FTM-00)  ==> JUMPER:  PIN(03) FALLING	(FTM-01)
     *	INPUT:	PIN(03) FALLING (FTM-01)  ==> OUTPUT:  PIN(32) RISING	(FTM-02)
     *	OUTPUT: PIN(32) RISING  (FTM-02)  ==> JUMPER:  PIN(10) RISING	(FTM-00)
     *	INPUT:  PIN(10) RISING  (FTM-00)
    API and Usage:
    Code:
     * HOWTO:
     * Usage: Sending and Receiving PPM Encoded Signals
     *
     * PPM myPPM;
     * Create a PPM object. Create only one instance of the PPM library.
     * The PPM library is designed to handle multiple simultaneous inputs and
     * outputs.  Therefore, create only one instance of the PPM library.
     *
     * Public Methods:  (Each described below)
     * 		PPM(void);
     *		void 	PPMInput(int polarity, int numIOs, ...);
     *		void 	PPMOutput(int polarity, int numIOs, ...);
     *		bool 	PPMRegisterInput(int polarity, int rxPin);
     *		bool 	PPMRegisterOutput(int polarity, uint8_t framePin, int txPin);
     *		int		dataAvl(int rxPin);
     *		float	dataRead(int rxPin, uint8_t channel);
     *		bool 	dataWrite(int txPin, uint8_t channel, float microseconds);
     * 		char	*getVersion(void);
     *		void 	PPMDebug(bool endis);
     *		friend 	void ftm0_isr(void);
     *		friend 	void ftm1_isr(void);
     *		friend 	void ftm2_isr(void);
     *
     * Public Method:  myPPM.PPMInput(POLARITY, numIOs, rxPin [, rxPin, rxPin, ...])
     * 		Valid input pins:  [3, 4, 5, 6, 9, 10, 20, 21, 22, 23, 25, 32]
     * 		Initializes a PPM input stream with one or more input pins.  Configures
     * 		each rxPin with appropriate polarity, timer channel, and interrupts.
     * 		Each rxPin will be ready to receive PPM pulses.  More than
     * 		one input pin may be specified.  PPMInput may also be called more than
     * 		once, should some pins have different polarity than others.
     * 		Example(s):
     * 			myPPM.PPMInput(RISING, 2, 4, 6); - Rising edge, two input pins [4, 6]
     * 			myPPM.PPMInput(FALLING, 1, 22); - Falling edge, one input pin [22]
     *
     * Public Method:   bool PPMRegisterInput(int polarity, int rxPin);
     * 		This is the "worker" function called by PPMInput().  For each pin in the
     * 		argument list of PPMInput, PPMRegisterInput is called.  This is the function
     * 		that actually sets up the appropriate polarity, timer channel, and interrupts.
     * 		Calling this method directly is allowed.
     * 		Example(s):
     * 			myPPM.PPMRegisterInput(RISING, 4); - Rising edge, input pin [4].
     * 			myPPM.PPMRegisterInput(FALLING, 9); - Falling edge, input pin [9].
     *
     * Public Method:  int myPPM.dataAvl(rxPin);
     * 		Returns the number of channels received, or -1 when no new data is available.
     * 		rxPin is a required argument to specify which input stream to read.
     * 		Example(s):
     *			num = myPPM.dataAvl(PPM_INPUT_10);
     *			if (num > 0) {
     * 			...
     * 			}
     *
     * Public Method:  float myPPM.dataRead(rxPin, channel);
     *		Returns channel data from the specific rxPin stream.
     *		rxPin is a required argument to specify which input stream to read.
     *		channel is a required argument to specify which channel of the rxPin
     *		input stream to return.
     *		The returned value is a float representing the number of microseconds
     *		between rising edges. The input "channel" ranges from 1 to the number of
     *		channels indicated by available(). Reading the last channel causes the
     *		data to be cleared (available will return zero until a new frame of PPM
     *		data arrives).
     * 		Example(s):
     *			num = myPPM.dataAvl(PPM_INPUT_10);
     *			if (num > 0) {
     *				for (i=1; i <= num; i++) {
     *					float val = myPPM.dataRead(PPM_INPUT_10, i);
     * 					Serial.println(val);
     * 				}
     * 			}
     *
     * Public Method:  myPPM.PPMOutput(POLARITY, numIOs, txPin [, txPin, txPin, ...])
     * 		Valid output pins:  [3, 4, 5, 6, 9, 10, 20, 21, 22, 23, 25, 32]
     * 		Initializes a PPM output stream with one or more output pins.  Configures
     * 		each txPin with appropriate polarity, timer channel, and interrupts.
     * 		Each txPin will be ready to send PPM pulses.  More than
     * 		one output pin may be specified.  PPMOutput may also be called more than
     * 		once, should some pins have different polarity than others.
     * 		Example(s):
     * 			myPPM.PPMOutput(RISING, 2, 4, 6); - Rising edge, two output pins [4, 6]
     * 			myPPM.PPMOutput(FALLING, 1, 22); - Falling edge, one output pin [22]
     *
     * Public Method:   bool PPMRegisterOutput(int polarity, int txPin);
     * 		This is the "worker" function called by PPMOutput().  For each pin in the
     * 		argument list of PPMOutput, PPMRegisterOutput is called.  This is the function
     * 		that actually sets up the appropriate polarity, timer channel, and interrupts.
     * 		Calling this method directly is allowed.
     * 		Example(s):
     * 			myPPM.PPMRegisterOutput(RISING, 4); - Rising edge, output pin [4].
     * 			myPPM.PPMRegisterOutput(FALLING, 9); - Falling edge, output pin [9].
     *
     * Public Method:  bool dataWrite(int txPin, uint8_t channel, float microseconds);
     *		This function populates the output stream of txPin array with channel
     * 		and timing data.  This function will overwrite any previous channel
     * 		data and replace with new data.  The next time an output frame is
     * 		generated, the data in the output channel array will be converted
     * 		to the appropriate timing register data to create a pulse wave of
     * 		the appropriate polarity and duration.
     * 		Example(s):
     *			float val = myPPM.dataRead(PPM_INPUT_03, i); - Reads from input stream
     *			myPPM.dataWrite(PPM_OUTPUT_32, (i % 16) + 1, val); - Writes it to a different output stream
     *
     * Public Method:  char *getVersion(void);
     * 		This function returns the PPM library version number string.
     * 		Example(s):
     * 			Serial.print("TeensyPPM Library Version Number:  ");
     *			Serial.println(myPPM.getVersion());
     *
     *  Public Method:  void PPMDebug(bool endis);
     * 		This function is provided to enable and disable debugging.
     * 		Currently no debugging exists in the shipping version of this
     * 		module.  The end user is responsible for writing their own
     * 		debugging code and may use this function to enable and disable
     * 		the function.
     *
     * Public Method:  friend void ftm0_isr(void);
     * Public Method:  friend void ftm1_isr(void);
     * Public Method:  friend void ftm2_isr(void);
     * 		Interrupt Service Routines for the FlexTimer Modules.
    Table Driven Approach:
    Code:
    PPMPinStruct PPMPins[] = {
    	/*- Flex Timer Base Registers				Flex Timer Channel Registers		Pin#	Previous	Write	Pin	FTM		Available	Channel		Debug	*/
    	/*- Flex Timer Base Registers				Flex Timer Channel Registers		Pin#	Value		Index	MUX	IRQ		Flag		Enabled		Enabled	*/
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C0SC,	22,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C1SC,	23,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C2SC,	9,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C3SC,	10,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C4SC,	6,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C5SC,	20,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C6SC,	21,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM0_SC,	(struct FlexTimerChannel_Struct *)&FTM0_C7SC,	5,	0,		255,	4,	IRQ_FTM0,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM1_SC,	(struct FlexTimerChannel_Struct *)&FTM1_C0SC,	3,	0,		255,	3,	IRQ_FTM1,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM1_SC,	(struct FlexTimerChannel_Struct *)&FTM1_C1SC,	4,	0,		255,	3,	IRQ_FTM1,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM2_SC,	(struct FlexTimerChannel_Struct *)&FTM2_C0SC,	32,	0,		255,	3,	IRQ_FTM2,	false,		false,		false},
    	{(struct FlexTimerBase_Struct *)&FTM2_SC,	(struct FlexTimerChannel_Struct *)&FTM2_C1SC,	25,	0,		255,	3,	IRQ_FTM2,	false,		false,		false}
    };

    Usage Example:
    Code:
    /* TeensyPPM loopback test.  Create multiple input/output pins and loop back the PPM
     * signals changing polarity on each jump.
     *
     * 	Taranis Dragon-Link input         ==> INPUT:   PIN(05) FALLING	(FTM-00)
     *	INPUT:	PIN(05) FALLING (FTM-00)  ==> OUTPUT:  PIN(04) RISING	(FTM-01)
     *	OUTPUT:	PIN(04) RISING	(FTM-01)  ==> JUMPER:  PIN(25) RISING	(FTM-02)
     *	INPUT:	PIN(25) RISING	(FTM-02)  ==> OUTPUT:  PIN(23) FALLING	(FTM-00)
     *	OUTPUT:	PIN(23) FALLING	(FTM-00)  ==> JUMPER:  PIN(03) FALLING	(FTM-01)
     *	INPUT:	PIN(03) FALLING (FTM-01)  ==> OUTPUT:  PIN(32) RISING	(FTM-02)
     *	OUTPUT: PIN(32) RISING  (FTM-02)  ==> JUMPER:  PIN(10) RISING	(FTM-00)
     *	INPUT:  PIN(10) RISING  (FTM-00)
     */
    
    #define  PPM_INPUT_05	5	// FALLING
    #define  PPM_OUTPUT_04	4	// RISING
    #Define  PPM_INPUT_25	25	// RISING
    #define  PPM_OUTPUT_23	23	// FALLING
    #define  PPM_INPUT_03	3	// FALLING
    #define  PPM_OUTPUT_32	32	// RISING
    #define  PPM_INPUT_10	10	// RISING
    
    #include <PPM.h>
    	PPM myPPM;	/* Instance our PPM class library	*/
    	int count=0;	/* Initialize our counter		*/
    
    
    void setup() {
    	Serial.print("TeensyPPM Library Version Number:  ");
    	Serial.println(myPPM.getVersion());				/* Print Version Number					*/
    	myPPM.PPMInput(FALLING, 2, PPM_INPUT_05, PPM_INPUT_03);		/* Instance pins 5 and 3 as FALLING edge input pins	*/
    	myPPM.PPMInput(RISING, 2, PPM_INPUT_25, PPM_INPUT_10);		/* Instance pins 25 and 10 as RISING edge input pins	*/
    	myPPM.PPMOutput(RISING, 2, PPM_OUTPUT_04, PPM_OUTPUT_32);	/* Instance pins 4 and 32 as RISING edge output pins	*/
    	myPPM.PPMOutput(FALLING, 1, PPM_OUTPUT_23);			/* Instance pin 23 as FALLING edge output pin		*/
    
    	/* Initialize PPM output on all 16 channels	*/
    	myPPM.dataWrite(PPM_OUTPUT_04, 1, 1000);
    	myPPM.dataWrite(PPM_OUTPUT_04, 2, 1030);
    	myPPM.dataWrite(PPM_OUTPUT_04, 3, 1060);
    	myPPM.dataWrite(PPM_OUTPUT_04, 4, 1090);
    	myPPM.dataWrite(PPM_OUTPUT_04, 5, 1120);
    	myPPM.dataWrite(PPM_OUTPUT_04, 6, 1150);
    	myPPM.dataWrite(PPM_OUTPUT_04, 7, 1180);
    	myPPM.dataWrite(PPM_OUTPUT_04, 8, 1210);
    	myPPM.dataWrite(PPM_OUTPUT_04, 9, 1240);
    	myPPM.dataWrite(PPM_OUTPUT_04, 10, 1270);
    	myPPM.dataWrite(PPM_OUTPUT_04, 11, 1300);
    	myPPM.dataWrite(PPM_OUTPUT_04, 12, 1330);
    	myPPM.dataWrite(PPM_OUTPUT_04, 13, 1360);
    	myPPM.dataWrite(PPM_OUTPUT_04, 14, 1390);
    	myPPM.dataWrite(PPM_OUTPUT_04, 15, 1450);
    	myPPM.dataWrite(PPM_OUTPUT_04, 16, 1500);
    }
    
    void loop() {
    	int i, num;
    
    /*	Receive input from Taranis DragonLink on PIN(03) FALLING Edge.  The
     *	DragonLink will emit 12 PPM channels.  Upon arrival, swap channels
     *	1 with channel 8, and leave all the others the same.
     *
     * 	Taranis Dragon-Link input         ==> INPUT:   PIN(05) FALLING	(FTM-00)
     *	INPUT:	PIN(05) FALLING (FTM-00)  ==> OUTPUT:  PIN(04) RISING	(FTM-01)
     */
     	num = myPPM.dataAvl(PPM_INPUT_05);
    	if (num > 0) {
    	for (i=1; i <= num; i++) {
    		float val = myPPM.dataRead(PPM_INPUT_05, i);
    		switch (i) {
    			case 1:
    				myPPM.dataWrite(PPM_OUTPUT_04, 8, val);
    				break;
    
    			case 8:
    				myPPM.dataWrite(PPM_OUTPUT_04, 1, val);
    				break;
    
    			default:
    				myPPM.dataWrite(PPM_OUTPUT_04, i, val);
    				break;
    			}
    		}
    	}
    
    /*	Receive input from PIN(25) and output on PIN(03).  Jumper PIN(23) to PIN(03).
     *      Move all of the data from channel-N to channel N+1.
     *
     *	INPUT:	PIN(25) RISING	(FTM-02)  ==> OUTPUT:  PIN(23) FALLING	(FTM-00)
     *	OUTPUT:	PIN(23) FALLING	(FTM-00)  ==> JUMPER:  PIN(03) FALLING	(FTM-01)
     */
    	num = myPPM.dataAvl(PPM_INPUT_25);
    	if (num > 0) {
    		for (i=1; i <= num; i++) {
    			float val = myPPM.dataRead(PPM_INPUT_25, i);
    			myPPM.dataWrite(PPM_OUTPUT_23, (i % 16) + 1, val);
    		}
    	}
    
    /*	Receive input from PIN(03) and output on PIN(32).  Jumper PIN(32) to PIN(10).
     *      Move all of the data from channel-N to channel N+1.
     *
     *	INPUT:	PIN(03) FALLING (FTM-01)  ==> OUTPUT:  PIN(32) RISING	(FTM-02)
     *	OUTPUT: PIN(32) RISING  (FTM-02)  ==> JUMPER:  PIN(10) RISING	(FTM-00)
     */
    	num = myPPM.dataAvl(PPM_INPUT_03);
    	if (num > 0) {
    		for (i=1; i <= num; i++) {
    			float val = myPPM.dataRead(PPM_INPUT_03, i);
    			myPPM.dataWrite(PPM_OUTPUT_32, (i % 16) + 1, val);
    		}
    	}
    
    /*	Receive input from PIN(10) and print the results on the Serial port debug
     *	output.
     *
     *	INPUT:  PIN(10) RISING  (FTM-00)
     */
    	num = myPPM.dataAvl(PPM_INPUT_10);
    	if (num > 0) {
    		count = count + 1;
    		Serial.print(count);
    		Serial.print(" :  ");
    		for (i=1; i <= num; i++) {
    			float val = myPPM.dataRead(PPM_INPUT_10, i);
    			Serial.print(val);
    			Serial.print("  ");
    		}
    		Serial.println();
    	}
    }
    Benchmark results:
    Benchmark results were calculated over 10000 execution cycles. There's two columns for each result:
    • AVG - Average execution time within the interrupt service routine. In this result, only non-overflow interrupt execution was measured. If an overflow interrupt occurred beforehand, then no results were tallied. Since overflow interrupts were repeatedly expected, this measurement allowed me to measure the execution of the ISR without the need to switch to the object-specific instance subroutines. Basically this tests my table driven approach versus the more pure object oriented approach.
    • TOTAL - Total execution time for all interrupt execution, including overflow interrupts. Total time in microseconds for 10000 iterations.


    Code:
    Input	AVG		Total
    PJRC	3.603717 uS	2962792 uS
    myPPM	3.579031 uS	2945915 uS
    
    Output	AVG		Total
    PJRC	3.654310 uS	8389553 uS
    myPPM	3.617892 uS	8378814 uS
    Something to mention about the benchmark results. I could turn off the Teensy, come back the next day, and get completely different results. The first time I ran the benchmarks, the Teensy output algorithm was faster than mine. But the next day, it was the other way around. Since I took five different sets of benchmarks, the numbers above represent the most common results I saw.

    Source Code:
    TeensyPPM source code version 1.0.0 is attached to this thread. The official home of TeensyPPM is here:
    http://www.rcollins.org/public/Teensy/TeensyPPM

    Photos, scope traces, data logs, and benchmark results are available at the link above for anybody who wants to see them.
    Attached Files Attached Files
    Last edited by PencilGeek; 05-29-2014 at 02:17 AM.

  2. #2
    Junior Member
    Join Date
    Dec 2014
    Posts
    3
    Excellent!... Was searching for it!... Will try!

  3. #3
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    15,729
    I've posted a link to this thread on the PulsePosition page. Hopefully that'll help anyone else more easily find this alternate library.

  4. #4
    Junior Member
    Join Date
    Dec 2014
    Posts
    3
    Thank Paul...

    Correct if Im wrong, but if you change the line 53 in PPM.h from
    #define FTM_CnSC_INPUT_REG_INIT FTM_CnSC_CHIE_VAL(FTM_CnSC_CHIE_ENABLE) | FTM_CnSC_MSx_VAL(FTM_CnSC_MSx_INPUT) | ((polarity == FALLING) ? FTM_CnSC_ELSx_VAL(FTM_CnSC_ELSx_INPUT_FALLING) : FTM_CnSC_ELSx_VAL(FTM_CnSC_ELSx_INPUT_RISING)) | FTM_CnSC_DMA_VAL(FTM_DMA_DISA)
    to
    #define FTM_CnSC_INPUT_REG_INIT FTM_CnSC_CHIE_VAL(FTM_CnSC_CHIE_ENABLE) | FTM_CnSC_MSx_VAL(FTM_CnSC_MSx_INPUT) | ((polarity == FALLING) ? FTM_CnSC_ELSx_VAL(FTM_CnSC_ELSx_INPUT_FALLING) : ((polarity == RISING) ? FTM_CnSC_ELSx_VAL(FTM_CnSC_ELSx_INPUT_RISING) : FTM_CnSC_ELSx_VAL(FTM_CnSC_ELSx_INPUT_EDGE))) | FTM_CnSC_DMA_VAL(FTM_DMA_DISA)
    you can use PPMInput(CHANGE, xxxx to capture single PWM pulses with hardware precision...?

    Thank you for those libraries.. you make my life MUCH easier!...

  5. #5
    Hi,

    I would simply like to send one PPM, 8 or 12 channels at best possible resolution to transmitters trainer port. I will use also SPI for reading encoders but nothing much else.

    What would be the best way to proceed, this library, the other library, or something else?

    I am using Teensy 3.2

  6. #6
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    15,729
    Quote Originally Posted by Garug View Post
    I would simply like to send one PPM, 8 or 12 channels at best possible resolution to transmitters trainer port.
    ...
    I am using Teensy 3.2
    Yup, use the PulsePosition library. When F_BUS is 48 MHz (Teensy runs at 48 or 96 MHz), the PPM output timing resolution is to the nearest 1/48e6, or about 21 ns!

  7. #7
    Thanks, I had some problems getting it working, because of some mine mistakes before finding the right example. Now running at 96 MHz, Do you mean it would be better to run 48 MHz?

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •