The Syma S107G is a small coaxial infrared helicopter whose original transmitter continuously sends the positions of four controls. Reproducing that signal with an Arduino makes the aircraft accessible to joysticks, scripts, or a larger control system without modifying the helicopter itself.

The protocol described here was recovered from an original transmitter with an oscilloscope. It is a reverse-engineered device protocol, not a published Syma standard, so confirm the carrier, timings, and neutral values against your own transmitter before flying a different hardware revision.
How the Helicopter Is Controlled
The two main rotors turn in opposite directions. Their reaction torques largely cancel when both rotate at the same effective speed, so the helicopter does not need a conventional torque-cancelling tail rotor. The four transmitted values have separate jobs:
- Throttle: changes the combined lift of the main rotors.
- Yaw: changes the speed difference between the counter-rotating rotors.
- Pitch: drives the small tail motor to tilt the helicopter forward or backward.
- Trim: shifts the yaw-neutral point to compensate for a persistent turn.
Throttle ranges from 0 to 127. Yaw, pitch, and trim also use 7-bit values, with 63 as the nominal center. The two selectable transmitter channels occupy one additional bit in the throttle byte.
Parts and Test Equipment
- Syma S107G with its working transmitter
- Arduino Uno or another board supported by Arduino-IRremote
- 940 nm infrared LED with a known forward voltage and pulsed-current rating
- NPN switching transistor such as a 2N2222 or BC337
- Base resistor, LED current-limiting resistor, and a 47 kΩ base-emitter pull-down resistor
- Oscilloscope or logic analyzer and, preferably, a photodiode or 38 kHz IR receiver for verification
A phone camera can often reveal whether an IR LED emits at all, but it cannot verify the 38 kHz carrier, pulse timing, or optical power. Use an instrumented check before treating the aircraft as the test probe.
Build a Proper IR LED Driver
Do not drive a high-power IR emitter directly from an Arduino pin. Connect the GPIO through a base resistor to an NPN transistor, connect the emitter to ground, and place the IR LED and its current-limiting resistor between the positive supply and the collector. The Arduino and emitter supply must share a common ground. A 47 kΩ resistor from base to emitter keeps the transistor off while the controller resets.
The LED resistor follows
\[ R_{LED}=\frac{V_S-\sum V_F-V_{CE(\mathrm{sat})}}{I_{peak}}. \]
Choose the peak current from the LED data sheet, then verify the transistor's collector-current rating, power dissipation, and required base current. The carrier duty cycle reduces average current, but it does not make an excessive peak current safe.

The original experiment reused the transmitter's three-emitter string, 9 V supply, and matching series resistor. The following drawing records that donor-specific circuit. Its 10 Ω LED resistor and 220 Ω base resistor are not universal values: recalculate both if the supply, transistor, or LEDs differ.
The 32-Bit Control Frame
Each frame uses a 38 kHz carrier and pulse-distance encoding. A mark means that the carrier is present; a space means that the LED is off. The frame begins with a 2 ms mark and a 2 ms space, carries 32 data bits most-significant bit first, and ends with a 300 µs footer mark.
| Element | Mark | Space |
|---|---|---|
| Header | 2000 µs | 2000 µs |
| Bit 0 | 300 µs | 300 µs |
| Bit 1 | 300 µs | 700 µs |
| Footer | 300 µs | Frame gap |
The bytes appear on the wire in this order:
Byte 0: 0YYYYYYY yaw (0 left, 63 center, 127 right)
Byte 1: 0PPPPPPP pitch (0 backward, 63 neutral, 127 forward)
Byte 2: CTTTTTTT channel (C) and throttle (T)
Byte 3: 0AAAAAAA trim (63 nominal center) There is no checksum in this observed frame. Masking each control value to 7 bits is therefore important: otherwise a caller could accidentally overwrite a reserved leading bit or the channel selector.
Sending a Frame with Arduino-IRremote
Install Arduino-IRremote with the Arduino Library Manager. Current releases use IRremote.hpp and the global IrSender instance. The generic pulse-distance sender also emits the required stop mark unless its stop-bit flag is explicitly suppressed.
#define NO_LED_SEND_FEEDBACK_CODE
#include <IRremote.hpp>
constexpr uint8_t IR_SEND_PIN = 3;
uint32_t buildSymaFrame(uint8_t yaw, uint8_t pitch, uint8_t throttle,
uint8_t trim, uint8_t channel) {
return (uint32_t(yaw & 0x7f) << 24)
| (uint32_t(pitch & 0x7f) << 16)
| (uint32_t((throttle & 0x7f) | ((channel & 1) << 7)) << 8)
| uint32_t(trim & 0x7f);
}
void sendSymaFrame(uint8_t yaw, uint8_t pitch, uint8_t throttle,
uint8_t trim, uint8_t channel) {
const uint32_t frame = buildSymaFrame(yaw, pitch, throttle, trim, channel);
IrSender.sendPulseDistanceWidth(
38, // carrier frequency in kHz
2000, 2000, // header mark and space
300, 700, // one mark and space
300, 300, // zero mark and space
frame, 32,
IRDATA_FLAGS_IS_MSB_FIRST,
0, 0 // repeat period and repeat count
);
} A frame lasts between roughly 24 ms and 36 ms depending on how many one bits it contains. The 20 ms delay used below is an additional quiet gap after transmission, not a 20 ms start-to-start update period.
Serial Control with a Throttle Failsafe
A host can send six bytes: two synchronization bytes followed by throttle, yaw, pitch, and trim. The Arduino keeps transmitting the latest valid state. If the host disconnects or stops sending, the watchdog forces throttle to zero after 250 ms instead of repeating the last lift command indefinitely.
constexpr uint8_t SYNC_1 = 0xfe;
constexpr uint8_t SYNC_2 = 0xab;
constexpr unsigned long COMMAND_TIMEOUT_MS = 250;
uint8_t throttle = 0;
uint8_t yaw = 63;
uint8_t pitch = 63;
uint8_t trim = 63;
uint8_t channel = 0;
uint8_t receiveState = 0;
uint8_t pendingControl[4] = {0, 63, 63, 63};
unsigned long lastCommandMillis = 0;
bool hasValidCommand = false;
void receiveControlByte(uint8_t value) {
switch (receiveState) {
case 0:
receiveState = value == SYNC_1 ? 1 : 0;
break;
case 1:
receiveState = value == SYNC_2 ? 2 : (value == SYNC_1 ? 1 : 0);
break;
case 2:
pendingControl[0] = value & 0x7f;
receiveState = 3;
break;
case 3:
pendingControl[1] = value & 0x7f;
receiveState = 4;
break;
case 4:
pendingControl[2] = value & 0x7f;
receiveState = 5;
break;
case 5:
pendingControl[3] = value & 0x7f;
throttle = pendingControl[0];
yaw = pendingControl[1];
pitch = pendingControl[2];
trim = pendingControl[3];
lastCommandMillis = millis();
hasValidCommand = true;
receiveState = 0;
break;
}
}
void setup() {
Serial.begin(115200);
IrSender.begin(IR_SEND_PIN);
}
void loop() {
while (Serial.available() > 0) {
receiveControlByte(uint8_t(Serial.read()));
}
if (!hasValidCommand || millis() - lastCommandMillis > COMMAND_TIMEOUT_MS) {
throttle = 0;
}
sendSymaFrame(yaw, pitch, throttle, trim, channel);
delay(20);
} This compact serial packet has no checksum or sequence number. For an installation where a corrupted command matters, add both and update the control state only after validating the complete packet. A gamepad process should also apply a dead zone around centered axes and ramp throttle deliberately rather than mapping noisy inputs directly.
Commissioning Without an Accidental Takeoff
- Test the sender with throttle fixed at zero and verify the optical waveform with an instrument.
- Confirm that channel 0 or channel 1 matches the switch position used by the original transmitter.
- Restrain the airframe or remove the rotor blades before testing nonzero throttle.
- Check the watchdog by disconnecting the serial host and confirming that throttle returns to zero.
- Begin with a low throttle limit and increase it only after yaw, pitch, and trim directions are correct.
Operate the helicopter indoors, within line of sight, and away from faces, animals, and fragile objects. Infrared control is directional and can be disrupted by distance, bright ambient light, a blocked emitter, or another transmitter using the same channel. The receiver cannot distinguish a lost link from a deliberate end of transmission, which is why the controller should never rely on a stale nonzero throttle command.