SBUS to PPM and PWM Decoder Using Arduino Timer Interrupts. PART 1: SBUS PC Joystick

by blopa1961 in Circuits > Arduino

3488 Views, 7 Favorites, 0 Comments

SBUS to PPM and PWM Decoder Using Arduino Timer Interrupts. PART 1: SBUS PC Joystick

SBUS2PPM2PWM12_Nano.jpg

This 4 part project is a set of programs used to decode Futaba’s Serial Bus (SBUS) protocol and output the received values via a Serial port, a PPM stream (for use with flight simulator USB dongles) and/or multiple PWM servo outputs (up to 12). The input is always an inverted pulse train which must be connected to a hardware serial port in the target MCU.

This is PART 1: Decoding SBUS and an SBUS PC Joystick

Click here to go to PART 2: SBUS to PPM (Trainer Port) Converter

Click here to go to PART 3: Porting to ESP01 and STM32F103

Click here to go to PART 4: SBUS to Servo decoder and PPM to Servo decoder

Full source code is included in this instructable and the latest version is also maintained in Github

Target audience:

The project is targeted at Arduino developers who would like to learn and understand how to use bare bones hardware timer interrupts for various Arduino processors, namely Pro Micro (ATMega32U4), Nano (ATMega328P), STM32F103 (bluepill) and ESP8266 (ESP01, ESP12, nodeMCU, Wemos D1 mini, etc) by analyzing the source code which does not use third party libraries nor external calls. The beauty of this project is that it has no display, no buttons to debounce, no external hardware. It’s a simple signal processor nicely fitted to learn interrupts.

The project is also targeted at radio control hobbyists who have knowledge of Arduino and its IDE. It’s not meant as a way to learn Arduino nor to teach how to setup a programming environment for the target MCU (like the ST-Link V2 necessary to program the bluepill MCU).

I will assume you know all this and have some knowledge of electronics (resistors, transistors, Arduinos, etc.)

You will need to build a signal inverter with a single NPN transistor and a couple of resistors. In the case of the ESP01 circuit below, the signal inverter will also work as a level shifter from 5V to the 3.3V required by the MCU.

Disclaimer:

No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO THE SUBJECT SOFTWARE.

In other words: use at YOUR OWN RISK, if you crash a 50000 dollar R/C Jet because an SBUS decoder fails or stops responding you are on your own. As a matter of fact, if you have a 50000 dollar R/C jet (or a 100 dollar balsa R/C plane for that matter) I suggest you buy a commercial SBUS decoder when needed.

License:

Attribution Non-commercial (by-nc)

Description:

These interrupt routines are different for every MCU platform and sometimes require low level access to MCU registers (i.e. ATMega328P and ATMega32U4 which share the same register structure), sometimes interrupts are directly supported by the board manager (but not clearly documented), but overall, the code is highly incompatible between different MCU architectures.

To make the code clear I decided to create an individual sketch for each MCU architecture and refrained from using conditional ifs to compile a single sketch with multiple MCU architectures and different interrupt structures. You will find a list of target MCUs in the header of each .ino file. Unless explicitly mentioned, no third party libs are used.

All the sketches in this instructable were created using Arduino IDE 2.2.1 with the latest Board Manager of each target MCU.


PART 1:

Inverting the signal, decoding SBUS data and making an HID PC Joystick.

Supplies

Inverter33BOM.jpg
  • Arduino Pro Micro or Leonardo (be careful, the Pro Mini is not a the same as the Pro Micro) with USB cable
  • Transistor: BC337 or BC547
  • Resistors: 2K2, 4K7 and 10K
  • Receiver plug: male connector (from a Futaba extension cable) or equivalent Dupont connector
  • Dupont connectors to connect the MCU to the inverter
  • A piece of perforated prototyping PCB (see photo)
  • A piece of exposed wire or Wire Wrapping wire
  • A piece of heat shrink tube (or electrical tape) to wrap the inverter

Signal Inverter and the SBUS Protocol

Inverter_Sch.jpg
Inverter5_PCB.jpg
Inverter3_PCB.jpg

Signal Inverter:

Let’s start by building a simple signal inverter, which is required for all these sketches. I’m including a simple PCB design that you can build in a perforated board (see photo) or protoboard. The photos above show the schematic and PCBs for both 5V (Inverter5_PCB) and 3.3V MCUs (Inverter3_PCB).

You should build the 5V inverter for most of these devices. It’s fit for all Arduinos in this project except the ESP8266 MCUs.

In PART 3 of this instructable we will build an ESP01 SBUS converter which has its own level shifter. You will not need this inverter for that project. Build the 3.3V level shifting inverter if you use a nodeMCU or Wemos D1 mini.

Futaba’s SBUS protocol (signal and protocol analysis):

The SBUS protocol uses a 25 byte packet made up of a bit stream containing 16 proportional channels with 11 bit resolution (2048 possible values) each. Two digital channels and failsafe information are packed in a trailing byte. All packets start with 0x0F and end with a 0 (zero). So, we have 0x0F, 16 x 11 bits= 22 bytes, the 24th byte containing failsafe flags and 2 digital channels, and the 25th byte which must be 0 (EOT).

The 25 byte stream is repeated every 14 milliseconds or 7 mS for high speed SBUS2. Different brand receivers will repeat the signal at different intervals and some receivers extend the protocol to accommodate more channels and/or telemetry data. We will only focus in the standard SBUS protocol in this instructable.

The SBUS protocol data uses a tweaked standard serial stream where the signal is inverted and transmitted at 100000 baud (sic, it is NOT 115200) with 2 stop bits and even parity. With the use of the hardware signal inverter described above we can directly receive the 25 byte packets using the MCUs’ standard UARTs.

CODE:

Serial1.begin(100000, SERIAL_8E2);


Decoding SBUS Data

SBUS2Joy.jpg

So, now that we know what is coming from the SBUS let’s create a sketch that will read, decode and dump the data to the PC so we can analyze it and make sure we are receiving what we expect.

For this sketch we only need the Arduino Pro Micro (with an USB cable for the PC) and the inverter connected to an SBUS R/C receiver. The Pro Micro’s RX1 (Serial1) port is connected via the inverter to the SBUS port of the receiver. The USB goes to the PC which should be running the Arduino IDE’s Serial Monitor or a terminal program at 115200 baud. The receiver is powered by the PC’s USB 5V by connecting the Pro Micro’s VCC pin to the inverter (which has power pass-through). This setup will also work for the Joystick we will build in the next step.

The following sketch decodes SBUS and outputs the channel values and flags (frame lost, failsafe) to the serial port connected to your PC at 115200 baud via the MCU’s USB serial bridge. It requires an MCU with at least 2 serial ports; one UART feeds Arduino’s USB (connected to the PC) and the other UART is used to receive the SBUS packets. It will work with Arduino Pro Micro, Leonardo or STM32F103. It should also work with a Mega or Mega Pro but I don’t have one to test it.

CODE:

// SBUS decoder - (c) 2023 Pablo Montoreano

/*********************************************************
  @file       SBUS_Decoder.ino
  @brief      SBUS protocol decoder
  @author     Pablo Montoreano
  @copyright  2023 Pablo Montoreano
  @version    1.1 - 05/oct/23  - bug fix (0x0F is a valid SBUS value)

  no 3rd party libraries used

  for Arduino Pro Micro (ATMega32U4) or Arduino Leonardo (2 serial ports are required)
  also works with STM32F103 (BluePill)
  does not work with Arduino Nano because it has a single serial port
*********************************************************/

// Arduino Pro Micro/Leonardo: connect inverted SBUS signal to RX1

// STM32F103: connect inverted SBUS to pin RXD1 (A10)
// enable CDC generic serial in Arduino IDE and connect USB to top connector

//  output to PC @115200 via USB

static unsigned int sbusByte, byteNmbr;
static byte frame[25];  // 25 bytes per SBUS frame
static unsigned int channel[17]; // 16 channels in SBUS stream + channels 17, 18 & failsafe in channel[0]
static unsigned int i; // a counter
static bool newFrame;

void decodeChannels() {
int bitPtr;   // bit pointer in SBUS byte being decoded
int bytePtr;  // byte pointer in SBUS frame
int chan;     // channel number being decoded
int chanBit;  // current channel bit being proccessed

  channel[0]= frame[23];
  bytePtr= 1;
  bitPtr= 0;
  for (chan= 1; chan <= 16; chan++){
    channel[chan]= 0;
    for (chanBit= 0; chanBit < 11; chanBit++) {
      channel[chan] |= ((frame[bytePtr] >> bitPtr) & 1) << chanBit;
      if (++bitPtr > 7) {
        bitPtr=0;
        bytePtr++;
      }
    }
  }
}

bool getFrame() {
  while (Serial1.available()) {
    sbusByte= Serial1.read();
// Bug fix: 0x0F is a valid value in the SBUS stream
// so we use a flag to detect the end of a packet (0) before enabling the capture of next frame
    if ((sbusByte == 0x0F) && newFrame) { // if this byte is SBUS start byte start counting bytes
      newFrame= false;
      byteNmbr= 0;
    }
    else if (sbusByte == 0) newFrame= true; // end of frame, enable start of next frame (to distinguish from 0x0F channel values)
    if (byteNmbr <= 24) {
      frame[byteNmbr]= sbusByte;
      byteNmbr++;
      if ((byteNmbr == 25) && (sbusByte == 0) && (frame[0] == 0x0F)) return true;  // byteNmbr is now invalid, ready for next frame
    }
  }
  return false;
}

void setup() {
  Serial.begin(115200);   // PC port speed
  Serial1.begin(100000, SERIAL_8E2);  // SBUS baud rate
  while (!Serial);  // wait for PC port ready
  Serial.println();
  Serial.println();
  byteNmbr= 255;  // invalidate current frame byte
  newFrame= false;
}

void loop() {
  if (getFrame()) {
    decodeChannels();
    Serial.println();
    for (i= 1; i <= 16; i++) {
      Serial.print("Ch");
      Serial.print(i);
      Serial.print(":");
      Serial.print(channel[i]);
      Serial.print(" ");
    }
    Serial.print("Ch17:");
    Serial.print((channel[0] & 1) ? "H" : "L");
    Serial.print(" Ch18:");
    Serial.print((channel[0] & 2) ? "H" : "L");
    if (channel[0] & 4) Serial.print(" FL");  // frame lost
    if (channel[0] & 8) Serial.print(" FS");  // failsafe
  }
}


Now that we are getting SBUS data and we decoded the bit stream with simple bit shifts (see the decodeChannels function) we can use the data for our next sketch, the HID PC Joystick.

SBUS PC Joystick

This joystick does NOT emulate a copy protection dongle; you will need a free (or properly licensed) flight simulator supporting third party joysticks to use it. It uses the Joystick (2.1.1) library by Matthew Heironimus and requires an MCU which can emulate HID devices like an Arduino Pro Micro or Leonardo.

Hardware setup is exactly the same used in our previous step, with a Pro Micro or Leonardo.

CODE:

// SBUS to Joystick interface
// (c) 2023 Pablo Montoreano

/*********************************************************
  @file       SBUS2Joy.ino
  @brief      SBUS to PC Joystick interface
  @author     Pablo Montoreano
  @copyright  2023 Pablo Montoreano
  @version    1.1 - 05/oct/23  - bug fix (0x0F is a valid SBUS value)

  3rd party library: Joystick (2.1.1) by Matthew Heironimus

  for Arduino Pro Micro (ATMega32U4) or Arduino Leonardo
  does not work with Arduino Nano because it cannot emulate PC Joystick interface
*********************************************************/

static const unsigned int maxChan= 8;         // number of channels in joystick
static const unsigned int signalLost= 100;    // if channel[3] below this value -> failsafe
static const unsigned long timeOutMs= 1000;   // timeout if no packet received after 1 sec
static const unsigned long fsResetTime= 250;  // flight sim reset time

static bool oldFsMode, newFsMode;
static unsigned long lastReception;
static unsigned int sbusByte, byteNmbr;
static byte frame[25];  // 25 bytes per frame
static unsigned int channel[17]; // 16 channels in SBUS stream + channels 17, 18 & failsafe in channel[0]
static unsigned int lastReported[9];  // 8 channels plus array[0]
static unsigned int i; // a counter
static bool newFrame;

#include <Joystick.h>

Joystick_ Joystick(
  JOYSTICK_DEFAULT_REPORT_ID,
  JOYSTICK_TYPE_JOYSTICK,  // do not use JOYSTICK_TYPE_MULTI_AXIS because it is not recognized by Win10
  5,  // button count 5
  0,  // hat switch count
  true, // X axis Aileron
  true, // Y axis Elevator
  true, // Z axis throttle
  true,    // x rotation CH6
  false,   // y rotation
  false,   // z rotation
  true,    // rudder
  false,   // throttle
  false, false, false);  // no accelerator, no brake, no steering

void decodeChannels() {
int bitPtr;   // bit pointer in SBUS byte being decoded
int bytePtr;  // byte pointer in SBUS frame
int chan;     // channel number being decoded
int chanBit;  // current channel bit being proccessed

  channel[0]= frame[23];
  bytePtr= 1;
  bitPtr= 0;
  for (chan= 1; chan <= 16; chan++){
    channel[chan]= 0;
    for (chanBit= 0; chanBit < 11; chanBit++) {
      channel[chan] |= ((frame[bytePtr] >> bitPtr) & 1) << chanBit;
      if (++bitPtr > 7) {
        bitPtr=0;
        bytePtr++;
      }
    }
  }
}

bool getFrame() {
  while (Serial1.available()) {
    sbusByte= Serial1.read();
// Bug fix: 0x0F is a valid value in the SBUS stream
// so we use a flag to detect the end of a packet (0) before enabling the capture of next frame
    if ((sbusByte == 0x0F) && newFrame) { // if this byte is SBUS start byte start counting bytes
      newFrame= false;
      byteNmbr= 0;
    }
    else if (sbusByte == 0) newFrame= true; // end of frame, enable start of next frame (to distinguish from 0x0F channel values)
    if (byteNmbr <= 24) {
      frame[byteNmbr]= sbusByte;
      byteNmbr++;
      if ((byteNmbr == 25) && (sbusByte == 0) && (frame[0] == 0x0F)) return true;  // byteNmbr is now invalid, ready for next frame
    }
  }
  return false;
}

void setup() {
  Serial1.begin(100000, SERIAL_8E2);  // SBUS baud rate
  byteNmbr= 255;  // invalidate current frame byte
  Joystick.setXAxisRange(0, 2047);    // Aileron
  Joystick.setYAxisRange(0, 2047);    // Elevator
  Joystick.setZAxisRange(0, 2047);
  Joystick.setRxAxisRange(0, 2047);   // Channel 6
//  Joystick.setRyAxisRange(0, 2047);
  Joystick.setRudderRange(0, 2047);
//  Joystick.setThrottleRange(0, 2047);
  for (i= 1; i <= maxChan; i++) lastReported[i]= 0;
  lastReception= 0;
  newFrame= false;
  Joystick.begin();
}

void loop() {
  if (getFrame()) {
    lastReception= millis();
    decodeChannels();
    newFsMode= ((channel[3] < signalLost) || (channel[0] & 8)); // if signal lost prepare to doReset
    for (i= 1; i <= maxChan; i++) if (lastReported[i] != channel[i]) {
      switch (i) {
        case 1: // Aileron
          Joystick.setXAxis(channel[i]);
          break;
        case 2: // Elevator
          Joystick.setYAxis(channel[i]);
          break;
        case 3: // throttle
          Joystick.setZAxis(channel[i]);
          break;
        case 4: // Rudder
          Joystick.setRudder(channel[i]);
          break;
        case 5:
          if (channel[i] <= 1023)
            Joystick.releaseButton(0);
          else
            Joystick.pressButton(0);
          break;
        case 6:
          Joystick.setRxAxis(channel[i]);
          break;
        case 7:
          if (channel[i] <= 1023)
            Joystick.releaseButton(1);
          else
            Joystick.pressButton(1);
          break;
        case 8:
          if (channel[i] <= 500) {
            Joystick.releaseButton(4);
            Joystick.pressButton(3);
          }
          else if (channel[i] > 1500) {
            Joystick.releaseButton(3);
            Joystick.pressButton(4);
          }
          else {
            Joystick.releaseButton(3);
            Joystick.releaseButton(4);
          }
          break;
      }
      lastReported[i]= channel[i];
    }
  }
  if (lastReception != 0) if ((millis() - lastReception) > timeOutMs) {
    newFsMode= true;
    lastReception= 0;
  }
  if (oldFsMode != newFsMode) { // detected signal lost
    oldFsMode= newFsMode;
    if (!newFsMode) { // exit failsafe mode
    // some flight sims require joystick button 3 pressed as "reset"
      Joystick.pressButton(2);
      delay(fsResetTime);
      Joystick.releaseButton(2);
    }
  }
}

You can change each channel's mapping to the PC joystick (or even add more channels) and adapt it to your flight simulator. I highly recommend you analyze Hieronimus' library and examples to do that.

In PART 2 of this instructable we will analyze the PPM protocol and build an SBUS to PPM converter.

Click here to go to PART 2 of this instructable