Robert Eisele
Engineer, Systems Architect and DBA

Operate a Syma S107G Remote Control Helicopter with an Arduino

Some time ago I saw people reverse engineering the infrared protocol of the Syma S107G toy helicopter and I wanted to get my hands on it as well. It is a coaxial helicopter, which means that it doesn't require a torque cancelling tail rotor. Yaw is controlled by using the speed difference to a second rotor underneath the main rotor, which spins in the opposite direction. It's really astonishing what good quality you get for just 20 bucks. It crashed quite often and it still flies like new.

After analyzing the infrared data transfer with my oscilloscope attached to the infrared LED, I wanted to check the timings and found some really interesting websites, where people described their findings and I was able to improve my understanding of the protocol.

Parts needed

Previous Work

The journey began in 2012 when Mike Kohn brought up a circuit to jam the infrared signal in the office to declare no-fly zones. After this, Mike Field had problems analyzing the signal using an IR receiver and took on using a logic analyzer. Kerry Wong later used a photo diode to analyze the data stream with an oscilloscope and confirmed some previous findings.

All of them confirmed that the data is transferred using a 32 bit sequence, modulated on a 38kHz (about 13 microseconds high, 13 microseconds high) carrier frequency. Jim Hung took it even further, analyzed the protocol with a logic analyzer as well and was able to correct some timings and found out, how to use the second channel. Besides some software he published, he also wrote some specs.

Hacking forward

My own measures were not too bad and I was able to fly already. After researching the already mentioned previous works, I corrected the timings - even if it wasn't necessary, since the receiver is quite robust. At the beginning, I used one 5V IR LED, with which I was able to fly only about 50cm. The original remote control used 3 9V LED's, so I decided to rip it apart and build a new remote control. If you don't want to take this step, you need to buy 940nm 9V IR LED's, that's actually all I needed from it. After that you can build a circuit like on this schematic, using a transistor (BC 548C) and some resistors:

Protocol

Even if it was documented quite often already, I compiled a little overview when I implemented the protocol myself. The sequence looks as follows:

Overview:
HH 0YYYYYYY 0PPPPPPP CTTTTTTT 0AAAAAAA F
  Y - Yaw 0-127, (0=left, 63=center, 127=right)
  P - Pitch 0-127, (0=backwards, 63=hold, 127=forward)
  T - Throttle 0-127, (63=50%, 127=100%)

Timings:
HH - Header:
  2ms HIGH
  2ms LOW
32 bit Command:
  "0": 0.3ms HIGH, 0.3ms LOW
  "1": 0.3ms HIGH, 0.7ms LOW
F - Footer:
  0.3ms HIGH

Software

Most of the software that is around for a while works just fine. However, what I found was not really optimized or reusable. So I spend some time coming up with a really small and optimized routine. It's so small, it doesn't even need a cross-reference and can be posted right here. Everything that is needed is this little function now:

#define SET_HIGH(t)    TCCR2A |= _BV(COM2B1); delayMicroseconds(t) // set pwm active on pin 3
#define SET_LOW(t)     TCCR2A &= ~_BV(COM2B1); delayMicroseconds(t) // set pwm inactive on pin3
#define SET_LOW_FINAL()   TCCR2A &= ~_BV(COM2B1)

void sendCommand(uint8_t yaw, uint8_t pitch, uint8_t throttle, uint8_t trim, uint8_t channel) {
    uint8_t data[4];

    data[3] = yaw;
    data[2] = pitch;
    data[1] = throttle | (channel << 7);
    data[0] = trim;

    SET_HIGH(2000);

    SET_LOW(2000);

    for (int8_t i = 31; i >= 0; i--) {

        SET_HIGH(300); // 312?

        if ((data[i / 8] >> (i % 8)) % 2) {
            SET_LOW(700); // 712?
        } else {
            SET_LOW(300); // 312?
        }
    }

    // 0.3ms HIGH
    SET_HIGH(300); // 312?

    // LOW till the next interrupt kicks in
    SET_LOW_FINAL();
}

In order to make it working in Arduino, a proper setup() routine is needed. Additionally, I use the TimerOne library, which can be installed via the Arduino Library Manager.

#include <TimerOne.h>

uint8_t Throttle = 0; // 0-127
uint8_t Yaw = 63; // 0-127, center=63
uint8_t Pitch = 63; // 0-127, center=63
uint8_t Trim = 63; // 0-127, center=63
uint8_t state = 0;

void setup() {

    Serial.begin(9600);

    pinMode(3, OUTPUT);
    digitalWrite(3, LOW);

    //setup interrupt interval: 180ms
    Timer1.initialize(180000);
    Timer1.attachInterrupt(timerISR);

    //setup PWM: f=38Khz PWM=50%
    // COM2A = 00: disconnect OC2A
    // COM2B = 00: disconnect OC2B; to send signal set to 10: OC2B non-inverted
    // WGM2 = 101: phase-correct PWM with OCRA as top
    // CS2 = 000: no prescaling
    TCCR2A = _BV(WGM20);
    TCCR2B = _BV(WGM22) | _BV(CS20);

    // Timer value
    OCR2A = 8000 / 38;
    OCR2B = OCR2A / 2; // 50% duty cycle
}

To create a repeating data stream we need the callback for TimerOne:

void timerISR() {
    sendCommand(Yaw, Pitch, Throttle, Trim, 0);
}

If you want to control two helicopters, simply add another sendCommand line with channel set to 1. I previously published the node game controller library. To fly the helicopter with a Playstation 2 controller, I send a simple data stream via the serial interface to the Arduino like so:

void loop() {

  if (Serial.available() > 0) {

    uint8_t r = (uint8_t) Serial.read();

    switch (state) {
      case 0: if (r == 0xFF) state++; break;
      case 1: if (r == 0xAA) state++; break;
      case 2: Throttle = r; state++; break;
      case 3: Yaw = r; state++; break;
      case 4: Pitch = r; state++; break;
      case 5: Trim = r; state = 0; break;
    }
  }
}

And on the computer connected to the Arduino in JavaScript:

const Gamecontroller = require("gamecontroller");
const ctrl = new Gamecontroller("ps2");

ctrl.connect();

const SerialPort = require("serialport");
const serialPort = new SerialPort("/dev/cu.usbserial-A702YPTD", {
  baudrate: 9600,
  autoOpen: false
});

function clamp(x, a, b) {
  return Math.max(Math.min(x, b), a);
}

var IS_OPEN = false;

serialPort.open(function(err) {
  if (err) {
    console.log(err);
    return;
  }
  IS_OPEN = true;
});

const state = [
  0xFF,
  0xAA,
  0, // Throttle
  63, // Yaw
  63, // Pitch
  63 // Trim
];

ctrl.on("data", function(data) {

  if (!IS_OPEN)
    return;

  state[2]+= (-data['axis:JOYL:Y'] + 0x80) / 100;
  state[3] = 63 + (-data['axis:JOYR:X'] + 0x80) / 2;
  state[4] = 63 + (data['axis:JOYR:Y'] - 0x80) / 2;

  state[2] = clamp(state[2], 0, 127);
  state[3] = clamp(state[3], 0, 127);
  state[4] = clamp(state[4], 0, 127);

  serialPort.write(new Buffer(state));
});

The complete source can also be found on Github in my Fritzing collection.

You might also be interested in the following

Leave a comment

6 + 7