Math Errors Ramping a Stepper Motor?

Posts are always better with a picture. :slight_smile:

Since this is my first post I’d like to say thanks to everyone who answers questions on these forums. I’ve found the posts in here to be immensely helpful. But I’ve finally hit a wall.

I built a durability tester out of some 3D printed parts and an Arduino Mega. I’ve got a Mercury stepper motor driven by an Easy Driver with a 7 segment display showing thousands of cycles through a 360° sweep. Since the output shaft is going through a 2:1 gearbox, one complete sweep is 3200 motor steps. Once it completes a sweep it switches direction and goes the other direction.

All of that works great but now I’m trying to get fancy and have the motor ramp its speed at the end of each Sweep. To speed the motor up and down I change the Delay that each Step takes during the ramp using a linear equation. Ramp up is one equation, cruise is a constant 450ms and ramp down is another equation.

The red “Math” line below is the Delay per Steps on the range of Sweep that I calculated in Excel. The blue “Monitor” line is what I copied from the Serial Monitor and reflects what the Arduino and stepper are actually doing.

I can’t figure out why the slopes are wrong and I really can’t figure out why the delay suddenly goes super negative on the last 7/8th of the Sweep.

Abbreviated code below:

int Dir = 0;         // 0 is CW, 1 is CCW. Used to toggle direction.
long Steps = 0;      // Number of motor steps we have taken.
long Delay = 0;      // Number of microseconds to delay between each step.
long Sweep = 3200;   // Number of Steps we have to go for a complete revolution. 1600 direct, 3200 gearbox.
long Revs = 0;       // Mumber of complete revolutions we have gone.
long kRevs = 0;      // Number of complete revolutions we have gone in THOUSANDS!
long Limit = 1234;   // Number of complete revolutions we want to go and then stop.

...

  if (Steps < (Sweep * 1 / 8))                  // Ramps motor speed up.
  {
    Delay = -5600 / Sweep * Steps + 1150;
    digitalWrite(11, HIGH);
    delayMicroseconds(Delay);                   // This decreases the delay as the motor speeds up.
    digitalWrite(11, LOW);
    delayMicroseconds(Delay);                   // This decreases the delay as the motor speeds up.
    Steps = Steps + 1;                          // Records this motor step.
  }
  else if (Steps > (Sweep * 7 / 8))             // Ramps motor speed down.
  {
    Delay = 5600 / Sweep * Steps - 4450;
    digitalWrite(11, HIGH);
    delayMicroseconds(Delay);                   // This increases the delay as the motor slows down.
    digitalWrite(11, LOW);
    delayMicroseconds(Delay);                   // This increases the delay as the motor slows down.
    Steps = Steps + 1;                          // Records this motor step.
  }
  else
  {
    Delay = 450;                                  // This is the default cruise speed after ramp up or down.
    digitalWrite(11, HIGH);
    delayMicroseconds(Delay);                     // This is the delay at constant cruise speed of the motor.
    digitalWrite(11, LOW);
    delayMicroseconds(Delay);                     // This is the delay at constant cruise speed of the motor.
    Steps = Steps + 1;                            // Records this motor step.
  }

Whole code for reference:

#include <SoftwareSerial.h>

SoftwareSerial Serial7Segment(7, 8); // RX pin, TX pin. TX goes to RX1 on the 7SegmentDisplay.

#define MODE_CMD  0x82

#define MODE_DATA     0
#define MODE_ANALOG   1
#define MODE_COUNTER  2

int Dir = 0;         // 0 is CW, 1 is CCW. Used to toggle direction.
long Steps = 0;      // Number of motor steps we have taken.
long Delay = 0;      // Number of microseconds to delay between each step.
long Sweep = 3200;   // Number of Steps we have to go for a complete revolution. 1600 direct drive, 3200 gearbox.
long Revs = 0;       // Mumber of complete revolutions we have gone.
long kRevs = 0;      // Number of complete revolutions we have gone in THOUSANDS!
long Limit = 1234;   // Number of complete revolutions we want to go and then stop.

void setup()
{
  pinMode(10, OUTPUT);    // Pin 10 goes to DIR on the Driver PCB.
  pinMode(11, OUTPUT);    // Pin 11 goes to STEP on the Driver PCB, this is the important pin.
  pinMode(12, OUTPUT);    // Pin 12 goes to SLP "Sleep" on the Driver PCB.
  pinMode(52, OUTPUT);    // Pin 52 goes to SDI on the 7SegmentDisplay, SDO is the "decrease" increment pin.
  pinMode(42, OUTPUT);    // Pin 42 is the Trigger out to the oscilliscope.
  digitalWrite(10, HIGH); // LOW is CW, HIGH is CCW.
  digitalWrite(11, LOW);  // STEP starts at LOW.
  digitalWrite(12, HIGH); // SLP gets used at the end so the motor doesn't overheat.
  digitalWrite(52, HIGH); // 7SegmentDisplay gets confused unless the signal is mostly HIGH and then pulses to LOW
  digitalWrite(42, LOW);  // Trigger starts at LOW.

  Serial.begin(9600);         // Puts Arduino at 9600 bps.
  Serial7Segment.begin(9600); // Puts Serial7Segment at 9600 bps.
  Serial7Segment.write('v');  // Reset the display - this forces the cursor to return to the beginning of the display.
  
  Serial.begin(57600);

  delay(10);
  Serial7Segment.write(MODE_CMD);     // Changes the mode of the display.
  Serial7Segment.write(MODE_COUNTER); // Sets the display to counter mode. Any pulse on SDI will go up. Any pulse on SDO will go down.
}

void loop()
{
  
  Serial.print(Steps);
  Serial.print(" ");
  Serial.print(Delay);
  Serial.print('\n');
  
  if (Steps < (Sweep * 1 / 8))                  // Ramps motor speed up.
  {
    Delay = -5600 / Sweep * Steps + 1150;
    digitalWrite(11, HIGH);
    delayMicroseconds(Delay);                   // This decreases the delay as the motor speeds up.
    digitalWrite(11, LOW);
    delayMicroseconds(Delay);                   // This decreases the delay as the motor speeds up.
    Steps = Steps + 1;                          // Records this motor step.
  }
  else if (Steps > (Sweep * 7 / 8))             // Ramps motor speed down
  {
    Delay = 5600 / Sweep * Steps - 4450;
    digitalWrite(11, HIGH);
    delayMicroseconds(Delay);                   // This increases the delay as the motor slows down.
    digitalWrite(11, LOW);
    delayMicroseconds(Delay);                   // This increases the delay as the motor slows down.
    Steps = Steps + 1;                          // Records this motor step.
  }
  else
  {
    Delay = 450;                                  // This is the default cruise speed after ramp up or down.
    digitalWrite(11, HIGH);
    delayMicroseconds(Delay);                     // This is the delay at constant cruise speed of the motor. If this is shorter than 300 the motor runs rough.
    digitalWrite(11, LOW);
    delayMicroseconds(Delay);                     // This is the delay at constant cruise speed of the motor. If this is shorter than 300 the motor runs rough.
    Steps = Steps + 1;                            // Records this motor step.
  }

  if (Steps == Sweep) // Check to see if we have taken enough steps for a full Sweep which is 1600 or 3200 motor steps, and then switch direction.
  {
    delayMicroseconds(500);
    Revs = Revs + 1;             // Increase internal revolution counter by one.
    Steps = 0;                   // Reset motor steps to zero.
    if (Dir == 0)                // Looks at direction.
    {
      digitalWrite(10, LOW);     // LOW is CW, HIGH is CCW.
    }
    if (Dir == 1)
    {
      digitalWrite(10, HIGH);    // LOW is CW, HIGH is CCW.
    }
    Dir = !Dir;                  // Switches direction.
  }

  if (Steps == 0) // This is the Trigger code.
  {
    digitalWrite(42, HIGH);
    delayMicroseconds(100);
    digitalWrite(42, LOW);
  }

  if (Revs == 1000) // This increments the THOUSANDS counter and resets the smaller counter.
  {
    digitalWrite(52, LOW);  // Sends a LOW pulse to the SDI pin of the display, which is in counter mode, to increase the counter by one.
    delay(50);              // Pause for at least 40 milliseconds to let the 7SegmentDisplay register the LOW.
    digitalWrite(52, HIGH); // Go back to HIGH.
    kRevs = kRevs + 1;      // Increase the THOUSANDS count by 1.
    Revs = 0;               // Reset small revolution counter to zero to avoid overloading the 8-bit integer.
  }

  if (kRevs == Limit)      // Now we check to see if we've hit our rev limit.
  {
    digitalWrite(12, LOW); // If we have hit the limit we put the Easy Driver to sleep.
    while (1)              // And start an infinite loop so the void loop stops running.
    {
    }
  }
}

My first impression is that you’re getting integer math’ed - if using integer datatypes (ie, not floats), the results of each intermediate calculation are truncated (rounded down). If any value exceeds what can be stored in the datatypes you’re using (constants default to signed integers unless declared otherwise), it will roll over and/or extra bits will be lost.

Look up type promotion in C for more information.

I had a coworker who actually codes review my math, it seems that Arduino and Excel don't do math the same way. My longs were getting divided into very small numbers, rounded and then blow up... losing a lot of accuracy along the way. Also the lack of parentheses didn't help.

So this:

Delay = -5600 / Sweep * Steps + 1150;
Delay = 5600 / Sweep * Steps - 4450;

Turned into this:

Delay = (-5600 * Steps / Sweep) + 1150;
Delay = (5600 * Steps / Sweep) - 4450;

And it worked like a charm. The machine is chugging away right now :slight_smile:

It is always indeed good practice To hint to the parser what you mean and especially for whoever reads the code afterwards

If you want to get more precise, cast everything as double in the formula (add .0 at the very least to constants) and cast the final result back to an int so that you round down only at the very end and don't accumulate errors

Something like

myInt = (int) ( ((double) a) * ((double) b) + ((double) c) - ((double) 50.0) );

Of course it's more verbose, will take more memory and time to compute but if you are not time or memory bound then it's always good to get the maths right :slight_smile:

and you can make it look simpler with a macro D(x) as ((double) x)