Skip to main content
3 of 3
Added photo of test setup.
Nick Gammon
  • 38.7k
  • 13
  • 67
  • 125

First, I want to thank you for making this post. I have a few remote-controlled cars/boats/helicopters, and I've often wondered how the remote control works. :)


To answer your question - and because I was too lazy to work out how to connect to an actual controller - I made a "remote simulator" like this:

// PPM generator

const byte SIGNAL_PIN = 3;
const int SIGNAL_COUNT = 6;
const unsigned long PULSE_WIDTH = 300;  // µs
const unsigned long BLANK_TIME = 25;    // ms
const int widths [SIGNAL_COUNT] = { 600, 2000, 1000, 1500, 2200, 2400 };

void setup ()
  {
  pinMode (SIGNAL_PIN, OUTPUT);
  }  // end of setup

void loop ()
  {
  delay (BLANK_TIME); 
  digitalWrite (SIGNAL_PIN, HIGH);  // start
  delayMicroseconds (PULSE_WIDTH);  
  digitalWrite (SIGNAL_PIN, LOW);   // end of start pulse
  
  for (byte i = 0; i < SIGNAL_COUNT; i++)
    {
    delayMicroseconds (widths [i] - PULSE_WIDTH);
    digitalWrite (SIGNAL_PIN, HIGH);  // start of pulse
    delayMicroseconds (PULSE_WIDTH);  
    digitalWrite (SIGNAL_PIN, LOW);   // end of pulse
    }  // end of for loop
   
  delay (1000);
  }  // end of loop

This is intended to generate a series of pulses (six in this case) which might be generated by some fancy toy fun device.


Output from it:

PPM generator

As you can see from the cursor, it is doing what it is bid. The pulse width shown by the scope cursor is indeed 2000 µs, as per the table of widths.

Note: Output on pin 3 on the Uno.


Now we need a decoder. So let's use interrupts:

const byte SIGNAL_PIN = 2;
const int SIGNAL_COUNT = 6;

volatile unsigned long widths [SIGNAL_COUNT + 1];
volatile byte count;

unsigned long lastPulse;

// ISR
void gotPulse ()
  {
  unsigned long now = micros ();
  
  // a long gap means we start again
  if ((now - lastPulse) >= 25000)
    count = 0;
    
  lastPulse = now;
  if (count >= (SIGNAL_COUNT + 1))
    return;
    
  widths [count++] = now;
  }
  
void setup ()
  {
  Serial.begin (115200);
  Serial.println ();
  attachInterrupt (0, gotPulse, RISING);
  EIFR = bit (INTF0);  // clear flag for interrupt 0
  }  // end of setup

void loop ()
  {
  if (count >= SIGNAL_COUNT)
    {
    Serial.println (F("Got sequence."));  
    for (int i = 1; i < SIGNAL_COUNT + 1; i++)
      {
      Serial.print ("Channel ");
      Serial.print (i);
      Serial.print (" = ");
      Serial.println (widths [i] - widths [i - 1]);
      }
    count = 0;      
    }
  
  }  // end of loop

This uses an interrupt (on pin 2 on the Uno) on the rising edge to detect the start of the pulses. It stores them, and then works out their widths later.


Results?

Got sequence.
Channel 1 = 612
Channel 2 = 2020
Channel 3 = 1016
Channel 4 = 1512
Channel 5 = 2220
Channel 6 = 2428
Got sequence.
Channel 1 = 612
Channel 2 = 2020
Channel 3 = 1016
Channel 4 = 1512
Channel 5 = 2220
Channel 6 = 2424
Got sequence.
Channel 1 = 616
Channel 2 = 2020
Channel 3 = 1012
Channel 4 = 1516
Channel 5 = 2220
Channel 6 = 2424
Got sequence.
Channel 1 = 616
Channel 2 = 2020
Channel 3 = 1012
Channel 4 = 1516
Channel 5 = 2216
Channel 6 = 2432

Highly consistent results, with a small error factor of around 16 µs compared to 600, 2000, 1000, 1500, 2200, 2400


In case it isn't clear, this test required two Unos. One to generate the pulses and a second one to decode them:

PPM Test setup

The one with the USB cable is the decoder, which writes to the serial port. The other one is powered by 5V/Gnd from the first one, and the white wire is the data with the pulses.

Nick Gammon
  • 38.7k
  • 13
  • 67
  • 125