Zum Inhalt

Code Überblick

Dieses Dokument beschreibt das allgemeine Codelayout und den Hauptcodefluss von Klipper.

Verzeichnis-Layout

Das Verzeichnis src/ enthält den C-Quellcode für den Mikrocontroller-Code. Die Verzeichnisse src/atsam/, src/atsamd/, src/avr/, src/linux/, src/lpc176x/, src/pru/ und src/stm32/ enthalten architekturspezifischen Mikrocontroller-Code. Das Verzeichnis src/simulator/ enthält Code-Stubs, die es ermöglichen, den Mikrocontroller auf anderen Architekturen testweise zu kompilieren. Das Verzeichnis src/generic/ enthält Hilfscode, der für verschiedene Architekturen nützlich sein kann. Der Build sorgt dafür, dass "board/somefile.h"-Includes zuerst im aktuellen Architektur-Verzeichnis (z.B. src/avr/somefile.h) und dann im generischen Verzeichnis (z.B. src/generic/somefile.h) gesucht werden.

Das Verzeichnis klippy/ enthält die Host-Software. Der größte Teil der Host-Software ist in Python geschrieben, das Verzeichnis klippy/chelper/ enthält jedoch einige C-Hilfsfunktionen. Das Verzeichnis klippy/kinematics/ enthält den Code für die Roboter-Kinematik. Das Verzeichnis klippy/extras/ enthält die erweiterbaren "Module" des Host-Codes.

Das Verzeichnis lib/ enthält externen Bibliothekscode von Drittanbietern, der zum Erstellen einiger Ziele benötigt wird.

Das config/ Verzeichnis enthält Beispiel Druckerkonfigurationsdateien.

Das Verzeichnis scripts/ enthält Skripte für die Erstellungszeit, die beim Kompilieren des Mikrocontroller-Codes nützlich sind.

Das Verzeichnis test/ enthält automatisierte Testverfahren.

Während der Kompilierung kann der Build ein Verzeichnis out/ anlegen. Dieses enthält temporäre Objekte der Erstellungszeit. Das endgültige erzeugte Mikrocontroller-Objekt ist out/klipper.elf.hex auf AVR und out/klipper.bin auf ARM.

Ablauf des Mikrocontroller-Codes

Die Ausführung des Mikrocontroller-Codes beginnt in architekturspezifischem Code (z. B. src/avr/main.c), der letztlich sched_main() in src/sched.c aufruft. Der Code von sched_main() führt zunächst alle Funktionen aus, die mit dem Makro DECL_INIT() markiert wurden. Anschließend führt er wiederholt alle Funktionen aus, die mit dem Makro DECL_TASK() markiert sind.

Eine der wichtigsten Task-Funktionen ist command_dispatch() in src/command.c. Diese Funktion wird vom platinenspezifischen Ein-/Ausgabecode aufgerufen (z. B. src/avr/serial.c, src/generic/serial_irq.c) und führt die Befehlsfunktionen aus, die den im Eingabestrom gefundenen Befehlen zugeordnet sind. Befehlsfunktionen werden mit dem Makro DECL_COMMAND() deklariert (weitere Informationen finden Sie im Dokument Protokoll).

Task-, Init- und Befehlsfunktionen laufen stets mit aktivierten Interrupts (sie können diese jedoch bei Bedarf vorübergehend deaktivieren). Diese Funktionen sollten lange Pausen, Verzögerungen oder Arbeiten vermeiden, die erheblich Zeit beanspruchen. (Lange Verzögerungen in diesen "Task"-Funktionen führen zu Scheduling-Jitter für andere "Tasks" - Verzögerungen über 100 µs können spürbar werden, Verzögerungen über 500 µs können zu erneuten Befehlsübertragungen führen, Verzögerungen über 100 ms können zu Watchdog-Neustarts führen.) Diese Funktionen planen Arbeit zu bestimmten Zeitpunkten, indem sie Timer einplanen.

Timer-Funktionen werden durch den Aufruf von sched_add_timer() (in src/sched.c) eingeplant. Der Code der Ablaufplanung sorgt dafür, dass die angegebene Funktion zur gewünschten Taktzeit aufgerufen wird. Timer-Interrupts werden zunächst in einem architekturspezifischen Interrupt-Handler behandelt (z. B. src/avr/timer.c), der sched_timer_dispatch() in src/sched.c aufruft. Der Timer-Interrupt führt zur Ausführung der eingeplanten Timer-Funktionen. Timer-Funktionen laufen stets mit deaktivierten Interrupts. Die Timer-Funktionen sollten immer innerhalb weniger Mikrosekunden abgeschlossen sein. Nach Abschluss des Timer-Ereignisses kann die Funktion sich selbst erneut einplanen.

Wird ein Fehler erkannt, kann der Code shutdown() aufrufen (ein Makro, das sched_shutdown() in src/sched.c aufruft). Der Aufruf von shutdown() bewirkt, dass alle mit dem Makro DECL_SHUTDOWN() markierten Funktionen ausgeführt werden. Shutdown-Funktionen laufen stets mit deaktivierten Interrupts.

Ein großer Teil der Funktionalität des Mikrocontrollers besteht in der Arbeit mit General-Purpose-Input/Output-Pins (GPIO). Um den hardwarenahen, architekturspezifischen Code vom übergeordneten Task-Code zu abstrahieren, werden alle GPIO-Ereignisse in architekturspezifischen Wrappern implementiert (z. B. src/avr/gpio.c). Der Code wird mit der gcc-Optimierung "-flto -fwhole-program" kompiliert, die Funktionen über Übersetzungseinheiten hinweg hervorragend inlinet, sodass die meisten dieser winzigen GPIO-Funktionen in ihre Aufrufer eingebettet werden und ihre Verwendung zur Laufzeit keine Kosten verursacht.

Klippy Code Übersicht

Der Host-Code (Klippy) ist dafür vorgesehen, auf einem kostengünstigen Computer (etwa einem Raspberry Pi) zusammen mit dem Mikrocontroller zu laufen. Der Code ist überwiegend in Python geschrieben, verwendet jedoch CFFI, um einen Teil der Funktionalität in C-Code umzusetzen.

Die anfängliche Ausführung beginnt in klippy/klippy.py. Dies liest die Kommandozeilenargumente ein, öffnet die Drucker-Konfigurationsdatei, instanziiert die Haupt-printer-objects und startet die serielle Verbindung. Die Hauptausführung der G-Code-Befehle erfolgt in der Methode _process_commands() in klippy/gcode.py. Dieser Code übersetzt die G-Code-Befehle in Aufrufe der printer objects, die die Aktionen häufig in Befehle übersetzen, welche auf dem Mikrocontroller ausgeführt werden (wie über das Makro DECL_COMMAND im Mikrocontroller-Code deklariert).

Im Klipper-Host-Code gibt es mehrere Threads:

  • Es gibt einen Python-"Hauptthread", der eingehende G-Code-Befehle verarbeitet und der Ausgangspunkt für die meisten Aktionen ist. Dieser Thread führt den Reactor (klippy/reactor.py) aus, und die meisten übergeordneten Aktionen stammen aus IO- und Timer-Event-Callbacks dieses Reactors.
  • Ein Thread zum Schreiben von Nachrichten ins Log, damit die anderen Threads nicht durch Log-Schreibvorgänge blockiert werden. Dieser Thread befindet sich im Code klippy/queuelogger.py, und seine Multithreading-Natur ist dem Haupt-Python-Thread nicht sichtbar.
  • Ein Thread pro Mikrocontroller, der das Low-Level-Lesen und -Schreiben von Nachrichten an diesen Mikrocontroller übernimmt. Er befindet sich im C-Code klippy/chelper/serialqueue.c, und seine Multithreading-Natur ist dem Python-Code nicht sichtbar.
  • Ein Thread pro Mikrocontroller zur Verarbeitung der von diesem Mikrocontroller empfangenen Nachrichten im Python-Code. Dieser Thread wird in klippy/serialhdl.py erzeugt. Bei aus diesem Thread aufgerufenen Python-Callbacks ist Vorsicht geboten, da dieser Thread direkt mit dem Haupt-Python-Thread interagieren kann.
  • Ein Thread pro Schrittmotor, der das Timing der Schrittmotor-Schrittimpulse berechnet und diese Zeiten komprimiert. Dieser Thread befindet sich im C-Code klippy/chelper/steppersync.c, und seine Multithreading-Natur ist dem Python-Code nicht sichtbar.

Ablauf des Codes bei einem Bewegungsbefehl

Eine typische Druckerbewegung beginnt, wenn ein "G1"-Befehl an den Klippy-Host gesendet wird, und ist abgeschlossen, wenn die entsprechenden Schrittimpulse auf dem Mikrocontroller erzeugt werden. Dieser Abschnitt beschreibt den Ablauf des Codes bei einem typischen Bewegungsbefehl. Das Dokument Kinematik enthält weitere Informationen zur Mechanik von Bewegungen.

  • Die Verarbeitung eines Bewegungsbefehls beginnt in gcode.py. Ziel von gcode.py ist es, G-Code in interne Aufrufe zu übersetzen. Ein G1-Befehl ruft cmd_G1() in klippy/extras/gcode_move.py auf. Der Code in gcode_move.py behandelt Änderungen des Ursprungs (z. B. G92), Wechsel zwischen relativen und absoluten Positionen (z. B. G90) sowie Einheitenwechsel (z. B. F6000=100mm/s). Der Codepfad für eine Bewegung lautet: _process_data() -> _process_commands() -> cmd_G1(). Letztlich wird die Klasse ToolHead aufgerufen, um die eigentliche Anforderung auszuführen: cmd_G1() -> ToolHead.move()
  • Die Klasse ToolHead (in toolhead.py) verwaltet das "Look-ahead" und verfolgt das Timing der Druckvorgänge. Der Hauptcodepfad für eine Bewegung lautet: ToolHead.move() -> LookAheadQueue.add_move(), dann ToolHead.move() -> ToolHead._process_lookahead() -> LookAheadQueue.flush() -> Move.set_junction(), und anschließend ToolHead._process_lookahead() -> trapq_append().

    • ToolHead.move() erzeugt ein Move()-Objekt mit den Parametern der Bewegung (im kartesischen Raum und in Einheiten von Sekunden und Millimetern).
    • Die Kinematik-Klasse erhält die Gelegenheit, jede Bewegung zu prüfen (ToolHead.move() -> kin.check_move()). Die Kinematik-Klassen befinden sich im Verzeichnis klippy/kinematics/. Der Code von check_move() kann einen Fehler auslösen, wenn die Bewegung ungültig ist. Wird check_move() erfolgreich abgeschlossen, muss die zugrunde liegende Kinematik die Bewegung ausführen können.
    • LookAheadQueue.add_move() legt das Bewegungsobjekt in der "Look-ahead"-Warteschlange ab.
    • LookAheadQueue.flush() bestimmt die Start- und Endgeschwindigkeiten jeder Bewegung.
    • Move.set_junction() implementiert den "Trapezgenerator" für eine Bewegung. Der "Trapezgenerator" zerlegt jede Bewegung in drei Teile: eine Phase konstanter Beschleunigung, gefolgt von einer Phase konstanter Geschwindigkeit, gefolgt von einer Phase konstanter Verzögerung. Jede Bewegung enthält diese drei Phasen in dieser Reihenfolge, einzelne Phasen können jedoch die Dauer null haben.
    • Wenn ToolHead._process_lookahead() fortgesetzt wird, sind alle Informationen zur Bewegung bekannt - ihre Startposition, ihre Endposition, ihre Beschleunigung, ihre Start-/Fahr-/Endgeschwindigkeit sowie die während Beschleunigung/Fahrt/Verzögerung zurückgelegte Strecke. Alle Informationen werden in der Klasse Move() gespeichert und liegen im kartesischen Raum in den Einheiten Millimeter und Sekunden vor.
    • Die Bewegungen werden anschließend über trapq_append() (in klippy/chelper/trapq.c) in eine "Trapez-Bewegungswarteschlange" eingereiht. Die trapq speichert alle Informationen der Klasse Move() in einer für den Host-C-Code zugänglichen C-Struktur.
    • Beachten Sie, dass der Extruder in einer eigenen Kinematik-Klasse behandelt wird: ToolHead._process_lookahead() -> PrinterExtruder.process_move(). Da die Klasse Move() die genaue Bewegungszeit festlegt und Schrittimpulse mit einem bestimmten Timing an den Mikrocontroller gesendet werden, sind die von der Extruder-Klasse erzeugten Schrittmotorbewegungen synchron mit der Kopfbewegung, obwohl der Code getrennt gehalten wird.
    • Aus Effizienzgründen wird die Schrittmotorbewegung im C-Code in einem Thread pro Schrittmotor erzeugt. Die Threads werden vom motion_queuing-Modul (klippy/extras/motion_queuing.py) benachrichtigt, wenn Schritte erzeugt werden sollen: PrinterMotionQueuing._flush_handler() -> PrinterMotionQueuing._advance_move_time() -> steppersyncmgr_gen_steps() -> se_start_gen_steps().
    • Klipper verwendet einen iterativen Löser, um die Schrittzeiten für jeden Schrittmotor zu erzeugen. Die Schrittzeiten werden im Hintergrundthread erzeugt (klippy/chelper/steppersync.c): se_background_thread() -> se_generate_steps() -> itersolve_generate_steps() -> itersolve_gen_steps_range() (in klippy/chelper/itersolve.c). Ziel des iterativen Lösers ist es, anhand einer Funktion, die aus einer Zeit eine Schrittmotorposition berechnet, die Schrittzeiten zu ermitteln. Dies geschieht, indem wiederholt verschiedene Zeiten "geraten" werden, bis die Positionsformel des Schrittmotors die gewünschte Position des nächsten Schritts liefert. Die Rückmeldung jedes Versuchs wird genutzt, um künftige Versuche zu verbessern, sodass der Prozess rasch zur gewünschten Zeit konvergiert. Die kinematischen Formeln zur Schrittmotorpositionsberechnung befinden sich im Verzeichnis klippy/chelper/ (z. B. kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c).
    • Nachdem der iterative Löser die Schrittzeiten berechnet hat, werden sie einem Array hinzugefügt: itersolve_gen_steps_range() -> stepcompress_append() (in klippy/chelper/stepcompress.c). Das Array (struct stepcompress.queue) speichert für jeden Schritt die zugehörigen Zählerstände des Mikrocontroller-Takts. Der Wert des "Mikrocontroller-Taktzählers" entspricht dabei unmittelbar dem Hardwarezähler des Mikrocontrollers – er ist relativ zum letzten Einschaltzeitpunkt des Mikrocontrollers.
    • Der nächste wichtige Schritt ist das Komprimieren der Schritte: stepcompress_flush() -> compress_bisect_add() (in klippy/chelper/stepcompress.c). Dieser Code erzeugt und kodiert eine Reihe von "queue_step"-Befehlen des Mikrocontrollers, die der in der vorherigen Phase erstellten Liste der Schrittmotor-Schrittzeiten entsprechen. Diese "queue_step"-Befehle werden anschließend in eine Warteschlange eingereiht, priorisiert und an den Mikrocontroller gesendet (über steppersync.c:steppersync und serialqueue.c:serialqueue).
    • Die Verarbeitung der queue_step-Befehle auf dem Mikrocontroller beginnt in src/command.c, wo der Befehl geparst und command_queue_step() aufgerufen wird. Der Code von command_queue_step() (in src/stepper.c) hängt lediglich die Parameter jedes queue_step-Befehls an eine Warteschlange pro Schrittmotor an. Im Normalbetrieb wird der queue_step-Befehl mindestens 100 ms vor dem Zeitpunkt seines ersten Schritts geparst und eingereiht. Die Erzeugung der Schrittmotor-Ereignisse erfolgt schließlich in stepper_event(). Diese Funktion wird vom Hardware-Timer-Interrupt zum geplanten Zeitpunkt des ersten Schritts aufgerufen. Der Code von stepper_event() erzeugt einen Schrittimpuls und plant sich dann selbst erneut für den Zeitpunkt des nächsten Schrittimpulses gemäß den gegebenen queue_step-Parametern ein. Die Parameter jedes queue_step-Befehls sind "interval", "count" und "add". Auf hoher Ebene führt stepper_event() 'count'-mal Folgendes aus: do_step(); next_wake_time = last_wake_time + interval; interval += add;

Das Obige mag nach einem hohen Aufwand für die Ausführung einer Bewegung aussehen. Wirklich interessant sind jedoch nur die Teile in den Klassen ToolHead und Kinematik. Dieser Teil des Codes legt die Bewegungen und ihre Zeitpunkte fest. Die übrige Verarbeitung besteht größtenteils nur aus Kommunikation und Verbindungslogik.

Hinzufügen eines Hostmoduls

Der Klippy-Host-Code verfügt über eine Funktion zum dynamischen Laden von Modulen. Wird in der Druckerkonfigurationsdatei ein Konfigurationsabschnitt namens "[my_module]" gefunden, versucht die Software automatisch, das Python-Modul klippy/extras/my_module.py zu laden. Dieses Modulsystem ist die bevorzugte Methode, um Klipper um neue Funktionalität zu erweitern.

Am einfachsten fügt man ein neues Modul hinzu, indem man ein vorhandenes Modul als Vorlage verwendet – siehe klippy/extras/servo.py als Beispiel.

Die folgenden Informationen könnten ebenfalls nützlich sein:

  • Die Ausführung des Moduls beginnt in der modulweiten Funktion load_config() (für Konfigurationsabschnitte der Form [my_module]) oder in load_config_prefix() (für Konfigurationsabschnitte der Form [my_module my_name]). Dieser Funktion wird ein "config"-Objekt übergeben, und sie muss ein neues "printer object" zurückgeben, das mit dem jeweiligen Konfigurationsabschnitt verknüpft ist.
  • Während der Instanziierung eines neuen printer object kann das config-Objekt verwendet werden, um Parameter aus dem jeweiligen Konfigurationsabschnitt zu lesen. Dies geschieht mit den Methoden config.get(), config.getfloat(), config.getint() usw. Achten Sie darauf, während der Konstruktion des printer object alle Werte aus der Konfiguration zu lesen - gibt der Anwender einen Konfigurationsparameter an, der in dieser Phase nicht gelesen wird, wird angenommen, dass es sich um einen Tippfehler in der Konfiguration handelt, und es wird ein Fehler ausgelöst.
  • Verwenden Sie die Methode config.get_printer(), um eine Referenz auf die Hauptklasse "printer" zu erhalten. Diese "printer"-Klasse speichert Referenzen auf alle bereits instanziierten "printer objects". Verwenden Sie die Methode printer.lookup_object(), um Referenzen auf andere printer objects zu finden. Nahezu die gesamte Funktionalität (selbst zentrale Kinematik-Module) ist in einem dieser printer objects gekapselt. Beachten Sie jedoch, dass bei der Instanziierung eines neuen Moduls noch nicht alle anderen printer objects instanziiert sein müssen. Die Module "gcode" und "pins" sind stets verfügbar, bei anderen Modulen empfiehlt es sich jedoch, das Lookup zu verzögern.
  • Registrieren Sie mit der Methode printer.register_event_handler() Event-Handler, wenn Ihr Code bei "Events" aufgerufen werden muss, die von anderen printer objects ausgelöst werden. Jeder Eventname ist ein String und ist üblicherweise der Name des Hauptmoduls, das das Event auslöst, zusammen mit einem kurzen Namen für die stattfindende Aktion (z. B. "klippy:connect"). Die an jeden Event-Handler übergebenen Parameter sind spezifisch für das jeweilige Event (ebenso wie Exception-Handling und Ausführungskontext). Zwei gängige Start-Events sind:
    • klippy:connect - Dieses Event wird ausgelöst, nachdem alle printer objects instanziiert wurden. Es wird üblicherweise verwendet, um andere printer objects zu finden, Konfigurationseinstellungen zu prüfen und einen ersten "Handshake" mit der Drucker-Hardware durchzuführen.
    • klippy:ready - Dieses Event wird ausgelöst, nachdem alle Connect-Handler erfolgreich abgeschlossen wurden. Es zeigt an, dass der Drucker in einen Zustand übergeht, in dem er für den normalen Betrieb bereit ist. Lösen Sie in diesem Callback keinen Fehler aus.
  • Liegt ein Fehler in der Konfiguration des Anwenders vor, achten Sie darauf, ihn während der Phase load_config() oder "connect event" auszulösen. Verwenden Sie entweder raise config.error("my error") oder raise printer.config_error("my error"), um den Fehler zu melden.
  • Speichern Sie keine Referenz auf das config-Objekt in einer Klassen-Member-Variable (noch an einer ähnlichen Stelle, die über das anfängliche Laden des Moduls hinaus bestehen bleibt). Das config-Objekt ist eine Referenz auf eine Klasse der "Konfigurationsladephase", und es ist nicht zulässig, ihre Methoden nach Abschluss dieser "Konfigurationsladephase" aufzurufen.
  • Verwenden Sie das Modul "pins", um einen Pin an einem Mikrocontroller zu konfigurieren. Dies geschieht typischerweise mit etwas Ähnlichem wie printer.lookup_object("pins").setup_pin("pwm", config.get("my_pin")). Das zurückgegebene Objekt kann anschließend zur Laufzeit angesteuert werden.
  • Definiert das printer object eine Methode get_status(), kann das Modul Statusinformationen über Makros sowie über den API-Server exportieren. Die Methode get_status() muss ein Python-Dictionary zurückgeben, dessen Schlüssel Strings sind und dessen Werte Ganzzahlen, Fließkommazahlen, Strings, Listen, Dictionaries, True, False oder None sind. Tupel (und benannte Tupel) können ebenfalls verwendet werden (diese erscheinen beim Zugriff über den API-Server als Listen). Exportierte Listen und Dictionaries müssen als "unveränderlich" behandelt werden - ändert sich ihr Inhalt, muss get_status() ein neues Objekt zurückgeben, da der API-Server diese Änderungen sonst nicht erkennt.
  • Benötigt das Modul Zugriff auf System-Timing oder externe Datei-Deskriptoren, verwenden Sie printer.get_reactor(), um Zugriff auf die globale "Event-Reactor"-Klasse zu erhalten. Diese Reactor-Klasse ermöglicht es, Timer zu planen, auf Eingaben von Datei-Deskriptoren zu warten und den Host-Code "schlafen zu legen".
  • Verwenden Sie keine globalen Variablen. Der gesamte Zustand sollte im printer object gespeichert werden, das von der Funktion load_config() zurückgegeben wird. Dies ist wichtig, da sonst der Befehl RESTART möglicherweise nicht wie erwartet funktioniert. Aus ähnlichen Gründen sollten Sie außerdem, falls externe Dateien (oder Sockets) geöffnet werden, unbedingt einen "klippy:disconnect"-Event-Handler registrieren und diese in dessen Callback schließen.
  • Vermeiden Sie den Zugriff auf interne Member-Variablen (oder das Aufrufen von Methoden, die mit einem Unterstrich beginnen) anderer printer objects. Die Einhaltung dieser Konvention erleichtert die Verwaltung zukünftiger Änderungen.
  • Es wird empfohlen, im Python-Konstruktor von Python-Klassen allen Member-Variablen einen Wert zuzuweisen. (Und dadurch die Möglichkeit von Python zu vermeiden, dynamisch neue Member-Variablen zu erzeugen.)
  • Soll eine Python-Variable einen Fließkommawert speichern, wird empfohlen, diese Variable stets mit Fließkomma-Konstanten zuzuweisen und zu bearbeiten (und niemals mit Ganzzahl-Konstanten). Bevorzugen Sie beispielsweise self.speed = 1. gegenüber self.speed = 1 und self.speed = 2. * x gegenüber self.speed = 2 * x. Die konsistente Verwendung von Fließkommawerten kann schwer zu debuggende Eigenheiten bei Python-Typumwandlungen vermeiden.
  • Wenn Sie das Modul zur Aufnahme in den Haupt-Klipper-Code einreichen, achten Sie darauf, oben im Modul einen Copyright-Hinweis anzugeben. Das bevorzugte Format finden Sie in den bestehenden Modulen.

Hinzufügen neuer Kinematiken

Dieser Abschnitt gibt einige Tipps zum Hinzufügen der Unterstützung weiterer Kinematik-Arten zu Klipper. Diese Art von Arbeit erfordert ein hervorragendes Verständnis der mathematischen Formeln der jeweiligen Kinematik. Zudem sind Softwareentwicklungskenntnisse erforderlich - allerdings muss in der Regel nur die Host-Software angepasst werden.

Nützliche Schritte:

  1. Beginnen Sie mit dem Studium des Abschnitts "Code-Ablauf eines Bewegungsbefehls" und des Kinematik-Dokuments.
  2. Sehen Sie sich die bestehenden Kinematik-Klassen im Verzeichnis klippy/kinematics/ an. Die Kinematik-Klassen haben die Aufgabe, eine Bewegung in kartesischen Koordinaten in die Bewegung jedes einzelnen Schrittmotors umzuwandeln. Eine dieser Dateien lässt sich in der Regel als Ausgangspunkt kopieren.
  3. Implementieren Sie für jeden Schrittmotor die C-Funktionen zur Kinematik-Positionsberechnung, falls diese nicht bereits verfügbar sind (siehe kin_cart.c, kin_corexy.c und kin_delta.c in klippy/chelper/). Die Funktion sollte move_get_coord() aufrufen, um eine gegebene Bewegungszeit (in Sekunden) in eine kartesische Koordinate (in Millimetern) umzuwandeln, und daraus anschließend die gewünschte Schrittmotorposition (in Millimetern) berechnen.
  4. Implementieren Sie die Methode calc_position() in der neuen Kinematik-Klasse. Diese Methode berechnet die Position des Werkzeugkopfs in kartesischen Koordinaten aus der Position jedes Schrittmotors. Sie muss nicht besonders effizient sein, da sie typischerweise nur bei Homing- und Sondierungsvorgängen aufgerufen wird.
  5. Weitere Methoden. Implementieren Sie die Methoden check_move(), get_status(), get_steppers(), home(), clear_homing_state() und set_position(). Diese Funktionen werden typischerweise verwendet, um kinematikspezifische Prüfungen bereitzustellen. Zu Beginn der Entwicklung kann hier jedoch Boilerplate-Code verwendet werden.
  6. Implementieren Sie Testfälle. Erstellen Sie eine G-Code-Datei mit einer Reihe von Bewegungen, die wichtige Fälle für die jeweilige Kinematik abdecken. Folgen Sie der Debugging-Dokumentation, um diese G-Code-Datei in Mikrocontroller-Befehle umzuwandeln. Dies ist nützlich, um Grenzfälle zu testen und Regressionen zu erkennen.

Portierung auf einen neuen Mikrocontroller

Dieser Abschnitt gibt einige Tipps zum Portieren von Klippers Mikrocontroller-Code auf eine neue Architektur. Diese Art von Arbeit erfordert gute Kenntnisse der Embedded-Entwicklung sowie praktischen Zugriff auf den Ziel-Mikrocontroller.

Nützliche Schritte:

  1. Beginnen Sie damit, alle Drittanbieter-Bibliotheken zu identifizieren, die während der Portierung verwendet werden. Gängige Beispiele sind "CMSIS"-Wrapper und herstellerspezifische "HAL"-Bibliotheken. Der gesamte Drittanbieter-Code muss mit der GNU GPLv3 kompatibel sein. Der Drittanbieter-Code sollte in das Klipper-Verzeichnis lib/ eingebracht werden. Aktualisieren Sie die Datei lib/README mit Informationen darüber, woher und wann die Bibliothek bezogen wurde. Es ist bevorzugt, den Code unverändert in das Klipper-Repository zu kopieren; sind jedoch Änderungen erforderlich, sollten diese explizit in der Datei lib/README aufgeführt werden.
  2. Legen Sie im Verzeichnis src/ ein neues Unterverzeichnis für die Architektur an und fügen Sie die anfängliche Kconfig- und Makefile-Unterstützung hinzu. Orientieren Sie sich dabei an den bestehenden Architekturen. Das Verzeichnis src/simulator bietet ein einfaches Beispiel für einen minimalen Ausgangspunkt.
  3. Die erste zentrale Programmieraufgabe besteht darin, die Kommunikationsunterstützung für die Zielplatine zum Laufen zu bringen. Dies ist der schwierigste Schritt bei einer neuen Portierung. Sobald die grundlegende Kommunikation funktioniert, sind die verbleibenden Schritte in der Regel deutlich einfacher. Während der ersten Entwicklung wird typischerweise ein serielles UART-Gerät verwendet, da diese Art von Hardware im Allgemeinen leichter zu aktivieren und anzusteuern ist. Nutzen Sie in dieser Phase großzügig den Hilfscode aus dem Verzeichnis src/generic/ (prüfen Sie, wie src/simulator/Makefile den generischen C-Code in den Build einbindet). In dieser Phase ist es außerdem notwendig, timer_read_time() zu definieren (welches die aktuelle Systemuhr zurückgibt), eine vollständige Unterstützung der Timer-IRQ-Behandlung ist jedoch noch nicht erforderlich.
  4. Machen Sie sich mit dem Werkzeug console.py vertraut (wie im Debugging-Dokument beschrieben) und prüfen Sie damit die Verbindung zum Mikrocontroller. Dieses Werkzeug übersetzt das Low-Level-Kommunikationsprotokoll des Mikrocontrollers in eine menschenlesbare Form.
  5. Fügen Sie Unterstützung für Timer-Dispatch über Hardware-Interrupts hinzu. Siehe Klipper-Commit 970831ee als Beispiel für die Schritte 1-5 am Beispiel der LPC176x-Architektur.
  6. Bringen Sie grundlegende GPIO-Ein- und -Ausgabeunterstützung zum Laufen. Siehe Klipper-Commit c78b9076 als Beispiel hierfür.
  7. Bringen Sie weitere Peripheriegeräte zum Laufen - siehe zum Beispiel die Klipper-Commits 65613aed, c812a40a und c381d03a.
  8. Erstellen Sie im Verzeichnis config/ eine Beispiel-Konfigurationsdatei für Klipper. Testen Sie den Mikrocontroller mit dem Hauptprogramm klippy.py.
  9. Erwägen Sie, im Verzeichnis test/ Build-Testfälle hinzuzufügen.

Zusätzliche Tipps zur Programmierung:

  1. Vermeiden Sie die Verwendung von "C-Bitfeldern" für den Zugriff auf IO-Register; bevorzugen Sie stattdessen direkte Lese- und Schreiboperationen von 32-Bit-, 16-Bit- oder 8-Bit-Ganzzahlen. Die C-Sprachspezifikation legt nicht eindeutig fest, wie der Compiler C-Bitfelder implementieren muss (z. B. Byte-Reihenfolge und Bit-Layout), und es ist schwer vorherzusagen, welche IO-Operationen bei einem Lese- oder Schreibzugriff auf ein C-Bitfeld tatsächlich stattfinden.
  2. Bevorzugen Sie das Schreiben expliziter Werte in IO-Register gegenüber Read-Modify-Write-Operationen. Das heißt, wenn ein Feld in einem IO-Register aktualisiert wird, dessen andere Felder bekannte Werte haben, ist es vorzuziehen, den vollständigen Inhalt des Registers explizit zu schreiben. Explizite Schreibvorgänge erzeugen Code, der kleiner, schneller und leichter zu debuggen ist.

Koordinatensysteme

Intern verfolgt Klipper die Position des Werkzeugkopfs vorrangig in kartesischen Koordinaten, die relativ zum in der Konfigurationsdatei angegebenen Koordinatensystem sind. Das heißt, der Großteil des Klipper-Codes erlebt niemals einen Wechsel des Koordinatensystems. Fordert der Anwender eine Änderung des Ursprungs an (z. B. über einen G92-Befehl), wird dieser Effekt dadurch erzielt, dass zukünftige Befehle in das primäre Koordinatensystem übersetzt werden.

In manchen Fällen ist es jedoch nützlich, die Position des Werkzeugkopfs in einem anderen Koordinatensystem zu ermitteln, und Klipper bietet dafür mehrere Werkzeuge. Dies lässt sich mit dem Befehl GET_POSITION beobachten. Zum Beispiel:

Send: GET_POSITION
Recv: // mcu: stepper_a:-2060 stepper_b:-1169 stepper_c:-1613
Recv: // stepper: stepper_a:457.254159 stepper_b:466.085669 stepper_c:465.382132
Recv: // kinematic: X:8.339144 Y:-3.131558 Z:233.347121
Recv: // toolhead: X:8.338078 Y:-3.123175 Z:233.347878 E:0.000000
Recv: // gcode: X:8.338078 Y:-3.123175 Z:233.347878 E:0.000000
Recv: // gcode base: X:0.000000 Y:0.000000 Z:0.000000 E:0.000000
Recv: // gcode homing: X:0.000000 Y:0.000000 Z:0.000000

Die "mcu"-Position (stepper.get_mcu_position() im Code) ist die Gesamtzahl der vom Mikrocontroller in positiver Richtung ausgegebenen Schritte abzüglich der in negativer Richtung ausgegebenen Schritte seit dem letzten Zurücksetzen des Mikrocontrollers. Befindet sich der Roboter zum Zeitpunkt der Abfrage in Bewegung, enthält der gemeldete Wert auf dem Mikrocontroller gepufferte Bewegungen, jedoch nicht die Bewegungen in der Look-ahead-Warteschlange.

Die "stepper"-Position (stepper.get_commanded_position()) ist die Position des jeweiligen Schrittmotors, wie sie vom Kinematik-Code nachverfolgt wird. Sie entspricht in der Regel der Position (in mm) des Schlittens entlang seiner Schiene, relativ zum in der Konfigurationsdatei angegebenen position_endstop. (Manche Kinematiken verfolgen Schrittmotorpositionen in Radiant statt in Millimetern.) Befindet sich der Roboter zum Zeitpunkt der Abfrage in Bewegung, enthält der gemeldete Wert auf dem Mikrocontroller gepufferte Bewegungen, jedoch nicht die Bewegungen in der Look-ahead-Warteschlange. Mit den Aufrufen toolhead.flush_step_generation() oder toolhead.wait_moves() lässt sich die Look-ahead- und Schritterzeugung vollständig leeren.

Die "kinematische" Position (kin.calc_position()) ist die kartesische Position des Werkzeugkopfs, wie sie aus den "stepper"-Positionen abgeleitet wird, relativ zum in der Konfigurationsdatei angegebenen Koordinatensystem. Diese kann aufgrund der Granularität der Schrittmotoren von der angeforderten kartesischen Position abweichen. Befindet sich der Roboter in Bewegung, wenn die "stepper"-Positionen erfasst werden, enthält der gemeldete Wert auf dem Mikrocontroller gepufferte Bewegungen, jedoch nicht die Bewegungen in der Look-ahead-Warteschlange. Mit den Aufrufen toolhead.flush_step_generation() oder toolhead.wait_moves() lässt sich die Look-ahead- und Schritterzeugung vollständig leeren.

Die "toolhead"-Position (toolhead.get_position()) ist die zuletzt angeforderte Position des Werkzeugkopfs in kartesischen Koordinaten, relativ zum in der Konfigurationsdatei angegebenen Koordinatensystem. Befindet sich der Roboter zum Zeitpunkt der Abfrage in Bewegung, enthält der gemeldete Wert alle angeforderten Bewegungen (auch solche, die noch in Puffern auf die Übergabe an die Schrittmotortreiber warten).

Die "gcode"-Position ist die zuletzt angeforderte Position eines G1- (oder G0-)Befehls in kartesischen Koordinaten, relativ zum in der Konfigurationsdatei angegebenen Koordinatensystem. Diese kann von der "toolhead"-Position abweichen, wenn eine G-Code-Transformation (z. B. bed_mesh, bed_tilt, skew_correction) aktiv ist. Sie kann auch von den tatsächlich im letzten G1-Befehl angegebenen Koordinaten abweichen, wenn der G-Code-Ursprung geändert wurde (z. B. durch G92, SET_GCODE_OFFSET, M221). Der Befehl M114 (gcode_move.get_status()['gcode_position']) meldet die letzte G-Code-Position relativ zum aktuellen G-Code-Koordinatensystem.

Die "gcode base" ist die Position des G-Code-Ursprungs in kartesischen Koordinaten, relativ zum in der Konfigurationsdatei angegebenen Koordinatensystem. Befehle wie G92, SET_GCODE_OFFSET und M221 verändern diesen Wert.

Das "gcode homing" ist die Position, die nach einem Homing-Befehl G28 als G-Code-Ursprung verwendet wird (in kartesischen Koordinaten, relativ zum in der Konfigurationsdatei angegebenen Koordinatensystem). Der Befehl SET_GCODE_OFFSET kann diesen Wert verändern.

Zeit

Grundlegend für die Funktionsweise von Klipper ist der Umgang mit Uhren, Zeiten und Zeitstempeln. Klipper führt Aktionen am Drucker aus, indem es Ereignisse für die nahe Zukunft plant. Um beispielsweise einen Lüfter einzuschalten, plant der Code möglicherweise eine Änderung eines GPIO-Pins in 100 ms. Es kommt selten vor, dass der Code versucht, eine sofortige Aktion auszuführen. Die Behandlung von Zeit innerhalb von Klipper ist daher entscheidend für den korrekten Betrieb.

Intern werden in der Klipper-Host-Software drei Arten von Zeiten nachverfolgt:

  • Systemzeit. Die Systemzeit verwendet die monotone Uhr des Systems - sie ist eine Fließkommazahl, die als Sekunden gespeichert wird, und ist (im Allgemeinen) relativ zum letzten Start des Host-Computers. Systemzeiten haben in der Software nur begrenzten Nutzen - sie werden vor allem bei der Interaktion mit dem Betriebssystem verwendet. Im Host-Code werden Systemzeiten häufig in Variablen namens eventtime oder curtime gespeichert.
  • Druckzeit. Die Druckzeit ist mit der Haupt-Mikrocontroller-Uhr synchronisiert (dem im Konfigurationsabschnitt "[mcu]" definierten Mikrocontroller). Sie ist eine Fließkommazahl, die als Sekunden gespeichert wird, und ist relativ zum letzten Neustart des Haupt-MCU. Eine "Druckzeit" lässt sich in die Hardware-Uhr des Haupt-Mikrocontrollers umrechnen, indem die Druckzeit mit der statisch konfigurierten Taktrate des MCU multipliziert wird. Der übergeordnete Host-Code verwendet Druckzeiten, um nahezu alle physischen Aktionen zu berechnen (z. B. Kopfbewegung, Heizungsänderungen usw.). Im Host-Code werden Druckzeiten in der Regel in Variablen namens print_time oder move_time gespeichert.
  • MCU-Takt. Dies ist der Hardware-Taktzähler auf jedem Mikrocontroller. Er wird als Ganzzahl gespeichert, und seine Aktualisierungsrate ist relativ zur Frequenz des jeweiligen Mikrocontrollers. Die Host-Software übersetzt ihre internen Zeiten vor der Übertragung an den MCU in Taktwerte. Der MCU-Code verfolgt Zeit ausschließlich in Takt-Ticks. Im Host-Code werden Taktwerte als 64-Bit-Ganzzahlen verfolgt, während der MCU-Code 32-Bit-Ganzzahlen verwendet. Im Host-Code werden Taktwerte in der Regel in Variablen gespeichert, deren Namen clock oder ticks enthalten.

Die Umrechnung zwischen den verschiedenen Zeitformaten ist hauptsächlich im Code klippy/clocksync.py implementiert.

Einige Dinge, auf die beim Lesen des Codes zu achten ist:

  • 32-Bit- und 64-Bit-Takte: Um die Bandbreite zu reduzieren und die Effizienz des Mikrocontrollers zu verbessern, werden Taktwerte auf dem Mikrocontroller als 32-Bit-Ganzzahlen verfolgt. Beim Vergleich zweier Taktwerte im MCU-Code muss stets die Funktion timer_is_before() verwendet werden, um sicherzustellen, dass Ganzzahlüberläufe korrekt behandelt werden. Die Host-Software wandelt 32-Bit-Taktwerte in 64-Bit-Taktwerte um, indem sie die höherwertigen Bits des zuletzt vom MCU empfangenen Zeitstempels anhängt - keine Nachricht vom MCU liegt jemals mehr als 2^31 Takt-Ticks in der Zukunft oder Vergangenheit, sodass diese Umrechnung nie mehrdeutig ist. Der Host wandelt 64-Bit-Taktwerte in 32-Bit-Taktwerte um, indem er einfach die höherwertigen Bits abschneidet. Um Mehrdeutigkeiten bei dieser Umrechnung auszuschließen, puffert der Code in klippy/chelper/serialqueue.c Nachrichten, bis sie sich innerhalb von 2^31 Takt-Ticks ihrer Zielzeit befinden.
  • Mehrere Mikrocontroller: Die Host-Software unterstützt die Verwendung mehrerer Mikrocontroller an einem einzigen Drucker. In diesem Fall wird der "MCU-Takt" jedes Mikrocontrollers separat verfolgt. Der Code in clocksync.py behandelt Taktdrift zwischen Mikrocontrollern, indem er die Art und Weise anpasst, wie "Druckzeit" in "MCU-Takt" umgerechnet wird. Auf sekundären MCUs wird die für diese Umrechnung verwendete MCU-Frequenz regelmäßig aktualisiert, um die gemessene Drift zu berücksichtigen.
Zurück zum Seitenanfang