Real-Time Graphical Representation | S.BUS Protocol
See the introduction page -> here or by following the breadcrumbs in the path above.
Real-Time Graphical Representation of the 16 Channels S.BUS Protocol
To simulate the results of the S.BUS we built processing (www.processing.org) sketch to visualize the 16 channels of the S.BUS protocol in real time.
The processing.org sketch is Java based and almost identical to the Arduino IDE sketch (Arduino IDE derived from Processing); so this was quite easy to convert.
The graphics are rather rough at the moment; but good enough for what we want to use it for.
Thanks goes to Futaba for making this S.BUS protocol so challenging for everyone to understand. I'm sure there are very good reasons and advantages for this - but at first glance we often wondered about "why make things simple if we can make things extremely difficult...!"
The processing sketch is available from our GIThub (also included below but that is not update)
https://github.com/robotmaker/Futaba-S-BUS-16-Channel-Simulation/
To get this Processing sketch to work, you'll need to have the S.BUS connected to the X8R or X4R receiver (or any other that support S.BUS) which in turn is connected via an Arduino or FTDI USB to TTL serial converter thingy which connects to a USB connector on your PC.
As the S.BUS is an inverted signal (yes - to make life even more fun), you'll need to invert the signal using either:
Using a simple transistor+2 resistors to invert the signal ..or...
If you have an FTDI USB to TLL serial converter there is a program (called FT_Prog) on the ftdiChip.com webpage which allows you to configure signal inversions automatically. See this website for examples of how to connect up and make a cable....or...
If you have an X4R you can hack the receiver to obtain non-inverted data directly; without the need to invert the signal by fixing a connector to available position show here (I soldered directly to inverter pin as I wanted to add a connector later).
The approach
The following video explains the concept and some detail of the approach we used.
S.BUS Protocol explained
The processing sketch is available from our GIThub (also included below but that is not update)
https://github.com/robotmaker/Futaba-S-BUS-16-Channel-Simulation/
It was not easy to understand the S.BUS protocol as we couldn't find any specification of the protocol from the manufacturers. But thanks to all those who documented and shared their understandings, we eventually also got our processing sketch and Arduino sketch to work too.
To help others, we have also shared our approach. This webpage is a combination of lessons-learned from all others and our own experiences. Please Note: that this is merely our own interpretation of the S.BUS which is probably not be 100% correct. Our solution hasn't been stress tested or performance tested, so we don't recommend to use this method for your main control of a quadcopter. It's probably however OK to be used for non-critical actuations e.g. turning on lights, buzzers, cameras etc, and is not critical if something should go wrong.
Inverted signal: Which means the Arduino can't read this straight out-the-box without being inverted. We just used a simple transistor + resistor circuit to invert the signal. Others have used invertors. Unfortunately it's not possible to read inverted signals on the Arduino UART, but on some other microcontroller this is possible (e..g certain Microchip PIC controllers can be configured to read inverted signals)
Baud Rate: 100000 (Yes - that is a non-standard baud rate and rather fast for some micro-controller UARTs' to keep up with.)
Partity: Even
StopBits: 2
Packet size: 25 Bytes in one packet- which contains all 16 channels and fail-safe information
Channel Bits: The Channels are 11 bits in length, so the bytes need to be split-up (explained below)
Start Byte: is B1111000 (240) which arrives as MSB (i.e. highest bit arrives first). We decided not to convert this to MSB and so we searched for B00001111 (15) as the start byte instead.
Stop Byte: B0000000
The bytes arrive in MSB, but are assembled as little Endian, which means some bytes from the subdequent byte need to comes before the 1st byte (confused? - you are not alone, but this is explained more below)
Logic of S.BUS packet to channel conversions
The program starts by checking the serial port for data. If data arrives, it then does some validation of that data such as:
Is this a start byte?
Is the byte counter at 1? or are we in the middle of a packet?
Is the 25th byte a '0'? - then we have a good packet, other wise the packet has been lost some bytes
Conversion of S.BUS to create Channel 1:
Buffer[0] contains the start byte value of 15, so this is ignored. The first byte appears in Buffer[1]
The Channels are 11 bits in length, so we use the 1st bytes (8 bits) + the 1st 3 bits of the second byte. As this little Endian we actual need the last 3 bits of the second byte which is in fact the first bytes. Pictures normally help here.
The 1st channel = 1st byte + 3 bits from the 2nd byte
This channel packet of 11 bits is then read backwards.
As the inByte variable is already converted to integers we can move bits along as follows:
byte 1 = 00000000 00000011 (say the value is 3)
byte 2 = 01100100 (say the value of the 2nd byte is 100)
but we are only interested in the last 3 bits which need to be moved in front of the 1st byte,...
byte 2 = 01100100 00000000 - ...So shift this byte along left by 8 and it looks like this
byte 2 = | (or) the two bytes together and you get this
byte 1 = 00000000 00000011
byte 2 = 01100100 00000000
Channel 1 = 01100100 00000011 <- the intermediate result
But we were only interested in the 1st 3 bits of byte 2 so we need strip the 1st 5 bits by and-ing with 2047 (11 bits) & (and) the two together and you get this....
Channel1 = 01100100 00000011
2047 = 00000111 11111111
Channel1 = 00000100 00000011 <- the final result
The bytes are litte Endian which means that to read the full
Channels[1] = ((buffer[1]|buffer[2]<<8) & 0x07FF); //The first Channel
Conversion of S.BUS to create Channel 2:
2nd Channel = last 5 bit of byte2 and 8 bits of byte 3. Here we need to play around some more.
Remove the 1st 3 bits of byte2 by pushing them off to the right
byte 2 = 00000000 01100100
byte 2 = 00000000 00001100 moved 3 to the right
byte 3 = 00000000 01010100 lets say that byte 3 is this
byte 3 = 00001010 10000000 moved 5 to the left to get the 5 bytes we need in place (remember this is MSB so bits in byte 3 comes before bits in byte 2)
| (or) byte 2 and 3 them together to get the 11 bits in place for channels2
byte 2 = 00000000 00001100
byte 3 = 00001010 10000000
Channel2 = 00001010 10001100 < - result of| (or-ing) them together
again we were only interested in 11 bits so strip the package by and-ing with 2047 (11 bits)
& (and) the two together and you get this....
Channel2 = 00001010 10001100 < - interim result of| (or-ing) them together
2047 = 00000111 11111111
Channel2 = 00000010 10001100 < - result of & (and-ing) them together
Channels[2] = ((buffer[2]>>3|buffer[3]<<5) & 0x07FF);
Conversion of S.BUS to create Channel 3:
3rd Channel = last 2 bit of byte3 and 8 bits of byte 4 and some bits from byte 5.
Here we have even more to play around with. Someone with a sense of humour surely designed this.
Talk about "why make things simple if we can make it very difficult..."
Remove the 1st 6 bits of byte3 by pushing them off to the right
byte 2 = 00000000 00001100
byte 3 = 00000000 00000001 moved 6 to the right
byte 4 = 00000000 10101010 lets say that byte 4 is this
byte 4 = 00000010 10101000 moved 2 to the left to get the bytes we need in place
| (or) byte 3 and 4 together to get the 11 bits in place for channels3
byte 3 = 00000000 00000001
byte 4 = 00000010 10101000
Channel3 = 00000010 10101001 < - result of| (or-ing) them together
byte 5 = 00000000 10110101 lets say that byte 5 is this
byte 5 = 11010100 00000000 moved 10 to the left to get the bytes we need in place
Channel3 = 00000010 10101001 < - result of prrevious| (or-ing) them together
Channel3 = 11010110 10101001 <- result of or-ing shift byte 5 and previous or-ing
| (or) byte 5 together with the current channel 3 to get the 11 bits in place for channels3
again we were only interested in 11 bits so strip the package by and-ing with 2047 (11 bits)
& (and) the two together and you get this....
Channel3 = 11010110 10101001 <- result of or-ing shift byte 5 and previous or-ing
2047 = 00000111 11111111
Channel3 = 00000110 10101001 < - result of & (and-ing) them together
...and so on
Processing Sketch available for download on github
The processing sketch is available from our GIThub (also included below but that is not update)
https://github.com/robotmaker/Futaba-S-BUS-16-Channel-Simulation/
Processing Sketch
// Graphical represetentation of Futuba SBUS - also used in FrySky Receivers
// Example by Colin Bacon
// ROBOTmaker
// www.robotmaker.eu
//To get this to work you'll need to have SBUS conneted receiver that is connected via an Arduiono or FTDI USB thingy to your PC
//As the SBUS is an inverted signal (yes - to make life even more fun), you'll need to invert the signal using a simple transitor+2 restistors or if you have an FTDI there is a program to invert the signals automatically
//Further detail of how to do this are on our website
// References
//https://developer.mbed.org/users/Digixx/notebook/futaba-s-bus-controlled-by-mbed/
//http://www.clubamcl.fr/techniques/la-technologie-futaba-s-bus/
//Thanks goes to https://www.ordinoscope.net/index.php/Electronique/Protocoles/SBUS for the arduino code which I converted to processing
import processing.serial.*;
Serial myPort; // The serial port
float x, y;
String myString = null;
int [] buffer = new int [25];
int [] channels = new int [18];
int errors = 0;
boolean failsafe = false;
int idx, inByte, lost, packetErrors;
long last_refresh = 0;
float ymag = 0;
float newYmag = 0;
float xmag = 0;
float newXmag = 0;
void setup()
{
size(1500, 600, FX2D); //give good dashboard resolutions
// size(1500, 600, P3D); //used for drawing cubes
//fullScreen(); //This sets it to full screen, but comment out the size commmand above
pixelDensity(1);
x = width * 0.3;
y = height * 0.5;
// Variable for drawing the cube
// List all the available serial ports
printArray(Serial.list());
// Open the port you are using at the rate you want:
myPort = new Serial(this, Serial.list()[0], 100000,'E',8,2);
myString = myPort.readStringUntil(15); //clean the first buffer
println(myPort); //Debug to show the port opened
}
void draw()
{
//Check for incoming Serial Port Data
if (myPort.available() == 0){
//If no incoming data then count lost packages
packetErrors++;
textSize(14); //set text size
text ("No Data at Serial Port", 400,20);
}
//When data is comming in then check for a start byte No.1=B11110000 and stop byte No.25=B00000000
while (myPort.available() > 0) {
inByte = myPort.read(); //Read the Byte that just came in
//if it's a new packet and the start byte is not B00001111 (DEC15) then it's an error.
//Note that the SBUS byte data is MSB, which causes some fun later
if (idx == 0 && inByte != 0x0F) // error - wait for the start byte
{
text("Package Error", 100,100);
}
// if it's a new packet and the start byte is B00001111 (DEC15) then start reading the next 25 bytes.
else {
buffer[idx++] = inByte; // fill the buffer with 25 Bytes
}
// if the buffer of 25 Bytes is reached then start to decode
if (idx == 25)
{
idx = 0; //reset the buffer count for the next cycle
if (buffer[24] != 0x00)
{ //Check that the packet size is 25 bytes long with stop byte b00000000 as the last byte
errors++; //Count the number of errors
println(errors); //Print to the error totals to the dashboard
}
else
{ //Start decoding the bits and bytes
//Buffer[0] contains the start byte value of 15, so this is ignored
//The Channels are 11 bits in length, so the bytes need to be split-up
//To make this conversion more 'interesting' as the bytes arrive as MSB i.e. the highest byte arrives first and the channels are
//assembled as little endian, which means the 1st 3 bits of the second byte need to comes before the 1st byte.
//--------------Channel 1 -------------------
// The 1st channel = 1st byte + 3 bits from the 2nd byte
// This channel packet of 11 bits is then read backwards.
// As the inByte variable is already converted to integers we can move bits along as follows:
// byte 1 = 00000000 00000011 (say the value is 3)
// byte 2 = 01100100 (say the value of the 2nd byte is 100)
// but we are only interested in the last 3 bits which need to be moved infront of the 1st byte,...
// byte 2 = 01100100 00000000 - ...So shift this byte along left by 8 and it looks like this
// byte 2 = | (or) the two bytes together and you get this
// byte 1 = 00000000 00000011
// byte 2 = 01100100 00000000
// Channel 1 = 01100100 00000011 <- the intermediate result
// But we were only interested in the 1st 3 bits of byte 2 so we need strip the 1st 5 bits by and-ing with 2047 (11 bits)
// & (and) the two together and you get this....
// Channel1 = 01100100 00000011
// 2047 = 00000111 11111111
// Channel1 = 00000100 00000011 <- the final result
//The bytes are litte Endian which means that to read the full
channels[0] = ((buffer[1]|buffer[2]<<8) & 0x07FF); //The first Channel
//--------------Channel 2 -------------------
// 2nd Channel = last 5 bit of byte2 and 8 bits of byte 3. Here we need to play around some more.
// Remove the 1st 3 bits of byte2 by pushing them off to the right
// byte 2 = 00000000 01100100
// byte 2 = 00000000 00001100 moved 3 to the right
// byte 3 = 00000000 01010100 lets say that byte 3 is this
// byte 3 = 00001010 10000000 moved 5 to the left to get the 5 bytes we need in place (remember this is MSB so bits in byte 3 comes before bits in byte 2)
// | (or) byte 2 and 3 them together to get the 11 bits in place for channels2
// byte 2 = 00000000 00001100
// byte 3 = 00001010 10000000
// Channel2 = 00001010 10001100 < - result of| (or-ing) them together
// again we were only interested in 11 bits so strip the package by and-ing with 2047 (11 bits)
// & (and) the two together and you get this....
// Channel2 = 00001010 10001100 < - interim result of| (or-ing) them together
// 2047 = 00000111 11111111
// Channel2 = 00000010 10001100 < - result of & (and-ing) them together
channels[1] = ((buffer[2]>>3|buffer[3]<<5) & 0x07FF);
//--------------Channel 3 -------------------
// 3rd Channel = last 2 bit of byte3 and 8 bits of byte 4 and some bits from byte 5.
// Here we have even more to play around with. Someone with a sense of humour surely designed this.
// Talk about "why make things simple if we can make it very difficult..."
// Remove the 1st 6 bits of byte3 by pushing them off to the right
// byte 2 = 00000000 00001100
// byte 3 = 00000000 00000001 moved 6 to the right
// byte 4 = 00000000 10101010 lets say that byte 4 is this
// byte 4 = 00000010 10101000 moved 2 to the left to get the bytes we need in place
// | (or) byte 3 and 4 together to get the 11 bits in place for channels3
// byte 3 = 00000000 00000001
// byte 4 = 00000010 10101000
// Channel3 = 00000010 10101001 < - result of| (or-ing) them together
// byte 5 = 00000000 10110101 lets say that byte 5 is this
// byte 5 = 11010100 00000000 moved 10 to the left to get the bytes we need in place
// Channel3 = 00000010 10101001 < - result of prrevious| (or-ing) them together
// Channel3 = 11010110 10101001 <- result of or-ing shift byte 5 and previous or-ing
// | (or) byte 5 together with the current channel 3 to get the 11 bits in place for channels3
// again we were only interested in 11 bits so strip the package by and-ing with 2047 (11 bits)
// & (and) the two together and you get this....
// Channel3 = 11010110 10101001 <- result of or-ing shift byte 5 and previous or-ing
// 2047 = 00000111 11111111
// Channel3 = 00000110 10101001 < - result of & (and-ing) them together
// ...and so on
channels[2] = ((buffer[3]>>6 |buffer[4]<<2 |buffer[5]<<10) & 0x07FF);
channels[3] = ((buffer[5]>>1 |buffer[6]<<7) & 0x07FF);
channels[4] = ((buffer[6]>>4 |buffer[7]<<4) & 0x07FF);
channels[5] = ((buffer[7]>>7 |buffer[8]<<1 |buffer[9]<<9) & 0x07FF);
channels[6] = ((buffer[9]>>2 |buffer[10]<<6) & 0x07FF);
channels[7] = ((buffer[10]>>5|buffer[11]<<3) & 0x07FF);
channels[8] = ((buffer[12] |buffer[13]<<8) & 0x07FF);
channels[9] = ((buffer[13]>>3|buffer[14]<<5) & 0x07FF);
channels[10] = ((buffer[14]>>6|buffer[15]<<2|buffer[16]<<10) & 0x07FF);
channels[11] = ((buffer[16]>>1|buffer[17]<<7) & 0x07FF);
channels[12] = ((buffer[17]>>4|buffer[18]<<4) & 0x07FF);
channels[13] = ((buffer[18]>>7|buffer[19]<<1|buffer[20]<<9) & 0x07FF);
channels[14] = ((buffer[20]>>2|buffer[21]<<6) & 0x07FF);
channels[15] = ((buffer[21]>>5|buffer[22]<<3) & 0x07FF);
channels[16] = ((buffer[23]));
//channels[16] = ((buffer[23]) & 0x0001) ? 2047 : 0;
//channels[17] = ((buffer[23] >> 1) & 0x0001) ? 2047 : 0;
//failsafe = ((buffer[23] >> 3) & 0x0001) ? 1 : 0;
//if ((buffer[23] >> 2) & 0x0001) lost++;
}
} //For loop
//pushMatrix();
int pos1=0,channelText = 0;
//Set the background colour of the bargraphs
background(199);
//Cycle through all channels and draw a bar chart for each channel value
for(int c=0;c<16;c++){
//Set the colour of the bar chart graphics
noStroke(); //remove the graphic boarder lines
fill(17,37,204); //set the colour of bar chart
//Map Channel bar graph
float z= float(channels[c]); //convert to a float
float x1 = map(z,160,2000,0,width*0.95);
int yoffSet = 50;
pos1 = pos1 + 20; //y postion of bar on the canvas
channelText = channelText + 1;
rect(100, pos1+yoffSet , x1, 15);
fill(0);
text("Channel" + channelText + " = ", 5,pos1+10+yoffSet);
text(channels[c],70,pos1+10 + yoffSet);
//Draw the dashboard title and status messages
textSize(26);
text("Open TX SBUS - Status Dashboard", width/2-150, 30); //Place the text in the centre of the dashboard
textSize(10); //Reset text size
//If the bits in byte23 are set, then there is an error. Display a warning
if (buffer[23] >= 12) {
fill(226,72,47);
ellipse(50,40,20,20); //Draw a red warning led
fill(0); //colour the text
textSize(14); //Reset text size
text("Signal Lost",30,20);
textSize(10); //Reset text size
}
else
{
fill(22,205,65);
ellipse(50,40,20,20); //Draw a green warning led
fill(0); //colour the text
textSize(14); //Reset text size
text("Signal OK",30,20);
textSize(10); //Reset text size
}
//Place the error counts message
textSize(14); //Reset text size
text("Errors", 150,20);
textSize(11); //Reset text size
text("bytes23= ", 140,40);
text(buffer[23],190,40);
text("Count = ", 140,55);
text(errors, 190,55);
//Place the lost package message
textSize(14); //Reset text size
text("Lost Packages", 220,20);
textSize(11); //Reset text size
text(packetErrors, 250,40);
} //if
} //While
}
References
Thanks goes to Brian Benchoff for providing a lot of details on the hack-a-day website
http://hackaday.com/2012/04/05/reverse-engineering-a-fubata-sbus-remote-control/
https://developer.mbed.org/users/Digixx/notebook/futaba-s-bus-controlled-by-mbed/
http://www.clubamcl.fr/techniques/la-technologie-futaba-s-bus/
https://developer.mbed.org/users/Digixx/code/SBUS-Library_16channel/
http://forum.autoquad.org/download/file.php?id=2319&sid=b9bf8e54c8231cfb51691db4a3733673
http://diydrones.com/profiles/blogs/futaba-sbus-rc-protocol-reverse-engineered
FTDI
http://www.ftdichip.com/Support/Utilities/FT_Prog_v3.0.60.276%20Installer.zip
http://clipvideo.ga/video/_-EouL2nNgE/how-to-make-an-frsky-programming-cable.html
Keywords:
Drone, UAV, Quads, Quadcopeter
Unmanned Aerial Vehicle
Processing.org, processing, P5, Sketch
Arduino, Sketch
360 Degree Proximity Sensor
S.Bus, S-Bus, SBUS
Telemetry