[solved] My linear encoder / motor / Arduino poops itself sometimes

Hello forum. This is my first post, so there's that.
As many others, I am new to Arduino and programming, so you can tear into my code all you want, as long as it's helpful :slight_smile:

I've ripped a inkjet apart and connected to the encoder and motor, so I should be able to control its position from the serial monitor.

For reference I've followed this video on how to handle the interrupts How to use linear encoders with Arduino. hardware, code, and demo. - YouTube

Now, when I enter a position in the serial monitor all is well and the carrier moves as it should, but SOMETIMES it goes haywire and moves waaaay further than it is supposed to and slams into the end of the rails.
If I leave it with the motor whining, it ends with this (see code for reference):

forward
stop
(forward) New absolute position: 32744
Enter new absolute position and press Enter

Does anyone know what is going on? Do I need to post anything else for debugging?

Thanks you in advance!
Regards

int encoderI = 2; //interrupt
int encoderQ = 3; //interrupt
int dir1PinA = 5;
int dir2PinA = 6;
int spdCtrl = 9; //motor speed ctrl
int Stop = 0;

volatile int count = 0;
int newPos = 0;
int newPosTemp = 0;
//int temp = 0;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(encoderI, INPUT);
  pinMode(encoderQ, INPUT);
  attachInterrupt(0, handleEncoder, CHANGE);
  pinMode(dir1PinA, OUTPUT);
  pinMode(dir2PinA, OUTPUT);
  pinMode(spdCtrl, OUTPUT);
  Serial.println("Enter new absolute position"); //prompt for first position
}

void loop() {

  if (Serial.available()) {
    byte incomingByte = Serial.read(); //read serial
    while (incomingByte != '\n') { //run 'while' as long as 'enter' hasn't been sent
//      while(newPos == newPosTemp && temp == 0){
//        if(temp == 0){
//          Serial.println("Enter new absolute position");
//          }
//          temp = 1;
//          }
      if (incomingByte >= '0' && incomingByte <= '9') //restrict to 0-9
      newPosTemp = newPosTemp * 10 + (incomingByte - '0'); //convert to int
      incomingByte = Serial.read();
    }

    if (newPosTemp >= 0 && newPosTemp <= 4001) { //restrict to 0-4000 so we don't slam in to end of carrier
      newPos = newPosTemp;
      //Serial.println(newPos);
      //Serial.println(newPosTemp);
      newPosTemp = 0; //reset
      Stop = 0; //ensure motor has a green light
    }
    else
    { Serial.println("Choose a position less than 4000");
    }
  }
  if (count != newPos) { //compare current pos. to requested pos.
    if (newPos >= count) {
      forward();
    }
    else
    { back();
    }
  }
}

//Functions
void handleEncoder() {
  if (digitalRead(encoderI) == digitalRead(encoderQ)) // linear encoder https://www.youtube.com/watch?v=0QLZCfqUeg4
  {
    count++;
  }
  else
  {
    count--;
  }
}

void forward() {
  while (count <= newPos && Stop == 0) { // as long as count is less or equal to newPos or we have a red light from the motor
    Serial.println("forward");
    analogWrite(spdCtrl, 150); 
    digitalWrite(dir1PinA, LOW);
    digitalWrite(dir2PinA, HIGH);
    if(newPos <= count){ //once we reach count = newPos OR pass it due to reaction time of Arduino / lack of active brake on motor
      StopMotor();
      Serial.print("(forward) New absolute position: ");
      Serial.println(count);
      Serial.println("Enter new absolute position and press Enter");
  }
 }
}

void back() {
  while (count >= newPos && Stop == 0) {
    Serial.println("back");
    analogWrite(spdCtrl, 150);
    digitalWrite(dir1PinA, HIGH);
    digitalWrite(dir2PinA, LOW);
    if(newPos >= count){
      StopMotor();
      Serial.print("(backwards) New absolute position: ");
      Serial.println(count);
      Serial.println("Enter new absolute position and press Enter");
  }
 }
}

void StopMotor(){ //turn off motor, set Stop to 1 (red light)
  Serial.println("stop");
  Stop = 1;
  analogWrite(spdCtrl, 0);
//  digitalWrite(dir1PinA, HIGH);
//  digitalWrite(dir2PinA, HIGH);
  digitalWrite(dir1PinA, LOW);
  digitalWrite(dir2PinA, LOW);
}

Since count is an integer (2 bytes - non atomic) and could be changing while it is being read you should turn off interrupts and make a protected copy to work with.

noInterrupts ();
int copy_count = count;  
interrupts ();

see

Thanks for the suggestion; it SEEMS to work now.

I've set it to a stress test - or endure the annoying sound test - with:

newPos = random(0,2000);
Stop = 0;
delay(200);

..with no end stop slams so far. It does seem to drift a little over time, but I'll investigate that.

This is how I implemented it:

void forward() {
  while (count <= newPos && Stop == 0) { // as long as count is less or equal to newPos or we have a red light from the motor
    //Serial.println("forward");
    detachInterrupt(0);
    countTemp = count;
    analogWrite(spdCtrl, 150); 
    digitalWrite(dir1PinA, LOW);
    digitalWrite(dir2PinA, HIGH);
    if(newPos <= countTemp){ //once we reach count = newPos OR pass it due to reaction time of Arduino / lack of active brake on motor
      StopMotor();
      Serial.println("(forward) New absolute position: ");
      Serial.print("newPos: "); Serial.println(newPos);
      Serial.print("count: ");  Serial.println(count);
      Serial.print("countTemp: ");  Serial.println(countTemp);
      Serial.println("Enter new absolute position and press Enter");
  }
  attachInterrupt(0, handleEncoder, CHANGE);   
 }
}

Which gives:
(forward) New absolute position:
newPos: 1111
count: 1111
countTemp: 1111

so I could probably dispense with the 'countTemp = count' ?

If I implement it like:
detachInterrupt(0);
countTemp = count;
attachInterrupt(0, handleEncoder, CHANGE);
analogWrite....etc..

I get this:
(forward) New absolute position:
newPos: 1111
count: 1126
countTemp: 1111
Enter new absolute position and press Enter

So I guess the last one is the most correct position (count = actual interrupts) and I should correct the carrier to this position at every run?

Thanks for the replay regardless.
Regards

Do not detach interrupts, you will lose counts.

Do exactly as suggested. The point is that count is two locations in memory
so reading its value can be interrupted halfway through by the ISR which changes
it under your nose, say from 0x1FF to 0x200. You read the 0xFF, then the 0x02,
and see the value as 0x2FF.

noInterrupts() and interrupts() both compile down to very short fast instructions
to defer and re-enable interrupt handling without any chance of losing an interrupt. detach/attachInterrupts are long slow functions that will cause interrupts to be lost
now and again

BTW the drift over time is because you are not accounting for every edge on both
encoder lines. You should field CHANGE interrupts on both encoder signals to
get correct accounting, nothing else will be reliable.

Okay, I though noInterrupt() was a quick way to describe detaching interrupts and not an actual function.

I just tried exchanging detachInterrupt() with noInterrupt, and the carrier just run forward and slams into the end stop.
I tried both variants twice to be sure, and detachInterrupt() works every time, whereas noInterrupt does not. Any idea why this is? I had removed attachInterrupt from setup() previously - d'oh

It does not improve the overshoot of 5-15 counts/interrupts compared to using de-/attachInterrupt

I'll look into using both edges on the encoder lines for accuracy.

Regards

The syntax is interrupts()
http://www.arduino.cc/en/Reference/Interrupts

and noInterrupts()
http://www.arduino.cc/en/Reference/NoInterrupts

I tried both variants twice to be sure, and detachInterrupt() works every time, whereas noInterrupt does not. Any idea why this is?

Please show the code you are currently using which demonstrates this problem.

cattledog:
The syntax is interrupts()
http://www.arduino.cc/en/Reference/Interrupts

and noInterrupts()
http://www.arduino.cc/en/Reference/NoInterrupts

Please show the code you are currently using which demonstrates this problem.

I've edited my post above :slight_smile:

Claws:
I've edited my post above :slight_smile:

Which one? Please post the code in a fresh message.

I'm not sure how your routine which reads one half of the available quadrature will lead to overshoot on a given move. I think it will just affect the resolution of the actual physical position depending upon the counts per revolution of the encoder and the gearing for the chasis movement.

Is the overshoot a difference between the actual final position and the target position by count? That is, you tell it to move 1000 counts and it moves 1015?

pass it due to reaction time of Arduino / lack of active brake on motor

Does the overshoot vary with the speed control? Does it vary with the move length? If the overshoot is mechanical, and you may want to address it by slowing the motor down as you get close to the desired position. How fast are you running the motor/encoder in counts/second or rpm? Do you know the encoder counts per revolution?

If the overshoot is the code lagging the actual position you might be able to speed the response up by replacing the digitalRead() in the interrupt routine with the faster bitRead().

void handleEncoder() {
  //if (digitalRead(encoderI) == digitalRead(encoderQ)) 
  if (bitRead(PinD,2)==bitRead(PinD,3))//for UNO, Nano, ProMini
  {
    count++;
  }
  else
  {
    count--;
  }
}

If the overshoot is a significant problem, I would suggest you start a new thread and post the current code, and the overshoot data along with any mechanical details or encoder specifications.

cattledog:
I'm not sure how your routine which reads one half of the available quadrature will lead to overshoot on a given move. I think it will just affect the resolution of the actual physical position depending upon the counts per revolution of the encoder and the gearing for the chasis movement.

Is the overshoot a difference between the actual final position and the target position by count? That is, you tell it to move 1000 counts and it moves 1015?
Yes

Does the overshoot vary with the speed control? Does it vary with the move length? If the overshoot is mechanical, and you may want to address it by slowing the motor down as you get close to the desired position. How fast are you running the motor/encoder in counts/second or rpm? Do you know the encoder counts per revolution?
Yes, it varies with speed control. It varies slightly with move length, but very little.
The count/sec calculates to around 6200 on long runs. Overshot of 5 on a request of 2000 (count = 2005)

If the overshoot is the code lagging the actual position you might be able to speed the response up by replacing the digitalRead() in the interrupt routine with the faster bitRead().

void handleEncoder() {

//if (digitalRead(encoderI) == digitalRead(encoderQ))
 if (bitRead(PinD,2)==bitRead(PinD,3))//for UNO, Nano, ProMini
 {
   count++;
 }
 else
 {
   count--;
 }
}



What is 'pinD' supposed to be declared as? (ref. says 'number' but which number?)
If I replace it with 'encoderI/Q', the forward motion works, but the backwards motion slams into the end stop consequently.


If the overshoot is a significant problem, I would suggest you start a new thread and post the current code, and the overshoot data along with any mechanical details or encoder specifications.
Overshoot is not a problem, as Im only learning from this printer carrier with no other purpose than learning, but I am of course curious as to what causes it. My bet is mechanical.

Thanks for your detailed answer :slight_smile:
Regards

What is 'pinD' supposed to be declared as? (ref. says 'number' but which number?)

Sorry, I made an error in syntax because I didn't copy the code from one of my examples and was going from memory.

There should be capitalization--

if (bitRead(PIND,2)==bitRead(PIND,3))//for UNO, Nano, ProMini

bitRead(PIND,x) says to read bit x of the 8 bit input register of Port D. In your case, bit 2 and 3 of that register correspond to the input values of digital pins 2 and 3 which is what digitalRead gets.

Okay, that works. It does not however, improve the overshot, so I'll have to look into the mechanical aspect to fix it.

I'll mark it solved. Thanks for the help :slight_smile:

Regards

I know this is an old post, but I'm trying to do the same. Sorry if bumping this up causes controversy.

Having worked with steppers for quite some time now, I could almost bet your overshooting of 5-15 counts is mostly due to the stepper stopping too fast / slow and loosing steps. I didn't see in your code if the speed is being changed as you start / finish your move. I'd go with a constant acceleration profile to simplify the planning.

Maybe it would help a lot if we knew more about the motor characteristics and any pulley / screw ratio between the motor and your sensor. Also knowing what's the resolution of the strip? I don't know how relevant it is to overshoot by 5 counts: is it a few millimeters or a few microns? Now if it's always the same, then compensate and send a position that's 5 counts shorter so your system stops right on the money.

But, anyways. Did it work at the end?

MarkT:
BTW the drift over time is because you are not accounting for every edge on both encoder lines. You should field CHANGE interrupts on both encoder signals to get correct accounting, nothing else will be reliable.

Nice comments as usual MarkT. Just a quick question only. Did you mean that interrupt counting should be carried out for both of the outputs from the encoder - rather than just use counts from one channel? Thanks MarkT.

[update --- just noticed the really ancient date that the topic was discussed --- 2015]

frivolas:
I know this is an old post, but I'm trying to do the same. Sorry if bumping this up causes controversy.

Probably would have been better if you had started your own thread, and you could have pasted the link to this thread in your own thread for reference.