oPossum 1,083 Posted July 1, 2014 Share Posted July 1, 2014 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). 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. #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. servo_test.cpp zeke and simpleavr 2 Quote Link to post Share on other sites
zeke 693 Posted July 1, 2014 Share Posted July 1, 2014 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! Quote Link to post Share on other sites
simpleavr 399 Posted July 1, 2014 Share Posted July 1, 2014 How practical is it to realize this on IR led / detector for an 8 channel servo control? Quote Link to post Share on other sites
oPossum 1,083 Posted July 2, 2014 Author Share Posted July 2, 2014 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. Quote Link to post Share on other sites
oPossum 1,083 Posted July 2, 2014 Author Share Posted July 2, 2014 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. Quote Link to post Share on other sites
roadrunner84 466 Posted July 2, 2014 Share Posted July 2, 2014 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); } Quote Link to post Share on other sites
zeke 693 Posted July 2, 2014 Share Posted July 2, 2014 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? Quote Link to post Share on other sites
oPossum 1,083 Posted July 2, 2014 Author Share Posted July 2, 2014 Although one could argue that some parts are like reinventing the wheel. I reinvent the wheel to make it smaller, faster, and smoother. Quote Link to post Share on other sites
simpleavr 399 Posted July 2, 2014 Share Posted July 2, 2014 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 Quote Link to post Share on other sites
zeke 693 Posted July 2, 2014 Share Posted July 2, 2014 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! Quote Link to post Share on other sites
roadrunner84 466 Posted July 3, 2014 Share Posted July 3, 2014 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.... Quote Link to post Share on other sites
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.