AccelStepper synchronized motors WITH acceleration

Dear past self,

For a long time, you've wanted to be able to use the AccelStepper library with synchronized motor movements WITH acceleration.

There are several reasons for this..

  • For one, it's smoother and looks better.
  • More importantly, acceleration is required to achieve higher speeds without stalling, because it allows the stepper motors to start slow (higher torque) and overcome any initial friction, and then speed up when that friction has been overcome. For example, when using lead screws.

Unfortunately, AccelStepper doesn't support coordinated movement of multiple motors with acceleration. The MultiStepper component within AccelStepper drives each motor at the exact consistent speed required for each motor to reach its destination at the same time without acceleration, which isn't suitable for many applications.

Well, fear not, your future self has managed to put together some working code that utilizes AccelStepper with synchronized movement and acceleration. Hopefully this helps someone else out there looking for something like this...

It's all thanks to @Robin2, who posted this article some years ago:

@Robin2 theorized that, what if each time you need to move the motors, you identify which motor will have the longest travel distance, set it to move at a particular speed and acceleration, and slave all of the other motors (including their acceleration) based on that motor?

For example, if you know that MotorX needs to travel 1000 steps, and you know MotorY needs to travel 500 steps, then you can tell MotorX to run like it normally would in AccelStepper, and meanwhile write some code that sends a pulse to MotorY every other time MotorX is sent a pulse. If MotorZ needs to travel 333 steps, then pulse MotorZ every third step of MotorX.

This means MotorY and MotorZ will follow the same acceleration pattern as MotorX, but in a slowed down manner.

I'm not the best coder in the world, but I was able to extrapolate Robin2's example code into a working format. My code also compensates for a situation I'm experiencing where one or two motors might still be missing 1 or 2 steps at the end and still need to move into position (presumably due to fractions of steps not being whole numbers). Some of this code might not be immediately plug-and-play, since I am extracting it from many files I'm working on. So, you may need to adjust it to your own project.

For anyone that comes by, feel free to comment/update with any feedback...this may not be perfect, but it is working for the most part. You can add as many additional motors as AccelStepper/your processor will handle.

I hope this might help someone else some day!...

And once again, thank you @Robin2! You are a genius and a blessing to the Arduino community, this is merely just one example.

Sample Code using AccelStepper with multiple, synchronized motors and acceleration
FYI I extracted this from my own files and tried simplifying it/condensing it so as to minimize references to external functions of my own, so sorry if there are any artifacts. Let me know if I made a mistake/missed something, and I'll try to clarify it for you.

//Make sure you include the AccelStepper library and setup your motors
//For example:
//#include <AccelStepper.h>
//AccelStepper motorX(1,MOTOR_X_STEP_PIN,MOTOR_X_DIR_PIN); // first argument "1" sets this as a "DRIVER", second argument is "step pin", third is "direction pin"
//etc...

//Variables for void runAllSimultaneously()
  float longestD2G;
  char longestMotor;

  float xD2G;
  float yD2G;
  float zD2G;
  
  float xStepDelay;
  float yStepDelay;
  float zStepDelay;

  float stepCounter;
  float xStepCounter;
  float yStepCounter;
  float zStepCounter;
 
  float lastD2G;

  int motorXDir;
  int motorYDir;
  int motorZDir;
  
  const int minPulseWidth = 20; //minimum pulse (in microseconds) width of manual steps initiated in Coordinated Move functions

void runAllSimultaneously(float xPos, float yPos, float zPos) //AccelStepper with slaved acceleration
  {
    static int stage = 0;

    if (stage == 0)
    {
      //Step 1: Who has the farthest distance to travel?

      motorX.setMaxSpeed(MAX_SPEED_X);
      motorY.setMaxSpeed(MAX_SPEED_Y);
      motorZ.setMaxSpeed(MAX_SPEED_Z);    
      
      resetAcceleration(); //it's a function I created that simply sets the max acceleration of each motor to some default value


      longestD2G = 0;

      xD2G = abs(xPos - motorX.currentPosition());
      yD2G = abs(yPos - motorY.currentPosition());
      zD2G = abs(zPos - motorZ.currentPosition());

      if (xD2G > longestD2G)
      {
      longestD2G = xD2G;
      longestMotor = 'x';   
      }

      if (yD2G > longestD2G)
      {
      longestD2G = yD2G;
      longestMotor = 'y';
      }

      if (zD2G > longestD2G)
      {
      longestD2G = zD2G;
      longestMotor = 'z';
      }

      //Step 2: Assign a step delay to each motor (e.g. after how many steps of the master motor should the slave motor be pulsed?)  

      if (xD2G > 0)
      {
        xStepDelay = longestD2G/xD2G;
      }
      else
      {
        xStepDelay = 0;
      }

      if (yD2G > 0)
      {
        yStepDelay = longestD2G/yD2G;
      }
      else
      {
        yStepDelay = 0;
      }

      if (zD2G > 0)
      {
        zStepDelay = longestD2G/zD2G;
      }
      else
      {
        zStepDelay = 0;
      }

     //Step 3: Set the initial stepcounter values  
      stepCounter = 0;  
      xStepCounter = xStepDelay - 1;
      yStepCounter = yStepDelay - 1; 
      zStepCounter = zStepDelay - 1; 
      

      //Step 4:   Set the motor travel moves and other values depending on which motor is the longest motor
      if (longestMotor == 'x')
      {
        Serial.println("longestMotor is X");

        motorX.moveTo(xPos);
        lastD2G = motorX.distanceToGo();

        //might need to add these so it knows there's a target position
        motorY.setTargetPosition(yPos);
        motorZ.setTargetPosition(zPos);
      }
      else if (longestMotor == 'y')
      {
        Serial.println("longestMotor is Y");

        motorY.moveTo(yPos);
        lastD2G = motorY.distanceToGo();

        //might need to add these so it knows there's a target position
        motorX.setTargetPosition(xPos);
        motorZ.setTargetPosition(zPos);
      }
      else //if (longestMotor == 'z')
      {
        Serial.println("longestMotor is Z");

        motorZ.moveTo(zPos);
        lastD2G = motorZ.distanceToGo();

        //might need to add these so it knows there's a target position
        motorX.setTargetPosition(xPos);
        motorY.setTargetPosition(yPos);
        
      }
     
       //Step 5: Set the direction of each motor
      if ((xPos - motorX.currentPosition()) < 0) //Negative means you are too far ahead, need to go backwards and direction will decrease
      {
        //Negative (Left) Direction moves motorX Counter Clockwise
        digitalWrite(MOTOR_X_DIR_PIN, LOW); //set to counter clockwise
        motorXDir = -1; //decrement X position 
      }
      else
      {
        digitalWrite(MOTOR_X_DIR_PIN, HIGH); //set to clockwise
        motorXDir = 1; //increment X position
      }

      if ((yPos - motorY.currentPosition()) < 0) //Negative means you are too far ahead, need to go backwards and direction will decrease
      {
        //Negative (Back) Direction moves motorY Clockwise
        digitalWrite(MOTOR_Y_DIR_PIN, HIGH); 
        motorYDir = -1; //decrement Y position
      }
      else
      {
        digitalWrite(MOTOR_Y_DIR_PIN, LOW); //opposite of above
        motorYDir = 1; //increment position tracking location
      }

      if ((zPos - motorZ.currentPosition()) < 0) //Negative means you are too high up, want to go down and decrement position 
      {
        //Negative (Down) Direction moves motorZ Clockwise
        digitalWrite(MOTOR_Z_DIR_PIN, HIGH); //clockwise
        motorZDir = -1; //decrement Z position
      }
      else
      {
        digitalWrite(MOTOR_Z_DIR_PIN, LOW); //opposite of above
        motorZDir = 1;
      }

     stage++;

    }
    
    if (stage == 1)
    {
      //Run the primary motor, and step the other motors in proportion to the primary (longest) motor
      if (longestMotor == 'x')
      {
        if (lastD2G != motorX.distanceToGo()) //if there has been a change in how much distance the motor has left to travel....then update the step counter so all the slave motors know when to move
        { 
          stepCounter += xStepDelay;
          lastD2G = motorX.distanceToGo();
        }            
        
        motorX.run(); //would be better to have a run safely command in here specific to the motor where it checks for endstops/obstructions instead of blindly running

        coordinatedYMove();
        coordinatedZMove();
      }
      else if (longestMotor == 'y')
      {
        if (lastD2G != motorY.distanceToGo())
        { 
          stepCounter += yStepDelay;
          lastD2G = motorY.distanceToGo();
        }  

        motorY.run();

        coordinatedXMove();
        coordinatedZMove();       
      }
      else if (longestMotor == 'z')
      {
        if (lastD2G != motorZ.distanceToGo())
        { 
          stepCounter += zStepDelay;
          lastD2G = motorZ.distanceToGo();
        }  
        
        motorZ.run();

        coordinatedXMove();
        coordinatedYMove();        
      }
      
      if (motorX.distanceToGo() == 0 && motorY.distanceToGo() == 0 && motorZ.distanceToGo() == 0) 
      {
        //reset the stage of this function so that it can begin again when called again in the future
        stage = 0;   
        Serial.println("runAllSimultaneously Completed.");
      }   

    }
    
  }

    

  void coordinatedXMove() //if this is not the longest motor, it will perform a step every [stepDelay] number of steps compared to the longest motor
  {
    if (xStepDelay > 0)
    {
      if (xStepCounter < stepCounter)
      {
        //Serial.println("Motor X Coordinated Pulse");
        digitalWrite(MOTOR_X_STEP_PIN, HIGH);
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_X_STEP_PIN, LOW);

        motorX.updateCurrentPosition(motorX.currentPosition() + motorXDir*1);

        xStepCounter += xStepDelay;

      }
      else if (lastD2G == 0 && motorX.distanceToGo() > 0) //motor hasn't traveled right enough yet
      {
         Serial.println("X Hasn't Moved Right Enough. ");
        //Serial.println("Motor X Coordinated Pulse");

        digitalWrite(MOTOR_X_DIR_PIN, HIGH); //clockwise
        digitalWrite(MOTOR_X_STEP_PIN, HIGH);
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_X_STEP_PIN, LOW);

        motorX.updateCurrentPosition(motorX.currentPosition() + 1);

        xStepCounter += xStepDelay;

      }
      else if (lastD2G == 0 && motorX.distanceToGo() < 0) //motor hasn't traveled left enough yet
      {
         Serial.println("X Hasn't Moved Left Enough. ");
         //Serial.println("Motor X Coordinated Pulse");

        digitalWrite(MOTOR_X_DIR_PIN, LOW); //counter clockwise
        digitalWrite(MOTOR_X_STEP_PIN, HIGH);
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_X_STEP_PIN, LOW);

        motorX.updateCurrentPosition(motorX.currentPosition() - 1);

        xStepCounter += xStepDelay;

      }
    }
  }


  void coordinatedYMove()
  {
    if (yStepDelay > 0)
    {
      if ((yStepCounter < stepCounter))
      {
        //Serial.println("Motor Y Coordinated Pulse");
        digitalWrite(MOTOR_Y_STEP_PIN, HIGH); //run the motors with a minPulseWidth.
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_Y_STEP_PIN, LOW);

        motorY.updateCurrentPosition(motorY.currentPosition() + motorYDir*1);

        yStepCounter += yStepDelay;

      }
      else if (lastD2G == 0 && motorY.distanceToGo() > 0) //motor hasn't traveled forward enough yet
      {

        //Debug
        Serial.println("Y Hasn't Moved Forward Enough. ");
       
        digitalWrite(MOTOR_Y_DIR_PIN, LOW); //set direction forward

        //Serial.println("Motor Y Coordinated Pulse");
        digitalWrite(MOTOR_Y_STEP_PIN, HIGH); //run the motors with a minPulseWidth. 
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_Y_STEP_PIN, LOW);

        motorY.updateCurrentPosition(motorY.currentPosition() + 1);

        yStepCounter += yStepDelay;     

      }
      else if (lastD2G == 0 && motorY.distanceToGo() < 0) //motor hasn't traveled backwards enough yet
      {
        //Debug
        Serial.println("Y Hasn't Moved Backward Enough. ");

        digitalWrite(MOTOR_Y_DIR_PIN, HIGH); //set direction backwards

        //Serial.println("Motor Y Coordinated Pulse");
        digitalWrite(MOTOR_Y_STEP_PIN, HIGH); //run the motors with a minPulseWidth.
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_Y_STEP_PIN, LOW);

        motorY.updateCurrentPosition(motorY.currentPosition() - 1);

        yStepCounter += yStepDelay;     

      }
    }
  }

  void coordinatedZMove()
  {
    if (zStepDelay > 0)
    {
      if ((zStepCounter < stepCounter))
      {
        //Serial.println("Motor Z Coordinated Pulse");
        digitalWrite(MOTOR_Z_STEP_PIN, HIGH);
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_Z_STEP_PIN, LOW);

        motorZ.updateCurrentPosition(motorZ.currentPosition() + motorZDir*1);

        zStepCounter += zStepDelay;

      }
      else if (lastD2G == 0 && motorZ.distanceToGo() > 0) //motor hasn't moved up enough
      {     
        Serial.println("Z Hasn't Moved Up Enough. "); 
        
        //Run the motor
        digitalWrite(MOTOR_Z_DIR_PIN, LOW);//counter clockwise = up
        digitalWrite(MOTOR_Z_STEP_PIN, HIGH);
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_Z_STEP_PIN, LOW);

        motorZ.updateCurrentPosition(motorZ.currentPosition() + 1);

        zStepCounter += zStepDelay;

      }
      else if (lastD2G == 0 && motorZ.distanceToGo() < 0) //motor hasn't down up enough
      {
        Serial.println("Z Hasn't Moved Down Enough. ");
        
        //Run the motor
        digitalWrite(MOTOR_Z_DIR_PIN, HIGH);//clockwise = down
        digitalWrite(MOTOR_Z_STEP_PIN, HIGH);
        delayMicroseconds(minPulseWidth);
        digitalWrite(MOTOR_Z_STEP_PIN, LOW);

        motorZ.updateCurrentPosition(motorZ.currentPosition() - 1);

        zStepCounter += zStepDelay;

      }
    }
  }

1 Like

Woah Nice

1 Like

// modified code working
// didnt check properly but working

#define MOTOR_X_STEP_PIN 2
#define MOTOR_X_DIR_PIN 3

#define MOTOR_Y_STEP_PIN 4
#define MOTOR_Y_DIR_PIN 5

#define MOTOR_Z_STEP_PIN 6
#define MOTOR_Z_DIR_PIN 7

long MAX_SPEED_X = 1000;
long MAX_SPEED_Y = 1000;
long MAX_SPEED_Z = 1000;

long MAX_Acc = 1000;

#include <AccelStepper.h>
AccelStepper motorX(1,MOTOR_X_STEP_PIN,MOTOR_X_DIR_PIN);
AccelStepper motorY(1,MOTOR_Y_STEP_PIN,MOTOR_Y_DIR_PIN);
AccelStepper motorZ(1,MOTOR_Z_STEP_PIN,MOTOR_Z_DIR_PIN);

//Variables for void runAllSimultaneously()
float longestD2G;
char longestMotor;

float xD2G;
float yD2G;
float zD2G;

float xStepDelay;
float yStepDelay;
float zStepDelay;

float stepCounter;
float xStepCounter;
float yStepCounter;
float zStepCounter;

float lastD2G;

int motorXDir;
int motorYDir;
int motorZDir;

const int minPulseWidth = 20; //minimum pulse (in microseconds) width of manual steps initiated in Coordinated Move functions

void setup() {
// Initialize your hardware, pins, serial communication, etc., here if needed.
// For example, you can set the motor pin modes:
pinMode(MOTOR_X_STEP_PIN, OUTPUT);
pinMode(MOTOR_X_DIR_PIN, OUTPUT);
pinMode(MOTOR_Y_STEP_PIN, OUTPUT);
pinMode(MOTOR_Y_DIR_PIN, OUTPUT);
pinMode(MOTOR_Z_STEP_PIN, OUTPUT);
pinMode(MOTOR_Z_DIR_PIN, OUTPUT);

// Initialize Serial communication if needed:
// Serial.begin(9600);
}

void loop() {
// Add code here to execute repeatedly, if needed.
// For example, if you want to run your function continuously, you can call it like this:

runAllSimultaneously(8000, 2000, 3000);

// Replace these values with your desired target positions.
// Delay for a second to slow down the loop. You can adjust this delay as needed.
}

void runAllSimultaneously(float xPos, float yPos, float zPos) //AccelStepper with slaved acceleration
{
static int stage = 0;

if (stage == 0)
{
  //Step 1: Who has the farthest distance to travel?

  motorX.setMaxSpeed(MAX_SPEED_X);
  motorY.setMaxSpeed(MAX_SPEED_Y);
  motorZ.setMaxSpeed(MAX_SPEED_Z);    

// resetAcceleration(); //it's a function I created that simply sets the max acceleration of each motor to some default value

  motorX.setAcceleration(MAX_Acc);
  motorY.setAcceleration(MAX_Acc);
  motorZ.setAcceleration(MAX_Acc);

  longestD2G = 0;

  xD2G = abs(xPos - motorX.currentPosition());
  yD2G = abs(yPos - motorY.currentPosition());
  zD2G = abs(zPos - motorZ.currentPosition());

  if (xD2G > longestD2G)
  {
  longestD2G = xD2G;
  longestMotor = 'x';   
  }

  if (yD2G > longestD2G)
  {
  longestD2G = yD2G;
  longestMotor = 'y';
  }

  if (zD2G > longestD2G)
  {
  longestD2G = zD2G;
  longestMotor = 'z';
  }

  //Step 2: Assign a step delay to each motor (e.g. after how many steps of the master motor should the slave motor be pulsed?)  

  if (xD2G > 0)
  {
    xStepDelay = longestD2G/xD2G;
  }
  else
  {
    xStepDelay = 0;
  }

  if (yD2G > 0)
  {
    yStepDelay = longestD2G/yD2G;
  }
  else
  {
    yStepDelay = 0;
  }

  if (zD2G > 0)
  {
    zStepDelay = longestD2G/zD2G;
  }
  else
  {
    zStepDelay = 0;
  }

 //Step 3: Set the initial stepcounter values  
  stepCounter = 0;  
  xStepCounter = xStepDelay - 1;
  yStepCounter = yStepDelay - 1; 
  zStepCounter = zStepDelay - 1; 
  

  //Step 4:   Set the motor travel moves and other values depending on which motor is the longest motor
  if (longestMotor == 'x')
  {
    Serial.println("longestMotor is X");

    motorX.moveTo(xPos);
    lastD2G = motorX.distanceToGo();

    //might need to add these so it knows there's a target position
    motorY.moveTo(yPos);
    motorZ.moveTo(zPos);
  }
  else if (longestMotor == 'y')
  {
    Serial.println("longestMotor is Y");

    motorY.moveTo(yPos);
    lastD2G = motorY.distanceToGo();

    //might need to add these so it knows there's a target position
    motorX.moveTo(xPos);
    motorZ.moveTo(zPos);
  }
  else //if (longestMotor == 'z')
  {
    Serial.println("longestMotor is Z");

    motorZ.moveTo(zPos);
    lastD2G = motorZ.distanceToGo();

    //might need to add these so it knows there's a target position
    motorX.moveTo(xPos);
    motorY.moveTo(yPos);
    
  }
 
   //Step 5: Set the direction of each motor
  if ((xPos - motorX.currentPosition()) < 0) //Negative means you are too far ahead, need to go backwards and direction will decrease
  {
    //Negative (Left) Direction moves motorX Counter Clockwise
    digitalWrite(MOTOR_X_DIR_PIN, LOW); //set to counter clockwise
    motorXDir = -1; //decrement X position 
  }
  else
  {
    digitalWrite(MOTOR_X_DIR_PIN, HIGH); //set to clockwise
    motorXDir = 1; //increment X position
  }

  if ((yPos - motorY.currentPosition()) < 0) //Negative means you are too far ahead, need to go backwards and direction will decrease
  {
    //Negative (Back) Direction moves motorY Clockwise
    digitalWrite(MOTOR_Y_DIR_PIN, HIGH); 
    motorYDir = -1; //decrement Y position
  }
  else
  {
    digitalWrite(MOTOR_Y_DIR_PIN, LOW); //opposite of above
    motorYDir = 1; //increment position tracking location
  }

  if ((zPos - motorZ.currentPosition()) < 0) //Negative means you are too high up, want to go down and decrement position 
  {
    //Negative (Down) Direction moves motorZ Clockwise
    digitalWrite(MOTOR_Z_DIR_PIN, HIGH); //clockwise
    motorZDir = -1; //decrement Z position
  }
  else
  {
    digitalWrite(MOTOR_Z_DIR_PIN, LOW); //opposite of above
    motorZDir = 1;
  }

 stage++;

}

if (stage == 1)
{
  //Run the primary motor, and step the other motors in proportion to the primary (longest) motor
  if (longestMotor == 'x')
  {
    if (lastD2G != motorX.distanceToGo()) //if there has been a change in how much distance the motor has left to travel....then update the step counter so all the slave motors know when to move
    { 
      stepCounter += xStepDelay;
      lastD2G = motorX.distanceToGo();
    }            
    
    motorX.run(); //would be better to have a run safely command in here specific to the motor where it checks for endstops/obstructions instead of blindly running

    coordinatedYMove();
    coordinatedZMove();
  }
  else if (longestMotor == 'y')
  {
    if (lastD2G != motorY.distanceToGo())
    { 
      stepCounter += yStepDelay;
      lastD2G = motorY.distanceToGo();
    }  

    motorY.run();

    coordinatedXMove();
    coordinatedZMove();       
  }
  else if (longestMotor == 'z')
  {
    if (lastD2G != motorZ.distanceToGo())
    { 
      stepCounter += zStepDelay;
      lastD2G = motorZ.distanceToGo();
    }  
    
    motorZ.run();

    coordinatedXMove();
    coordinatedYMove();        
  }
  
  if (motorX.distanceToGo() == 0 && motorY.distanceToGo() == 0 && motorZ.distanceToGo() == 0) 
  {
    //reset the stage of this function so that it can begin again when called again in the future
    stage = 0;   
    Serial.println("runAllSimultaneously Completed.");
  }   

}

}

void coordinatedXMove() //if this is not the longest motor, it will perform a step every [stepDelay] number of steps compared to the longest motor
{
if (xStepDelay > 0)
{
if (xStepCounter < stepCounter)
{
//Serial.println("Motor X Coordinated Pulse");
digitalWrite(MOTOR_X_STEP_PIN, HIGH);
delayMicroseconds(minPulseWidth);
digitalWrite(MOTOR_X_STEP_PIN, LOW);

    motorX.setCurrentPosition(motorX.currentPosition() + motorXDir*1);

    xStepCounter += xStepDelay;

  }
  else if (lastD2G == 0 && motorX.distanceToGo() > 0) //motor hasn't traveled right enough yet
  {
     Serial.println("X Hasn't Moved Right Enough. ");
    //Serial.println("Motor X Coordinated Pulse");

    digitalWrite(MOTOR_X_DIR_PIN, HIGH); //clockwise
    digitalWrite(MOTOR_X_STEP_PIN, HIGH);
    delayMicroseconds(minPulseWidth);
    digitalWrite(MOTOR_X_STEP_PIN, LOW);

    motorX.setCurrentPosition(motorX.currentPosition() + 1);

    xStepCounter += xStepDelay;

  }
  else if (lastD2G == 0 && motorX.distanceToGo() < 0) //motor hasn't traveled left enough yet
  {
     Serial.println("X Hasn't Moved Left Enough. ");
     //Serial.println("Motor X Coordinated Pulse");

    digitalWrite(MOTOR_X_DIR_PIN, LOW); //counter clockwise
    digitalWrite(MOTOR_X_STEP_PIN, HIGH);
    delayMicroseconds(minPulseWidth);
    digitalWrite(MOTOR_X_STEP_PIN, LOW);

    motorX.setCurrentPosition(motorX.currentPosition() - 1);

    xStepCounter += xStepDelay;

  }
}

}

void coordinatedYMove()
{
if (yStepDelay > 0)
{
if ((yStepCounter < stepCounter))
{
//Serial.println("Motor Y Coordinated Pulse");
digitalWrite(MOTOR_Y_STEP_PIN, HIGH); //run the motors with a minPulseWidth.
delayMicroseconds(minPulseWidth);
digitalWrite(MOTOR_Y_STEP_PIN, LOW);

    motorY.setCurrentPosition(motorY.currentPosition() + motorYDir*1);

    yStepCounter += yStepDelay;

  }
  else if (lastD2G == 0 && motorY.distanceToGo() > 0) //motor hasn't traveled forward enough yet
  {

    //Debug
    Serial.println("Y Hasn't Moved Forward Enough. ");
   
    digitalWrite(MOTOR_Y_DIR_PIN, LOW); //set direction forward

    //Serial.println("Motor Y Coordinated Pulse");
    digitalWrite(MOTOR_Y_STEP_PIN, HIGH); //run the motors with a minPulseWidth. 
    delayMicroseconds(minPulseWidth);
    digitalWrite(MOTOR_Y_STEP_PIN, LOW);

    motorY.setCurrentPosition(motorY.currentPosition() + 1);

    yStepCounter += yStepDelay;     

  }
  else if (lastD2G == 0 && motorY.distanceToGo() < 0) //motor hasn't traveled backwards enough yet
  {
    //Debug
    Serial.println("Y Hasn't Moved Backward Enough. ");

    digitalWrite(MOTOR_Y_DIR_PIN, HIGH); //set direction backwards

    //Serial.println("Motor Y Coordinated Pulse");
    digitalWrite(MOTOR_Y_STEP_PIN, HIGH); //run the motors with a minPulseWidth.
    delayMicroseconds(minPulseWidth);
    digitalWrite(MOTOR_Y_STEP_PIN, LOW);

    motorY.setCurrentPosition(motorY.currentPosition() - 1);

    yStepCounter += yStepDelay;     

  }
}

}

void coordinatedZMove()
{
if (zStepDelay > 0)
{
if ((zStepCounter < stepCounter))
{
//Serial.println("Motor Z Coordinated Pulse");
digitalWrite(MOTOR_Z_STEP_PIN, HIGH);
delayMicroseconds(minPulseWidth);
digitalWrite(MOTOR_Z_STEP_PIN, LOW);

    motorZ.setCurrentPosition(motorZ.currentPosition() + motorZDir*1);

    zStepCounter += zStepDelay;

  }
  else if (lastD2G == 0 && motorZ.distanceToGo() > 0) //motor hasn't moved up enough
  {     
    Serial.println("Z Hasn't Moved Up Enough. "); 
    
    //Run the motor
    digitalWrite(MOTOR_Z_DIR_PIN, LOW);//counter clockwise = up
    digitalWrite(MOTOR_Z_STEP_PIN, HIGH);
    delayMicroseconds(minPulseWidth);
    digitalWrite(MOTOR_Z_STEP_PIN, LOW);

    motorZ.setCurrentPosition(motorZ.currentPosition() + 1);

    zStepCounter += zStepDelay;

  }
  else if (lastD2G == 0 && motorZ.distanceToGo() < 0) //motor hasn't down up enough
  {
    Serial.println("Z Hasn't Moved Down Enough. ");
    
    //Run the motor
    digitalWrite(MOTOR_Z_DIR_PIN, HIGH);//clockwise = down
    digitalWrite(MOTOR_Z_STEP_PIN, HIGH);
    delayMicroseconds(minPulseWidth);
    digitalWrite(MOTOR_Z_STEP_PIN, LOW);

    motorZ.setCurrentPosition(motorZ.currentPosition() - 1);

    zStepCounter += zStepDelay;

  }
}

}

1 Like

Cool, glad it worked! For some reason, the code you posted got separated into some bits that have "CODE" tags around them and some that don't, so you might want to fix that for anyone that comes back to this thread.

Did you have to make modifications or just define the values for the variables such as the STEP PINS, MAX SPEED, etc.?

FYI to anyone who comes by, I just posted a related post which is about performing a sequence of commands, such as a sequence of synchronized motor movements. While in this current post, we essentially created a function called

runAllSimultaneously(xPos, yPos, zPos)

which allows you to tell several motors to move to their target locations at the same time and with acceleration...

What we did not cover is how do you get something like a robot arm to...
Step 1: runAllSimultaneously to a particular position
Step 2: Then go to another position
Step 3: trigger a sensor or check something
Step 4: Go somewhere else..
etc..

So for that, I have created this post: Cycling through multiple robotic commands in sequence by waiting until the previous move has finished

If you are looking for more steps/sec than AccelStepper can handle, you might look towards the Bresenham algorithm and how it is implemented in GCode processors like GRBL, Marlin, Teacup, or MarginallyClever's demos at GitHub - MarginallyClever/GcodeCNCDemo: a simple example of making a CNC machine from an Adafruit Motor Shield

The Bresenham algorithm does what Robin's floating point math does in a couple fast lines of integer math.

See this and others:

https://forum.arduino.cc/search?q=bresenham%20order%3Aviews

1 Like

Thank you for those links! I'll have to look into the Bresenham algorithm more deeply...seems very interesting. From my initial scroll through some of those links, two considerations I saw brought up, although not sure if I captured them correctly are:

  • What happens when the ratio between two motors' distances is not a whole integer: There might be cases where, when one motor has arrived at its target location, the other motor still has 1 step to go due to fractional differences. Not 100% sure if that's what they were referring to here, but that phenomena is something I compensated for =in the code above.

  • The calculation of the motors' target distances and acceleration should be done initially, but then not re-calculated every single cycle as the motors are traveling. If you re-calculate every single cycle, I recall there being some odd behavior in a real world scenario.

Anyway, those are just some things I noticed while working on this, but I'll try to spend time learning more about the Bresenham algorithm because it seems pretty cool. If they already successfully implemented it for GCODE/CNC applications, then they obviously overcame those issues I brought up.

And thank you for the MarginallyClever link...since I'm not a programming expert, I took the easier path of migrating to a Teensy 4.1 to handle the faster steps per second... :sweat_smile:

It can work with non-integer ratios with by working with the deltaY and deltaX directly See Bresenham's line algorithm - Wikipedia

and its snippet:

plotLine(x0, y0, x1, y1)
    dx = x1 - x0
    dy = y1 - y0
    D = 2*dy - dx
    y = y0

    for x from x0 to x1
        plot(x, y)
        if D > 0
            y = y + 1
            D = D - 2*dx
        end if
        D = D + 2*dy

...where they add (two) dy in each step, and subtract (two)dx whenever the sum becomes positive.

If there's 13 horizontal steps (dx=13) and 7 vertical steps and the slope is the non-integer 7/13 ratio, it steps 13 times in x (adding 2*7 each to the sum , and 7 times in y (subtracting 2*13) to get back to 0. The 13 steps of +14 and adds the multiple of 2*7*13=182 and also subtracts exactly the same common multiple of 2*13*7=182. If there's a rational ratio between the two motors distances (which there will be if they are stepper motor steps) the algorithm won't have an extra step leftover.

Robin2's and the Bresenham algorithm keeps the steps on each axis synchronized, no matter what changes the acceleration does to the timing of the fastest/controlling axis.

The Teacup firmware code (the first RepRap firmware to implement acceleration according to https://reprap.org/wiki/Teacup_Firmware ) implements a few different acceleration schemes in its bresenham implementations in https://github.com/Traumflug/Teacup_Firmware/blob/master/dda.c file.

practically You could have
either acceleration
or
ultra-precise movement

This picture shows how the bresenham-algorithm deals with fractional slopes
you have a small deviation that goes up and down for a single step

If you use stepper-motors you have discrete steps. The resolution is not infinite small.

Whenever adding the fraction stays under the next higher digit left of the decimal point it is one step less.
Whenever adding the fraction goes above the next higher digit left of the decimal point it is one step more.

example-numbers

averaged over a bigger distance the bresenham algorithm comes to the exact coordinates that you defined.

Here a LibreOffice spread-sheet that shows the numbers and the graph


best regards Stefan

Why not both? If you have acceleration for the fast axis, you can use the Bresenham algorithm for precise synchonization of the other axes.

This is the way it is done with the bresenham algorithm.
The axle that has more steps to do is the leading axle.

This is the reason why you have to make case-differentiation if X-axis or Y-axis has more steps and into which direction in the sense of plus/minus.

If the acceleration is adjusted for the faster moving axle the slower moving will cope easily with the slower values.

Without acceleration this approach would be possible:
You use a timer-interrupt at highest possible frequency and set the step-pulses for X-axis and Y-axis into the correct ratio and then start creating pulses at the exact same time
easy to follow numbers example
x-Axis 417 pulses per second Y-axis 281 pulses per second.

If the all pulses are created at the exact time the stepper shall move the movement would be exact straight forward linear. In reality granulated by the timer-interrupt-frequency that is used as the frequency you use as the timing base for creating each n-th timer-interrupt a step-pulse

But then you can't use acceleration because in the acceleration phase the timing changes from step to step.

For the acceleration itself a pre-calculated array with the time-values is used. In this way the timing-values don't have to be calculated on the fly

best regards Stefan

Part of why AccelStepper is slow is that it does recalculate the speed with each step:

If you rely on AccelStepper() to calculate the accelerations at each long-axis step, as @Robin2's code and @coolarj10 do, and also use the @Robin2/@coolarj10 or the Bresenham algorithm to manage the ratios to synchronize the motions into a straight lines, you get the smaller, smooth accelerations on the subordinate axes.

If you pre-calculate an array of timing and speed changes, you don't need AccelStepper (or at least the slow speed recalculation part of AccelStepper::run();). But you still need to take the time to calculate the array.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.