2

This is my first project working with stepper motors, so I may have a bit of a shaky understanding of the electronic side of the project. I'm trying to create a simple device with 4 buttons connected to a stepper motor driver/NEMA 17 motor, with the ability to increase/decrease RPM, switch motor spin direction, and run the motor with the button being held down. (LCD screen included for output as well)

I'm noticing that when changing the RPM on my motor, i.e. from 3.2 to 3.1 to 3.0, the speed of the motor is really slow, then when set to 3.0, suddenly increases a lot, but goes down when setting the RPM back down to 2.8 or so. This occurs multiple times along different intervals of RPMs that I have tested as well.

I think that there may be an issue with the method with which I am calculating the pulse delay given the RPM in my function setRPM() at the end of my code or with the way that I am micro-stepping, which I am doing because I need extremely precise movement.

Included below is the code that I used to run with my TMC2208 driver. (Sorry if it is rather long, but I included comments to document my thought process when programming the Arduino.)

Included list of RPMs and pulse delays

rpm pulse_delay (us)
1 37500 (too fast)
2 18750 (too fast)
2.25 16666.6667 (works)
2.5 15000 (works)
3 12500 (works)
5 7500 (works)
10 3759 (works)
// Stepper motor run code with TMC2208 driver and LCD code
#include <LiquidCrystal_I2C.h>
#include <Wire.h>
#include <Stepper.h>
// Define Pins for Step Motor Driver
#define stepPin 7  // define pin for step
#define dirPin 8   // define pin for direction
#define enPin 9    // define pin for enable ^^ Do I need this?

// Define Buttons for Speed and Direction #define buttGo 10 // define pin for button input #define buttSwitch 11 // define pin for button input #define incSpeed 12 // define button for increasing speed #define decSpeed 13 // define button for decreeasing speed

// Define LCD pinout const int en = 2, rw = 1, rs = 0, d4 = 4, d5 = 5, d6 = 6, d7 = 7, bl = 3; // Adapter Pinout const int i2c_addr = 0x27; // Adapter pinout LiquidCrystal_I2C lcd(i2c_addr, en, rw, rs, d4, d5, d6, d7, bl, POSITIVE);

/* Formulas for determining RPM Assuming one step per pulse, delay is: wait = (μs/min)(1/rpm)(1/pulses per revolution) - overhead Overhead = extra time it takes to run digitalWrite twice and loop RPM = (steps per second)/(steps per revolution) * (60 seconds/minute) step angle = 1.8 degrees ; 200 steps per revolution ; .005 revolutions per step Solving for Steps Per Second: SPS = (RPM)/((REV_P_STEP)*(60 sec/min))

According to Notes: C0 = 15 * M_PI ; Motor X Circumference XSPR = 1 * (200 * 11) ; Motor X Steps per Rotation dVx = dS * (XSPR/C0) ; Desired X-Axis time from mm/s to x-axis steps/sec (dS represents desired x-axis speed) Assuming we use a button/knob, each increment/decrement would change the mm/s by 0.1
So, to get the necessary pulse delay, increment/decrement by: dVx = 0.1 * (1 * (200 * 11))/(15*M_PI)

Example: If we have an initial target rpm of 10, that gives us a dS of 10 As such, dVx which is our speed in steps will be 10 * (1 * (200 * 11)) / (15 * M_PI) All of these variables are set globally wait = (microsecondsPminute)(1/RPM)(1/pulsesPrevolution) - overhead rpm / 60 = rps 60 / rpm = spr (60 / rpm)/360 = spd ((60 / rpm)/360) * 1.8 = sps


Frequency = (RPM)/((Resolution/360)60) Resolution = 360/(Steps/Revolution) For us, Resolution = 360/(200); so Resolution = 1.8* Frequency = (RPM)/(0.005 * 60) = RPM/(0.3)

T_inc = incremental torque produced with each microstep T_hfs = holding torque (full-step operation) SDR = step division ratio (number of microsteps per full step)

T_inc = T_hfs * sin(90/SDR) T_inc = 0.14 * sin(90/256) T_inc = 0.00085902385 */ float resolution = 1.8; float rpm = 10; float pulse_delay; // Temporary starting value that will change after void setup bool buttonState = false; bool onState = false;

// Set up output types void setup() { Serial.begin(9600); // Change this baud rate to whatever rate the LCD screen runs on - should generally be 9600 // Set up LCD screen pulse_delay = setRPM(rpm, resolution); lcd.begin(16, 2); // set up the LCD's number of columns and rows: lcd.setCursor(0, 0); lcd.print("Current RPM: "); lcd.setCursor(12, 0); lcd.print(rpm); lcd.setCursor(0, 1); lcd.print("MOVE RIGHT");

// Sets up buttons pinMode(buttSwitch, INPUT); pinMode(buttGo, INPUT); pinMode(incSpeed, INPUT); pinMode(decSpeed, INPUT);

// Establish initials pinMode(enPin, OUTPUT); digitalWrite(enPin, HIGH); // deactivate driver pinMode(dirPin, OUTPUT); digitalWrite(dirPin, HIGH); pinMode(stepPin, OUTPUT); digitalWrite(enPin, LOW); // activates driver }

// Revolutions per second should be able specified to the tenths // Run code to continual void loop() { // Read Buttons Being Pressed int pressSwitch = digitalRead(buttSwitch); int pressGo = digitalRead(buttGo); int pressInc = digitalRead(incSpeed); int pressDec = digitalRead(decSpeed); pulse_delay = setRPM(rpm, resolution); //pulse_delay = (rpm * 200)/60;

if (pressSwitch == HIGH) // Moves motor Right (Counter-Clockwise) { if (buttonState == 0) { digitalWrite(dirPin, LOW); // sets direction of the motor turning buttonState = 1; lcd.setCursor(0, 1); lcd.print("MOVE LEFT"); delay(500); } else { if (buttonState == 1) { digitalWrite(dirPin, HIGH); // sets direction of the motor turning buttonState = 0; lcd.setCursor(0, 1); lcd.print("MOVE RIGHT"); delay(500); } } }

if (pressGo == HIGH) // Moves motor { digitalWrite(stepPin, HIGH); // This LOW to HIGH change is what creates the "Rising Edge" so the easydriver knows when to step. delayMicroseconds(pulse_delay); digitalWrite(stepPin, LOW); }

if (pressInc == HIGH) // Increases RPM { rpm = rpm + 0.1; delay(150); lcd.setCursor(12, 0); lcd.print(rpm); }

if (pressDec == HIGH) // Decreases RPM { rpm = rpm - 0.1; delay(150); lcd.setCursor(12, 0); lcd.print(rpm); } }

// Function to get pulse delay time based off of inputted RPM value float setRPM(float rpm, float resolution) { /* float temper = ((60 / rpm) / 360); float pulsetime = ((temper * resolution) * 1000000); 1 rpm = 37500 us delay (too fast) 2 rpm = 18750 us delay (too fast) 2.25 rpm = 16666.6667 us delay 2.5 rpm = 15000 us delay 3 rpm = 12500 us delay 5 rpm = 7500 us delay 10 rpm = 3750 us delay / unsigned int pulsetime = (60000000 / (1600 rpm)); return pulsetime; // Converts to microseconds }

jonathan
  • 76
  • 7
  • While moving (pressGo == HIGH) your high part of the pulse has a variable duration. How do you control the low part? -- Did you measure the generated signal, for example with a frequency counter or oscilloscope? – the busybee Jul 13 '23 at 06:34
  • I made a quick table and it revealed that going from 3.0 to 3.1 in rpm crosses the "255/256 border" in pulse_delay. You might want to temporarily change your sketch to adjust pulse_delay directly to check this in detail. – the busybee Jul 13 '23 at 08:31
  • @thebusybee I haven't been able to get my hands on a frequency counter or an oscilloscope, but I have used a multimeter just to see if there was an issue I was making with my hardware setup - which I don't think there is. I'll measure the generated signal to check that out!

    What do you mean by crossing the 255/256 border? I have changed my pulse_delay directly in my pressGo if statement and it seems to work alright when changed directly.

    Thank you for the quick response!

    – jonathan Jul 13 '23 at 16:44
  • pulse_delay is a float, but you call delayMicroseconds() with it. This function expects an unsigned int according to the documentation, so the value is converted. The unsigned int uses multiple bytes, and values from 0 to 255 "use" only the least significant byte. Starting with 256, the next byte(s) carry other values than 0. That's what I mean by "255/256 border". -- Which range of values did you check concretely? – the busybee Jul 13 '23 at 19:09
  • Thank you for the clarification on the byte "limit". I converted my pulse_delay variable to unsigned int since it makes sense to me that I should utilize it when using the delayMicroseconds() function. I also switched to the equation defined in my setRPM function to the one defined in this post. Using this equation I concretely checked rpms of 3, 5, and 10. – jonathan Jul 13 '23 at 22:01
  • That change is fine, as it clarifies the working for the future reader, most probably your future self in some weeks. ;-) -- When I asked for the range tested, I meant the range of pulse_delay you checked out. Does the stepper run as expected? I would start with some high value (for low rpms, like 2000 µs) and decrease it down to some low value (for high rpms, like 100µs). If there is a problem with that mentioned border, you will still see this saw-tooth-like transfer function, the repetitions nicely matching multiples of 256. You might want to [edit] your question with a table. – the busybee Jul 14 '23 at 05:47
  • Sorry about the confusion - I checked pulse delays from 37500 us to 3750 us and I noticed that it was running as expected up until the low value of around 18750 us delay which correlates to an rpm of 2. The stepper run stops running as expected after I start reaching lower RPMs/higher pulse delays. I also added a table of pulse delays and RPMs that I had tested to the original question as well. As for the saw-tooth-like transfer functions you are referring to the voltage displayed on an oscilloscope? I believe there is an issue with running accurately at lower speeds. – jonathan Jul 14 '23 at 16:55
  • I cannot reproduce my table, I don't know where my calculations were wrong. So forget about the 255/256 border. -- Anyway, I cannot reproduce your values, too. For example, for rpm=2.5, temper=(60/rpm)/360=0.0666... and pulse_delay=(temper1.8)1000000=120000. But you list pulse_delay=15000. Please make sure that source and table results match, and correct. – the busybee Jul 15 '23 at 12:14

2 Answers2

2

The documentation says:

Currently, the largest value that will produce an accurate delay is 16383; larger values can produce an extremely short delay.

The value given to delayMicroseconds() seems to be processed by a modulo 16384 operation. That means that the value is divided by 16384, and the remainder of this division is the effective delay in microseconds.

If we apply this to the delays in your table, we get:

desired rpm calculated delay (µs) effective delay (µs) effective rpm
1 37500 4732 7.9
2 18750 2366 15.8
2.25 16666 16666 2.25
2.5 15000 15000 2.5
3 12500 12500 3
5 7500 7500 5
10 3750 3750 10

One possible solution is presented in the same documentation:

For delays longer than a few thousand microseconds, you should use delay() instead.

You might want to consider to use both functions, depending on the necessary delay.


And then there is the other issue with the time when the output is low. Currently, it is determined by the time the code takes to quit loop(), do its internal looping, call loop() and your statements until the pin goes high again.

You might want to divide the pulse period in two halves and take control of this low phase, too.

the busybee
  • 2,002
  • 7
  • 17
  • Thank you for the response! For the second part of your answer, did you mean that I should split the pressGo motor run statement into stepPin High, (1/2 pulse delay), stepPin Low, (1/2 pulse delay)? if (pressGo == HIGH) { digitalWrite(stepPin, HIGH); delayMicroseconds(pulse_delay); digitalWrite(stepPin, LOW); delayMicroseconds(pulse_delay); } Here is my code for reference. I have also included another if statement such that if my pulse_delay is greater than that 16384 for the delayMicrosecond function, it uses delay() instead. – jonathan Jul 17 '23 at 17:17
  • @jonathan Yes, you can go that route. Just be aware that the time to "turn-around" is currently and generally undetermined. You might want to measure it, for example with an oscilloscope or similar device. As long as it is small compared to the delays, you can consider to ignore it. Or you take it into account for the low phase. – the busybee Jul 17 '23 at 17:26
1

For reference for any future readers who have had similar problems using a TMC2208 Stepper Motor - I have attached my annotated code to this answer for your use. In the annotation, I also include equations that can be used to determine spacings for a coiling setup.

// Stepper motor run code with A4988 driver and LCD code
#include <LiquidCrystal_I2C.h>
#include <Wire.h>
#include <Stepper.h>
// Define Pins for Step Motor Driver
#define stepPin 7  // define pin for step
#define dirPin 8   // define pin for direction
#define enPin 9    // define pin for enable ^^ Do I need this?

// Define Buttons for Speed and Direction #define buttGo 10 // define pin for button input #define buttSwitch 11 // define pin for button input #define incSpeed 12 // define button for increasing speed #define decSpeed 13 // define button for decreeasing speed

// Define LCD pinout const int en = 2, rw = 1, rs = 0, d4 = 4, d5 = 5, d6 = 6, d7 = 7, bl = 3; // Adapter Pinout const int i2c_addr = 0x27; // Adapter Pinout LiquidCrystal_I2C lcd(i2c_addr, en, rw, rs, d4, d5, d6, d7, bl, POSITIVE);

/* Formulas for determining RPM Assuming one step per pulse, delay is: wait = (μs/min)(1/rpm)(1/pulses per revolution) - overhead Overhead = extra time it takes to run digitalWrite twice and loop RPM = (steps per second)/(steps per revolution) * (60 seconds/minute) step angle = 1.8 degrees ; 200 steps per revolution ; .005 revolutions per step Solving for Steps Per Second: SPS = (RPM)/((REV_P_STEP)*(60 sec/min))

According to John Craig: C0 = 15 * M_PI ; Motor X Circumference XSPR = 1 * (200 * 11) ; Motor X Steps per Rotation dVx = dS * (XSPR/C0) ; Desired X-Axis time from mm/s to x-axis steps/sec (dS represents desired x-axis speed) Assuming we use a button/knob, each increment/decrement would change the mm/s by 0.1
So, to get the necessary pulse delay, increment/decrement by: dVx = 0.1 * (1 * (200 * 11))/(15*M_PI)

Example: If we have an initial target rpm of 10, that gives us a dS of 10 As such, dVx which is our speed in steps will be 10 * (1 * (200 * 11)) / (15 * M_PI) All of these variables are set globally wait = (microsecondsPminute)(1/RPM)(1/pulsesPrevolution) - overhead rpm / 60 = rps 60 / rpm = spr (60 / rpm)/360 = spd ((60 / rpm)/360) * 1.8 = sps


Assume from measured diameter with calipers that it is 12 mm (Check 11.85 as well) To get Spacings: V_extruder / RPM_rod V_extruder = M_PI Diam_extruder * RPM_extruder (in (mmrev)/min) RPM_rod = 13 rpm (set from original bought parts) V_extruder = M_PI 12 mm * X Spacing = (M_PI * 12 * X) / 9.64507699 RPM Spacing = 3.90863773 * X For Spacings of 3 mm... 0.76753084 rev/min ~= 0.77 rev/min For Spacings of 5 mm... 1.27921807 rev/min ~= 1.28 rev/min For Spacings of 7 mm... 1.79090529 rev/min ~= 1.79 rev/min

y = 1.0715x + 2.6653 y = Voltage Setting x = RPM For Voltage Setting of 13, RPM = 9.64507699

T_inc = incremental torque produced with each microstep T_hfs = holding torque (full-step operation) SDR = step division ratio (number of microsteps per full step)

T_inc = T_hfs * sin(90/SDR) T_inc = 0.14 * sin(90/256) T_inc = 0.00085902385 */ float resolution = 1.8; float rpm = 3; unsigned int pulse_delay; // Temporary starting value that will change after void setup bool buttonState = false; bool onState = false;

// Set up output types void setup() { Serial.begin(9600); // Change this baud rate to whatever rate the LCD screen runs on - should generally be 9600 // Set up LCD screen pulse_delay = setRPM(rpm); lcd.begin(16, 2); // set up the LCD's number of columns and rows: lcd.setCursor(0, 0); lcd.print("Current RPM: "); lcd.setCursor(12, 0); lcd.print(rpm); lcd.setCursor(0, 1); lcd.print("MOVE RIGHT");

// Sets up buttons pinMode(buttSwitch, INPUT); pinMode(buttGo, INPUT); pinMode(incSpeed, INPUT); pinMode(decSpeed, INPUT);

// Establish initials pinMode(enPin, OUTPUT); digitalWrite(enPin, HIGH); // deactivate driver pinMode(dirPin, OUTPUT); digitalWrite(dirPin, HIGH); pinMode(stepPin, OUTPUT); digitalWrite(enPin, LOW); // activates driver }

// Revolutions per second should be able specified to the tenths // Run code to continual void loop() { // Read Buttons Being Pressed int pressSwitch = digitalRead(buttSwitch); int pressGo = digitalRead(buttGo); int pressInc = digitalRead(incSpeed); int pressDec = digitalRead(decSpeed); pulse_delay = setRPM(rpm); //pulse_delay = (rpm * 200)/60;

if (pressSwitch == HIGH) // Moves motor Right (Counter-Clockwise) { if (buttonState == 0) { digitalWrite(dirPin, LOW); // sets direction of the motor turning buttonState = 1; lcd.setCursor(0, 1); lcd.print("MOVING LEFT"); delay(500); } else { if (buttonState == 1) { digitalWrite(dirPin, HIGH); // sets direction of the motor turning buttonState = 0; lcd.setCursor(0, 1); lcd.print("MOVING RIGHT"); delay(500); } } } // Both of the next functions move the motor, but switches between Microseconds and Milliseconds in delay time to fit. if (pressGo == HIGH && pulse_delay >= 16383) // Moves motor { digitalWrite(stepPin, HIGH); // This LOW to HIGH change is what creates the "Rising Edge" so the easydriver knows when to step. delay(pulse_delay/1000); digitalWrite(stepPin, LOW); }

if (pressGo == HIGH && pulse_delay < 16383) { digitalWrite(stepPin, HIGH); // This LOW to HIGH change is what creates the "Rising Edge" so the easydriver knows when to step. delayMicroseconds(pulse_delay); digitalWrite(stepPin, LOW); }

if (pressInc == HIGH) // Increases RPM { rpm = rpm + 0.01; delay(150); lcd.setCursor(12, 0); lcd.print(rpm); }

if (pressDec == HIGH) // Decreases RPM { rpm = rpm - 0.01; delay(150); lcd.setCursor(12, 0); lcd.print(rpm); } }

// Function to get pulse delay time based off of inputted RPM value float setRPM(float rpm) { /* float temper = ((60 / rpm) / 360); float pulsetime = ((temper * resolution) * 1000000); 1 rpm = 37500 us delay (too fast) 2 rpm = 18750 us delay (too fast) 2.25 rpm = 16666.6667 us delay 2.5 rpm = 15000 us delay 3 rpm = 12500 us delay 5 rpm = 7500 us delay 10 rpm = 3750 us delay / unsigned int pulsetime = (60000000 / (1600 rpm)); return pulsetime; // Converts to microseconds }

jonathan
  • 76
  • 7