A simple stepper motor control algorithm

This post presents a simple stepper motor control algorithm with adjustable acceleration/deceleration curves. The original implementation was part of an AVR project, but I have recreated the relevant components in Arduino to improve accessibility and make things easy for those new to steppers.

You can download the Arduino file below:

For those interested in understanding the example code and its application, I have compiled a brief guide detailing some of the underlying theory, the hardware I used for testing, and a breakdown of the Arduino code.

 

Theory

This methodology is loosely based on some aspects of the AVR446 linear speed controller proposed by Atmel, which can be found here.

Stepper drivers generally operate using discrete pulses that prompt the hardware inside the driver to energise motor windings in a particular order. Both stator and rotor are toothed so that a single pulse rotates the motor to the next point where the magnetic poles are in equilibrium. These points are called steps. The stator and rotor are mechanically offset, which defines the step angle resolution of the given stepper motor. Step angle α is calculated as:

 
 

Where, Ns and Nr represent the number of stator and rotor teeth, respectively.

Unlike DC motors driven by PWM, stepper motor angular velocity and acceleration are manipulated by varying the delay between each successive step. The total distance travelled by the stepper is directly related to the number of pulses sent to the driver. This last aspect is a key advantage of stepper motors. By simply counting step pulses, the driver essentially behaves as an open-loop controller—provided the motor isn’t slipping and everything is working as expected.

To produce a smooth ramping curve, we’d like to base our inter-step delay calculation on an equation that gradually tends toward some limit. Practically, we’d like to calculate this delay directly after a pulse is sent to the driver so that we know how long to wait until the next pulse. To generate the delay between pulses at a given step during acceleration, we use the function:

 
 

Where Dn is our inter-step delay at step n. We then invert the converging component to provide the deceleration curve:

 
 

The tricky part is finding the initial step delay—which sets the rate at which we converge to our desired velocity. The initial step delay can be calculated as:

 
 

Where α is the step angle of the motor, and acc is the desired acceleration. In our case, this calculation is scaled by a factor of 1,000 so that the resulting value aligns with the Timer 1 prescaler used to generate our step pulse interrupt. The stepper motor used in this project has a step angle of 1.8°.

Lowering acc results in a higher initial step delay. A higher initial step delay increases the number of steps required to reach our desired velocity by decreasing the rate of change of velocity, i.e., acceleration. The graph below illustrates the change in inter-step delay over time for various acc values.

 
 

Now that we have a working model to compute our step delays, we can move onto the hardware used for testing.

 

Hardware

 

The driver is wired to the Arduino, PSU and stepper as follows:

 
 

Driver dip switches are configured as:

SW1 SW2 SW3 SW4 SW5 SW6 SW7 SW8
ON ON ON ON ON ON ON ON

SW 1, 2, 3: Peak dynamic current set to 1A
SW 4: Standstill current set to dynamic current (1A)
SW 5, 6, 7, 8: Step resolution set to 200steps/rev

Driver jumpers J1 and J3 have been left as open circuit—the default driver setting with the following logic:

Pin Logic
PUL- Pulse on rising edge (1.5µ second duration minimum)
DIR- HIGH level: anti-clockwise direction
ENA- HIGH level: driver enabled
 

Arduino code

We start by defining several constant functions:

//------------------------------------------Defines
//Direct pin access used for faster toggles

#define STEP_HIGH PORTB |= bit(PB3);           //Digital 11  
#define STEP_LOW PORTB &= ~bit(PB3);

#define STEP_DIR_CLOCK PORTB &= ~bit(PB4);     //Digital 12
#define STEP_DIR_ANTICLOCK PORTB |= bit(PB4);

#define STEP_DISABLE PORTB &= ~bit(PB5);       //Digital 13, built-in LED
#define STEP_ENABLE PORTB |= bit(PB5);

#define TIMER1_INTERRUPT_ON TIMSK1 |= (1 << OCIE1A);    //Enable interrupt for 1A only
#define TIMER1_INTERRUPT_OFF TIMSK1 &= ~(1 << OCIE1A);
//------------------------------------------Defines

Defining constants are a great way to reduce complexity when we have a few simple commands that are used frequently. In the example code, this simplifies the way in which bits are toggled by providing a clear descriptor we can call. Note that direct pin access has been used for faster manipulation of IO pins. You can read more about port registers here.

Next, we define our variables:

//------------------------------------------Variables
unsigned long step_counter;
unsigned long total_steps;
unsigned long ramp_counter;
unsigned long total_ramp_steps;

float min_delay;
float first_step_delay;
float step_delay;
//------------------------------------------Variables

Nothing too complicated here. Variables related to step count have been defined as unsigned longs since we won’t need to store negative values. Floats are used for variable that require a bit more precision. Note the following:

  • min_delay relates to the maximum velocity of the stepper. One driver pulse moves the motor by one step, which means that a lower delay between steps results in a higher angular velocity.

  • first_step_delay sets the rate at which we converge to our desired velocity and can be thought of as stepper acceleration—as that’s the only variable we use when calculating this value.

With our defines out of the way, we can initiate our Arduino:

//------------------------------------------Init
void setup() {
  Serial.begin(115200);
  DDRB = B00111000;     //Direct port access. Sets pins 11,12,13 as outputs

  noInterrupts();       //Timer 1 ISR setup
  TCCR1A = 0;           //Sets Timer/Counter1 control Register A
  TCNT1 = 0;            //Sets Timer/Counter1 to 0
  TCCR1B = 0x0B;        //Set Timer/Counter1 control register B prescaler to 64 (8 MHz/64 = 125000 Hz clock) and sets CTC mode (clear timer on compare)
  interrupts();

  STEP_DIR_CLOCK;
  STEP_ENABLE;

  min_delay = 90;
  first_step_delay = 1000*(sqrt(2*1.8)/0.6);

  delay(500);

  Serial.println("---------------- Hardware code loaded ----------------");
  Serial.println("|                      Controls                      |");
  Serial.println("| '0' to '4'       set acceleration examples         |");
  Serial.println("| '5' to '9'       set max velocity examples         |");
  Serial.println("| 'q' to 'u'       move stepper example commands     |");
  Serial.println("|                                                    |");
  Serial.println("------------------------------------------------------");
  Serial.println("");
}
//------------------------------------------Init

Serial comms has been added to provide a way for users to interact with the driver system but is not required and could be removed for your project.

DDRB = B00111000 sets port 11, 12 and 13 as outputs for our direct port manipulation method. DDRB refers to the Arduino PORTB data direction register, which maps to digital pins 8 to 13.

We define our interrupt by setting Timer 1 control registers A (TCCR1A) and B (TCCR1B). Our prescaler is set to 64, which means Timer 1 is running at (8MHz/64) 125,000 Hz. Counter 1 TCNT1 is incremented every clock cycle, and when its value is equal to the 8-bit OCR1A compare register, the interrupt is triggered and TCNT1 is reset. This lets us set the delay between interrupts in 8 microsecond increments by writing the desired value to OCR1A. More info on these registers can be found in the ATmega328P datasheet.

setup() continues by setting the stepper direction as clockwise, enabling the driver (energising the stepper), setting the default max velocity (min_delay), populating the initial step delay (first_step_delay) and printing a welcome banner.

The main loop provides the serial interface for users to interact with the system:

//------------------------------------------Main loop
void loop() {
  byte rByte = Serial.read();

  if (rByte == '0')      { setAcc(0.1); }
  else if (rByte == '1') { setAcc(0.2); }
  else if (rByte == '2') { setAcc(0.6); }
  else if (rByte == '3') { setAcc(0.8); }
  else if (rByte == '4') { setAcc(1.6); }

  else if (rByte == '5') { setMaxVel(40); }
  else if (rByte == '6') { setMaxVel(60); }
  else if (rByte == '7') { setMaxVel(80); }
  else if (rByte == '8') { setMaxVel(90); }
  else if (rByte == '9') { setMaxVel(150); }

  else if(rByte=='q'){ moveNsteps(2000); }
  else if(rByte=='w'){ moveNsteps(-2000); }
  else if(rByte=='e'){ moveNsteps(5000); }
  else if(rByte=='r'){ moveNsteps(-5000); }
  else if(rByte=='t'){ moveNsteps(10000); }
  else if(rByte=='y'){ moveNsteps(-20000); }
  else if(rByte=='u'){ moveNsteps(50000); }
}
//------------------------------------------Main loop

A few examples are included here to show the user how to set stepper acceleration setAcc(), maximum velocity setMaxVel(), and move the stepper by a fixed number of steps moveNsteps(). These functions are defined below the main loop:

//------------------------------------------Functions
void moveNsteps(long steps) {
  if(!steps){return;}

  STEP_ENABLE;
  if (steps < 0) {STEP_DIR_ANTICLOCK;}
  else {STEP_DIR_CLOCK;}

  step_counter = 0;
  total_steps = abs(steps);

  ramp_counter = 0;
  total_ramp_steps = 0;  

  step_delay = first_step_delay;

  Serial.print("Moving ");
  Serial.print(steps);
  Serial.print(" steps...");

  OCR1A = (unsigned int)step_delay; //Timer compare register
  TIMER1_INTERRUPT_ON;
}

void setAcc(float acceleration) {
  if(acceleration <= 0){return;}
  first_step_delay =1000*(sqrt(2*1.8)/acceleration);

  Serial.print("Acceleration ->  ");
  Serial.println(acceleration);
}

void setMaxVel(int max_velocity) {
  if(max_velocity <= 0){return;}
  min_delay = max_velocity;

  Serial.print("Max velocity ->  ");
  Serial.println(min_delay);
}
//------------------------------------------Functions

moveNsteps() requires a long integer type as input, where positive values indicate a clockwise rotation, and negative, anticlockwise. This function primes the associated variables for stepper movement, writing our initial step delay to the Timer 1 compare register OCR1A. Finally, the interrupt is enabled, setting the stepper into motion.

setAcc and setMaxVel() set the acceleration and stepper maximum velocity, respectively. Finally, we define our interrupt service routine:

//------------------------------------------ISR
ISR(TIMER1_COMPA_vect) {
  if (step_counter >= total_steps) {
    TIMER1_INTERRUPT_OFF;
    STEP_DISABLE; //comment out line to keep stepper locked when stationary
   Serial.println("done");
  }
  else {
    STEP_LOW;
    delayMicroseconds(4);
    STEP_HIGH;
    step_counter++;
  }

  if(total_ramp_steps == 0){
    ramp_counter = step_counter - 1;

    if(ramp_counter == 0){step_delay = first_step_delay;}
    else{step_delay = (step_delay * (4 * ramp_counter - 1)) / (4 * ramp_counter + 1);}    

    if (step_delay <= min_delay) {
      step_delay = min_delay;
      total_ramp_steps = step_counter;
      }

    if(step_counter >= (total_steps / 2)){total_ramp_steps = step_counter;}
  }

  else if (step_counter >= (total_steps - total_ramp_steps)) {
    ramp_counter = total_steps - step_counter;
    step_delay = (step_delay * (4 * ramp_counter + 1)) / (4 * ramp_counter - 1);
  }

  OCR1A = (unsigned int)step_delay;
}
//------------------------------------------ISR

The ISR is responsible for generating the pulses sent to the driver, calculating the delays between steps and keeping track of stepper position.

This section can be a bit confusing, as it really depends on your preferred approach and how you think about your first step. In our case, step 1 is considered to prime the stepper motor before acceleration can begin, which means our initial step delay Dn is applied between steps 1 and 2. Consequent step_delay D0 calculations therefore only apply from step 2 onward. step_counter is incremented before calculating the delay until the next pulse, so we need an offset of 1 (ramp_counter) to ensure the step_delay calculation starts at 1—instead of 2.  

step_delay continues to decrease, accelerating the motor until it hits the max velocity limit (min_delay), at which point total_ramp_steps is set to the current value of step_counter. Since total_ramp_steps is no longer 0, step_delay (motor velocity) does not change until the total number of steps left in the motion is equal to the number of steps it took to accelerate to our desired maximum velocity. This ensures there are enough steps left to follow the same deceleration curve. 

The current position of the stepper (step_counter) is checked against the desired position (total_steps) during the acceleration phase. If the motor is still accelerating when the halfway point has been reached, we begin the deceleration phase immediately. 

 

Below is a video demonstrating the example code executing a series of stepper movements with various accelerations and max velocities.

 
 
 

Hopefully this guide helps improve stepper control for your application. Please feel free to distribute my example code, take the parts you need, and drop the parts you don’t.

 
Previous
Previous

Identifying strong object orientations within an image

Next
Next

Communicating with the Dobot Magician using raw protocol