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.
servo_test.cpp 3.59KB
0 downloads
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 3.59KB
0 downloads