Jump to content
43oh

RC PPM encode and decode


Recommended Posts

PPM (pulse position modulation) is a common protocol used with radio control. It used to be the over that air protocol, but has been mostly replaced with PCM (pulse code modulation). It lives on in the form of trainer ports, radio to tx module link, and even some modern PCM receivers can output PPM (typically on the battery connector). It is also a handy way to drive up to 8 servos using a single pin of the microcontroller (and a CD4017 decoder). PPM is sort of a concatenation of several (often 8) RC servo signals in succession. The time between narrow (a few hundred microseconds) pulses determines with width of each successive servo pulse. A duration of more than about 4000 microseconds designates the end of a frame. The typical total frame duration is 20,000 microseconds (20 ms).

 

post-229-0-40047400-1404173046_thumb.jpg

 

The PPM transmit code uses a single timer capture/compare unit in toggle mode. The initial state is set with OUTMOD_0 to allow for active low or active high output. The elegantly simple MSP430 timer makes this code very compact and efficient.

 

static void init_ppm_tx(unsigned pol)           // - Initialize PPM transmission
{                                               // pol = 0: idle low, pulse high
                                                // pol = 1: idle high, pulse low
                                                //
    P2DIR |= BIT0;                              // PPM output on P2.0
    P2SEL |= BIT0;                              // Enable timer compare output
    P2SEL2 &= ~BIT0;                            //
                                                //
    TA1CCTL0 = OUTMOD_0 | (pol ? OUT : 0);      // Set initial state of output (polarity)
    TA1CCTL0 = OUTMOD_4 | CCIE;                 // Set timer output to toggle mode, enable interrupt
    TA1CCR0 = TA1R + 1000;                      // Set initial interrupt time
}                                               //
                                                //
static unsigned st[8];                          // Servo transmit times
                                                //
static const unsigned pulse_duration = 200;     // Duration of on time of each pulse
static const unsigned frame_duration = 20000;   // Total duration of a complete frame
                                                //
#pragma vector = TIMER1_A0_VECTOR               // - ISR for PPM transmission
__interrupt void isr_ccr0(void)                 //
{                                               //
    static unsigned state = 0;                  // State / pulse index
    static unsigned td = 0;                     // Total duration of channel pulses
                                                //
    if(state < (sizeof(st) / sizeof(st[0])) * 2 + 1) { // Check if not done with all times
        if(state & 1) {                         // Alternate between rising & falling edges
                                                // Setup the time until the next rising edge
            const unsigned t = st[state >> 1];  // Get time from array
            TA1CCR0 += (t - pulse_duration);    // Add to falling edge time, compensate for pulse width
            td += t;                            // Update total frame duration
        } else {                                //
            TA1CCR0 += pulse_duration;          // Add pulse duration to rising edge time
        }                                       //
        ++state;                                // Increment state
    } else {                                    // Final pulse in frame (off time only)
        TA1CCR0 += (frame_duration - pulse_duration - td); // Set rising edge time to make desired frame duration
        td = 0;                                 // Reset total frame time
        state = 0;                              // Reset state
    }                                           //
}                                               //
                                                //
The PPM decoder is a bit more complicated. It checks for vaild pulse widths and ignores anything out of spec. Two capture/compare units are used. The first handles measuring the time between edges, and the second provides a timeout that detects an idle line (to prevent timer wraparound from creating alias pulses). Either the rising of falling edge can be used.

 

static void init_ppm_rx(unsigned edge)          // - Initialize PPM reception
{                                               // edge = 0: capture on rising edge
                                                // edge = 1: capture on falling edge
                                                //
    P2DIR &= ~BIT1;                             // PPM input on P2.1
    P2SEL |= BIT1;                              // Enable time capture input
    P2SEL2 &= ~BIT1;                            //
                                                //
    TA1CCTL1 = (edge ? CM_2 : CM_1) | CCIS_0 | CAP | CCIE; // CCR1 capture mode, enable interrupt
    TA1CCTL2 = CCIE;                            // CCR2 enable interrupt
}                                               //
                                                //
static unsigned sr[8];                          // Servo receive times
static unsigned rx_frame = 0;                   // Incremented after every received frame
                                                //
static const unsigned min_pw = 1500 - 1100;     // 400 us minimum
static const unsigned max_pw = 1500 + 1100;     // 2600 us maximum
static const unsigned min_reset = 4000;         // minimum time between frames
static const unsigned rx_timeout = 24000;       // maximum time between pulses
                                                //
#pragma vector = TIMER1_A1_VECTOR               // - ISR for PPM reception
__interrupt void isr_ccr12(void)                //
{                                               //
    static unsigned pt, et;                     // Previous time, elapsed time
    static unsigned state;                      // Received pulse index / state
    static unsigned pd[8];                      // Received pulse durations - the size of this array must
                                                //  match what the transmitter is sending
                                                //
    switch(TA1IV) {                             //
        case 0x02:                              // - CCR1
            et = TA1CCR1 - pt;                  // Calculate elapsed time since last edge
            pt = TA1CCR1;                       // Save current edge time
            if(et > min_reset) {                // Check for a pulse that is too long to be a channel
                                                // Check if all pulses received, and no more
                if(state == sizeof(pd) / sizeof(pd[0])) {
                    memcpy(sr, pd, sizeof(pd)); // Copy to foreground array
                    ++rx_frame;                 // Increment frame count
                }                               //
                state = 0;                      // Begin next frame
            } else if(et < min_pw || et > max_pw) { // Check if pulse is out of range
                state = 0x80;                   // Go to idle state
            } else {                            // Save pulse if room in array
                if(state < sizeof(pd) / sizeof(pd[0]))
                    pd[state] = et;             //
                if(state < 0x40) ++state;       // Next state - limit max value in case of noisy input
            }                                   //
            TA1CCR2 = pt + rx_timeout;          // Reset timeout
            break;                              //
        case 0x04:                              // - CCR2
            state = 0x81;                       // Go to timeout state
            break;                              //
    }                                           //
}                                               //
                                                //
Here is a simple test program that prints the pulse times. The times are represented as the deviation from 1500 microseconds. That is the typical center time for a RC servo. Each field is a fixed width with a sign followed by 3 digits. Jumper P2.0 to P2.1 to feed the PPM output to the PPM input.

 

post-229-0-60367000-1404173645_thumb.png

 

#include <msp430.h> 
#include <stdlib.h>

static void putc(char c)                        // -  Put char to serial
{                                               //
    while(!(IFG2 & UCA0TXIFG));                 //
    UCA0TXBUF = c;                              //
}                                               //
                                                //
static void put_pw(unsigned pw)                 // - Print pulse width to serial
{                                               // Sign and three digits
    unsigned n;                                 // Deviation from 1500 us
    if(pw < 1500) {                             // If less than 1500
        n = 1500 - pw;                          // Calculate deviation
        putc('-');                              // Print sign
    } else {                                    // Equal to or more than 1500
        n = pw - 1500;                          // Calculate deviation
        putc((pw == 1500) ? ' ' : '+');         // Print sign
    }                                           //
    div_t d = div(n, 100);                      //
    putc(d.quot + '0');                         // First digit - hundreds
    d = div(d.rem, 10);                         //
    putc(d.quot + '0');                         // Second digit - tens
    putc(d.rem + '0');                          // Third digit - ones
}                                               //
                                                //
static const unsigned long smclk_freq = 8000000UL; // SMCLK frequency in hertz
static const unsigned long bps = 9600UL;        // Async serial bit rate
                                                //
int main(void)                                  //
{                                               //
    const unsigned long brd = (smclk_freq + (bps >> 1)) / bps;  // Bit rate divisor
                                                //
    WDTCTL = WDTPW | WDTHOLD;                   //
                                                //
    DCOCTL = 0;                                 // Run DCO at 8 MHz
    BCSCTL1 = CALBC1_8MHZ;                      //
    DCOCTL  = CALDCO_8MHZ;                      //
                                                //
    P1DIR = 0;                                  //
    P1SEL = BIT1 | BIT2;                        // Enable UART pins
    P1SEL2 = BIT1 | BIT2;                       //
                                                //
    P2DIR = 0;                                  //
    P2SEL = 0;                                  //
    P2SEL2 = 0;                                 //
                                                //
    TA1CTL = TASSEL_2 | ID_3 | MC_2;            // Setup timer 1 for SMCLK / 8, continuous mode
                                                //
    init_ppm_rx(0);                             // Initialize PPM receive
                                                //
    init_ppm_tx(0);                             // Initialize PPM transmit
                                                //
                                                // Initialize UART
    UCA0CTL1 = UCSWRST;                         // Hold USCI in reset to allow configuration
    UCA0CTL0 = 0;                               // No parity, LSB first, 8 bits, one stop bit, UART (async)
    UCA0BR1 = (brd >> 12) & 0xFF;               // High byte of whole divisor
    UCA0BR0 = (brd >> 4) & 0xFF;                // Low byte of whole divisor
    UCA0MCTL = ((brd << 4) & 0xF0) | UCOS16;    // Fractional divisor, oversampling mode
    UCA0CTL1 = UCSSEL_2;                        // Use SMCLK for bit rate generator, release reset
                                                //
                                                //
    st[0] = 1000;                               // Setup servo transmit times
    st[1] = 1200;                               //
    st[2] = 1400;                               //
    st[3] = 1500;                               //
    st[4] = 1600;                               //
    st[5] = 1800;                               //
    st[6] = 2000;                               //
    st[7] = 1520;                               //
                                                //
    _enable_interrupts();                       // Enable all interrupts
                                                //
    for(; {                                   //
        unsigned n;                             //
                                                // Print received pulse times to serial
        for(n = 0; n < sizeof(sr) / sizeof(sr[0]); ++n)
            put_pw(sr[n]);                      //
        putc('\r'); putc('\n');                 //
    }                                           //
                                                //
    return 0;                                   //
}                                               //
A bargraph can be displayed with some custom software.

 

post-229-0-82057000-1404173746_thumb.png

servo_test.cpp

 

Link to post
Share on other sites

First thing. Your coding style is definitely ASM!  Did you start with the PIC or the x86?

 

Next, I am sad now because I gave away all my old Airtronix RC transmitters six months ago. I could have used them to try this out.

 

 

What other awesome projects are you working on?

 

Good work @@oPossum!  

Link to post
Share on other sites

First thing. Your coding style is definitely ASM!  Did you start with the PIC or the x86?

 

 

Started with the RCA CDP1802 in the early 80's, then the Motorola 6809 and Zilog Z80. After that is quite a blur - learned so many over the next 10 years. Started using PIC in the late 80's. Latest chip for me is the Parallax Propeller - it is very nice but not easy to work with.

 

What other awesome projects are you working on?

 

 

Several things related to building a quadcopter. There will be future postings on conversion of old PPM RC radios to PCM. This post was just the first step. Next up will be TMS (transition minimized signaling) using a 8b/11b code I have developed.

 

Also working on a MIDI driven laser spirograph. Have to build the galvos next.

 

Link to post
Share on other sites

How practical is it to realize this on IR led / detector for an 8 channel servo control?

 

I think that would work. Range could be up to about 20 feet if a modulated (~38 kHz) carrier is used. It should work about as well as a TV remote - not perfect but good enough for some applications.

Link to post
Share on other sites

First thing. Your coding style is definitely ASM!  Did you start with the PIC or the x86?

What makes you think this is ASM style? I code in a similar fashion, and I think it is C code.

 

Although one could argue that some parts are like reinventing the wheel. You could use this easier to read routine, but it might be significantly slower:

static void put_pw(unsigned pw)                 // - Print pulse width to serial
{
  if (pw == 1500) printf(" 000\n\r");
  else printf("%+03d\n\r", pw - 1500);
}
Link to post
Share on other sites

Uhm, I think my words are unclear. I am attempting to describe oPossum's typesetting format.

 

When I look at oPossum's coding style, I immediately recognize that he writes his C code in the format of an assembly language programmer.

 

I have noticed that his variable name choices also indicate that he has many hours coding in assembly.

 

I know he is writing C code but his style has all the hallmarks of an assembly programmer. Efficient, lean, quickly to the point type of coding.

 

Is that more clear?

Link to post
Share on other sites

Uhm, I think my words are unclear. I am attempting to describe oPossum's typesetting format.

 

 

It looks more COBOL to me. I hate it when I steal code from him. Anything I change mess up the file and I have to re-adjust lines. I always do this 1st thing

:1,$s/\s*\/\/.*$//g
Link to post
Share on other sites

 

It looks more COBOL to me. I hate it when I steal code from him. Anything I change mess up the file and I have to re-adjust lines. I always do this 1st thing

:1,$s/\s*\/\/.*$//g

 

Oh Crap! RUN!!!  It's a perl substitution command!!

 

I have no idea what it means but I looks like it's going to kill me!

:biggrin:  :wink:

Link to post
Share on other sites

I think it means

:1,$ from the first line until the last line

s/ substitute

\s* any number of spaces

\/\/ followed by "//"

.* and then any number of characters

$ followed by an end of line

// and substitute it with nothing

g for all matching occurrences.

 

But my regex is a little rusty....

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...