3D Printed Robot Arm - Acceleration / Deceleration Planner Algorithm?

Hi @Geronimo

Just in case you are still interested on having a Trapezoidal Planner or for for anyone else interested interested on this topic, using code created by @jlauer some time ago I have created a library to implement a trapezoidal trajectory planner that works on top of SimpleFOC.

Basically is his code encapsulated in a C++ class instead oaf a bunch of methods and global variables.

TrapezoidalPlanner.h :


#ifndef __TRAPEZOIDAL_PLANNER__H
#define __TRAPEZOIDAL_PLANNER__H
#include <SimpleFOC.h>

class TrapezoidalPlanner
{
public:
    TrapezoidalPlanner(int);
    void doTrapezoidalPlannerCommand(char *command);
    void linkMotor(BLDCMotor*);
    void runPlannerOnTick();
    bool isPlannerMoving();
private:
    BLDCMotor* motor;
    unsigned long plannerTimeStap;
    int plannerPeriod; // 1000 / this number = Hz, i.e. 1000 / 100 = 10Hz, 1000 / 10 = 100Hz, 1000 / 5 = 200Hz, 1000 / 1 = 1000hZ
    float Vmax_;    // # Velocity max (rads/s)
    float Amax_;    // # Acceleration max (rads/s/s)
    float Dmax_;    // # Decelerations max (rads/s/s)
    float Y_;
    float Yd_;
    float Ydd_;
    float Tf_, Xi_, Xf_, Vi_, Ar_, Dr_, Vr_, Ta_, Td_, Tv_, yAccel_;
    unsigned long plannerStartingMovementTimeStamp;
    bool isTrajectoryExecuting;
    float sign(float val);
    float sign_hard(float val);
    bool calculateTrapezoidalPathParameters(float Xf, float Xi, float Vi, float Vmax, float Amax, float Dmax);
    void startExecutionOfPlannerTo(float newPos);
    void computeStepValuesForCurrentTime(float currentTrajectoryTime);
};

#endif

TrapezoidalPlanner.cpp

#include <Arduino.h>
#include <TrapezoidalPlanner.h>

//#define __debug

TrapezoidalPlanner::TrapezoidalPlanner(int tickPeriod ){
    plannerPeriod = tickPeriod;
    isTrajectoryExecuting = false;
    return;
}

void TrapezoidalPlanner::linkMotor(BLDCMotor *motorReference){
    motor = motorReference;
}

bool TrapezoidalPlanner::isPlannerMoving(){
    return isTrajectoryExecuting;
}

void TrapezoidalPlanner::doTrapezoidalPlannerCommand(char *gCodeCommand){
    #ifdef __debug
        Serial.print("GGode command: G");
        Serial.println(gCodeCommand);
    #endif
    
    // Parse this string for vals
    String commandSrt = String(gCodeCommand);
    float commandValue;
    switch (gCodeCommand[0]){
    case 'V':
        // Remove V so can convert to a float
        commandSrt = commandSrt.substring(1);
        commandValue = commandSrt.toFloat();
        Vmax_ = commandValue;
        // Try calling the planner to use this new velocity value
        // We have to use the current pos, vel, and accel
        // calc_plan_trapezoidal_path_at_start(Xf_, Xi_, Vi_, Vmax_, Amax_, Dmax_);
        calculateTrapezoidalPathParameters(Xf_, motor->shaft_angle, motor->shaft_velocity, Vmax_, Amax_, Dmax_);
        #ifdef __debug
            Serial.print("User wants velocity change. Vmax_: ");
            Serial.println(Vmax_);
        #endif
        break;
    case 'A':
        // Remove A so can convert to a float
        commandSrt = commandSrt.substring(1);
        commandValue = commandSrt.toFloat();
        Amax_ = commandValue;
        Dmax_ = Amax_;
        // Try calling the planner to use this new acceleration value
        // calc_plan_trapezoidal_path_at_start(Xf_, Xi_, Vi_, Vmax_, Amax_, Dmax_);
        calculateTrapezoidalPathParameters(Xf_, motor->shaft_angle, motor->shaft_velocity, Vmax_, Amax_, Dmax_);
        #ifdef __debug
            Serial.print("User wants acceleration change. Amax_: ");
            Serial.println(Amax_);
        #endif 
        break;
    case 'L':
        //  TODO  TODO TODO TODO
        //  TODO  TODO TODO TODO
        break;
    default:
        commandValue = commandSrt.toFloat();
        #ifdef __debug
            Serial.print("Move to new position (rads): ");
            Serial.println(commandValue);
        #endif 
        // We start moving to the new position
        startExecutionOfPlannerTo(commandValue);
        break;
    }
}

void TrapezoidalPlanner::runPlannerOnTick(){
    // This should get entered 100 times each second (100Hz)
    if ((unsigned long)(millis() - plannerTimeStap) > plannerPeriod){
        plannerTimeStap = millis();
        // see if we are in a move or not
        if (isTrajectoryExecuting){
            // we are in a move, let's calc the next position
            float timeSinceStartingTrajectoryInSeconds = (millis() - plannerStartingMovementTimeStamp) / 1000.0f;
            computeStepValuesForCurrentTime(timeSinceStartingTrajectoryInSeconds);
            motor->target = Y_;

            // see if we are done with our move
            if (timeSinceStartingTrajectoryInSeconds >= Tf_){
                // we are done with move
                // motor.monitor_downsample = 0; // disable monitor
                #ifdef __debug
                    Serial.println("Done with move");
                #endif 
                isTrajectoryExecuting = false;
            }
        }
    }
}

float TrapezoidalPlanner::sign(float val){
    if (val < 0)
        return -1.0f;
    if (val == 0)
        return 0.0f;
    // if val > 0:
    return 1.0f;
}

float TrapezoidalPlanner::sign_hard(float val){
    if (val < 0)
        return -1.0f;
    return 1.0f;
}

bool TrapezoidalPlanner::calculateTrapezoidalPathParameters(float Xf, float Xi, float Vi, float Vmax, float Amax, float Dmax){
    float dX = Xf - Xi;                          // Distance to travel
    float stop_dist = (Vi * Vi) / (2.0f * Dmax); // Minimum stopping distance
    float dXstop = sign(Vi) * stop_dist;         // Minimum stopping displacement
    float s = sign_hard(dX - dXstop);            // Sign of coast velocity (if any)
    Ar_ = s * Amax;                              // Maximum Acceleration (signed)
    Dr_ = -s * Dmax;                             // Maximum Deceleration (signed)
    Vr_ = s * Vmax;                              // Maximum Velocity (signed)

    // If we start with a speed faster than cruising, then we need to decel instead of accel
    // aka "double deceleration move" in the paper
    if ((s * Vi) > (s * Vr_)){
        Ar_ = -s * Amax;
    }

    // Time to accel/decel to/from Vr (cruise speed)
    Ta_ = (Vr_ - Vi) / Ar_;
    Td_ = -Vr_ / Dr_;

    // Integral of velocity ramps over the full accel and decel times to get
    // minimum displacement required to reach cuising speed
    float dXmin = 0.5f * Ta_ * (Vr_ + Vi) + 0.5f * Td_ * Vr_;

    // Are we displacing enough to reach cruising speed?
    if (s * dX < s * dXmin){
        // Short move (triangle profile)
        Vr_ = s * sqrt((Dr_ * sq(Vi) + 2 * Ar_ * Dr_ * dX) / (Dr_ - Ar_));
        Ta_ = max(0.0f, (Vr_ - Vi) / Ar_);
        Td_ = max(0.0f, -Vr_ / Dr_);
        Tv_ = 0.0f;
    }
    else{
        // Long move (trapezoidal profile)
        Tv_ = (dX - dXmin) / Vr_;
    }

    // Fill in the rest of the values used at evaluation-time
    Tf_ = Ta_ + Tv_ + Td_;
    Xi_ = Xi;
    Xf_ = Xf;
    Vi_ = Vi;
    yAccel_ = Xi + Vi * Ta_ + 0.5f * Ar_ * sq(Ta_); // pos at end of accel phase

    #ifdef __debug
        Serial.println("--------------------------------- ");
        Serial.println(" Calculated trapezoidal Values:   ");
        Serial.println("     Tf: " + String(Tf_));
        Serial.println("     Ta: " + String(Ta_));
        Serial.println("     Tv: " + String(Tv_));
        Serial.println("     Td: " + String(Td_));
        Serial.println("     --------------------- ");
        Serial.println("     Ar: " + String(Ar_));
        Serial.println("     Vr: " + String(Vr_));
        Serial.println("     Dr: " + String(Dr_));
        Serial.println("     --------------------- ");
        Serial.println("     Xf: " + String(Xf));
        Serial.println("     Xi: " + String(Xi));
    #endif

    return true;
}

void TrapezoidalPlanner::startExecutionOfPlannerTo(float newPos){
    
    // set our global of the new position
    Xf_ = newPos;
    // At this poin we are atarting to move following the trapezoidal profile
    plannerStartingMovementTimeStamp = millis();
    // take the position from SimpleFOC and set it to our start position
    Xi_ = motor->shaft_angle;
    // TODO: If we are being asked to move but are already in motion, we should go with the current velocity, position, and acceleration
    // and keep moving.
    // Now we need to do our calcs before we start moving
    calculateTrapezoidalPathParameters(Xf_, Xi_, Vi_, Vmax_, Amax_, Dmax_);
    motor->target = Y_; // could possibly put this in the method above
    // Tell user how long this move will take
    #ifdef __debug
        Serial.println("Starting to move to a new position");
        Serial.print("Time to complete move (secs):");
        Serial.println(Tf_);
        // Velocity and Accel of move
        Serial.print("Velocity for move: ");
        Serial.print(Vmax_);
        Serial.print(", Acceleration: ");
        Serial.println(Amax_);
    #endif
    // set our global bool so the tick method knows we have a move in motion
    isTrajectoryExecuting = true;
}
void TrapezoidalPlanner::computeStepValuesForCurrentTime(float currentTrajectoryTime){
    // Step_t trajStep;
    if (currentTrajectoryTime < 0.0f){ 
        // Initial Condition
        Y_ = Xi_;
        Yd_ = Vi_;
        Ydd_ = 0.0f;
        //Serial.println("--- Initial Stage ---");
        #ifdef __debug
            Serial.print(String(currentTrajectoryTime) + ",");
            Serial.print(Y_, 5);
            Serial.println();
        #endif
    }
    else if (currentTrajectoryTime < Ta_){ 
        // Accelerating
        Y_ = Xi_ + Vi_ * currentTrajectoryTime + 0.5f * Ar_ * sq(currentTrajectoryTime);
        Yd_ = Vi_ + Ar_ * currentTrajectoryTime;
        Ydd_ = Ar_;
        #ifdef __debug
        // Serial.print("Accelerating  ");
            Serial.print(String(currentTrajectoryTime) + ",");
            Serial.print(Y_, 5);
            Serial.println();
        #endif
    }
    else if (currentTrajectoryTime < Ta_ + Tv_){
        // Coasting
        Y_ = yAccel_ + Vr_ * (currentTrajectoryTime - Ta_);
        Yd_ = Vr_;
        #ifdef __debug
            // Serial.print("Coastting ");
            Serial.print(String(currentTrajectoryTime) + ",");
            Serial.print(Y_, 5);
            Serial.println();
        #endif
    }
    else if (currentTrajectoryTime < Tf_){ 
        // Deceleration
        float td = currentTrajectoryTime - Tf_;
        Y_ = Xf_ + 0.5f * Dr_ * sq(td);
        Yd_ = Dr_ * td;
        Ydd_ = Dr_;
        // Serial.print("Decelerating  ");
        #ifdef __debug
            // Serial.print("Accelerating  ");
            Serial.print(String(currentTrajectoryTime) + ",");
            Serial.print(Y_, 5);
            Serial.println();
        #endif
    }
    else if (currentTrajectoryTime >= Tf_){ // Final Condition
        Y_ = Xf_;
        Yd_ = 0.0f;
        Ydd_ = 0.0f;
        #ifdef __debug
            //Serial.print("--- Final Stage ---");
            Serial.print(String(currentTrajectoryTime) + ",");
            Serial.print(Y_, 5);
            Serial.println();
        #endif
    }
    else{
        // TODO: Error halnling here
    }
    return; 
}

To use it you just need to declare an instance of this class and register it using the Commander. Yuo also need to provide a reference to your motor and at the loop() call at least every 10 ms the function runPlannerOnTick().

....
TrapezoidalPlanner planner(10);

void doPlanner(char *cmd){
  planner.doTrapezoidalPlannerCommand(cmd);
}

void setup(){
......
  //  GCode move Gxx, GVxx, or GAxx - Example: G30 moves to position in rads. 
  //  GV10 sets velocity to 10 rads/s. GA5 sets acceleration to 5 rads/s/s.");
  planner.linkMotor(&motor);
  commander.add('G', doPlanner, "Motion Planner");
......
}

void loop() {
......
  commander.run();
  planner.runPlannerOnTick();
......
}

Here you can see an example of a capture of a trajectory using SimpleFOCStudio, in this case it is more triangular than trapezoidal.

Here you can see another capture moviing from 0 to 20 rads with a maximum velocity of 10 rads/s and acceleration of 10 rads/s^2

4 Likes