Stepper Motor Control Using Sinewave Input

Hello,

I am using a stepper motor with encoder to move a linear rail carriage back and forth 5mm. I am doing all control using a Teensy4.1 microcontroller. I orignianlly had this control working just fine when using the AccelSteper.h library and telling it to move(steps) and shift directions when the distanceToGo=0.

However, I have more recently been told I need to have a sinewave input to control the motion. In the code below, the function "LinearMotion()" attempts to do this. It works decently when I use an amplitude of 2.5; however, when I bump this up to the desired 5.0 mm amplitude it does not work (see attached figures). I am using moveTo() since it is giving me the appropriate +/-amplitude about zero.

If anyone has any suggestions about how to address this issue or improve the coding, I would greatly appreciate it!

Thanks,
Kate

And to note...I am switching up the way I previously coded the stepper motor because I am also trying to program a servo motor to go +/-10 deg at the same time as the linear motion. I was having phase shift problems between the two motor outputs with this which is why I am going the sine wave input route. The thought is if I use sinewave inputs for both stepper and servo motor, then I should be able to get rid of the phase shift during syncing.

#include <AccelStepper.h>
#include <math.h>
#include <ezButton.h>
#include <Encoder.h>
#include <Servo.h>

// Define stepper motor connections and steps per revolution
#define STEP_PIN 3
#define DIR_PIN 4
#define STEPS_PER_REVOLUTION 200

#define STEPS_PER_REV 200
#define MICROSTEPS_PER_STEP 16
#define pitch 5
#define Encoder_CPR 4000.00

int cycles = 10;

// Define the parameters for the sinusoidal input
float AMPLITUDE_MM = 5.0;    // Amplitude of the sinusoidal input in millimeters
float FREQUENCY_HZ = 2.0;    // Frequency of the sinusoidal input in Hz

// Create a new instance of the AccelStepper class
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);

int A_PLUS = 41;
int B_PLUS = 40;
Encoder myEnc(A_PLUS,B_PLUS); // encoder setup

ezButton limitSwitch_Red(21);   // limit switch WHITE
ezButton limitSwitch_Blue(22);  // limit switch BLUE

// CALCULATED VARIABLES USED FOR LINEAR MOTION

float linear_hertz = 2.0; // Change to 2.0 if UNI, Change to 4.0 if MULTI
float speed_desired = 21.0; // mm per s --> Change to 21.0 if UNI, Change to 25.0 if MULTI
int stroke_mm = 5; // stroke in mm 
float t0 = 0.015;

float rev_per_mm = 1.0 / pitch;
float steps_per_mm = rev_per_mm * (MICROSTEPS_PER_STEP)*STEPS_PER_REV;
int steps_per_mm_int = static_cast<int>(steps_per_mm);  //640

float speed_steps = speed_desired * steps_per_mm;  //steps per second
float milliseconds_per_cycle = ((stroke_mm * 2) / speed_desired) * 1000;
int stroke_steps = stroke_mm * steps_per_mm_int;  // Stroke given in steps 3200

// CALCULATIONS USED FOR ACCELERATION

float period = 1.0/(2.0*linear_hertz);
float t1 = period - t0;
int accel_steps = (stroke_steps) / ((t0/(2*linear_hertz))-(t0*t0)); // adjusted on 9-11-23 

// CALCULATIONS USED FOR STEPPER ENCODER
float encoder_offset = (STEPS_PER_REV * MICROSTEPS_PER_STEP) / Encoder_CPR;

// DEFINED VARIABLES
unsigned long currentTime = 0;
static unsigned long lastTime;
float movement_distance_mm;
float x = 0.0;
unsigned long startTime;
float newPosition_mm; 
int newPosition_steps; 
int movement_steps;
int y =0;
int freq_ms = 1000/(FREQUENCY_HZ*2);

// FUNCTIONS USED
void Limits();
void Troubleshoot_Data_Output();
void LinearMotion();


void setup() {
  // Set up the stepper motor properties
  stepper.setMaxSpeed(speed_steps);          // steps/s
  stepper.setAcceleration(accel_steps);  // steps/s/s
  
  // Limit switches
  limitSwitch_Red.setDebounceTime(2);
  limitSwitch_Blue.setDebounceTime(2);


  stepper.setMinPulseWidth(25);

  myEnc.write(0); // Sets encoder possition to 0
  stepper.setCurrentPosition(0);
}


void loop() {
  LinearMotion(); 
}

void LinearMotion(){
  
  startTime = millis();

  while (x <= cycles){

    currentTime = millis();
    stepper.run();

    // Calculate the movement distance based on a sinusoidal inputs
    movement_distance_mm = 5.0 * sin(2.0 * PI * (currentTime-startTime) * FREQUENCY_HZ / 1000.0); // Sine wave for stepper

    // Convert the movement distance to steps
    movement_steps = (movement_distance_mm) * steps_per_mm_int;
    
    // Move the stepper motor by the calculated distance
    stepper.moveTo(movement_steps);

    // Run the stepper motor
    stepper.run();

    newPosition_steps = (myEnc.read()*encoder_offset)*-1.0;
    newPosition_mm = (myEnc.read()*encoder_offset) / steps_per_mm_int *-1.0; 

   Troubleshoot_Data_Output();
   Limits();
}
}

void Troubleshoot_Data_Output(){

  unsigned long time_period = 25; // sampling rate of 25ms resulting in anticipated 20 position readings per cycle

  if (millis() - lastTime >= time_period){

    if (y % freq_ms == 0){ // Records the cycle time based on cycles!
      x = x + 0.5;
    }
    
    Serial.print(millis()-startTime);
    Serial.print("\t");
    Serial.print(x);
    Serial.print("\t");
    Serial.print(movement_steps);
    Serial.print("\t");
    Serial.print(newPosition_steps);
    Serial.print("\t");
    Serial.print(movement_distance_mm);
    Serial.print("\t");
    Serial.println(newPosition_mm);
  
    Limits();
    lastTime = millis();
    y = y + time_period;
  }
}

By "sine wave" do you mean reciprocation motion?
Back and forth, like a piston in a cylinder?
If your graphs are supposed to be sine waves, they seem to have lines that are too straight.

@Paul_KD7HB Yes, by sine wave I mean the reciprocating motion. The input waves look decent but the output is what is interesting to me. I am sampling data at 40Hz. Even with a higher sampling rate I get more linear lines for the output.

And this might be where the AccelStepper.h might not be great for a sinewave input since the move commands incorporate velocity/acceleration. I have been thinking about switching libraries but am not sure if it would really influence the output or what.

-Kate

If you want the stepper to move according to a sine wave, you not only have to specify the positions, but also change the speed of the stepper. If the speed is constant, you will always have a linear movement. The stepper must move more slowly near the reversal points.

@MicroBahner I was under the impression that the AccelStepper.h library took care of the acceleration/deceleration when using the .move() and .moveTo() functions...

It accelerates/decelerates from point to point. But I don't think that's what you need.

And this comment is not true:

run() only creates one step if a step is due regarding the speed. You must use
runToPosition();
if you want to wait until the target is reached.
With run() your while loop will not wait until your movement_steps are done, but will compute new ones before the former are executed.

@MicroBahner Thanks for point that out! That comment was actually from a previous iteration of the code. It should just be "// Run the stepper motor". runToPosition() may be a good alternative however I don't want a blocking function since I will have to run a servo motor at the same time as the linear motor...

I suggest you need to vary the TIME between steps to make your movement more sinusoidal.

Yes, definitely. It's what I already suggested in #4. @kjb_bsu should change the code in that manner. With only changing steps at constant speed it's impossible to reach a sinusoidal movement.

A sinusoidal position also has a sinusoidal velocity profile. Since AcccelStepper inherently has linear acceleration I think you will struggle to achieve smooth sinusoidal motion with any degree of accuracy.

Probably the simplest path is to implement your own motion profile and drive the stepper directly. If you need closed loop control, then it will need a more sophisticated PID loop.

I suggest you need to vary the TIME between steps to make your movement more sinusoidal.

Yes, definitely. It's what I already suggested in #4. @kjb_bsu should change the code in that manner. With only changing steps at constant speed it's impossible to reach a sinusoidal movement.

@MicroBahner @Paul_KD7HB Could I get a little more insight on this? From the library documentation, "the run() function first calls runSpeed() to cause a step at the current speed and direction. Then it calls computeNewSpeed() which computes a new speed (step interval) and sets that as the current speed."

The AccelStepper library is slightly confusing to me because from that info it sounds like the speed would automatically be recalculated each time the run() function is called? Or am I thinking of that incorrectly?

Thanks!

The speed is actually the time between steps and that is set by you. Acceleration lengthens that time a bit to do the acceleration. After acceleration period, the time between steps is constant.

To do a REAL sine wave the way you want, people have created a table of varying time delays that will match the sine wave and then code their own stepping loop using the table times for the delay between each step.

Just for fun, I wrote some code to generate a sinusoidal motion.

#if 0
#define X_DIR 62
#define X_STEP 63
#define X_EN  48
#else
#define X_STEP_PIN         25
#define X_DIR_PIN          23
#define X_ENABLE_PIN       27
#endif

#define DEG2RAD(a) (a * M_PI/ 180.0)

float min_speed = 1.0;
float max_speed = 3200.f;

byte stepPin = X_STEP_PIN;
byte dirPin = X_DIR_PIN; 
byte enPin = X_ENABLE_PIN;

void step_time (float angle)
{
  float velocity = cos (DEG2RAD(angle)) * max_speed;

  if (velocity < 0)
    digitalWrite (dirPin, 0);
  else
    digitalWrite (dirPin, 1);

  digitalWrite (stepPin, 1);

  velocity = fabs(velocity);
  if (velocity < min_speed)
    velocity = min_speed;

  unsigned long step_time = 1e6 / velocity;

  delayMicroseconds (step_time);
  digitalWrite (stepPin, 0);

#if 0
  Serial.print (angle);
  Serial.print (',');
  Serial.println (step_time);
#endif
}

void setup() 
{
  Serial.begin (115200);

  pinMode(stepPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  pinMode(enPin, OUTPUT);

  digitalWrite(enPin, LOW);
  digitalWrite(dirPin, HIGH);
}

float angle = 90;

void loop() 
{

  step_time (angle);

  angle += 0.05;

  if (angle > 360)
    angle = angle - 360;

}

Some notes:

  1. it uses float, if the target is Teensy4.1 it has a hardware FPU. Might need optimising for other targets
  2. I had trouble when the velocity reaches zero, I apply a min_speed
  3. not sure how to configure the displacement.