Hallo in die Runde,
ich habe ein Frage,
ich benutze einen Arduino Mega um 5 Schrittmotoren unabhängig voneinander zu steuern. Es handelt sich um eine Vorschubeinheit, deren Rollen unterschiedlich schnell laufen müssen.
Bei den Schrittmotoren handelt es sich um 34HS59-6004S.
Diese sind an einen Schrittmotorentreiber angeschlossen (StepperOnline Digital Stepper Driver DM860I).
Das Netzteil ist ein S-350-60 von Stepper Online. Da ich für die Anwendung ein GUI brauche, habe ich mit TKinter ein GUI für Python geschrieben.
Eigentlich funktioniert auch soweit alles wie es das soll. allerdings gibt es ein Problem. Die Controller sind auf 3200 microsteps eingestellt (tiefer will ich nicht, da ich langsame Geschwindigkeiten fahren muss und bei allen microstepeinstellungen unterhalb die Motoren zu stark vibrieren. Da es sich bei der Anwendung um eine Vorschubeinheit handelt, gebe ich jetzt mal die Geschwindigkeit in m/min an. Wenn ich einen Motor drehen lasse, kann ich ohne Probleme 10 m/min fahren. Schalte ich einen zweiten dazu, der sich ebenfalls mit 10 m/min dreht, fängt sich dieser an zu drehen, der zuerst angeschaltete reduziert dann aber seine Geschwindigkeit, worauf hin beide auf einer Geschwindigkeit laufen, die nicht 10 m/min entspricht.
Ist es Möglich, dass der Arduino für diese Funktion zu langsam ist? Oder liegt es an einer ineffizienten Programmierung meinerseits?
Was für mich dafür spricht, dass der Arduino zu langsam ist, ist dass wenn ich die Anzahl der Microsteps am Treiber runter stelle, die Problematik erst bei höheren Geschwindigkeiten auftaucht. Wenn ich die Anzahl der Microsteps erhöhe, tritt die Problematik bereits bei niedrigeren Geschwindigkeiten auf.
P.s. dass es mit der Stromversorgung zutun hat ist ausgeschlossen, da ich zeitweise nur zwei Motoren auch wirklich angeschlossen hatte. Lasse ich einen Motor auf 10 m/min laufen und schalte einen nicht angeschlossenen mit 10 m/min zu, reduziert sich die Umdrehungszahl ebenfalls obwohl kein weiterer Verbraucher hinzu gekommen ist (und so wie ich das verstehen würde es auch keinen Sinn machen, dass ein Spannungsabfall oder ähnliches dazu führen kann, dass sich ein Schrittmotor langsamer dreht (oder könnte er prinzipiell dann Schritte nicht schaffen und sich langsamer drehen? egal). Zudem werden alle Motoren über ihr eigenes Netzteil betrieben und sind an unterschiedliche Stromquellen angeschlossen.
Hier mein Code:
Arduino:
#include <AccelStepper.h>
#include <MultiStepper.h>
#define NUM_VE 5
float v_ve[NUM_VE] = {0, 0, 0, 0, 0}; // Initial speeds set to 0
float d_ve[NUM_VE]; // Diameter of rollers in meters
int uev_ve[NUM_VE]; // Gear ratios
int stpu_ve[NUM_VE]; // Steps per revolution
const int ena_pins[NUM_VE] = {41, 44, 47, 50, 53};
const int dir_pins[NUM_VE] = {40, 43, 46, 49, 52};
const int pul_pins[NUM_VE] = {39, 42, 45, 48, 51};
AccelStepper steppers[NUM_VE] = {
AccelStepper(AccelStepper::DRIVER, pul_pins[0], dir_pins[0]),
AccelStepper(AccelStepper::DRIVER, pul_pins[1], dir_pins[1]),
AccelStepper(AccelStepper::DRIVER, pul_pins[2], dir_pins[2]),
AccelStepper(AccelStepper::DRIVER, pul_pins[3], dir_pins[3]),
AccelStepper(AccelStepper::DRIVER, pul_pins[4], dir_pins[4])
};
MultiStepper multiStepper;
bool running = false;
int direction = 1; // 1 for forward, -1 for reverse
void setup() {
Serial.begin(115200); // Updated baud rate to 115200
for (int i = 0; i < NUM_VE; i++) {
pinMode(ena_pins[i], OUTPUT);
digitalWrite(ena_pins[i], LOW); // Enable on startup
steppers[i].setMaxSpeed(1000); // Set a reasonable max speed
steppers[i].setAcceleration(800); // Set a reasonable acceleration
multiStepper.addStepper(steppers[i]);
}
setDefaultSettings();
}
void loop() {
if (Serial.available() > 0) {
String input = Serial.readStringUntil('\n');
if (input.length() > 0) {
if (input.startsWith("M")) {
int motor_index = input.charAt(1) - '1';
float speed = input.substring(3).toFloat();
if (motor_index >= 0 && motor_index < NUM_VE) {
v_ve[motor_index] = speed;
updateSpeed(motor_index);
}
} else if (input.startsWith("ENABLE")) {
int motor_index = input.charAt(6) - '1';
bool enable = input.substring(8) == "true";
digitalWrite(ena_pins[motor_index], enable ? LOW : HIGH);
} else if (input == "START") {
running = true;
for (int i = 0; i < NUM_VE; i++) {
updateSpeed(i); // Reinitialize movement when starting
}
} else if (input == "STOP") {
running = false;
for (int i = 0; i < NUM_VE; i++) {
steppers[i].stop();
steppers[i].setCurrentPosition(0); // Reset the current position
}
} else if (input.startsWith("DIR:")) {
direction = (input.substring(4) == "REVERSE") ? -1 : 1;
for (int i = 0; i < NUM_VE; i++) {
steppers[i].moveTo(direction * 100000000L);
}
} else if (input.startsWith("SETTINGS")) {
int new_stpu_ve = input.substring(9, input.indexOf(' ', 9)).toInt();
float new_d_ve = input.substring(input.indexOf(' ', 9) + 1, input.lastIndexOf(' ')).toFloat();
int new_uev_ve = input.substring(input.lastIndexOf(' ') + 1).toInt();
for (int i = 0; i < NUM_VE; i++) {
stpu_ve[i] = new_stpu_ve;
d_ve[i] = new_d_ve;
uev_ve[i] = new_uev_ve;
}
}
}
}
if (running) {
for (int i = 0; i < NUM_VE; i++) {
if (steppers[i].distanceToGo() == 0) {
steppers[i].moveTo(direction * 100000000L); // Keep moving continuously
}
steppers[i].run();
}
}
}
void updateSpeed(int motor_index) {
float U_ve = PI * d_ve[motor_index]; // Circumference of the roller
float um_ve = 1.0 / U_ve; // Revolutions per meter
float sm_ve = um_ve * stpu_ve[motor_index] * uev_ve[motor_index]; // Steps per meter
float spm_ve = v_ve[motor_index] * sm_ve; // Steps per minute
int spsr_ve = int(spm_ve / 60); // Steps per second, rounded
steppers[motor_index].setMaxSpeed(spsr_ve);
steppers[motor_index].setSpeed(spsr_ve * direction);
if (running) {
steppers[motor_index].moveTo(direction * 100000000L); // Move to a large distance for continuous motion
}
}
void setDefaultSettings() {
for (int i = 0; i < NUM_VE; i++) {
d_ve[i] = 0.05;
uev_ve[i] = 1;
stpu_ve[i] = 3200;
}
}
Python:
import customtkinter as ctk
import serial.tools.list_ports
import serial
import threading
import sys
import tkinter as tk
class ConsoleRedirector:
def __init__(self, text_widget):
self.text_widget = text_widget
def write(self, message):
self.text_widget.insert(tk.END, message)
self.text_widget.see(tk.END) # Scroll to the end
def flush(self):
pass
class ThreeColumnGUI:
def __init__(self, root):
self.root = root
self.root.title("Three Column GUI")
self.root.geometry("800x400") # Adjusted window size for extra space
# Initialize motor direction and settings
self.motor_direction = "FORWARD"
self.stpu_ve = 3200 # Default value
self.d_ve = 0.05 # Default value in meters
self.uev_ve = 1 # Default value
# Create a tab view
self.tabview = ctk.CTkTabview(self.root)
self.tabview.pack(expand=True, fill="both")
# Add tabs
self.tabview.add("Control")
self.tabview.add("Settings")
self.tabview.add("Console")
# Create Control tab content
self.create_control_tab(self.tabview.tab("Control"))
# Create Settings tab content
self.create_settings_tab(self.tabview.tab("Settings"))
# Create Console tab content
self.create_console_tab(self.tabview.tab("Console"))
# Serial connection attributes
self.serial_connection = None
# Start periodic check for connection status
self.check_connection_status()
# Bind the close event
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def create_control_tab(self, tab):
# Create three columns
self.create_column(tab, 0, "Connect")
self.create_separator(tab, 1)
self.create_column(tab, 2, "Setup")
self.create_separator(tab, 3)
self.create_column(tab, 4, "Control")
# Configure tab to expand vertically and horizontally
tab.grid_rowconfigure(0, weight=1)
for i in [0, 2, 4]:
tab.grid_columnconfigure(i, weight=1)
def create_settings_tab(self, tab):
settings_frame = ctk.CTkFrame(tab)
settings_frame.pack(pady=20)
stpu_label = ctk.CTkLabel(settings_frame, text="Select Steps per Revolution (stpu_ve):", font=("Arial", 14))
stpu_label.pack(pady=5)
self.stpu_var = ctk.StringVar(value="3200") # Default value
stpu_menu = ctk.CTkComboBox(settings_frame, variable=self.stpu_var,
values=["400", "800", "1600", "3200", "6400"])
stpu_menu.pack(pady=5)
d_ve_label = ctk.CTkLabel(settings_frame, text="Enter Diameter of Rollers (d_ve) in meters:",
font=("Arial", 14))
d_ve_label.pack(pady=5)
self.d_ve_entry = ctk.CTkEntry(settings_frame, width=100, placeholder_text="Diameter (m)")
self.d_ve_entry.insert(0, "0.05")
self.d_ve_entry.pack(pady=5)
uev_ve_label = ctk.CTkLabel(settings_frame, text="Enter Gear Ratio (uev_ve):", font=("Arial", 14))
uev_ve_label.pack(pady=5)
self.uev_ve_entry = ctk.CTkEntry(settings_frame, width=100, placeholder_text="Gear Ratio")
self.uev_ve_entry.insert(0, "1")
self.uev_ve_entry.pack(pady=5)
apply_button = ctk.CTkButton(settings_frame, text="Apply", command=self.apply_settings)
apply_button.pack(pady=5)
def create_console_tab(self, tab):
# Create a text box for the console output
self.console_text = ctk.CTkTextbox(tab, width=750, height=350)
self.console_text.pack(pady=20)
# Redirect stdout to the console text box
sys.stdout = ConsoleRedirector(self.console_text)
sys.stderr = ConsoleRedirector(self.console_text)
def apply_settings(self):
try:
self.stpu_ve = int(self.stpu_var.get())
self.d_ve = float(self.d_ve_entry.get())
self.uev_ve = int(self.uev_ve_entry.get())
print(f"Settings applied - stpu_ve: {self.stpu_ve}, d_ve: {self.d_ve}, uev_ve: {self.uev_ve}")
if self.serial_connection and self.serial_connection.is_open:
command = f"SETTINGS {self.stpu_ve} {self.d_ve} {self.uev_ve}\n"
self.serial_connection.write(command.encode())
print(f"Sent command: {command.strip()}")
self.send_speeds() # Send speeds after applying settings
except ValueError:
print("Invalid input. Please enter valid numbers for stpu_ve, d_ve, and uev_ve.")
def create_column(self, parent, column_index, header_text):
column_frame = ctk.CTkFrame(parent, width=200)
column_frame.grid(row=0, column=column_index, padx=5, pady=5, sticky="nsew")
# Add header label
header_label = ctk.CTkLabel(column_frame, text=header_text, font=("Arial", 16, "bold"))
header_label.pack(pady=10)
if header_text == "Connect":
# Create an inner frame to center the dropdown and button
inner_frame = ctk.CTkFrame(column_frame)
inner_frame.pack(expand=True)
# Add dropdown and connect button in the first column
self.com_var = ctk.StringVar()
self.com_menu = ctk.CTkComboBox(inner_frame, variable=self.com_var, values=self.get_com_ports())
self.com_menu.pack(pady=10)
self.connect_button = ctk.CTkButton(inner_frame, text="Connect", command=self.connect_serial)
self.connect_button.pack(pady=10)
# Add connection status light
self.connection_status = ctk.CTkLabel(inner_frame, text=" ", width=20, height=20, corner_radius=10,
fg_color="red")
self.connection_status.pack(pady=10)
if header_text == "Setup":
# Create an inner frame to center the switches and text inputs
inner_frame = ctk.CTkFrame(column_frame)
inner_frame.pack(expand=True)
# Add switches and text inputs to enable/disable motors and set speed
self.motor_switches = []
self.speed_entries = []
for i in range(5):
switch_frame = ctk.CTkFrame(inner_frame)
switch_frame.pack(pady=5, anchor="w") # Align to the left
switch_label_motor = ctk.CTkLabel(switch_frame, text=f"Motor {i + 1}")
switch_label_motor.pack(side=ctk.LEFT, padx=5)
switch_label_disable = ctk.CTkLabel(switch_frame, text="Disable")
switch_label_disable.pack(side=ctk.LEFT, padx=5)
motor_switch = ctk.CTkSwitch(switch_frame, text="Enable", command=lambda i=i: self.toggle_motor(i))
motor_switch.pack(side=ctk.LEFT, padx=5)
motor_switch.select() # Set all motors to enable on startup
self.motor_switches.append(motor_switch)
speed_entry = ctk.CTkEntry(switch_frame, width=100, placeholder_text="Speed (m/min)")
speed_entry.pack(side=ctk.LEFT, padx=5)
self.speed_entries.append(speed_entry)
# Add a frame to hold the Send Speeds button and align it to the right
button_frame = ctk.CTkFrame(column_frame)
button_frame.pack(pady=10, anchor="e")
# Add a button to send all speeds
self.send_speeds_button = ctk.CTkButton(button_frame, text="Send Speeds", command=self.send_speeds)
self.send_speeds_button.pack(side=ctk.RIGHT, padx=5)
# Add a switch to toggle direction
self.direction_switch = ctk.CTkSwitch(button_frame, text="Forward", onvalue="Forward", offvalue="Backward",
command=self.toggle_direction)
self.direction_switch.pack(side=ctk.LEFT, padx=5)
self.direction_switch.select() # Set to "Forward" on startup
if header_text == "Control":
# Create an inner frame to center the control buttons
inner_frame = ctk.CTkFrame(column_frame)
inner_frame.pack(expand=True)
# Add start/stop switch in the third column
start_stop_switch = ctk.CTkSwitch(inner_frame, text="Start/Stop", command=self.toggle_start_stop)
start_stop_switch.pack(pady=10)
self.start_stop_switch = start_stop_switch
def create_separator(self, parent, column_index):
separator = ctk.CTkFrame(parent, fg_color="gray", width=2)
separator.grid(row=0, column=column_index, sticky="ns")
def get_com_ports(self):
"""Returns a list of available COM ports"""
ports = serial.tools.list_ports.comports()
return [port.device for port in ports]
def connect_serial(self):
"""Connect to the selected COM port"""
selected_port = self.com_var.get()
if selected_port:
try:
self.serial_connection = serial.Serial(selected_port, 115200) # Updated baud rate to 115200
print(f"Connected to {selected_port}")
# Send enable command to all motors on startup
for i in range(5):
self.toggle_motor(i)
# Update connection status light to green
self.connection_status.configure(fg_color="green")
# Start a thread to read from the serial port
self.serial_thread = threading.Thread(target=self.read_serial)
self.serial_thread.daemon = True
self.serial_thread.start()
except serial.SerialException as e:
print(f"Failed to connect to {selected_port}: {e}")
# Update connection status light to red
self.connection_status.configure(fg_color="red")
else:
print("No COM port selected")
# Update connection status light to red
self.connection_status.configure(fg_color="red")
def read_serial(self):
"""Read from the serial port and display in the console"""
while self.serial_connection.is_open:
try:
line = self.serial_connection.readline().decode('utf-8').strip()
if line:
print(line)
except serial.SerialException:
break
def toggle_motor(self, motor_index):
"""Toggle motor enable/disable state"""
if self.serial_connection and self.serial_connection.is_open:
state = self.motor_switches[motor_index].get()
enable_str = "true" if state else "false"
command = f"ENABLE{motor_index + 1} {enable_str}\n"
self.serial_connection.write(command.encode())
print(f"Sent command: {command.strip()}")
# Set speed if enabled
if state:
speed = self.speed_entries[motor_index].get()
if speed:
try:
speed_value = float(speed)
speed_command = f"M{motor_index + 1} {speed_value}\n"
self.serial_connection.write(speed_command.encode())
print(f"Set speed for Motor {motor_index + 1}: {speed_value} m/min")
except ValueError:
print(f"Invalid speed value for Motor {motor_index + 1}")
else:
print("Serial connection is not open")
def send_speeds(self):
"""Send speed settings for all motors"""
if self.serial_connection and self.serial_connection.is_open:
for i in range(5):
speed = self.speed_entries[i].get()
if speed:
try:
speed_value = float(speed)
speed_command = f"M{i + 1} {speed_value}\n"
self.serial_connection.write(speed_command.encode())
print(f"Set speed for Motor {i + 1}: {speed_value} m/min")
except ValueError:
print(f"Invalid speed value for Motor {i + 1}")
else:
print("Serial connection is not open")
def toggle_start_stop(self):
"""Toggle start/stop state of all motors"""
if self.serial_connection and self.serial_connection.is_open:
if self.start_stop_switch.get():
self.serial_connection.write(b"START\n")
print("Sent command: START")
else:
self.serial_connection.write(b"STOP\n")
print("Sent command: STOP")
else:
print("Serial connection is not open")
def toggle_direction(self):
"""Toggle direction of all motors"""
if self.serial_connection and self.serial_connection.is_open:
self.motor_direction = "REVERSE" if self.motor_direction == "FORWARD" else "FORWARD"
command = f"DIR:{self.motor_direction}\n"
self.serial_connection.write(command.encode())
direction_text = "Forward" if self.motor_direction == "FORWARD" else "Backward"
self.direction_switch.configure(text=direction_text)
print(f"Sent command: {command.strip()}")
else:
print("Serial connection is not open")
def check_connection_status(self):
"""Periodically check the connection status and update the light"""
if self.serial_connection:
try:
self.serial_connection.in_waiting # Attempt to read
self.connection_status.configure(fg_color="green")
except (OSError, serial.SerialException):
self.connection_status.configure(fg_color="red")
self.serial_connection = None
else:
self.connection_status.configure(fg_color="red")
# Check again after 1000ms (1 second)
self.root.after(1000, self.check_connection_status)
def on_closing(self):
"""Handle the window close event"""
if self.serial_connection and self.serial_connection.is_open:
# Disable all motors before closing
for i in range(5):
command = f"ENABLE{i + 1} false\n"
self.serial_connection.write(command.encode())
print(f"Sent command: {command.strip()}")
self.root.destroy()
if __name__ == "__main__":
ctk.set_appearance_mode("light") # Modes: "System" (default), "Dark", "Light"
ctk.set_default_color_theme("blue") # Themes: "blue" (default), "green", "dark-blue"
root = ctk.CTk()
gui = ThreeColumnGUI(root)
root.mainloop()
Beste Grüße