I have worked with two types of S107G helicopters. One is a 2-channel (A and B) and the other is a 3-channel (A, B, and C) version. Their protocols differ significantly. The more common 2-channel (32-bit) version’s protocol is well documented elsewhere, so here I will only document the 3-channel (30-bit) version.
(First posted at rcgroups.)
The protocol for this is 30 bits long.
The order of the bits is as follows:
CC00 PPPP TTTT TTTT YYYY XXXX RRRR RR
C – channel
P – pitch
T – throttle
Y – yaw
X – checksum
R – trim
There are a few other things to note:
1) It has a checksum. The 21-24th bits are a bitwise XOR of 4-bit words with the two zeros appended to the end of the bitstring. Thus you can compute the checksum for the first packet:
1000 0000 1000 1100 0000 1111 1111 11
with:
1000 ^ 0000 ^ 1000 ^ 1100 ^ 0000 ^ 1111 ^ 1100 = 1111
and for the second packet:
1000 0000 0011 1001 0000 0001 1111 11
with
1000 ^ 0000 ^ 0011 ^ 1001 ^ 0000 ^ 1111 ^ 1100 = 0001
2) The bits in the throttle, pitch, and yaw are reversed, so for throttle bit 16 is the most significant bit with bit 9 being the least significant for throttle.
3) Bit 20 is the direction bit for yaw and then bits 19-17 (aka reversed) are the speed bits. So for full yaw in one direction you do 1111 and for full yaw in the other direction you do 1110
4) Bit 8 is the direction bit for pitch with the speed bits being just like yaw.
5) The trim bits are probably reversed or something like that too but I just set them to 0 and didn’t worry.
Here’s my code for an Arduino. To use the code, open the Serial Monitor (Tools > Serial Monitor) and use the following commands:
/* S107 3-channel with checksum helicopter control code
* Copyright (C) 2012, Andrew Barry, Dan Barry
*
* Uses an Arduino to control a S107 helicopter
*
*
* Instructions:
* Connect an IR LED array to pin 8 (using a FET to amplify the signal)
* and use the serial monitor to send commands to the system
*
*/
#define LED 8
#define STATUS 13
//#define TAKEOFF_THROTTLE 240
//#define HOLDING_THROTTLE 130
byte yawCmd, pitchCmd, throttleCmd, trimCmd;
// Set this value for the default channel
// A = 0
// B = 1
// C = 2
byte channel = 0;
/*
* Setup function that initializes the serial port and
* sets some default values for the control variables.
* Also sets up the pins we'll be using.
*/
void setup()
{
Serial.begin(9600);
pinMode(STATUS,OUTPUT);
digitalWrite(STATUS,LOW);
pinMode(LED,OUTPUT);
digitalWrite(LED,LOW);
yawCmd = 8;
pitchCmd = 8;
trimCmd = 0;
throttleCmd = 0;
Serial.println("throttle = 0, standing by for commands.");
}
/*
* Function that does the actual work of converting commands into
* IR LED pulses and changes the pins in the appropriate manner.
*/
byte sendPacket(byte yaw, byte pitch, byte throttle, byte trim)
{
int packetData[100];
int pulseNum;
digitalWrite(STATUS,HIGH);
float channelDelayValue = 136500;
// channel A B or C
// A is 10 with 136500us packet delay
// B is 01 with 105200us packet delay
// C is 11 with 168700us packet delay
if (channel == 0)
{
packetData[0] = 1;
packetData[1] = 0;
channelDelayValue = 136500;
} else if (channel == 1)
{
packetData[0] = 0;
packetData[1] = 1;
channelDelayValue = 105200;
} else {
packetData[0] = 1;
packetData[1] = 1;
channelDelayValue = 168700;
}
packetData[2] = 0;
packetData[3] = 0;
// pitch
packetData[7] = (pitch & 0b1000) >> 3; // direction bit
if (pitch < 8)
{
pitch = 8 - pitch;
}
packetData[6] = (pitch & 0b0100) >> 2; // others are speed bits, note that they are reversed
packetData[5] = (pitch & 0b0010) >> 1;
packetData[4] = (pitch & 0b0001);
// throttle
// bits are reversed in the throttle command
packetData[15] = (throttle & 0b10000000) >> 7;
packetData[14] = (throttle & 0b01000000) >> 6;
packetData[13] = (throttle & 0b00100000) >> 5;
packetData[12] = (throttle & 0b00010000) >> 4;
packetData[11] = (throttle & 0b00001000) >> 3;
packetData[10] = (throttle & 0b00000100) >> 2;
packetData[9] = (throttle & 0b00000010) >> 1;
packetData[8] = (throttle & 0b00000001);
// yaw
packetData[19] = (yaw & 0b1000) >> 3; // direction bit
if (yaw < 8)
{
yaw = 8 - yaw;
}
packetData[18] = (yaw & 0b0100) >> 2;
packetData[17] = (yaw & 0b0010) >> 1;
packetData[16] = (yaw & 0b0001);
// these 4 bits are the checksum, so make sure they
// are 0s so they don't change the XOR later on
packetData[20] = 0;
packetData[21] = 0;
packetData[22] = 0;
packetData[23] = 0;
// yaw trim / yaw adjust (the little dial on the controller)
// 6 bits
packetData[24] = 0;
packetData[25] = 0;
packetData[26] = 0;
packetData[27] = 0;
packetData[28] = 0;
packetData[29] = 0;
// these bits are never sent but we do the checksum
// computation in 4-bit chunks, with the trailing two
// bits set to zero, so we set them to zero here to make
// the checksum a bit easier to compute
packetData[30] = 0;
packetData[31] = 0;
int i;
int checksum[10];
checksum[0] = 0;
checksum[1] = 0;
checksum[2] = 0;
checksum[3] = 0;
// compute checksum -- bitwise XOR of 4-bit chuncks
// with two zeros padding the *end* of the last two bits
for (i=0; i<32; i+=4)
{
// XOR
checksum[0] = packetData[i + 0] ^ checksum[0]; // the "^" operator is bitwise XOR (exclusive OR)
checksum[1] = packetData[i + 1] ^ checksum[1];
checksum[2] = packetData[i + 2] ^ checksum[2];
checksum[3] = packetData[i + 3] ^ checksum[3];
}
// now set bits 21-24 (array values 20-23) to the checksum
packetData[20] = checksum[0];
packetData[21] = checksum[1];
packetData[22] = checksum[2];
packetData[23] = checksum[3];
/*
* Uncomment for realtime display of packet data
*/
/*
for (i=0; i<30; i++)
{
Serial.print(packetData[i]);
if ((i+1)%4 == 0)
{
Serial.print(" ");
}
}
Serial.println(" ");
*/
// Send the packet by flashing the LEDs. Also remember how long the packet takes to send
// so we can properly compute how long to wait before sending the next packet.
int bitsum = 0;
for (i=0; i<30; i++)
{
// a "0" bit is 16 pulses and a "1" bit is 32 pulses
if (packetData[i] == 1)
{
bitsum ++;
pulseNum = 32;
} else {
pulseNum = 16;
}
// flash pulseNum times
// a "0" bit is 16 pulses and a "1" bit is 32 pulses
while(pulseNum--)
{
digitalWrite(LED,LOW);
delayMicroseconds(9);
digitalWrite(LED,HIGH);
delayMicroseconds(8);
}
// there is a 300 microsecond delay between pulses of the LED
delayMicroseconds(300);
}
// channel A B or C
// A is 10 with 136500us packet delay
// B is 01 with 105200us packet delay
// C is 11 with 168700us packet delay
//
// that is the delay between sending 30 bit packets
// note that this does not change if our packets are longer
// or shorter, so we must take that into account
return((channelDelayValue - bitsum * 272)/1000); // in ms.
}
void HoldCommand(int yawIn, int pitchIn, int throttleIn, int delayTime)
{
Serial.print("Holding: Yaw:");
Serial.print(yawIn);
Serial.print(" Pitch: ");
Serial.print(pitchIn);
Serial.print(" Throttle: ");
Serial.print(throttleIn);
Serial.print(" for ");
Serial.print(delayTime);
Serial.println("ms");
int i;
int delayConst = 50;
int delayAmount = delayTime/delayConst;
int packetDelay;
while (delayTime > 0)
{
if (Serial.available() == true)
{
Serial.println("HOLD ABORTED");
break;
}
packetDelay = sendPacket(yawIn, pitchIn, throttleIn, trimCmd);
delayTime = delayTime - packetDelay;
delay(packetDelay);
delay(delayAmount);
delayTime = delayTime - delayAmount;
}
Serial.println("Done holding.");
}
void Land()
{
static int i;
Serial.println("Landing");
for(i=throttleCmd;i>0;i--){
HoldCommand(8,8,throttleCmd,50);
}
throttleCmd = 0;
}
/*
* Function that manages recieving data from the serial port.
* Mostly changes the global variables that are passed to the
* control functions.
*/
void serialEvent()
{
char cmd = Serial.read();
Serial.println();
Serial.print("command received is ");
Serial.println(cmd);
switch (cmd)
{
// Take off with 't'
case 't':
Serial.println("Taking Off");
// Yaw: 1-15
// 8 = no turn
// 1 = max right turn
// 15 = max left turn
//
// Pitch: 1-15
// 8 = no pitch
// 15 = max forward
// 1 = max backwards
//
// Throttle: 0-255
// 0 = off
// ~130 = steady flight
// ~240 = fast climb
// First, go up with lots of throttle for 650ms
// yaw: 8 --> no yaw
// pitch: 8 --> no pitch
// throttle: 240 --> fast climb
// delay: 650ms --> enough time to climb, not too long so won't hit ceiling
// HoldCommand: a function that sends the same data for a given amount of time
// HoldCommand(yaw, pitch, throttle, time-to-hold-in-ms);
HoldCommand(8, 8, 240, 650);
// set the *global* throttle to steady flight throttle
throttleCmd = 130;
break;
// land with 'x' or 'q'
case 'x':
case 'q':
Land();
break;
// throttle commands
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
throttleCmd = atoi(&cmd) * 25; //single character, so we can go from 0 to 255 by inputting 0 to 9 in the serial monitor
break;
// turn left
case 'a':
if (yawCmd < 15)
{
yawCmd ++;
}
Serial.print("Yaw is ");
Serial.println(yawCmd);
break;
// turn right
case 'd':
if (yawCmd > 1)
{
yawCmd --;
}
Serial.print("Yaw is ");
Serial.println(yawCmd);
break;
// move forwards
case 'w':
if (pitchCmd < 15){
pitchCmd ++; // moves forward
}
Serial.print("Pitch is ");
Serial.println(pitchCmd);
break;
// move backwards
case 's':
if (pitchCmd > 1)
{
pitchCmd --; // moves backward
}
Serial.print("Pitch is ");
Serial.println(pitchCmd);
break;
// increase throttle
case 'u':
if (throttleCmd < 255 - 6)
{
throttleCmd += 6;
}
Serial.print("Throttle is ");
Serial.println(throttleCmd);
break;
// decrease throttle
case 'j':
if (throttleCmd > 6)
{
throttleCmd -= 6;
}
Serial.print("Trottle is ");
Serial.println(throttleCmd);
break;
// change channel
case 'c':
Serial.println("Changing channel");
if (channel >= 2)
{
channel = 0;
} else
{
channel ++;
}
Serial.print("Channel is: ");
Serial.println(channel);
break;
// reset yaw and pitch
case 'r':
Serial.println("resetting yaw and pitch");
yawCmd = 8;
pitchCmd = 8;
break;
default:
Serial.println("Unknown command");
}
Serial.print("Throttle is at ");
Serial.println(throttleCmd);
}
/*
* Loops continuously sending and delaying for the transmission
*/
void loop()
{
// Note that serialEvent() gets called on each path of the loop
// and runs if there is data at the serial port
// we call delay here on the return value of sendPacket because that will
// cause us to put the right amount of time between packets. The delay is
// not constant, but is instead based on how long the packet was
// that we sent
delay(sendPacket(yawCmd, pitchCmd, throttleCmd, trimCmd));
}
03/21/12