In the Interest of Future Flying Objects: XBypass

Friday, December 30th, 2011 @ 23:43 | Ballcopter, Emergency Quadrotor, Project Build Reports, Reference Posts

Despite losing the Spruce Goose of Quadrotors to an unfortunate semi-controlled flight into terrain where I was the terrain (from which I’m still recovering… the square area of skin has recovered by more than 75%), I haven’t quite given up on the prospect of building a flying thing yet – but for now, they will be smaller and more easily batted from the sky. For instance, I’ve figured out why Ballcopter never worked properly. The very last update left it in a state of limbo, which I can sum up in one sentence: No, it didn’t work, but more on that later.

Eventually, Ballcopter was scrapped as its foam structure was slowly eroded away by virtue of existing in MITERS.

But there will be a day.

In the mean time, I wanted to satisfy one of my persistent self-stabilizing-flying-thing itches: how to pass control inputs from a handheld  radio to the flying thing in question without having it process several consecutive servo pulse lengths. For the most common & simplest Arduino-compatible balancing algorithms, this limits the maximum control update frequency to 50hz, since the microcontroller has to actually measure 10 milliseconds of pulse commands (for, say, up to 5 or 6 channels) out of every 20 milliseconds. The remaining time is often taken up by software floating point multiplication and division.

The reason it takes so much of the loop time is because the abomination that is Arduino’s pulseIn() command. It’s a “busy wait” loop that sits and increments a counter while the pulse is being detected. It’s neither interrupt based nor uses the hardware timers (the loop is timed empirically). So while taking in a servo pulsewidth, the microcontroller can literally do nothing else. A little inefficient, in my mind.

More sophisticated ATmega based flight controllers like the Ardupilot use the chip’s hardware timers and internally generated interrupts. For instance, as far as I can tell in the Ardupilot, Timer 1 is constantly running (counting) and overflowing (resetting to count more), and pin change interrupts on the input pins are used to capture snapshots of the counter status, and the resultant pulse width is just the difference between the values when the pin changed from low to high (pulse started) and from high to low again (pulse ended). And if the counter overflowed while the pulse was high, no problem: just add the max possible value of the counter to the pulse width variable and keep counting. Yes, I spent a few hours reading Ardupilot source code to figure that out.

And no, I’m not going to steal it and use it, because it’s written all in straight Atmel C, and so it might as well be gibberish to me. I can read and  understand it, but hell if I’m going to write it.

One method around the onboard servo processing problem is completely bypassing hobby radios as an interface medium, which people often do with joysticks (with or without tethering to a computer). For example, there is the Arbotix Commander, which is essentially an XBee and Arduino in a completely unergonomic, overpriced package which seems to have functionality problems based on a few reports from my peers – I had the displeasure of dealing with the first version personally. Shane tethers his USB Playstation controller to a laptop, but that means the laptop has to be deployed whenever something has to be operated (The upside is infinite debugging and telemetry ability, which a plain R/C radio doesn’t give you).

So really what I would like is the ability to replace the transmitter-air-receiver-servo_pulses interface with something much more direct – like serial over wireless; something that just takes the transmitter inputs and pipes them directly to my vehicle, where it can be buffered and read at will. XBees make the serial over wireless problem very easy to solve. The result would be an XBee using a cheap R/C radio as a brainless “trainer box”: most radios have a “trainer port” over which servo pulses can be sent (over a cable) to a master radio, for situations like when you’re actually training someone who is utterly clueless about flying things (me) and want to take command of the vehicle if anything bad should happen. If this pulsetrain is read and parsed at the transmitter, then I effectively offload the overhead of processing servo pulses from the vehicle – allowing it to process much faster (hundreds or thousands of Hz) and making closed loop control less granular. My inputs would still be updated at 50hz, of course, but I doubt I can react to an event as fast as 0.02 seconds anyway. The vehicle can keep itself stable much faster.

Anyways, that’s exactly what I built. An Arduino Nano, reading the pulsetrain from a transmitter trainer port, and piping the read values as bytes over to an XBee.

It’s not nearly as epic as I made it seem to be, seriously.

I have two cheap 2.4ghz transmitters, one of which I got ages ago for Cold Arbor, and another one I picked up over the summer for Ballcopter. Which really means I have 2 of the same radio. They are both made by FlySky, but the right one is a Hobbyking rebadge. These things cost $28 brand new, with receiver, and some of the consequences of that cheapness show through – for instance, they don’t come with external servo reversing switches like every other radio, but the holes and pads are present internally to use them and if you mount your own switches the functionality is even there in the microcontroller, but no we will absolutely not spent 20 more cents to install those damned switches.  They have a 4-pin mini-DIN (also known as S-Video) connector on the back which is a trainer port.  And as usual with small Chinese gadgets, there is a community devoted to hacking them. It took me all of 10 seconds of Googling to find the pinout of that trainer port.

A little scope probing of pin 1:


The PPM pulsetrain


And zoomed in further.

The biggest difference between this “PPM” signal and the servo-style “PWM” (which isn’t even “PWM” but pulse-duration modulation… which collection of idiots agreed on these terms?) is that

  • It is falling edge driven – the time between falling edges of the square wave is actually the duration of the “PWM” pulse. When the receiver detects one of these falling edges, it will stop outputting the pulse on one channel and immediately start it on the next.
  • As a result, the actual “High” time of the pulse only goes between approx. 600 to 1600 microseconds on my radio
  • The low time is fixed duration at 400us. Makes sense – the minimum pulse lengths on the servo side, then is roughly 1000 to 2000us as it should be.
  • It “idles high” – meaning the gap between signal is +5v.

So I’d have to come up with a way to distinguish a signal pulse from the long idle pulse. Arduino’s pulseIn() doesn’t even allow in-pulse timeouts. It allows pre-detection timeouts, so you don’t wait forever for a pulse if it never comes, but it does not have “hey, this pulse is over the length I care about… let’s move on with life”. I’d have to get around this by throwing away the first update and “syncing” to the rest of the pulses, since I know how many there are and about how long they should be.

The first step of the hardware hacking was disabling the internal radio. As I found out while building the Deathcopter, 2.4ghz radios will tend to step on eachother. I didn’t want to permanently take this radio module out, however. Leaving myself the chance to return this radio to ‘normal’ duty if I ever figured out something better, I just added a pin header connection to the power supply wire. It would be left disconnected when I’m using my hack.

Next was throwing the hardware together on a perfboard. This little arrangement costs way more than the transmitter ($17-34 for an Arduino Nano and $22-25 for an XBee regular 1mW), so I really hope this isn’t the terminal solution. It’s also way underutilizing the Arduino right now, since I only have Tx and Rx pins connected, and one other digital pin hooked up to the PPM output. The transmitter provides 5 volts from the trainer port, so this board has no other power supply.

The XBee is mounted on an Adafruit XBee Adapter.

And that’s it.

Yeah, I didn’t even bother trying to source a 4-miniDIN for this. At least, not until my Digikey order arrives – until then, it’s wires jammed into sockets.

I took a few minutes hours to write the single pin read, serial write software for the Nano. The hard part was, again, thinking my code does one thing but really having it do something different – just logic errors, all caused by having to differentiate the long idle pulse from the servo pulses. But at the end of it:

There we go. I’m only piping back 5 channels out of 6 here, but this was successfully done over the air between 2 XBees. This is all done using Serial.print(), which converts everything to ASCII and thus has unnecessary overhead. When commanding a vehicle, I’d just send the raw byte values because I’m fairly sure my vehicles are not literate.

So how do I test this contraption? With someone elses’s flying object, of course.

4pcb became the subject of a few transmission tests and a whole lot of floor whomping, as seen in the test video:

While not the most economical option, it does work, and it does solve the problem I wanted to solve. I’ll probably look to refine this board or permanently mounting it inside the transmitter, possibly with a plain ATmega chip by itself so I at least don’t have to buy even more Arduini. Otherwise, creating a dongle using Arduino USB host shields and an XBee would open the floor up to using just about any gamepad or joystick as a remote.

The “XBypass” sketche as used on 4pcb is here. The general idea was to wait for the first time the 9ms long timing pulse was received, then immediately start taking in n channels, storing them, and then processing them into single-byte values (for transmission convenience as well as because 4pcb takes such single byte commands).

 

Recently

  • Chibi-Mikuvan and the Invasion of Miku Expo 2014
  • Chibi-Mikuvan & The New York Maker Faire 2014 Recap
  • Dragon Con 2014: The Wrapup, or, Operation: I FEEL GASSY, plus Panel Resources
  • The Dragon Con 2014 All-Robots Update
  • Silly Go-Kart Design: The 2014 Summer Season
  • Chibi-Mikuvan: Detroit Recap and The Conclusion
  • Chibi-Mikuvan: The Penultimate Update!
  • Chibi-Mikuvan: Getting “Tired” of All This; Making the HCL Actually Function; Plus Other Work
  • Chibi-Mikuvan: Experimenting with the Trackstar 200A and the Hysterical Current Limiter
  • Beyond Unboxing: The Great Cambridge Chainsaw Massacre; Ryobi RY40511 Cordless Chainsaw
  •  

    12 Responses to “In the Interest of Future Flying Objects: XBypass”

    1. aEx155 Says:

      One way to eliminate the Xbee:

      There are some receivers that will output the PPM stream from the transmitter, like the trainer port does. So rather than replace the transmitter of the radio, you just move the signal processing to the receiver end. Of course, you would still have to deal with processing it, but that could be solved in the same fashion: adding a second processor to convert the PPM signal to serial.

      If your receiver doesn’t have a PPM-output, it doesn’t help. But if it does that can eliminate the Xbee.

    2. the chuxxor Says:

      As far as I can tell, the cheap HK 2.4g receivers do not have such a thing. It seems to be a feature on legit, high end radios.

      But certainly, one way to get around it is to have a servant microcontroller do all the grunt work of reading pulses. However, it clearly has been done on one microcontroller in a robust, hardware-supported fashion… just not very understandable or Arduino-y. There isn’t really a point to trying to use 2 Arduini, IMO.

    3. aEx155 Says:

      Maybe you could just get a programmed Atmega328 and the supporting components rather than a full fledged Arduino board and hook that up to your receiver?

      Or maybe find a way to convert the “PDM” signals into analog voltages then just read them with the main Arduino. Taking an analog voltage reading doesn’t take as much time as waiting however long for the pulseIn() command, right?

    4. Matt C Says:

      Ardweeny kits from Solarbotics are very convenient. A tiny minimalist arduino compatible kit you solder up yourself. $10 each. BYO regulated power and FTDI to USB connector. But that’s about the same as just using the plain ATMEGA chip as you already stated.

    5. Shane Says:

      For the record, the video is all Charles flying with little/no practice. I flew it with the XBypassed transmitter and it is as easy as flying it with USB for me. The only thing I would request is an XBee Pro so that it doesn’t freak out every time I fly under a WiFi access point.

    6. beak90 Says:

      There is a super simple solution to the problem you describe. You also described the solution, but there is an easier way of doing it that I doubt ardupilot uses. Here is some code that does exactly what you need. It reads 5 servo inputs and only spends a teeny tiny amount of time doing it. The misc pin is a boolean switch. This code works quite well using a spektrum radio but it should work with any radio. I didn’t implement any overflow handling code, because it takes 70 minutes for the micros() value to overflow. I hope this helps!

      #include // available at http://www.arduino.cc/playground/Main/PinChangeInt
      #include

      // timing for rc input with interrupts
      unsigned long risingThrustTime = 0;
      unsigned long risingAileronTime = 0;
      unsigned long risingElevatorTime = 0;
      unsigned long risingRudderTime = 0;
      unsigned long risingMiscTime = 0;

      double desiredRoll, desiredPitch, inputThrust,

      [start code in setup()]
      pinMode(THRUSTPIN, INPUT); // Set receiver pins as inputs
      pinMode(AILERONPIN, INPUT);
      pinMode(ELEVATORPIN, INPUT);
      pinMode(RUDDERPIN, INPUT);
      pinMode(MISCPIN, INPUT);

      // interrupt setup
      PCintPort::attachInterrupt(THRUSTPIN, risingThrust, RISING); // attach a PinChange Interrupt to thrust pin on the rising edge
      PCintPort::attachInterrupt(THRUSTPIN, fallingThrust, FALLING); // attach a PinChange Interrupt to thrust pin on the falling edge
      PCintPort::attachInterrupt(AILERONPIN, risingAileron, RISING); // attach a PinChange Interrupt to aileron pin on the rising edge
      PCintPort::attachInterrupt(AILERONPIN, fallingAileron, FALLING); // attach a PinChange Interrupt to aileron pin on the falling edge
      PCintPort::attachInterrupt(ELEVATORPIN, risingElevator, RISING); // attach a PinChange Interrupt to elevator pin on the rising edge
      PCintPort::attachInterrupt(ELEVATORPIN, fallingElevator, FALLING); // attach a PinChange Interrupt to elevator pin on the falling edge
      PCintPort::attachInterrupt(RUDDERPIN, risingRudder, RISING); // attach a PinChange Interrupt to rudder pin on the rising edge
      PCintPort::attachInterrupt(RUDDERPIN, fallingRudder, FALLING); // attach a PinChange Interrupt to rudder pin on the falling edge
      PCintPort::attachInterrupt(MISCPIN, risingMisc, RISING); // attach a PinChange Interrupt to misc pin on the rising edge
      PCintPort::attachInterrupt(MISCPIN, fallingMisc, FALLING); // attach a PinChange Interrupt to misc pin on the falling edge

      [end code in setup]

      // interrupt functions

      void risingThrust() {
      risingThrustTime = micros();
      }

      void fallingThrust() {
      inputThrust = (double)((int)(micros()-risingThrustTime)-1100); // set throttle input
      }

      void risingAileron() {
      risingAileronTime = micros();
      }

      void fallingAileron() {
      desiredRoll = (((double)((int)(micros()-risingAileronTime)-1500))*((double)0.001308996938996)); // aileron input scaled to a value between -pi/6 and pi/6 or -30 degrees and 30 degrees
      }

      void risingElevator() {
      risingElevatorTime = micros();
      }

      void fallingElevator() {
      desiredPitch = (((double)((int)(micros()-risingElevatorTime)-1500))*((double)0.001308996938996)); // elevator input scaled to a value between -pi/6 and pi/6 or -30 degrees and 30 degrees
      }

      void risingRudder() {
      risingRudderTime = micros();
      }

      void fallingRudder() {
      inputYaw = ((double)((int)(micros()-risingRudderTime)-1500))/((double)10.0); // rudder input
      }

      void risingMisc() {
      risingMiscTime = micros();
      }

      void fallingMisc() {
      if((micros()-risingMiscTime)>1500) miscRCInput = true;
      else miscRCInput = false;
      }

    7. beak90 Says:

      Oops the includes got ripped out since they look like html. You need to include “PinChangeInt.h” and “PinChangeIntConfig.h”. Oh and in one of those files you have to change the max number of interrupts to however many you need. In this case 10. The info for that is here: http://www.arduino.cc/playground/Main/PinChangeInt

    8. the chuxxor Says:

      Good, someone made a library to do it!

      This is the optimal solution, IMO, since it allows the use of a totally stock radio. Thanks for pointing me to the PinChangeInt library – I’ll try messing around with it.

    9. Max Says:

      With the disclaimer that I don’t know all that much about XBee radios, it does seem to me that according to [1], they do have a provisions to do and communicate A/D measurements directly. Which, if you would connect the joystick pots to the A/D inputs, would make the entire internal electronics of the RC transmitter AND the Arduino nano redundant, leaving just an XBee module in the case.

      Then again, I might be talking rubbish and not even know it…

    10. Max Says:

      …sorry, missing [1] reference. Following below.

      [1] http://www.digi.com/support/kbase/kbaseresultdetl.jsp?kb=180

    11. the chuxxor Says:

      Yup, and I’ve actually used this direct I/O passing in a previous project (the skate controllers). The problem is the atmega328 just doesn’t have enough ADCs to handle converting all four radio channels AND sensors, on the vehicle side… For one or two analog readings, though, that would be a good solution.

    12. butch alline Says:

      message for “beak90″

      I am having some trouble with your Arduino code. Please contact me.

      occamrazr yahoo.com

      thanks