Threading für Schrittmotorsteuerung

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
elchico
User
Beiträge: 29
Registriert: Dienstag 10. März 2015, 00:06

Hallo zusammen,

ich bastle gerade an einem Skript, welches theoretisch einen 3D Drucker steuern könnte. Ob ich jemals das ganze in die Praxis umsetze, bleibt abzuwarten, allerdings interessiert mich die theoretische Umsetzung. Für meine Tests habe ich drei LEDs für die drei Motoren stellvertretend genommen.

Hierfür stelle ich mir vor, dass ich eine Text Datei bekomme (in der praktisch das zu druckende Objekt als 0 und 1 hinterlegt ist):

Code: Alles auswählen

3D_Druck
motorx,motory,motorz
0,0,0
0,0,0
1,1,1
0,0,0
0,1,1
0,0,0
1,1,1
0,0,0
0,1,1
0,0,0
Dieses wird dann von meinem 3D_Drucker Skript umgewandelt in an/ aus (1/ 0) und ausgeführt. Dafür initiiere ich für jeden Motor einen Thread, dem eine spezifische Datei zugeteilt wird, in welcher dann etwas steht wie (motorx): 0010001000.

Code: Alles auswählen

#!/usr/bin/env python

import threading
import RPi.GPIO as gpio
import time
gpio.cleanup()

gpio.setwarnings(False)
gpio.setmode(gpio.BCM)								

#pins + dateinamen
motorpinx = 4
motorpiny = 17
motorpinz = 27
dateix = "3D_x.txt"
dateiy = "3D_y.txt"
dateiz = "3D_z.txt"


class motor (threading.Thread):

	def __init__(self, pin, dateiname):

		#erste anlaufstelle, wenn motor() erschaffen wird
		#uebergabe der ganzen variablen
		threading.Thread.__init__(self)
		self.pin = pin
		self.pin = int(self.pin)
		self.dateiname = dateiname
		
		#initiieren der motorpins
		gpio.setup(self.pin, gpio.OUT)
		gpio.output(self.pin, False)
	
	
	def run(self):
		
		#gehe auf Position Null
		for i in range (5):
			gpio.output(self.pin, True)
			time.sleep(0.3)
			gpio.output(self.pin, False)
			time.sleep(0.3)
		
		
		#fuehre eingaben aus
		#erst datei lesen, dann jedes einzelne zeichen umsetzen in 1 (an) oder 0 (aus)
		dateiname = self.dateiname
		motor_file_in_read = open(dateiname)
		motor_file_in = motor_file_in_read.read()
		
		for zeichen in range(len(motor_file_in)):
			if motor_file_in[zeichen] == "1":
				gpio.output(self.pin, True)
				time.sleep(0.7)
				gpio.output(self.pin, False)
				time.sleep(0.3)
			else:
				time.sleep(1)
		
		motor_file_in_read.close()
		
		#gehe auf Position Null
		for i in range (5):
			gpio.output(self.pin, True)
			time.sleep(0.3)
			gpio.output(self.pin, False)
			time.sleep(0.3)
		
	


def main():
	
	#alle einzelnen linien aus der ursprungsdatei
	lines = []
	
	#code-dateien auf null setzen
	deleting = open("3D_x.txt", "w")
	deleting.write("")
	deleting = open("3D_y.txt", "w")
	deleting.write("")
	deleting = open("3D_z.txt", "w")
	deleting.write("")
	
	#alle eingangs- +ausgangsdateien öffnen
	file_in = open("3D_Druck.txt")
	file_outx = open("3D_x.txt", "w")
	file_outy = open("3D_y.txt", "w")
	file_outz = open("3D_z.txt", "w")
	
	#jede einzelne zeile aus ursprungsdatei einlesen
	for line in file_in.readlines():
		lines.append(line)
		file_in.close()
	
	#ueberpruefen, ob datei wirklich ein 3D_Auftrag beinhaltet + erste beiden zeilen loeschen, sonst Fehler
	#entsprechen: "3D_Druck" + "motorx,motory,motorz,extruder\n"
	if lines[0] == "3D_Druck\n":
		lines.remove("3D_Druck\n")
		lines.remove("motorx,motory,motorz,extruder\n")
	else:
		raise NameError("kein 3D_Auftrag")
	
	#fuer jede zeile wird der absatz entfernt und anschließend die ziffern (0 und 1) in ausgangsdatei geschrieben
	for i in range(len(lines)):
		lines[i] = lines[i].replace("\n", "")
		a = lines[i]
		file_outx.write(a[0])
		file_outy.write(a[2])
		file_outz.write(a[4])
	
	#alle dateien schließen	
	file_in.close()
	file_outx.close()
	file_outy.close()
	file_outz.close()
	
	#einzelne motoren erschaffen
	motorx = motor(motorpinx, dateix)
	motory = motor(motorpiny, dateiy)
	motorz = motor(motorpinz, dateiz)
	
	#motoren starten
	motorx.start()
	motory.start()	
	motorz.start()
		
		
		
if __name__=='__main__':
    main()
Hardware: Raspberry Pi B+
3 LEDs für Motoren x,y,z

Meine Frage ist jetzt, unabhängig von der Hardware: laufen damit wirklich alle drei initiierten Threads (motorx, motory & motorz) ganz genau gleichzeitig? Weil dies ist natürlich nötig, um eine gute Qualität für den Druck zu gewährleisten. Und hat jemand eine Idee, was ich noch verbessern könnte?
Also meine LEDs blinken (sichtbar) gleichzeitig, allerdings heißt das noch lange nicht, das dem auch so ist... ;)

Für die, die auch die Hardware verstehen wollen: später soll dann eine 0 heißen: nichts. eine 1 heißt: nach "vorne" und eine -1 heißt: nach "hinten".

Vielen Dank für Eure Antworten :)
VG
elchico

PS: meine Kommentare sind etwas unschön, die waren eigentlich auch nur für mich bestimmt. Tut mir Leid, falls deswegen beim lesen Schwierigkeiten entstehen könnten.
BlackJack

@elchico: Auf einem Raspi B+ läuft nichts wirklich gleichzeitig weil der ja nur einen Prozessorkern hat. Auf einem Raspi B V2 könnten theorethisch vier Threads gleichzeitig laufen, aber bei CPython praktisch immer nur einer gleichzeitig Python-Bytecode ausführen. Das warten mit `time.sleep()` machen die dann aber echt parallel. ;-)

Ich verstehe aber erst mal so überhaupt nicht was Du Dir davon versprichst das so zu machen‽ Ich sehe da nur unnötige zusätzliche Komplexität und überhaupt keinen Vorteil.
elchico
User
Beiträge: 29
Registriert: Dienstag 10. März 2015, 00:06

BlackJack hat geschrieben:@elchico: Auf einem Raspi B+ läuft nichts wirklich gleichzeitig weil der ja nur einen Prozessorkern hat. Auf einem Raspi B V2 könnten theorethisch vier Threads gleichzeitig laufen, aber bei CPython praktisch immer nur einer gleichzeitig Python-Bytecode ausführen. Das warten mit `time.sleep()` machen die dann aber echt parallel. ;-)

Ich verstehe aber erst mal so überhaupt nicht was Du Dir davon versprichst das so zu machen‽ Ich sehe da nur unnötige zusätzliche Komplexität und überhaupt keinen Vorteil.
Danke erstmal für deine Antwort :)

Hast du ein wenig Ahnung davon? Meinst du, es wäre (mit deinen Kenntnissen) gleichzeitig genug? Bzw.: Wie teste ich denn, wie viel Zeitunterschied die Threads haben?
Und warum kann, trotz vier Kernen beim Pi V2 nur ein Pythoncode gleichzeitig ausgeführt werden?

Warum ich das probiere: weil ich viel über 3D Drucker gelesen habe und mich interessiert hat (Projekt-mäßig und zum Python üben), wie das mit Python umzusetzen wäre. Ich habe zwar verschiedene Anleitungen gelesen mit Arduino-Komponenten, allerdings setze ich das Projekt wahrscheinlich nie in die Praxis um => wie gesagt: Python üben :)
BlackJack

Ich verstehe wie gesagt nicht warum Du das überhaupt auf Threads aufteilst und damit erst das Problem erschaffst das die zeitlich auseinanderlaufen können. Ob die zeitliche Genauigkeit von einem normalen Betriebssystem grundsätzlich für einen 3D-Drucker ausreicht, beziehungsweise wie präzise so ein Druck dann werden kann, weiss ich nicht. Grundsätzlich sind Mehrbenutzer- und Mehrprozesssysteme dafür nicht so ohne weiteres geeignet. Da würde man eher auf einen Mikrokontroller setzen. Vielleicht auch in Verbindung zu einem ”normalen” PC wie dem Raspi. Also das der die Daten anliefert, ”unregelmässig” aber schneller als sie verarbeitet werden müssen, und ein Mikrokontroller dann die Hardware präzise ansteuert.

CPython, also die in C geschriebene Referenzimplementierung von python.org, führt Bytecode immer nur in einem einzigen Thread aus, wenn es also mehrere Threads gibt, dann müssen die warten bis der gerade aktive die globale Sperre („Global Interpreter Lock”, GIL) freigibt und der nächste eine Weile dran kommt. Wenn man das nicht *so* machen würde, müsste man deutlich mehr Sperren auf ganz viele Datenstrukturen setzen, was den Code für den Interpreter umständlicher und damit auch im Single-Thread-Fall unnötig langsamer macht. Es wäre dann auch komplizierter Erweiterungsmodule in C zu schreiben, weil man dort dann auch immer alles mögliche sperren und wieder freigeben müsste. Es wäre dann auch einfach den Interpreter zum Stehen zu bringen wenn man dabei nicht *sehr* sorgfältig arbeitet und irgendeinen Fall vergisst bei dem eine Sperre freigegeben werden müsste, oder umgekehrt wenn man vergisst etwas zu sperren, kann man schnell hübsche, nichtdeterministische Abstürze provozieren.
elchico
User
Beiträge: 29
Registriert: Dienstag 10. März 2015, 00:06

ok, aber eine Frage noch: ich dachte, es ist überhaupt erst möglich, mehrere "Sachen" gleichzeitig zu machen, indem ich auf threading zurück greife? Wie würdest du die "Gleichzeitigkeit" umsetzen?

VG
BlackJack

@elchico: Kommt halt auf die Definition von ”gleichzeitig” an. Ich sehe da aber um es *noch mal* zu sagen gar nicht wo man da Threads braucht. Man kann das auch in einem lösen. Dann hat man immer noch die im System begründete Ungenauigkeit, aber wenigstens laufen die drei Motoren dann nicht ”auseinander”.
elchico
User
Beiträge: 29
Registriert: Dienstag 10. März 2015, 00:06

Mein Hintergedanke war der, dass wenn ich alle Threads "gleichzeitig" starte, alle Threads "gleichzeitig" ihre entsprechenden Daten einlesen und so "gleichzeitig" auch ausführen, weil keiner vom anderen ausgebremst wird (mal System-unabhängig).

Wenn ich alle Motoren zusammen fassen würde, könnte ja nicht nur vom System immer nur ein Motor gleichzeitig angefahren werden, sondern zusätzlich wird auch Code-seitig noch ein Riegel vor die "Gleichzeitigkeit" gelegt oder? Schließlich kann ein Thread auch immer nur ein Motor anfahren => zwei Bremsen für die Gleichzeitigkeit, während in meinem Code nur noch das System eine Bremse darstellt.

Darum für jeden Motor einen unabhängigen Thread.

Ich habe mir jetzt überlegt, ich baue mir einfach mal eine x,y-Platte zusammen, die von zwei verschiedenen Schrittmotoren angesteuert wird (hab alles nötige daheim) und bau mir ein Blatt Papier drauf und ein Stift dran, der einen Punkt auf mein Blatt Papier malt. Und dann lass ich einfach mal verschiedene Szenarien durchlaufen (etwas eckiges geht ja ohne Probleme, da dann immer nur ein Motor läuft (entweder x- oder y-achsig)), deswegen probiere ich dann mal was "rundes" (Kreis etc.). Dann sehen wir zumindest mal vorläufig, wie "gleichzeitig" das läuft :)

Ist, denke ich, die einfachste Möglichkeit, das jetzt auf die Schnelle auszuprobieren, oder? :)
BlackJack

@elchico: Gleichzeitig die Daten einlesen klappt ja schon mal nicht weil die Threads dann ja um den Zugriff auf die SD-Karte konkurrieren. Wirklich *gleichzeitig* können da ja nicht einmal Threads auf einem Mehrkernprozessor darauf zugreifen die sich nicht durch etwas wie das GIL blockieren. Die können nach dem Einlesen der Daten also schon einen deutlichen und im Grunde zufälligen, zeitlichen Versatz haben.

Und natürlich kannst Du auch mit einem Thread mehr als einen Motor ansteuern. In CPython deutlich synchroner als mit einem Thread pro Motor. Das passiert dann nicht gleichzeit wenn man die drei Motoren in einer Schleife direkt hintereinander anspricht, aber schneller/genauer wird das in CPython sowieso nicht gehen. Selbst wenn es das GIL nicht gäbe hat man immer noch das Problem dass das Betriebssystem entscheidet welcher Prozess/Thread auf welchem Kern und wann läuft. Auf so einem Linux läuft ein ganzer Haufen Prozesse gleichzeitig die alle den selben Prozessor nutzen müssen. Und ich würde dann auch erst einmal das GPIO-Modul bis zur Hardware verfolgen, denn wenn *das* am Ende den Zugriff wieder serialisiert um thread-sicher zu sein, kann man das auch mit einem Thread deutlich weniger komplex selber machen.
Sirius3
User
Beiträge: 17797
Registriert: Sonntag 21. Oktober 2012, 17:20

@elchico: wirkliche parallele Abarbeitung von Code wirst Du, wenn Du nicht alles auf Hardwareebene schreibst, sowieso nie hinbekommen. Das Einlesen der Datei am Anfang Deiner Threads ruft Systemroutinen auf, wo Du sicher sein kannst, dass egal in welcher Sprache Du schreibst und egal wie viele Kerne Dein Prozessor hat, Du nicht mehr synchron bist.
Das einfachst und stabilste und parallelste was Du hinbekommst, ist wirklich, alle drei Motoren in einer Schleife anzusprechen. Das setzen eines Pins von low nach high dürfte wohl in deutlich unter einer Millisekunde geschehen. Um dann auch noch zwischen den Impulsen exakte Sekundenabstände hinzubekommen, solltest Du nicht auf sleep vertrauen. Da brauchst Du eine Referenz zu einer real-time-clock.

Zum Code an sich hat BlackJack noch gar nichts geschriebeb :wink:.
Einrücktiefe sind immer 4 Leerzeichen, das macht den Code besser lesbar. Dateien sollte man auch wieder schließen, was bei den Zeilen 79ff nicht passiert. Die sind übrigens sowieso überflüssig, weil 8 Zeilen später sowieso alles gelöscht wird. Warum kopierst Du überhaupt den Inhalt von einer Datei in 3 und hältst nicht alles von Anfang an im Speicher?
Zeile 20: Klassen schreibt man groß, damit man sie als Klassen erkennt
Zeile 23: Leerzeilen nach der Funktionsdefinition machen den Code unleserlich
Zeile 52: Die for-range-len-Schleife ist ein antipattern, da man direkt über den String iterieren kann. "zeichen" ist kein Zeichen, sondern ein Index.
Zeile 93: Über Fileobjekte kann man direkt Iterieren, readlines ist also überflüssig
Zeile 95: das close ist falsch eingerückt.
Zeile 99ff: bei anderen Zeile-Ende-Zeichen funktioniert das ganze nicht mehr.
Zeile 103: NameError ist die falsche Exception.
Zeile 106: Anitpattern, s.o.
Zeile 107ff: die Datei muß aufs Byte so aussehen, wie Du es erwartest, ich würde das als Fehler ansehen.
elchico
User
Beiträge: 29
Registriert: Dienstag 10. März 2015, 00:06

@BlackJack: okay, habe ich verstanden. Ich werde das ganze mal in eine 2D-Platte umsetzen mit 2 Motoren und mir die Genauigkeit/ Gleichzeitigkeit (was ja irgendwie zusammen hängt) anschauen :)

@Siruis: hey cool danke :) werde deine Verbesserungen übernehmen :)
Wegen: Warum nicht alles im Speicher: Du meinst, warum ich nicht das ganze z.B. als Liste speicher: List(00101011010) <= sowas? Hatte ich mir überlegt, aber ich war mir nicht sicher, wie groß so eine Liste werden darf, ohne Probleme zu verursachen und das mit der Extra Datei fand ich irgendwie besser :D Aber habe mir da ehrlich gesagt nicht so viele Gedanken gemacht: Liste => nein, weil keine Ahnung ob das geht bei größeren "3D Objekten" mit vielen "Motorbefehlen" und dann kam sofort: Extra-Datei.
BlackJack

@elchico: Die Argumentation zu der Liste im Speicher verstehe ich nicht so ganz denn Du liest ja in der `main()`-Funktion die komplette Datei in den Speicher. Also passt sie ganz offensichtlich komplett in den Speicher. Statt jetzt drei einzelne Dateien zu schreiben, deren Inhalt dann in den drei Threads auch wieder komplett in den Speicher gelesen werden, hätte man den Umweg über die Dateien weglassen können und deren Inhalt direkt den Threads mitgeben können.

Eine Liste kann so gross werden wie Speicher dafür vorhanden ist. Nur bei Rechnern mit *sehr* viel Speicher könnte man Probleme mit der Anzahl der Elemente bekommen, das halte ich aber eher für theoretisch.

Ich glaube den Style Guide for Python Code hat hier im Thema noch niemand explizit erwähnt.

Den `GPIO.cleanup()`-Aufruf würde ich ans Ende des Programms setzen. In einen ``finally``-Zweig, damit der auch ausgeführt wird wenn das Programm wegen einer Ausnahme abbricht. Das und auch das `setmode()` würde ich mit in die Hauptfunktion verschieben. Ein Modul sollte importierbar sein ohne das irgendwelche Seiteneffekte entstehen, insbesondere ohne das Hardware angesprochen wird.

Das zweimalige öffnen der Ausgabedateien ist unnötig, genau wie das schreiben einer leeren Zeichenkette, was gar keinen Effekt hat. Das alleinige öffnen im Schreibmodus ('w') sorgt schon dafür das die Datei leer ist.

Das Einlesen in der `main()` ist unnötig umständlich. Wie Sirius3 schon sagte kann man sich den `readlines()`-Aufruf sparen weil man direkt über das Dateiobjekt iterieren kann. Oder man spart sich die Schleife, denn `readlines()` liefert eine Liste mit einer Zeile pro Listenelement. Deine Schleife kopiert die Elemente dieser Liste nur in eine weitere Liste. Alternativ zu `readlines()` hätte man auch einfach `list()` mit dem Dateiobjekt als Argument aufrufen können.

Da die Datei vom Format her scheinbar eine CSV-Datei ist, kann man das `csv`-Modul aus der Standardbibliothek zum einlesen verwenden.

Wenn man von `Thread` erbt und nur die `__init__()`- und die `run()`-Methode implementiert, dann greift die Faustregel das man in der Regel eine unnötige Klasse statt einer einfachen Funktion vor sich hat wenn man nur die `__init__()` und eine weitere Methode hat. Man hätte hier auch eine Funktion schreiben können und die mit einem `Thread`-Objekt mit den Argumenten `target` und `args` asynchron ausführen können.

Sowohl Code- wie auch Datenwiederholungen sollte man als Programmierer vermeiden. Immer wenn man per kopieren, einfügen, und leicht verändern arbeitet sollte man sich überlegen ob man den Code nicht in eine Funktion heraus ziehen kann, oder die sich unterscheidenden Daten in eine Struktur über die man dann in einer Schleife iteriert um den Code nur einmal schreiben zu müssen. Ganz offensichtlich betrifft das das zurückfahren der Motoren auf die Ausgangsposition, denn der Quelltext ist 1:1 kopiert. Man kann aber auch noch das senden eines Pulses an einen/mehrere Pins als Funktion extrahieren.

Ich komme dann ungefähr bei so etwas heraus (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
import csv
from itertools import izip
from time import sleep
from RPi import GPIO

#: Pin numbers for driving the motors for X, Y, and Z plane.
MOTOR_PINS = [4, 17, 27]


def load_movement_data(filename):
    with open(filename) as lines:
        reader = csv.reader(lines)
        row = next(reader)
        if row != ['3D_Druck']:
            raise ValueError('Kein 3D-Auftrag')
        next(reader)  # Skip header line.
        # 
        # Take the first three elements of each row because the header
        # line suggests there is a fourth column which we don't need.
        # 
        return [row[:3] for row in reader]


def initialize_motors(pins):
    for pin in pins:
        GPIO.setup(pin, GPIO.OUT)
        GPIO.output(pin, False)


def send_pulse(pins, on_time, off_time):
    for pin in pins:
        GPIO.output(pin, True)
    sleep(on_time)
    for pin in pins:
        GPIO.output(pin, False)
    sleep(off_time)


def move_motors_home(pins):
    for _ in xrange(5):
        send_pulse(pins, 0.3, 0.3)


def main():
    try:
        GPIO.setmode(GPIO.BCM)
        motor_data = load_movement_data('3D_Druck.txt')
        initialize_motors(MOTOR_PINS)
        move_motors_home(MOTOR_PINS)
        for row in motor_data:
            send_pulse(
                [p for m, p in izip(row, MOTOR_PINS) if m == '1'], 0.7, 0.3
            )
        move_motors_home(MOTOR_PINS)
    finally:
        GPIO.cleanup()


if __name__ == '__main__':
    main()
Zuletzt geändert von BlackJack am Samstag 4. April 2015, 12:30, insgesamt 1-mal geändert.
Grund: Korrektur Dank Sirius3
Sirius3
User
Beiträge: 17797
Registriert: Sonntag 21. Oktober 2012, 17:20

@BlackJack: kleine Korrektur: send_pulse erwartet als erstes Argument eine Liste.
Antworten