Saturday, December 16, 2017

On "Delay change per step" pitfall and salient features of the stepper motor bolt drive.

Time to geek out about motor technology a bit. As a starting point an excerpt from FDL-2 non-plus firmware:

void StepRange(int stepperPin, double startDelay, double endDelay, double steps){
 
    double delayChangePerStep = (endDelay - startDelay) / steps;
 
    double loopDelay = startDelay;
 
    for (int index = 0 ; index < steps ; index += 1) {
        digitalWrite(stepperPin, HIGH);
        delayMicroseconds(loopDelay / 2);
        digitalWrite(stepperPin, LOW);
        delayMicroseconds(loopDelay / 2);
     
        loopDelay += delayChangePerStep;
    }
}
Okay, we've all heard the usual runaround about open-loop stepper drives for feeding darts in nerf guns. The one about too much torque decay at high speeds to run reliably at higher ROF and all that. Well, this function above is right at the core of the matter, and no offense, there's a pretty big and apparently very common blunder in it. Angular acceleration of a synchronous motor can be expressed as commutations per second squared. Steps per second per second; or if dealing with the period of a signal, delay change per second. Not per step - per second. The motor commutation frequency is increasing, so "delay change per step" doesn't generate anything resembling a linear velocity/constant acceleration profile. Rather, it gives linearly increasing acceleration, and quadratically increasing velocity.

That is the very LAST thing we want for actually driving something, to accelerate harder as speed climbs and the torque curve drops off.

How about a graphic long timescale example of what this is doing:


https://www.youtube.com/watch?v=25XOH7JPU5w (Check the code in the description!)




Let's zoom out a bit:

bool spinPusherToSwitch(){
 
    //800 full spin
    //spin enough to let go of the switch (1/2 wayish)
 
    int rofPercent = getROF();
 
    int accStepsStart = map(rofPercent, 0, 100, 400, 80);
 
    int accStepsMid = 400 - accStepsStart;
 
    int accStartDelay = 460;
    int accMidDelay = 150;
    int accEndDelay = 110;
 
    StepRange(pusherStep, accStartDelay, accMidDelay, accStepsStart);
    StepRange(pusherStep, accMidDelay, accEndDelay, accStepsMid);

    for(int stepIndex = 0; stepIndex < 500; stepIndex++){
       StepRange(pusherStep, accEndDelay, accEndDelay, 1);
     
       if(digitalRead(pusherSenseIn) == HIGH){
           return true;
       }
    }
 
    return false;
}
So at least this is tempered by doing a two-phase acceleration, but just stop and think about that lumpy velocity profile from these two StepRange calls, which generate quadratic ramps. That can't be helping the matter.

And this is some code from Project T19, whose single-backplane modular electronics package is morphing into a universal solution for single-trigger controlled, fully AC-driven blasters. The pusher in these is obviously a stepper drive, using a OSM 17HS16-2004S1 motor (NEMA 17, 200s/rev, 2 phase bipolar, low inductance) and a 3D printer style DRV8825 driver card, familiar parts to users of the FDL-2 non-plus.
void fire(){
 //loop called to fire a shot
  
 //set distance to run bolt forward
 stepsToGo = 720;
 //if continuing a previous instance add 80 steps
 if(!boltHomed) {stepsToGo += 80;}
  
 //get start point for first ramp
 if(currSpeed < startSpeed){
  //bolt already running
  stepdelay = currSpeed;
 } else {
  //bolt not running
  stepdelay = startSpeed;
 }
 // do first ramp if speed below shiftpoint
 while(stepdelay > shiftSpeed){
  commutate(stepdelay); 
  stepdelay = stepdelay*(1-accelPhaseOne*stepdelay*stepdelay);
  stepsToGo--;
 }
 //do second speed ramp if speed above shift but below running speed
 while(stepdelay > runSpeed){
  commutate(stepdelay);
  stepdelay = stepdelay*(1-accelPhaseTwo*stepdelay*stepdelay);
  stepsToGo--;
 }
 //do constant speed run until out of steps
 while(stepsToGo > 0){
  commutate(stepdelay);
  stepsToGo--;
 }
 currSpeed = stepdelay;
 boltHomed = 0;
}


So you can see some more going on. I am using a linear velocity approximation of the sort described in http://hwml.com/LeibRamp.pdf. (Note that void commutate(double commDelay) just does what the digitalWrite/delayMicroseconds bits are doing in FDL, and accelPhaseOne, accelPhaseTwo are constants that were precalculated for how hard I want to accelerate. Furthermore, note that fire() only implements part of a cycle, and the rest plus bolt position management is handled by a separate set of functions, bool decelerateBoltToSwitch() and bool reverseBoltToSwitch() found elsewhere.) Gives nice smooth accels so far.

You might also notice the variable currSpeed and logic surrounding it. It's a minor bit of statefulness so that back-to-back fire() calls mate their speed ramps up smoothly, and don't do what FDL does; which is to jerk violently at every cycle rollover from ~600rpm to ~160rpm instantly with no deceleration ramp (Possibly jumping a few steps?) and then have to repeat the entire same acceleration ramp as is used for a start from standstill. I was at one point considering a decel at the end and re-accel at the beginning of every cycle if I find it to benefit reliability; but let's look at angle endpoints of FDL ramps at max ROF settings:

int accStepsStart = map(rofPercent, 0, 100, 400, 80);
int accStepsMid = 400 - accStepsStart;
int accStartDelay = 460;
int accMidDelay = 150;
int accEndDelay = 110;
StepRange(pusherStep, accStartDelay, accMidDelay, accStepsStart); StepRange(pusherStep, accMidDelay, accEndDelay, accStepsMid);
At rofPercent=100, that gives:
StepRange(pusherStep, 460, 150, 80);
StepRange(pusherStep, 150, 110, 320);

So we're at 150us by 80 steps and 110us by 400 steps. Motor is 200 steps/rev with DRV8825 on 4:1 microstepping mode, so 800 commutation pulses per rev or 0.45 degree per pulse, and that means it reaches 150us by 36 degrees, and then goes from 150us to 110us by 180 degrees respectively.

At 36 degrees off bottom dead center, the scotch yoke drivetrain has barely started to engage the dart (with a VERY high mechanical advantage in the linkage at this time) or perhaps hasn't even touched the dart at all, having moved the bolt roughly a quarter inch. 150us is 6.666kHz. At 800 pulses per rev that is 8.333 rps, or a dead-on 500rpm that we are reaching at this early point in the cycle. So is there a reliability reason to slam the pusher velocity suddenly down to 460us (163rpm) on every cycle rollover and re-accelerate? I doubt it, these things seem to be running into the dart with the motor going at 400, 500 rpm anyway. So I see no reason not to keep the motor going.

Some testing before hooking the motor to stuff.



Very conservative ramp parameters, and theoretical running speed is 600rpm. Probably more like 500 with the laggy Arduino IDE calls in my commutate() function that could use replacing with register writes to save about 100 AVR instructions per commutation. Also see the Hy-Con test cage revving, because this is single trigger control.

Now for the proof. This is the "Model Pandora" T19 prototype partially assembled with the above motor attached to a scotch-yoke 45mm stroke drivetrain, mowing through a packed-out 22 round workermag without a hitch - aside from those used waffles that were decapitated by the Hy-Con.


I have implemented the bolt limit switch and all bolt position management logic which will be detailed later, and the system has proved highly robust and seamless in recovering from obstructions and sticky feeds within one cycle, has never failed to reset the bolt home within one cycle no matter what, and self-resets on power on. ROF is around 8.3 with aforementioned commutate() version, has that reserved "chugging" rhythm of the M60 or Maxim gun - I am very satisfied with this sort of ROF. Higher is mostly not actually useful. I know I can push it harder though, and I will.

So at this point, in the face of all the FDL-2+ hype, high speed Rapidstrike rigs and other stuff that torches away at 15rps and more and goes bragging all over about that, I should address what exactly it is that I see in this technology.


  • No gears: Steppers are a practical, available, and very cheap form of high-pole-count motor which delivers very high torque and low speed compared to other motors of the same power rating and are suited to direct-drive a bolt linkage without reduction. Eliminating gears reduces design complexity, and removes the most common point of mechanical weakness and failure in the drivetrain rendering it easy to build a totally crashproof system. The vast majority of gearboxes in nerf are not so - very few small COTS DC gearmotors and custom geartrains that have DC motors attached to them are designed to withstand the stall torques of their motors.
    • As an example, consider a 1000g*cm stall torque 180 motor found on a DC pusher drive, say a RS box. After the 60:1 gear ratio, the peak output torque is theoretically 60kg*cm - this would instantly convert the gearset of a RS box to dust, bend the shafts, and/or crack the gearcase. Such boxes survive purely on not encountering immovable objects.
    • Eliminating the screaming motor speeds of DC gear drives also reduces the impact of inertia. The reason the monster torquey motors are used in things like RS boxes and produce improved trigger response and braking is that most of that starting torque and braking torque is being eaten up accelerating and decelerating the rotor itself from 0 to 35,000 rpm and back. It's not the best way to design this, just a simple and cheap one.
  • Brute durability overall: Following on from the above is that the motor itself in the case of a stepper is designed and operated, and achieves its rated performance, with the assumption of rated current being applied continuously as would be found in a position-holding application. Thus, such a motor with its driver set at rated current is already safe under the worst case, and cannot be thermally damaged by stall or overload of any duration. Achieving stallproof operation with a DC or a normal PMSM (like a hobby brushless) would at the least require a custom controller if not be impossible to combine with the required dynamic performance in what is normally a small and stressed motor. Further, the NEMA frame motors are very overbuilt with large shafts and large ball bearings, which are more than adequate as the main bearings of the scotch yoke crank without any additional hardware.
  • The normal AC drive advantages: No brush wear, brush arcing, brushgear losses, commutator damage, or dirty switching noise to fuck with electronics. And operated properly for this application, they run very cool, very quiet, and the controllers use very little current, at most an amp or so RMS from the bus. Compare to a RS box, whose DC motor might draw a 30A inrush spike and consume several amps while running.
  • Easy position awareness: To run a traditional stepper drive, you have to be aware of individual commutations in some form, and thus you are aware of where the motor is, or at least should be. Combined with a limit switch for startup and recovery from any occasional loss of synchronism (i.e. jumped or dropped steps), you can have a quite robust and hack-free strategy for making a bolt cycle and return home predictably.
The flip side of stepper pitfalls, in total fairness:
  • Speed capabilities: Especially on low bus voltage, the limiting factor on ROF is the back-EMF and inductance of the motor.
  • Motor mass and bulk: For their power output, the motors are rather large and heavy and a scotch-yoke drivetrain incorporating one has a characteristic clunky form factor. This is offset by the absence of the gearbox, however.
  • Burden of commutation timing placed on the programmer/system designer: A traditional stepper driver with the "STEP/DIR interface" is not self-contained and is not technically a motor controller until you add external hardware and software which creates commutation pulses with the correct timing, implements sane acceleration/velocity profiles, and the like. All the driver does is control current and energize phases in the correct order with currents determined by the selected mode - so you are left needing to think about matters like acceleration profiles and write code for the entire higher-level half of motor control in addition to blaster management.
  • Timing headaches: Because of the high pole count of the motor, even a low speed like 600rpm results in dealing with kHz frequencies within your firmware and this can present a challenge. Distorted signal timing tries to appear in the rotor velocity, so dirty signals from poor software design or resource contention will make the motor chug and rattle and misfire. At worst the torque spikes from uneven commutations acting against the inertia of the rotor and load can exceed the torque capabilities of the motor and make it desynchronize.
These have led many innovators like Project FDL off this path and back into DC land, and I believe that is a mistake. I see in the stepper pushers my own design philosophy. There is such elegance in their mechanical simplicity and ruggedness. Most of all I don't think this technology was given anything near a fair shot by others. There is a lot more to explore.
  • Velocity profile optimizations: My code does better than old FDLs did and will support more aggressive tuning, but it's still fairly caveman, as is the 8-bit AVR hardware of my blasters. Especially with way more processor power in the 32-bit guns like FDLs and people who are better at math than me, there is room to work out and program better.
  • Higher bus voltage: The crashing torque curves at high speed are, as with all motors, a function of back-EMF and winding inductance increasingly overcoming the available drive voltage and steering the achievable winding current toward zero as speed increases. Why 11.1V? The drivers can handle a LOT more voltage. Simply designing blasters with higher battery voltage would go a long way.
  • Closed-loop commutation: Traditionally, a "stepper" is a PMSM that is driven blindly without rotor position feedback to the controller (while "brushless DC" and "servomotors" are always closed loop with encoders, Hall sensors, or back-EMF detection to measure rotor angle). The torque capabilities of the stepper motors are sufficient to power the load of a pusher at very high speeds if the controller were aware of the rotor position. The limiting factor on open-loop speed is the necessity to account for the absolute maximum load torque that can be applied and then select a speed at which the torque curve is above that by a safety margin - since the commutations coming from the oblivious drive will never slow down, and if the load forces the rotor to lag the rotating stator field by too much, it will "jump over" the stator field. This is a desynchronization, and it may just be a lost step if the rotor is able to re-accelerate within one commutation. If the stator field is going too fast though, the rotor stalls and the motor sits there buzzing. You've seen steppers do that.

No comments:

Post a Comment