Home > Electronics > Stepper Motor Controller Update

Stepper Motor Controller Update

October 5th, 2009

I’ve recently posted about my new stepper motor controller. Well, I was unhappy with the implementation used in the RepRap project, and what started as a code cleanup has turned into a near full rewrite. I’m fairly happy with the code now and finished my preliminary testing. I based my original version of the re-written code turned out to be a variant of Bresenham’s algorithm (though I found this out after I had written the code). The algorythm would determine the axis which required the most travel, step that axis, then add the required travel from one of the other axis to an error term for that axis. When the error term was equal to or greater than one, it would step that axis and subtract one from the error term. This resulted in stepping synchronized with the major axis timebase. Looking at the output of the stepper motor driver, this pattern emerged.

steppring rates.jpg

Here the G-Code command was “G1 X3 Y2 Z1″ — and indeed the ratio of steps is 3:2:1, but notice that the Y-steps are required to align with the X-Steps (master axis because it moves the furthest), but because 3 is not divisible by 2, it skips every third step. This motion is bad for the stepper motor, and it can cause all sorts of problems.

It is *extremely* important the steps be evenly spaced, unless you’ll be content with slow, unreliable operation. Jitter in the step pulse train will limit your top speed, as it *will* cause the stepper to stall. -Ray L.

So I spent some time thinking about it, and came to the conclusion that the Bresenham’s Algorithm only makes sense if you are dealing with a pixelated output (its main purpose is for fast rendering of lines on computer screens). I realized that I needed was a temporal based one, and came up with something. I still need to do a literature search to see if someone has come up with it yet (would be surprising if no one has) but this is basically what I came up with.

  1. Determine the number of steps per axis
  2. Determine the distance of travel for the command, and based on the feedrate, compute the duration of the command.
  3. Compute the time between steps (TBS) for each axis (duration / steps)
  4. Start the main loop.
    1. If the time elapsed is equal to 1/2 the TBS, then step that particular axis.
    2. Wait 1/2 TBS, then go to step 5.

I prototyped this in python, and came up with

maxtime = 100.0; #duration of motion in microseconds
steps = 41.0;  #number of y steps
timePERstep = maxtime/steps; #time per step
y=0;
stepped = 0;
oldTimeIntoSlice = 0;
for time in range(0,int(maxtime)):
        timeIntoSlice = (time%timePERstep);
        if (timeIntoSlice < oldTimeIntoSlice):
                stepped = 0;
        oldTimeIntoSlice = timeIntoSlice
        if (timeIntoSlice >= 0.5*timePERstep) and (stepped == 0):
                        #print 'step'
                        stepped = 1;
                        y = y + 1;
        print '%d,%d' % (time,y)

Which generated this sort of a stepping profile. Notice that the steps are pretty evenly spread out..

temporal.jpg

After rewriting the arduino code, I tried the same test (with G1 X1 Y2 Z3 this time) and got much better results

temporal stepping.jpg

Notice how everything is nicely spread out. Looking at the timestamps, the maximum step jitter is about 150uS. The next step is to hook this up to some stepper motors and see what it does.

I’ve also released the code for the arduino here in case it might be of help to anyone. I’m thinking of making a daughter board for the arduino with the stepper controller ICs on it in order to simplify deployment for others — if there is interest.


Electronics , , , , ,

  1. Carl Sorensen
    | #1

    I like the thought of this algorithm. It probably needs some acceleration compensation.

    In looking at your python implementation, you should recognize that the error is all on one side of the desired line. This is bad. You should readjust your algorithm so the desired line goes down the middle of the actual path.

    Thanks for this work. It looks very interesting.

    Carl

  2. admin
    | #2

    Actually, what you should be looking at it is the distance between steps, which is evenly spaced – it’s a little misleading looking at that way I presented the data in that graph.

  3. Arvin
    | #3

    I left you a note on the reprap forum. Just ignore it. Answered my own Q by reading it again.

    Looking at your python code. How do you derive “maxtime = 100.0; #duration of motion in microseconds”? Calculated? I know it takes about 750 Microseconds to do one step on one of my steppers. Skip if trying to go faster.

    Since floating point math is time intensive it looks like I could just use that number for “timePERstep” instead of a divide. “0.5*timePERstep” could be “timePERstep << 1" since 0.5* is the same as /2 which is just a shift left by one.

    Yes, No?

    "0.5*timePERstep"

  4. admin
    | #4

    Sorry, I don’t check the reprap forum often. Glad your sorted though.

    The maxtime is arbitrary – I played with it to see how the stepping was effected.

    And yes, floating point math is difficult – but the python code was just a proof of concept of the algorithm. The actual code does do a bitshift (although most modern compilers will do this automatically if you divide by a power of 2).

     
    while (xaxis->delta_steps || yaxis->delta_steps || zaxis->delta_steps) {
        time = micros() - starttime;
        for (i=0; i<3; i++) {
          a = axis_array[i];
          // find out how far into the time segment we are in microsecods 
          timeIntoSlice = (time%a->timePerStep);
          // clear the step when we ener a new timeslice
          if (timeIntoSlice < a->oldTimeIntoSlice) {
            a->stepped = false;
          }
          a->oldTimeIntoSlice = timeIntoSlice;
    
          //check if we need to step, and step (timeIntoSlice >= timePerStep/2)
          if (!a->stepped && (timeIntoSlice >= ((a->timePerStep)>>1))) {
            digitalWrite(a->step_pin, HIGH);
            digitalWrite(a->step_pin, LOW);
            a->stepped = true;
            a->delta_steps--;
          }
        }
      }
    
  5. Arvin
    | #5

    What kind of struct are you referencing here “a->timePerStep”.

  6. admin
  7. Arvin
    | #7

    Ok. I have looked at your code….Lots of work there. Now lots of questions for you. :)

    The only part that I currently am concerned with boils down to the single idea of making a step by checking if “half” the time slice is past. Why half?
    if (!a->stepped && (timeIntoSlice >= ((a->timePerStep)>>1))) {

    Why not 1/4 or 3/4 or 15/16 or some other fraction….Yeah I see the >>1, so just for making it quick/easy?

    Have you got those motors hooked up and running?

    Do all three stages manage to get done at the same time? If not, how close to the same time? Microseconds or milliseconds from the same time would be close enough I suppose. Is it noticeable without a scope?

    Do you see any improvement over the Bresenham method? In other words do I need to rewrite my method to receive the benefits of your?

    Have started a rewrite but have not succeeded yet.

  8. Reza
    | #8

    @Arvin
    I should have scanned this page I used to make my calculations. I assumed motion on the X-Y axis only and drew lines at various angles to simulate different X/Y travel. Assuming a constant X-travel speed, and you then mark where the y-steps need to occur to travel smooth and even, and you compare that to a distribution of timeslices equal to the number of required steps, you’ll find that the transitions need to happen at 1/2 the timeslice for the best results.

    I’m hoping to get my mill in this week, though I might not have a chance to test for another 2 weeks.

    The results I posted are using a logic analyzer to measure the step timing. I’m not sure what you mean by get done at the same time. They all move in synchrony to ensure even stepping for all axis. The goal is to have all the timeslices be equal, and I measured a maximum deviation of 150uS which seems reasonable to me.

    The improvement is noticeable if you compare the first stepping patter in the post to the second. If you look at the first graph, you’ll notice that the Y-axis steps very unevenly. Based on my research, this can cause all sorts of problems.

    Why not just use my code? If you have custom code, I can help incorporate it.

    I’ve also been in touch with a friend who is going to manufacture (for sale) an arduino shield based on my design. It will support up to 2A/axis (configurable) at 35V max and will have all 3 axis populated. I just doing this because I think it’ll help people get up to speed quickly and more economically than having to have a dedicated PC.

  9. Arvin
    | #9

    Hey. I have used PORTD and PORTB with my custom software. It should get rid of the delay you will have using digitalWrite(). I didn’t use a for next loop to change axis. I would zip up my code and let you have it but it doesn’t look like this site allows attachments.

    To be able to step with the same equal timeslice, you are talking about, I give each axis a time to step equal to the longest time for the axis taking the most steps divided by the number of steps by each axis (after checking for zero of course). All axis will, I hope(untested), finish up their move at the same time and “get there at the same time”.

    I copied it and will paste it here….not going to be pretty…indentation will be all messed up I’m sure. Hmmm. Looks pretty good for just pasting it here. I did use ctrl-T to format it. Replaces tabs with spaces.
    Just used your code as inspiration, so any resemblance is incidental. This is going to be an awfully long post. Sorry.

    //———————- arduino code follows ———————

    boolean takeSteps(long xsteps, long ysteps, long zsteps){
    long elapsedTime, oldTime=0, curTime=0, numSteps=0, timeSteps;
    long xtime,ytime,ztime;
    long startTime = micros();//time at beginning of function.
    long stepTime = long(stepDelay + delayVal);//total time of a step.
    boolean stepped=false;

    //probably should be done before entering takeSteps()
    setDirection(xsteps, ysteps, zsteps);

    // simple function to return the largest number of steps.
    numSteps = mostSteps(xsteps, ysteps, zsteps);
    timeSteps = numSteps * stepTime;
    // timeSteps is the total time of the move.
    // xtime,ytime,ztime will be the time neccessary
    // for one step on that axis.
    // NO divide by zero errors!
    if (xsteps>0){
    xtime = timeSteps / xsteps;
    }
    else{
    xtime=timeSteps;
    }
    if (ysteps>0){
    ytime = timeSteps / ysteps;
    }
    else{
    ytime=timeSteps;
    }
    if (zsteps>0){
    ztime = timeSteps / zsteps;
    }
    else{
    ztime=timeSteps;
    }
    // dropping the fractional time. A fraction of a microsecond is
    // less than a 1,000,000th of a second. ie. 0.000001 sec.

    curTime = micros();
    oldTime = curTime;

    if(enabled){
    while(numSteps > 0){
    stepped = false;
    //clear pins 4,5,6, leave all other pins untouched
    PORTD = PORTD & (B10001111);
    elapsedTime = curTime – oldTime;

    // following if’s set pins to on if enough time has ellapsed.
    if(elapsedTime >= xtime){
    PORTD = PORTD | (B01000000);
    // delay to allow pulse to be effective.
    delayMicroseconds(stepDelay);
    stepped = true;
    }
    if(elapsedTime >= ytime){
    PORTD = PORTD | (B00100000);
    delayMicroseconds(stepDelay);
    stepped = true;
    }
    if(elapsedTime >= ztime){
    PORTD = PORTD | (B00010000);
    delayMicroseconds(stepDelay);
    stepped = true;
    }
    if(stepped){
    numSteps–;
    }
    curTime = micros();
    }
    }
    else{
    // think of it as a pause button.
    // used here for an emergency stop!
    // Should also use a mechanical switch.
    //clear pins 4,5,6, leave all other pins untouched
    PORTD = PORTD & (B10001111);
    return false;
    }
    // if all steps have been taken return true.
    PORTD = PORTD & (B10001111);
    return true;
    }

  10. admin
    | #10

    I’m not sure why you bother accessing the ports directly and follow it with a delay if your goal is to get rid of the delay. And if you are delaying by the time for the step, then your code will fail as the other steps are not happening when they should. The command

    digitalWrite(a->step_pin, HIGH);
    digitalWrite(a->step_pin, LOW);

    takes exactly 5uS to complete, which seems about reasonable and avoids my having to implement a delay. I also suggest you either use the macro _BV(x) or else specify the bits in question using a bitshift operator (1<<x) — its' a lot easier to read and avoid possible bugs from having one extra or one too few 0's.

    I'm actually into code aesthetics — I think it would look better if you did

    xtime = timeSteps /((xsteps)?xsteps:1);

    while (numSteps)

    But again, that’s just aesthetics. Your code also will generate variable width pulses depending on what’s going on — not sure if you want that or not. You also base your maximum speed on the maximum speed of a single axis which is a fine approach. Though you can supply a speed via the g-code, which your code seems to ignore by operating at the maximum speed of an axis. My calculations are centered around the desired speed of travel, not the maximum.

    I’ve not spent a lot of time figuring out if the code will work, but my biggest concern is that you do not track individual steps per axis and I suspect it will not work because of that.

    I highly suggest you buy a logic (http://www.sparkfun.com/commerce/product_info.php?products_id=8938) and use that to debug your logic. Or just use my code :)

  11. Arvin
    | #11

    Which delay are you talking about. There’s only one and it’s a short one after the port write(10uS but 2uS would be ok.). The port write sends all three stepper data in one step. With the for loop you use 6 digitalWrites as opposed to 2 port writes so 6*5 is 30 vs. 2? Neither should be a problem. I have no scope to check.

    _BV() is new to me. I could use it but why. (B00000000) highlights in blue on my system if it has 8 bits so I know exactly when it’s eight. I can also see exactly which bits I’m working with. “Beauty is in the eye of the beholder”! ;)

  12. Arvin
    | #12

    > Though you can supply a speed via the g-code, which your code seems
    > to ignore by operating at the maximum speed of an axis.

    delayMicroseconds(stepDelay);
    is the delay for the modal feedrate which is set in earlier code that parses the actual line of gcode. My global variable for feedrate is stepDelay.

    I had it written in three places. Thanks for the heads up. Only needed it in the if(stepped) code.

    It’s all moot until I can get the time to actually run some gcode through this new firmware and see if it works. I was just rewriting my Breshenham algorithm which was working to be able to use your timing idea.

    This has been fun but I’d rather be writing code! :)

  13. | #13

    Your Maximum Feed Rates defines are they in mm or inches?

  14. admin
    | #14

    In the .h file you’ll find both mm and inch definitions; one of the g-code’s sets the units in inches or mm.

  15. | #15

    Thanks for such an interesting post, it is great! ;-)

  16. | #16

    Don’t waste money on the Saleae Logic. The hardware is absolutely not worth the cost, and the software isn’t spectacular either. The product is mostly marketing. (It even uses the USB product description strings from firmware published in a well-known and popular book about USB.) I would suggest an Open Logic Sniffer (see http://dangerousprototypes.com/docs/Open_Bench_Logic_Sniffer) instead, which costs less than a third, is completely open source hardware and firmware, has actual hardware triggers, and runs with the original Java client from sump.org where the FPGA design was developed, a fork of that software which looks beautiful, as well as the open source sigrok logic analyzer software project. (see http://sigrok.org/)

    Or if you are really ready to spend what the Saleae costs then I would suggest a Digilent Spartan-3E FPGA board (see http://www.digilentinc.com/Products/Detail.cfm?Prod=S3EBOARD) instead, which is packed with the FPGA and a bunch of other chips, among others the only chip that is actually in the Saleae. I blogged about how to put the sump.org logic analyzer design on that Digilent board from Linux. (see http://peter.stuge.se/spartan-3e-logic-analyzer)

  17. admin
    | #17

    I’m not sure why your opposed to the logic unit. My last logic analyzer (USBee) cost $950, and I like the Saleae much more. It’s a nice compact unit in a very nice machined aluminum case — which I would much rather use than a giant PCB with no enclosure. I think it’s perfect for casual use and I have definitely gotten my money’s worth out of it. I also hate java and would prefer not to use it. The included software is great, fast, clean interface. I’ve also met the guy who makes them — really nice guy and I wish him the best of luck with this project. Have you tried the unit & software or is this just from the specs & price?

  1. No trackbacks yet.