How to track & compensate for error when needing non-whole stepper pulses (code)

Hi all.

I hope some of you could give me a hand with the code for my project. Any comments are welcome!

I have a rotating thing, and a thing than needs rotating by a user-selectable in:out ratio.

An electronic variable ratio gearbox if you will, but with no gears :slight_smile:

The input doesn't rotate at a constant speed. It needs to be free to accellerate, decellerate, stop and start, change direction and the output needs to remain in sync.

So far I have a rotary encoder and a stepper motor which are matched to my speed and torque requirements.

So far, every pulse of the encoder is sent to the microstepping driver and it works great. Accurate and responsive and lovely.

By changing the microstepping I can halve/double the output speed as expected, but I need more flexibility.

  • I've ruled out expensive servo drives due to cost.

  • I'm happy to keep the drive open-loop - again due to cost/complexity

  • I've ruled out trying to build my own stepper driver with H-bridge (s) to achieve uber microstepping, because this would need it's own AVR and I'm trying to keep things simple/cheap. Also it blows my mind just thinking of it.

  • I've settled with the idea of keeping my minimum step as a 1/32, and I'm happy to keep track of the difference between required step and actual step and compensate that error whenever possible.

So for example, if my in/out divider requires me to make a 0.23 step for every one encoder pulse. I'm happy to count this value in a variable and make a step only when the sum exceeds 1, then take the 1 away and keep counting the difference. Kinda like an error buffer.

Likewise, if I need to make 1.34 of a step, I'm happy to make one full step and set the 0.34 aside and add the next remainder and so on, making a step when the error amount exceeds a 1.

How would I go about implementing this "error buffer" in code?

Here is what I have so far (it's basic):

#include <LiquidCrystal_I2C.h>
#include <Wire.h>

LiquidCrystal_I2C	lcd(0x27,2,1,0,4,5,6,7); // 0x27 is the I2C bus address for an unmodified backpack

volatile long counter = 0; //This variable will increase or decrease depending on the rotation of encoder
int rotations = 0;
int dirPin = 8;
int stepperPin = 7;

void setup() {
  Serial.begin (9600);
  lcd.begin (16,2); // for 16 x 2 LCD module
  lcd.setBacklightPin(3,POSITIVE);
  lcd.setBacklight(HIGH);

  //Setting interrupts
  //A rising pulse from encodenren activated ai0(). AttachInterrupt 0 is DigitalPin nr 2 on moust Arduino.
  attachInterrupt(0, ai0, RISING);

  //B rising pulse from encodenren activated ai1(). AttachInterrupt 1 is DigitalPin nr 3 on moust Arduino.
  attachInterrupt(1, ai1, RISING);
  
  pinMode(dirPin, OUTPUT);
  pinMode(stepperPin, OUTPUT);
}

void loop() {
  if (counter>=477) {
    rotations++;
    counter=0;
  } 
  
  if (counter<=-477) {
    rotations--;
    counter=0;
  }

  // Send the value of counter
  Serial.println (counter);
  lcd.setCursor(0,0);
  lcd.print(counter);
  lcd.print("     "); // clear line
  lcd.setCursor(0,1);
  lcd.print(rotations);
  lcd.print("     "); // clear line
}

void ai0() { 
  // ai0 is activated if DigitalPin nr 2 is going from LOW to HIGH
  // Check pin 3 to determine the direction
  if(digitalRead(3)==LOW) {
    counter++;
    step(true,1);
  }else{
    counter--;
    step(false,1);
  }
}

void ai1() {
  // ai0 is activated if DigitalPin nr 3 is going from LOW to HIGH
  // Check with pin 2 to determine the direction
  if(digitalRead(2)==LOW) {
    counter--;
    step(false,1);
  }else{
    counter++;
    step(true,1);
  }
}

void step(boolean dir,int steps){
  digitalWrite(dirPin,dir);
  digitalWrite(stepperPin, HIGH);
  digitalWrite(stepperPin, LOW);
}

Thanks for any pointers!

Bresenham's Algorithm may help here. Basically when drawing a line on the screen, you take whole steps in one direction and keep track of the "error" in the other coordinate. When the error gets big enough to take a step, you step and subtract a whole step from the error.

The algorithm extends to circles and other shapes too.

Your description is exactly what I think I'm looking for :slight_smile:
Thanks!
Will give it a read.

@MorganS:
It was a very interesting read but it went right over my head.

But it must have planted something in my head, because after a few hours I figured it out myself.

The flow is:

    • Define a float variable which is the number of stepper steps needed for every encoder pulse (2.01511335013 in my case)
    • Strip the decimal value off by casting this float into a new int variable which has the effect of a floor() function - rounds down to the nearest whole number.
    • Add the difference of the two (.01511335013) to a buffer variable.
    • Perform the number of full steps.
    • check the buffer variable, if the content is higher than 1 - perform a full step, and minus 1 from the buffer variable.

And it works great! The buffer keeps filling with every step, and clears after it reaches 1.

Still seems to have plenty of cpu cycles and no missed steps, so I'm not going to lose sleep over those long floats :slight_smile:

Here's the code

#include <LiquidCrystal_I2C.h>
#include <Wire.h>

LiquidCrystal_I2C	lcd(0x27,2,1,0,4,5,6,7); // 0x27 is the I2C bus address for an unmodified backpack

volatile long counter = 0; //This variable will increase or decrease depending on the rotation of encoder
int rotations = 0;
int dirPin = 8;
int stepperPin = 7;
float stepAmount = 2.01511335013; 
int stepsFloored = 0;
float stepDeltaBuffer = 0;
int encoderTally = 0;
int stepTally = 0;

void setup() {
  Serial.begin (9600);
  lcd.begin (16,2); // for 16 x 2 LCD module
  lcd.setBacklightPin(3,POSITIVE);
  lcd.setBacklight(HIGH);

  //Setting interrupts
  //A rising pulse from encodenren activated ai0(). AttachInterrupt 0 is DigitalPin nr 2 on moust Arduino.
  attachInterrupt(0, ai0, RISING);

  //B rising pulse from encodenren activated ai1(). AttachInterrupt 1 is DigitalPin nr 3 on moust Arduino.
  attachInterrupt(1, ai1, RISING);
  
  pinMode(dirPin, OUTPUT);
  pinMode(stepperPin, OUTPUT);
}

void loop() {
  if (counter>=477) {
    rotations++;
    counter=0;
  } 
  
  if (counter<=-477) {
    rotations--;
    counter=0;
  }

  // Send the value of counter
  Serial.println (counter);
  lcd.setCursor(0,0);
  //lcd.print(counter);
  lcd.print(encoderTally);
  lcd.print("     "); // clear line
  lcd.setCursor(0,1);
  lcd.print(rotations);
  lcd.print("     "); // clear line
  lcd.setCursor(8,0);
  lcd.print(stepDeltaBuffer);
  //lcd.print(stepsFloored);
  //lcd.print(stepAmount);
  lcd.print("        "); // clear line
  lcd.setCursor(8,1);
  lcd.print(stepTally);
  lcd.print("        "); // clear line
}

void ai0() { 
  // ai0 is activated if DigitalPin nr 2 is going from LOW to HIGH
  // Check pin 3 to determine the direction
  if(digitalRead(3)==LOW) {
    counter++;
    encoderTally++;
    step(true,stepAmount);
  }else{
    counter--;
    encoderTally--;
    step(false,stepAmount);
  }
}

void ai1() {
  // ai0 is activated if DigitalPin nr 3 is going from LOW to HIGH
  // Check with pin 2 to determine the direction
  if(digitalRead(2)==LOW) {
    counter--;
    encoderTally--;
    step(false,stepAmount);
  }else{
    counter++;
    encoderTally++;
    step(true,stepAmount);
  }
}

void step(boolean dir,float stepAmount){
  
  stepsFloored=(int) stepAmount;
  stepDeltaBuffer=stepDeltaBuffer+(stepAmount-stepsFloored);
  for(int i=0;i<stepsFloored;i++){
    digitalWrite(dirPin,dir);
    digitalWrite(stepperPin, HIGH);
    digitalWrite(stepperPin, LOW);
    if (dir==true) {
      stepTally++;
    } else if (dir==false) {
      stepTally--;
    }
    
  }
  if (stepDeltaBuffer >= 1) {
    digitalWrite(dirPin,dir);
    digitalWrite(stepperPin, HIGH);
    digitalWrite(stepperPin, LOW);
    stepDeltaBuffer=stepDeltaBuffer-1;
    if (dir==true) {
      stepTally++;
    } else if (dir==false) {
      stepTally--;
    }
  }
}

You could probably do it without floats but if it's working fast enough for you then don't change it.