The Forthcoming RageBridge: Stick Calibration

They’re here!

All of the RageBridges! All of them ever!

Or rather, 100 of them, in neatly packaged panels of two. Here’s what one of the manufactured panels looks like.

Pretty cool.

Okay, so these actually got in last week, but I’ve been doing some background and infrastructural work since then. I’m planning on gathering a few friends and flashing, populating headers, and testing all of these this coming weekend. I decided to leave the chips unflashed because I was not yet satisfied with the firmware (and the flashing was a few hundred extra dollars), and the headers are purposefully left unpopulated because many people prefer soldered-on pigtails or their own wiring configuration. It’s definitely a little more legwork on our end, but for such a small run (100, maybe with a few duds) I think it’s still manageable, and will allow the fine settling into best practices that inevitably accompany any parts business. Time to be a little adventurous.

From a firmware perspective, the only thing RageBridge has been missing since the last board revision is a way to calibrate the servo PWM range. This is generally preferred because not all radios are created equally, and some may have more stick travel & adjustable PWM endpoints than others. For instance, my (nice for 2006) Spektrum radio can hit the full 1000-2000uS servo range if I navigate through the menus and tell it to stretch its range a bit. But my cheesy Hobbyking 6 channel radios can’t do the same unless connected to a computer to program them – the stock range is more like 1200 to 1800us. So, any fixed input range will inevitably be a compromise – for some people, the stock settings will be too twitchy (i.e. stick travel range is greater than the board’s accepted input range), or you might not ever hit full throttle (because the radio stick travel doesn’t go far enough to match the controller input range).

Calibration has always been one of those things that I accepted as working, but never thought about implementing. Depending on how it’s done, it can be relatively simple, or a mess of state machine code.

One of the most common modern calibration paradigms is the “stick-high” method, used by most R/C model controllers, whether air, sea, or ground. This involves powering on the controller with a receiver attached (and generally, already powered and outputting valid pulsewidths) and you commanding full positive travel. If the first thing that the ESC sees, or sees within 1-2 seconds, is a full positive signal, then it waits for a few seconds and checks again. If you are still holding full positive, then it beeps or blinks (accepting the full positive position), and then you back off to full negative and hold for a few seconds, then back to neutral. For single channel ESCs without many other features, it’s pretty much the way to go.

I decided to implement stick-high calibration for RB, except modifying it for both channels at once. This entailed adding a timed loop in the middle where normally you would just check for full positive and full negative signals. The idea is that during the timed loop, the user just swirls the sticks around all at once and the controller will capture both ranges simultaneously. At any point, if the signal returns to neutral for more than 2 seconds, the calibration routine exits and writes the results to EEPROM so it is persistent.

Of course, there’s other little touches to make the system more robust like checking for pulse validity (rejecting malformed or misread pulses) and fail-safe performance if the user bails out in the middle. In all, it took me a few hours of planning and a few hours of beasting Arduino code, and I’m quite pleased with the final result.

So, in a completely out of context brain bump, here’s RageBridge’s stick calibration code. When I ship it through e0designs.com, the full source code will be released along with board files, but I’m putting this early because I’m particularly proud of it.

I subscribe to the Expository Essay school of code commenting – I literally write what every line and every procedure does, in detail, and in natural english. To hardcore and experienced coders this might seem like a waste of characters, but I find it particularly easy to read my way back through old code and decipher what the hell I thought I was doing when I wrote it. So there’s actually plenty of paragraphs in the comments.

As a bit of backstory since I didn’t post all the variable definitions, anything with a _1 or _2 is referring to the channel of RB it is relevant to. The code is written with Arduino 1.0.1 using the PinChangeInt and EEPROM libraries (PCI is not used in this section as the interrupt procedures that capture servo pulsewidths are not shown).

  • RCMAX, et. al. are all declared as signed integers at the beginning of the code (not shown).
  • CALIBRATION_CONSTANT is a fraction of the total stick travel, right now 0.66. In other words, anything ABOVE 66% of the current maximum travel is grounds for entering calibration mode. This is one of the tricks I added to prevent calibrating yourself out of your own radio travel bounds. So, basically what it entails is each time you calibrate you could reduce the range by 1/3rd if you had to.
  • neutral_counter and NEUTRAL_CYCLES_MAX refer to how many iterations of the neutral signal checking routine can run before the calibration procedure is declared to be done.
  • NEUTRAL_DELTA is the pulse width deviation around the current neutral point that can be recognized as a new valid neutral point. For instance, the default neutral pulsewidth for servo PWM is 1500us. If NEUTRAL_DELTA is defined as 100, which it is, then anything from 1400 to 1600 may be set as a new neutral point. This is to make sure you cannot drift the center stick zero throttle point too far, even on purpose. You can, of course, do so by running multiple calibration cycles. But why!?
  //Collect the user calibration endpoints from EEPROM. If the EEPROM values return 255
  //then calibration has never occurred, so use defaults.
  RCMAX_1 = (signed int)EEPROM.read(100);
  RCMAX_1 += (signed int)(EEPROM.read(101) << 8);
  RCMIN_1 = (signed int)EEPROM.read(102);
  RCMIN_1 += (signed int)(EEPROM.read(103) << 8);
  RCCENTER_1 = (signed int)EEPROM.read(104);
  RCCENTER_1 += (signed int)(EEPROM.read(105) << 8);
  RCMAX_2 = (signed int)EEPROM.read(106);
  RCMAX_2 += (signed int)(EEPROM.read(107) << 8);
  RCMIN_2 = (signed int)EEPROM.read(108);
  RCMIN_2 += (signed int)(EEPROM.read(109) << 8);
  RCCENTER_2 = (signed int)EEPROM.read(110);
  RCCENTER_2 += (signed int)(EEPROM.read(111) << 8);

  //Bytes will return 255 if they were never read; full 16 bit result is 65535
  if(RCMAX_1 == 0xffff ) {
    RCMAX_1 = RCMAX_DEFAULT;
  }
  if(RCMIN_1 == 0xffff) {
    RCMIN_1 = RCMIN_DEFAULT;
  } 
  if(RCCENTER_1 == 0xffff) {
    RCCENTER_1 = RCCENTER_DEFAULT;
  }
  if(RCMAX_2 == 0xffff) {
    RCMAX_2 = RCMAX_DEFAULT;
  }
  if(RCMIN_2 == 0xffff ) {
    RCMIN_2 = RCMIN_DEFAULT;
  } 
  if(RCCENTER_2 == 0xffff) {
    RCCENTER_2 = RCCENTER_DEFAULT;
  }  

  delay(1000); //Just chill for a bit to make sure radios have bound and begun sending out good signal.

  //Calibration mode procedure.
  //After initialization, check for good signal on channels 1 and 2
  //If the signal is above the calibration threshold on both channels, keep waiting. If not, enter main program loop.
  //If at the end of 5 seconds the stick has been held, enter calibration mode. If it has not, enter main program loop.
  //Even if there is no good signal, proceed to main loop anyway - the main loop begins in failsafe mode. 
  if(is_pulse_good(vol_ch1_pw,RC_ABS_MAX,RC_ABS_MIN) && is_pulse_good(vol_ch2_pw,RC_ABS_MAX,RC_ABS_MIN)) {
    signed int init_calib_threshold_1 = (signed int)(CALIBRATION_CONSTANT*(RCMAX_1-RCCENTER_1))+RCCENTER_1;
    signed int init_calib_threshold_2 = (signed int)(CALIBRATION_CONSTANT*(RCMAX_2-RCCENTER_2))+RCCENTER_2;

    if(vol_ch1_pw > init_calib_threshold_1 && vol_ch2_pw > init_calib_threshold_2) {

      //To check for stick hold, begin the timer and set a flag that will be cleared if the stick is released
      unsigned long calibration_timer = millis();
      boolean entered_calibration = true;

      //Enter 5 second wait loop, constantly checking for stick release. Invalid signal also constitutes "stick release".
      while(millis() - calibration_timer < 5000) {
        if(is_pulse_good(vol_ch1_pw,RC_ABS_MAX,RC_ABS_MIN) && is_pulse_good(vol_ch2_pw,RC_ABS_MAX,RC_ABS_MIN)) {
          if(vol_ch1_pw < init_calib_threshold_1 || vol_ch2_pw < init_calib_threshold_2) {
            entered_calibration = false;
            break;
          } //End stick release check
        } else {
          entered_calibration = false;
        } //End pulse good check

      } //End wait for 5 seconds to check for stick hold

      //If the flag is still true, then the user has held the stick for > 5 seconds above the threshold
      //Calibration mode description:
      //Resetting the old max and min bounds to a lower threshold (CALIBRATION_CONSTANT * RCMAX_n)
      //If a newly captured value exceeds the thresholds, save it. 
      //If a newly captured value is within a certain % of neutral (NEUTRAL_CONSTANT * RCCENTER_n above or below RCCENTER), start a timer
      //After 3 seconds of this, assume the user has returned stick to desired neutral and take several samples for neutral position
      //If any pulse is invalid, bail out of calibration and retain old values.
      if(entered_calibration) { 

        digitalWrite(BLINK_FURIOUSLY,HIGH); //Set the LED on solid to let the user know that calibration has begun

        //Set initial thresholds to exceed. The thresholds are a certain % of the current set of maxes and mins
        //such that the controller can never be "calibrated out". By only exceeding the threshold very slightly
        //subsequent iterations of calibration can narrow the range of signals too, if it is necessary.
        signed int rcmax_1_temp = (signed int)(CALIBRATION_CONSTANT*(RCMAX_1-RCCENTER_1)) + RCCENTER_1;
        signed int rcmin_1_temp = RCCENTER_1 - (signed int)(CALIBRATION_CONSTANT*(RCCENTER_1-RCMIN_1));
        signed int rcmax_2_temp = (signed int)(CALIBRATION_CONSTANT*(RCMAX_2-RCCENTER_2)) + RCCENTER_2;
        signed int rcmin_2_temp = RCCENTER_2 - (signed int)(CALIBRATION_CONSTANT*(RCCENTER_2-RCMIN_2));
        signed int rccenter_1_temp = RCCENTER_1;
        signed int rccenter_2_temp = RCCENTER_2;

        boolean am_calibrating = true;
        boolean commit_calibration = false;

        byte neutral_counter = 0;

        //Set the main calibration loop going. 
        //First, check for good pulses. If any are bad, the routine is immediately ended and changes are NOT saved.
        //Next, check for pulses near neutral. If any are, increment a counter. If the counter is over a threshold,
        //then the user has returned the stick to netural and calibratoin can exit/changes be saved.
        //If any subsequent pulses are outside of the neutral bound, zero the counter. 
        //Next, expand the pulse range if the new set of pulsewidths are greater in magnitude deviation
        while(am_calibrating) {

          //Sanity check - if any pulses are out of range or invalid, exit.
          if(is_pulse_good(vol_ch1_pw,RC_ABS_MAX,RC_ABS_MIN) && is_pulse_good(vol_ch2_pw,RC_ABS_MAX,RC_ABS_MIN)) {

            //Save a copy of the current "volatile" pulsewidths (which can change randomly due to the interrupt)
            ch1_pw = vol_ch1_pw;
            ch2_pw = vol_ch2_pw;

            //Neutral check. If the signal is within the box of valid neutrals for enough cycles, exit and save changes.
            if(ch1_pw < (RCCENTER_1 + NEUTRAL_DELTA) && ch2_pw < (RCCENTER_2 + NEUTRAL_DELTA)) {
              if(ch1_pw > (RCCENTER_1 - NEUTRAL_DELTA) && ch2_pw > (RCCENTER_2 - NEUTRAL_DELTA)) {
                if(neutral_counter > NEUTRAL_CYCLES_MAX) {
                  am_calibrating = false;
                  commit_calibration = true;

                } else {
                  neutral_counter++; //Within bounds, but not exceeding number of allowed cycles, so increment and move on.
                  rccenter_1_temp = ch1_pw;
                  rccenter_2_temp = ch2_pw;
                }
              } else {
                neutral_counter = 0; //If any of the within-bounds checks fails, reset the counter since the stick has moved.
              }
            } //End neutral check

            //Bounds check; If the new pulses exceeds the current set of temporary calibration bounds in either direction
            //set them as the new bounds.
            if(ch1_pw > rcmax_1_temp) rcmax_1_temp = ch1_pw; else if(ch1_pw < rcmin_1_temp) rcmin_1_temp = ch1_pw;
            if(ch2_pw > rcmax_2_temp) rcmax_2_temp = ch2_pw; else if(ch2_pw < rcmin_2_temp) rcmin_2_temp = ch2_pw;
          } else {
            am_calibrating = false;
            commit_calibration = false; //Something has gone wrong and caused the calibration loop to terminate early...
          } //End sanity check
          delay(20);
        } //End main calibration loop

        //Save changes?
        if(commit_calibration) {

          //First, reassign the constants for this power cycle...
          RCMAX_1 = rcmax_1_temp;
          RCMIN_1 = rcmin_1_temp;
          RCCENTER_1 = rccenter_1_temp;
          RCMAX_2 = rcmax_2_temp;
          RCMIN_2 = rcmin_2_temp;
          RCCENTER_2 = rccenter_2_temp;

          //Next, write these new values to EEPROM!
          byte rcmax_1_lowbyte = (RCMAX_1 & 0x00ff);
          byte rcmax_1_highbyte = ((RCMAX_1 >> 8) & 0x00ff);
          EEPROM.write(100,rcmax_1_lowbyte);
          EEPROM.write(101,rcmax_1_highbyte);

          byte rcmin_1_lowbyte = (RCMIN_1 & 0x00ff);
          byte rcmin_1_highbyte = ((RCMIN_1 >> 8) & 0x00ff);
          EEPROM.write(102,rcmin_1_lowbyte);
          EEPROM.write(103,rcmin_1_highbyte);

          byte rccenter_1_lowbyte = (RCCENTER_1 & 0x00ff);
          byte rccenter_1_highbyte = ((RCCENTER_1 >> 8) & 0x00ff);
          EEPROM.write(104,rccenter_1_lowbyte);
          EEPROM.write(105,rccenter_1_highbyte);

          byte rcmax_2_lowbyte = (RCMAX_2 & 0x00ff);
          byte rcmax_2_highbyte = ((RCMAX_2 >> 8) & 0x00ff);
          EEPROM.write(106,rcmax_2_lowbyte);
          EEPROM.write(107,rcmax_2_highbyte);

          byte rcmin_2_lowbyte = (RCMIN_2 & 0x00ff);
          byte rcmin_2_highbyte = ((RCMIN_2 >> 8) & 0x00ff);
          EEPROM.write(108,rcmin_2_lowbyte);
          EEPROM.write(109,rcmin_2_highbyte);

          byte rccenter_2_lowbyte = (RCCENTER_2 & 0x00ff);
          byte rccenter_2_highbyte = ((RCCENTER_2 >> 8) & 0x00ff);
          EEPROM.write(110,rccenter_2_lowbyte);
          EEPROM.write(111,rccenter_2_highbyte);
        }

        //We're done with calibration! Flash the LED a few times to indicate success.
        for(byte caliblink = 0; caliblink < 10; caliblink++) {
          PORTB |= (1 << 5);
          delay(BLINK_DURATION);
          PORTB &= ~(1 << 5);
          delay(BLINK_DURATION);
        } 
      } //End of master calibration routine

    } //End of check signal over calibration threshold

  } //End of check signal good

The State of the Überclocker Address

It’s the end of January. Where’s my damned robot!?

After about a month of dormancy and on-and-off work, it’s time to get serious… lest I pigeonhole myself, again, in the position of working on the damn thing a day before setting out for Motorama 2013. I’m generally confident that Clocker was designed with the best and latest of my design-for-assembly methods in mind, and the progress (mostly in the past week or so for actual work) should demonstrate that here.

RageBridge and DeWut?

No matter how much I get done, though, the availability of the RageBridge assembled boards and the DeWut?!s will be critical in the robot’s completion. As of even right now, I don’t have a concrete bail plan for the DeWuts in particular if the parts do not arrive in time. Fortunately, I received notification that RB shipped this past Friday, and the DeWut order has been completed and may go out on Monday. Here’s hoping the magic of modern express logistics gets them in my hand by the end of this week. Both will probably need a little legwork on my end before I can offer them up to you all, but that’s all part of the battle plan. On a related note, the Hall sensor boards and mounts are ready for your experimentation!

We begin the build of Überclocker with my favorite production machine, the abrasive waterjet.

These parts were made from one of three plates of 7075 aluminum I caught a great deal for on eBay. 7075 may actually be my most favorite material because it’s one of the strongest aluminum alloys, yet you really can’t tell by machining the stuff. I probably could have made some of the material areas smaller to take advantage of the 50% increased yield strength of 7075 over 6061, but elected to not make design changes at the last minutes to save an ounce or two.

The plates were all machined without incident, save for two of them, where the insides are shifted relative to the border. This is a classic failure mode of constant-height waterjet cutters before motorized Z-axes were fashionable – if any part of the previous cut interferes with the head, the machine generally bumps the part into a new coordinate system.

While the damage was minor enough on one of them (the top X part) that I could have milled out the slots, it would have weakened the joint significantly due to the loss of material-on-material interference in the joint. The other one pretty much needed replacement, since the error occurred (seemingly) right before the final profile runaround.  I elected to redo both parts at the earliest opportunity.

Also lined up for the first batch was the main lift gear. It’s the same pitch as Überclocker’s previous lifter gear, at 12DP, but the reduction ratio is higher (5:1) instead of about 3:1. This is to make up for the loss of the 216:1 integrated dual frankenboxen for speed reduction purposes. While the difference between a DeWalt gearbox in low gear (52:1) and another 5:1 is still outmatched by the reduction ratio of the IDFB, I think it’s less likely to destroy itself. The DeWalt motors are innately more powerful and torque-balanced than the 550 motors, so perhaps a 260:1 reduction is enough. In fact, it’s more than enough, but the maximum top speed of the lift would be an unnecessary ~15 in/s at the periphery. I’ll deal with the increased current draw, though, because hopefully RageBridge’s low speed exponential response and dynamic braking will make up for it. Maybe it’s time for a closed loop speed feedback…

The small gear is a steel pinion I purchased from McMaster whose hub will be removed and bore broached for a 1/8″ keyway.

Round two of parts. The top and bottom plates are made of my most recent favorite top and bottom material, 1/8″ G10/FR4 garolite in black. There’s some of the usual delamination from high pressure piercing. In the past, I’ve taken care of this by injecting copious amounts of CA glue into the bubble and then slamming it in a vise. A perhaps imperfect repair, but it at least brings some of the strength back in the bubble area.

The tensioner and drive sprockets were also cut at this time. These used the profile shifted sprockets I designed for Chibikart to account for waterjet taper. The tensioners are basically sprocket rings glued to a ball bearing as shown in the topmost example.

A little bit of stuffing with Loctite 609 retaining compound later, and I had the tensioner sprockets. The bore was designed such that they were a near perfect tapered press-fit as cut on one of the MIT waterjets I frequent the most. Different machines would necessitate familiarization before I am able to do such a thing.

Continuing the steps of small, easily pressables, I installed the fork shaft bushings and the outboard support bearing for the lifter motor. The bushings needed finish-reaming after installation since this bore wasn’t that perfect – luckily, I was able to borrow a 1″ adjustable reamer from one of the campus shops. A ring of 609 ensures their retention. After the finish-reaming, I decided to increase the diameter a little further to allow for some alignment slop when it came time to assemble the frame, since otherwise bushings will lock up with any small amount of misalignment.

Round 3 of cutting sees the front “reactive outrigger” parts finished and the replacement frame rails also finished. Now I can really get onto assembling the robot’s structure.

The legs, now that I actually hold them in my hand, are massive. If the bot ends up a little overweight, these are the first parts getting selectively lightened!

The order of assembly of Clocker this time mandates the fork mounting structure be assembled first. This then slides, with the frame’s back member, into the sides. In previous Clocker iterations, this would of course have guaranteed the need to disassemble the entire bot before any work can be done on it, but I hope I correctly allotted space this time around to swap motors and repair drive components without needing to do so.

A first look at the assembled frame of the bot. These pieces are just shoved together for now – there are more parts to make and assemble before I can install all of the t-nuts.

Another item of minor fabrication is attaching the clamp hub shaft collars to the components they will be driving. The two fork tine collars will be tightened securely, while the one on the gear will function as the slip clutch for the system.  #10-32 screws were used for this effort since they fit flush into the counterbored holes in the shaft collar, and plenty of high-strength Loctite 262 were dumped into the threads to make sure I can never, ever take these things apart again. Ideally, I would never need to…

With most of the minor assembly complete, I turned my attention to solving an issue that had been on my mind since the first time Clocker was in a tournament. The clamp actuator has always been really slow, in part because I’ve been using highly geared motors on the Acme thread. Many matches in Clocker’s history have had missed grabs because the clamp just didn’t come down fast enough. Other clampbots in the past have used pnuematics and R/C servos for the clamp arm, so they’re quicker (but each has its own downsides).

One way to solve the problem would have been with a fast-travel leadscrew such as the one I used on Make-a-Bot with 8 threads per inch and 2 starts (so basically a 4 thread per inch). that would net me a 2.5x speed increase. Problem is, that would also entail remaking the Acme threaded sprocket – and I didn’t have either nut or sprocket one on hand. I decided that the force loss was acceptable enough to just take out one stage of the Harbor Freight drill innards which made up the gearbox for the clamp motor. This was a 36:1 gearbox, so taking one stage out is a 6:1 increase in speed.  Because Clocker’s clamp is hypothetically not backdrivable (unless something truly terrible has happened), I don’t actually need that much clamping force to hang onto someone, especially with the big squishy rubber bumper on there.

So, onto the lathe the ring gear goes, and one pass with a parting tool was enough.

The actuator, reclosed with 1″ long #6 screws. I forgot about the fact that my little tension roller standoffs existed, though, and had to go back and trim down two of the 1.5″ long screws that were in this duty so they would fit those.

With that matter taken care of, I embarked on Epic Standoff Evening where I popped out many little round threaded things from tinylathe. With the exception of the tubular spacers (for the fork) at the top, These parts are actually all 7075 too – due to the magic of eBay, once again, I caught a great deal on 3/8″ and 1/2″ rods of 7075. Because some of these parts, namely the axle standoffs, were modeled as steel, I ought to be creeping slowly further down from the initial weight estimate, which is good.

Threading the ends of the standoffs led me to come up with quite possibly the worst tapping fixture known to mankind. No taps were harmed (I think…) in the production of this image. I only used the other drill to hold the piece steady – it was not counter-rotated. Really the way this came about was trying to figure out how to hold the round piece still to thread it without damaging the precision-ground surface, like what would happen if I threw it in a vise like I usually do.

One of the other simple operations was to trim off the hub from the spur gear. At this point, my 1/8″ keyway broach had not yet arrived, so I couldn’t broach the thing, but at the very least it can be prepared.

We conclude this address with pretend-o-bot #1. Still to go in this picture are making the drive wheels, machining the fork’s main axle from the giant aluminum shaft, machining the front leg parts, and finish machining the top and bottom plates. I’ve hopefully ordered the last round of random hardware needed to get the build done. Past that, it’s just waiting for the hired out parts, and possibly formulating a ditch plan…