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.