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;
}
}
}