Using Incremental Encoder as a limit for Stepper Motor

Hey everyone,

I'm currently working on a project that's giving me some trouble wrapping my head around. Here's the setup: I've got a Stepper Motor connected to my Arduino Uno via a Motor Shield. Alongside that, I have an incremental rotary encoder, mechanically linked to the stepper rotation using a gear from Baumer electric. You can find the datasheet for the encoder here.

My goal is to use the encoder to keep track of the stepper motor's position, effectively using it as a count. Once the stepper reaches its maximum count, I want it to stop. Then, after a defined period of time, I want the stepper to start turning in the opposite direction until it reaches the minimum count of the encoder.

Ive tried different kind of codes but everyone of them makes the stepper stutter once the encoder is turned, i think that it was because of the program is alternating between two or more void in which its getting the position and making the stepper turn.

#include <Stepper.h>
#include <Encoder.h>
#include <digitalWriteFast.h>
#include <EnableInterrupt.h>

#define encoderA 2
#define encoderB 3
#define encoderZ 4

// Define number of steps per revolution:
const int stepsPerRevolution = 200;

#define pwmA 3
#define pwmB 11
#define brakeA 9
#define brakeB 8
#define dirA 12
#define dirB 13

// Initialize the stepper library on the motor shield:
Stepper myStepper = Stepper(stepsPerRevolution, dirA, dirB);


volatile int countA = 0;
volatile int countB = 0;
volatile int cumulativeCountA = 0;
volatile int cumulativeCountB = 0;
int pulsesPerRev = 900;
int Dir = 0;  // 1 = CW
              // 0 = Stationary
              // -1 = CCW
void setup() {
  Serial.begin(9600);
  // Set the PWM and brake pins so that the direction pins can be used to control the motor:
  pinMode(pwmA, OUTPUT);
  pinMode(pwmB, OUTPUT);
  pinMode(brakeA, OUTPUT);
  pinMode(brakeB, OUTPUT);

  digitalWrite(pwmA, HIGH);
  digitalWrite(pwmB, HIGH);
  digitalWrite(brakeA, LOW);
  digitalWrite(brakeB, LOW);
  // Set the motor speed (RPMs):
  myStepper.setSpeed(30);

  pinMode(encoderA, INPUT);
  pinModeFast(encoderB, INPUT);
  pinMode(encoderZ, INPUT);

  enableInterrupt(encoderA, pulseA, RISING);
  enableInterrupt(encoderB, pulseB, RISING);
  enableInterrupt(encoderZ, pulseZ, RISING);
}

void loop() {
  Serial.print(countA);
  Serial.print('\t');
  Serial.print(countB);
  Serial.print('\t');
  //Serial.print(getAngle());
  //Serial.print('\t');
  Serial.print(cumulativeCountA);
  Serial.print('\t');
  Serial.print(cumulativeCountB);
  Serial.print('\t');
  Serial.println(Dir);

  checkDirection();
  if (countA >= (pulsesPerRev - 50)) {
    if (Dir == 1) {
      myStepper.step(countA - (pulsesPerRev - 50));
    } else if (Dir == 0) {
      myStepper.step(countA - (-pulsesPerRev + 50));
    }
  }
}


void checkDirection() {
  if ((bool)digitalReadFast(encoderB) == HIGH) {  //digitalReadFast() is faster than digitalRead()
    Dir = 1;
  } else {
    Dir = -1;
  }
}

void pulseA() {
  checkDirection();
  countA += Dir;
  cumulativeCountA += Dir;
}

void pulseB() {
  countB += Dir;
  cumulativeCountB += Dir;
}

void pulseZ() {
  countA = 0;  //reset counters at "home" reference point
  countB = 0;
}

Any suggestions or insights into how I can achieve this would be greatly appreciated!

How do You initialize the system on power up?
The use of an encoder looks unnecessary, overkill. Do that in software.

I'm currently developing a passive radar system, and it's crucial for me to accurately track the position of the platform housing the antenna. Upon system power-up, I need it to determine its exact position using the Channel Z input of the encoder. Subsequently, I intend to have it emit a spike signal each time it completes a full rotation.

Make it simple by using a kind of home switch. At start up rotate the system until the switch triggers. Reset/set the position internally in the code. Then count steps.
Using an absolute encoder, mechanically adjusted..... No, not needed, too much.

I'm currently working on my thesis and have received the encoder and other necessary components from a friend. Given the time constraints, as I have only 7 weeks left to complete my project, it's crucial for me to get everything working efficiently. While I initially considered using a limit switch as a backup plan, I'm inclined to utilize the encoder fully, as it seems capable of fulfilling all requirements.

However, I'm facing a challenge in writing the code to achieve the desired functionality. I'm particularly interested in a solution that extracts the position count from the encoder and smoothly controls the stepper motor within a single function, ensuring no stuttering occurs. If you or anyone else has encountered a similar situation or has a code snippet that accomplishes this, I'm eager to hear your suggestions.

Hey @Railroader,

After receiving advice from various sources, I've transitioned my project to utilize two limit switches instead of an encoder. The functionality is as follows: upon startup, the stepper motor rotates clockwise until it reaches limit switch 1, and then continues rotating until it engages limit switch 2.

I've opted for the AccelStepper.h library for better control over the stepper motor.

#include <ezButton.h>
#include <AccelStepper.h>

// Define number of steps per revolution:
const int stepsPerRevolution = 200;

// Give the motor control pins names:
#define pwmA 3
#define pwmB 11
#define brakeA 9
#define brakeB 8
#define dirA 12
#define dirB 13

// Define the AccelStepper interface type:
#define MotorInterfaceType 2

#define DIRECTION_CCW -1
#define DIRECTION_CW 1

#define STATE_CHANGE_DIR 1
#define STATE_MOVE 2
#define STATE_MOVING 3

#define MAX_POSITION 0x7FFFFFFF  // maximum of position we can set (long type)

ezButton limitSwitch_1(2);  // create ezButton object that attach to pin A0;
ezButton limitSwitch_2(3);  // create ezButton object that attach to pin A1;

// Create a new instance of the AccelStepper class:
AccelStepper stepper = AccelStepper(MotorInterfaceType, dirA, dirB);

int stepperState = STATE_MOVE;
int direction = DIRECTION_CW;
long targetPos = 0;
long stepCount = 0;  // Variable to count steps

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

  // Set the PWM and brake pins so that the direction pins can be used to control the motor:
  pinMode(pwmA, OUTPUT);
  pinMode(pwmB, OUTPUT);
  pinMode(brakeA, OUTPUT);
  pinMode(brakeB, OUTPUT);

  digitalWrite(pwmA, HIGH);
  digitalWrite(pwmB, HIGH);
  digitalWrite(brakeA, LOW);
  digitalWrite(brakeB, LOW);

  limitSwitch_1.setDebounceTime(50);  // set debounce time to 50 milliseconds
  limitSwitch_2.setDebounceTime(50);  // set debounce time to 50 milliseconds

  stepper.setMaxSpeed(80.0);      // set the maximum speed
  stepper.setAcceleration(10.0);  // set acceleration
  stepper.setSpeed(40);           // set initial speed
  stepper.setCurrentPosition(0);  // set position
}

void loop() {
  limitSwitch_1.loop();  // MUST call the loop() function first
  limitSwitch_2.loop();  // MUST call the loop() function first

  if (limitSwitch_1.isPressed()) {
    stepperState = STATE_CHANGE_DIR;
    Serial.println(F("The limit switch 1: TOUCHED"));
  }

  if (limitSwitch_2.isPressed()) {
    stepperState = STATE_CHANGE_DIR;
    Serial.println(F("The limit switch 2: TOUCHED"));
  }

  switch (stepperState) {
    case STATE_CHANGE_DIR:
      direction *= -1;  // change direction
      Serial.print(F("The direction -> "));
      if (direction == DIRECTION_CW)
        Serial.println(F("CLOCKWISE"));
      else
        Serial.println(F("ANTI-CLOCKWISE"));

      stepperState = STATE_MOVE;  // after changing direction, go to the next state to move the motor
      stepCount = 0;              // Reset step count
      break;

    case STATE_MOVE:
      targetPos = direction * MAX_POSITION;
      stepper.setCurrentPosition(0);  // set position
      stepper.moveTo(targetPos);
      stepperState = STATE_MOVING;  // after moving, go to the next state to keep the motor moving infinity
      break;

    case STATE_MOVING:
      if (stepper.distanceToGo() == 0) {  // if motor moved to the maximum position
        stepper.setCurrentPosition(0);    // reset position to 0
        stepper.moveTo(targetPos);        // move the motor to maximum position again
        Serial.print("Total steps: ");
        Serial.println(stepCount);  // Print total steps
      } else {
        stepCount++;  // Increment step count during movement
      }
      break;
  }

  stepper.run();  // MUST be called in loop() function
}

I've attempted to implement a homing function by introducing a new case STATE_HOMING and setting it as the first case in the setup section. Unfortunately, it doesn't perform as intended. Do you have any suggestions on how to implement a homing function during startup?

Additionally, my next objective is to map the steps to degrees. For instance, if the stepper turns 170 steps clockwise, the platform would rotate 10° from north to east, and so forth.

My plan involves manually setting the platform to the north position and then powering up the system. Upon startup, the homing function would move to switch one, designating it as the home position. For example, if my rotation angle ranges from 0° to 175°, with north set at 105°, I would manually position the limit switch to the north position. Upon powering up, it would save this position as north and proceed to rotate clockwise until limit switch 1 is engaged. This position would then be saved as the home position.

"It doesn't perform as intended" tells nothing helping us.
Your talk about angles, north, stepping direction.... doesn't give a clear picture of the project. Try making a drawing showing how it all comes together.

You're correct, my apologies for the oversight. I've scripted the homing function as follows. However, unfortunately, once switch 2 is pressed, the rotation doesn't revert back to clockwise (CW). The Flag targetPos indicates that the moveTo function is being correctly updated.

#include <ezButton.h>
#include <AccelStepper.h>

// Define number of steps per revolution:
const int stepsPerRevolution = 200;

// Give the motor control pins names:
#define pwmA 3
#define pwmB 11
#define brakeA 9
#define brakeB 8
#define dirA 12
#define dirB 13

// Define the AccelStepper interface type:
#define MotorInterfaceType 2

#define DIRECTION_CCW -1
#define DIRECTION_CW 1

#define STATE_CCW 1
#define STATE_CW 2
#define STATE_HOMING 3

#define MAX_POSITION 0x7FFFFFFF  // maximum of position we can set (long type)

ezButton limitSwitch_1(2);  // create ezButton object that attach to pin 2;
ezButton limitSwitch_2(3);  // create ezButton object that attach to pin 3;

// Create a new instance of the AccelStepper class:
AccelStepper stepper = AccelStepper(MotorInterfaceType, dirA, dirB);

int stepperState = STATE_HOMING;  // Initial state: Homing
int direction = DIRECTION_CW;
long targetPos = 0;

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

  // Set the PWM and brake pins so that the direction pins can be used to control the motor:
  pinMode(pwmA, OUTPUT);
  pinMode(pwmB, OUTPUT);
  pinMode(brakeA, OUTPUT);
  pinMode(brakeB, OUTPUT);

  digitalWrite(pwmA, HIGH);
  digitalWrite(pwmB, HIGH);
  digitalWrite(brakeA, LOW);
  digitalWrite(brakeB, LOW);

  limitSwitch_1.setDebounceTime(50);  // set debounce time to 50 milliseconds
  limitSwitch_2.setDebounceTime(50);  // set debounce time to 50 milliseconds

  stepper.setMaxSpeed(80.0);      // set the maximum speed
  stepper.setAcceleration(10.0);  // set acceleration
  stepper.setSpeed(80);           // set initial speed
}

void loop() {
  limitSwitch_1.loop();       // MUST call the loop() function first
  limitSwitch_2.loop();       // MUST call the loop() function first
  Serial.println(targetPos);  //Flag for debugging

  switch (stepperState) {
    case STATE_HOMING:
      Serial.println("HOMING");
      if (limitSwitch_1.isPressed()) {
        stepper.setCurrentPosition(0);  // Set current position to 0 when limit switch 1 is pressed
        targetPos = (DIRECTION_CCW * MAX_POSITION);
        stepper.moveTo(targetPos);
        direction = DIRECTION_CCW;  // Set direction to counterclockwise
        stepperState = STATE_CCW;   // Move to the next state: Move
      } else {
        stepper.setSpeed(40);                         // Set speed for homing
        stepper.setAcceleration(5.0);                 // Set acceleration for homing
        stepper.moveTo(DIRECTION_CW * MAX_POSITION);  // Move stepper motor counter-clockwise until limit switch 1 is pressed
        stepper.run();
      }
      break;

    case STATE_CCW:
      // Serial.print("CCW");
      // Serial.println(DIRECTION_CCW);
      if (limitSwitch_2.isPressed()) {
        targetPos = (DIRECTION_CW * MAX_POSITION);
        direction = DIRECTION_CW;  // Set direction to clockwise
        stepperState = STATE_CW;   // Move to the next state: Move
      } else {
        stepper.moveTo(targetPos);
        stepper.run();
      }
      break;

    case STATE_CW:
      // Serial.print("CW");
      // Serial.println(DIRECTION_CW);
      if (limitSwitch_1.isPressed()) {
        targetPos = (DIRECTION_CCW * MAX_POSITION);
        direction = DIRECTION_CCW;  // Set direction to counterclockwise
        stepperState = STATE_CCW;   // Move to the next state: Move
      } else {
        stepper.moveTo(targetPos);
        stepper.run();
      }
      break;
  }
}

I'm going to create a schematic illustrating the fundamental logic behind the system.

Posting it in the next hour.

Cheers

@Railroader

It took a bit longer than expected, but I've managed to draft the diagram of my planned process. This is what I've come up with in this short amount of time. I'll refine it further, but I hope this provides some clarity. If you have any questions or need additional information, I'm more than happy to provide it.

Cheers

Two ideas from under the umbrella, I have not flown low over your code.

  if (limitSwitch_1.isPressed()) {
    stepperState = STATE_CHANGE_DIR;

Instead of saying "change direction", hitting a limit switch shoukd just directly set the direction it needs to go to get off the switch.

So you don't rely on logic like

    case STATE_CHANGE_DIR:
      direction *= -1;  // change direction

You could

    case STATE_GO_LEFT :
      direction = -1;  // set direction to go left

and same same for going right. Or forward and backwards, whatever.

Also, it's the 21st century, if you don't need grandpa's slow serial comms, bump it up to 115200. Sometimes slow serial printing can interfere with pesky timing, or hide issues that will pop up if you aren't unwittingly slowing things down.

HTH

a7

Hey @alto777 , thanks for the feedback! I initially thought I only needed to match the baud rate within the code and monitor, but I didn't realize it would make such a difference. :sweat_smile: I've adjusted my code to adhere to the standard of the year 2024.

You can find the updated code in the latest comment.

Problem:

However, unfortunately, once switch 2 is pressed, the rotation doesn't revert back to clockwise (CW). The Flag targetPos indicates that the moveTo function is being correctly updated.

Cheers

OK, but in the future don't edit previous posts beyond typos and such, as it can make nonsense of later replies.

If you have a new version that compiles and you are testing, post it, along with any changes to the symptoms of any problems that remain or have arrived newly.

And a bit more about the serial printing - it was the result of a long investigation I read somewhere on these fora that removing some slow printing sped others things up enough to get them in trouble - the inquirer was baffled as to why removing some diagnostical informative printing broke his code.

I'll say I learned something from that thread.

a7

Got it, thank you. The code compiles successfully. It enters the homing routine, and when S1 is pressed, the motor turns counterclockwise (CCW). However, when S2 is pressed, it stutters. After releasing S2, the motor continues to turn CCW. Interestingly, in the serial monitor, I receive output indicating a clockwise (CW) target position.

15:51:13.284 -> 2147483647
#include <ezButton.h>
#include <AccelStepper.h>

// Define number of steps per revolution:
const int stepsPerRevolution = 200;

// Give the motor control pins names:
#define pwmA 3
#define pwmB 11
#define brakeA 9
#define brakeB 8
#define dirA 12
#define dirB 13

// Define the AccelStepper interface type:
#define MotorInterfaceType 2

#define DIRECTION_CCW -1
#define DIRECTION_CW 1

#define STATE_CCW 1
#define STATE_CW 2
#define STATE_HOMING 3

#define MAX_POSITION 0x7FFFFFFF  // maximum of position we can set (long type)

ezButton limitSwitch_1(2);  // create ezButton object that attach to pin 2;
ezButton limitSwitch_2(3);  // create ezButton object that attach to pin 3;

// Create a new instance of the AccelStepper class:
AccelStepper stepper = AccelStepper(MotorInterfaceType, dirA, dirB);

int stepperState = STATE_HOMING;  // Initial state: Homing
int direction = DIRECTION_CW;
long targetPos = 0;

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

  // Set the PWM and brake pins so that the direction pins can be used to control the motor:
  pinMode(pwmA, OUTPUT);
  pinMode(pwmB, OUTPUT);
  pinMode(brakeA, OUTPUT);
  pinMode(brakeB, OUTPUT);

  digitalWrite(pwmA, HIGH);
  digitalWrite(pwmB, HIGH);
  digitalWrite(brakeA, LOW);
  digitalWrite(brakeB, LOW);

  limitSwitch_1.setDebounceTime(50);  // set debounce time to 50 milliseconds
  limitSwitch_2.setDebounceTime(50);  // set debounce time to 50 milliseconds

  stepper.setMaxSpeed(80.0);      // set the maximum speed
  stepper.setAcceleration(10.0);  // set acceleration
  stepper.setSpeed(80);           // set initial speed
}

void loop() {
  limitSwitch_1.loop();       // MUST call the loop() function first
  limitSwitch_2.loop();       // MUST call the loop() function first
  Serial.println(targetPos);  //Flag for debugging

  switch (stepperState) {
    case STATE_HOMING:
      Serial.println("HOMING");
      if (limitSwitch_1.isPressed()) {
        stepper.setCurrentPosition(0);  // Set current position to 0 when limit switch 1 is pressed
        targetPos = (DIRECTION_CCW * MAX_POSITION);
        stepper.moveTo(targetPos);
        direction = DIRECTION_CCW;  // Set direction to counterclockwise
        stepperState = STATE_CCW;   // Move to the next state: Move
      } else {
        stepper.setSpeed(40);                         // Set speed for homing
        stepper.setAcceleration(5.0);                 // Set acceleration for homing
        stepper.moveTo(DIRECTION_CW * MAX_POSITION);  // Move stepper motor counter-clockwise until limit switch 1 is pressed
        stepper.run();
      }
      break;

    case STATE_CCW:
      // Serial.print("CCW");
      // Serial.println(DIRECTION_CCW);
      if (limitSwitch_2.isPressed()) {
        targetPos = (DIRECTION_CW * MAX_POSITION);
        direction = DIRECTION_CW;  // Set direction to clockwise
        stepperState = STATE_CW;   // Move to the next state: Move
      } else {
        stepper.moveTo(targetPos);
        stepper.run();
      }
      break;

    case STATE_CW:
      // Serial.print("CW");
      // Serial.println(DIRECTION_CW);
      if (limitSwitch_1.isPressed()) {
        targetPos = (DIRECTION_CCW * MAX_POSITION);
        direction = DIRECTION_CCW;  // Set direction to counterclockwise
        stepperState = STATE_CCW;   // Move to the next state: Move
      } else {
        stepper.moveTo(targetPos);
        stepper.run();
      }
      break;
  }
}

I can't look closely at the logic or your flow diagram now.

But stuttering may point up a power supply issue.

You may enjoy building your project in the wokwi simulator, where there are no power supply issues, no broken (or breaking!) parts or dodgy wiring &c.

I dare say it's intuitive, give it a shot. As a side benefit, it will increase the number of ppl here who will actually run the code and experience what you are seeing for themselves, and bring whatever skills they may have to finding the problems, if indeed they do not magically disappear in the simulation.

It's so useful and easy that many times I build small projects and run them, getting up from my mouse and keyboard only for coffee and to see what the cat is up to. And I don't need to look for that rotary encoder I know is around here somewhere.

If you are careful laying out the wires, it will also be a half-reasonable substitute for a proper scematic:

L8R

a7

What motor shield exactly?

Also, again just looking through the tiny window here, I see

#define MAX_POSITION 0x7FFFFFFF  // maximum of position we can set (long type)

which is indeed a maximum. I get nervous around such big numbers, and wnoder if you couldn't just use a very big number that will certainly be big enough, like

#define MAX_POSITION 1000000L  // maximum of position we can set (long type)

May never make a difference. But shouldn't hurt, either.

a7

1 Like

Maybe you need to take a step back and regard your entire system. Your system consists of: an antenna, a motor, a position sensor and a controller. In this system there is 1 variable: position.

It is possible to use your encoder to obtain an absolute position IF the gear between antenna and encoder is 1:1. If not, another element is necessary.
You use 2 counters to represent this position, one of them is superfluous.
You use A and B to determine direction, but you control the motor so you know the direction (unless you expect movement without steering the motor).
You use pwmA and pwmB, making it look like you refer to the encoder. Using pwmCW and pwmCCW would make more sense (or pwmMotor and dirMotor).

Thank you for your input!
I'll be sharing about the Arduino Motor Shield Rev 3 on Wokwi within the next couple of days. Stay tuned for updates!

I've created a Wokwi sketch to simplify understanding of how my project functions. It offers an easier way to grasp its workings.

Cheers

There are several problems in your sketch.

#define pwmA 3
...
ezButton limitSwitch_2(3);  // create ezButton object that attach to pin 3;
...
  pinMode(pwmA, OUTPUT);

The problem of magic numbers. your limitSwitch_2 will not work.

The next problem ( which @alto777 already addressed ) ist your MAX_POSITION - especially with your very slow acceleration. When you hit your limit switch Accelstepper will not directly change direction, but decelerate ( overshoot the limit switch ) and then when it has decelerated to 0 change the direction.From this point to your new targetposition AccelStepper will produce an internal overflow for the needed steps. That's why it doesn't change direction.
Use a faster Acceleration and change your MAX_POSITION to a more reasonable max value.

void loop() {
  limitSwitch_1.loop();       // MUST call the loop() function first
  limitSwitch_2.loop();       // MUST call the loop() function first
  Serial.println(targetPos);  //Flag for debugging

You print in every loop cycle. This will slow down your loop dramatically - even with 115200 baud.

Additionally:

AccelStepper stepper = AccelStepper(MotorInterfaceType, dirA, dirB);

This naming is very confusing. AccelStepper uses a step and a dir output, not two dir outputs.

  stepper.setSpeed(80);           // set initial speed

don't use setSpeed() if you use run(). It doesn't make any sense, as setSpeed() is used internally by run() to change the speed while accelerating/deceleration.
setSpeed is only useful together with runSpeed().

        stepperState = STATE_CW;   // Move to the next state: Move
      } else {
        stepper.moveTo(targetPos);
        stepper.run();
      }

It dosn't make sense to set the target again every time you call run(). So call moveTo only once when you change your targetPos. That's sufficent.

1 Like

What is your encoder's counts per revolution?