Aller au contenu

Aperçu du code

Ce document décrit la disposition générale du code et le flux de code principal de Klipper.

Disposition du répertoire

Le répertoire src/ contient les sources C du code du micro-contrôleur. Les répertoires src/atsam/, src/atsamd/, src/avr/, src/linux/, src/lpc176x/, src/pru/, et src/stm32/ contiennent le code du micro-contrôleur spécifique à chaque architecture. Le répertoire src/simulator/ contient des éléments de remplacement de code permettant de tester la compilation du micro-contrôleur sur d'autres architectures. Le répertoire src/generic/ contient du code d'assistance utile sur différentes architectures. La compilation fait en sorte que les inclusions de "board/somefile.h" soient d'abord recherchées dans le répertoire de l'architecture courante (par exemple, src/avr/somefile.h) et ensuite dans le répertoire générique (par exemple, src/generic/somefile.h).

Le répertoire klippy/ contient le logiciel hôte. La majorité du logiciel hôte est écrit en Python, cependant le répertoire klippy/chelper/ contient quelques assistants en code C. Le répertoire klippy/kinematics/ contient le code lié à la cinématique du robot. Le répertoire klippy/extras/ contient les "modules" extensibles du code hôte.

Le répertoire lib/ contient du code de bibliothèque externe de tierce partie nécessaire à la construction de certaines cibles.

Le répertoire config/ contient des exemples de fichiers de configuration d'imprimante.

Le répertoire scripts/ contient des scripts de construction utiles à la compilation du code du micro-contrôleur.

Le répertoire test/ contient des cas de tests automatisés.

Pendant la compilation, le constructeur peut créer un répertoire out/. Celui-ci contient des objets temporaires de compilation. L'objet micro-contrôleur final qui est construit est out/klipper.elf.hex sur AVR et out/klipper.bin sur ARM.

Flux de code du micro-contrôleur

L'exécution du code du microcontrôleur débute dans le code spécifique à l'architecture (par exemple, src/avr/main.c) qui appelle finalement sched_main() situé dans src/sched.c. Le code sched_main() commence par exécuter toutes les fonctions marquées avec la macro DECL_INIT(). Il exécute ensuite de manière répétée toutes les fonctions marquées par la macro DECL_TASK().

L'une des principales fonctions de la tâche est command_dispatch() située dans src/command.c. Cette fonction est appelée à partir du code d'entrée/sortie spécifique à la carte (par exemple, src/avr/serial.c, src/generic/serial_irq.c) et elle exécute les fonctions de commande associées aux commandes trouvées dans le flux d'entrée. Les fonctions de commande sont déclarées à l'aide de la macro DECL_COMMAND() (voir le document protocole pour plus d'informations).

Les fonctions de tâche, d'initialisation et de commande s'exécutent toujours avec les interruptions activées (toutefois, elles peuvent désactiver temporairement les interruptions si nécessaire). Ces fonctions doivent éviter les longues pauses, les retards, ou effectuer un travail qui dure un temps significatif. (Les longs retards de ces fonctions "tâches" entraînent une gigue de programmation pour d'autres "tâches" - les retards de plus de 100us peuvent devenir perceptibles, les retards de plus de 500us peuvent entraîner des retransmissions de commandes, les retards de plus de 100ms peuvent entraîner des redémarrages du chien de garde). Ces fonctions planifient le travail à des moments précis en programmant des minuteries.

Les fonctions de temporisation sont programmées en appelant sched_add_timer() (situé dans src/sched.c). Le code du planificateur s'arrangera pour que la fonction donnée soit appelée au moment demandé. Les interruptions de temporisation sont initialement traitées dans un gestionnaire d'interruption spécifique à l'architecture (par exemple, src/avr/timer.c) qui appelle sched_timer_dispatch() situé dans src/sched.c. L'interruption de la temporisation entraîne l'exécution des fonctions de temporisation planifiées. Les fonctions de temporisation sont toujours exécutées avec des interruptions désactivées. Les fonctions de temporisation devraient toujours se terminer en quelques microsecondes. À la fin de l'événement de temporisation, la fonction peut choisir de se reprogrammer.

En cas de détection d'erreur, le code peut invoquer shutdown() (une macro qui appelle sched_shutdown() située dans src/sched.c). L'appel à shutdown() entraîne l'exécution de toutes les fonctions marquées par la macro DECL_SHUTDOWN(). Les fonctions d'arrêt s'exécutent toujours avec des interruptions désactivées.

Une grande partie des fonctionnalités du micro-contrôleur implique de travailler avec des broches d'entrée/sortie à usage général (GPIO). Afin de séparer le code de bas niveau spécifique à l'architecture du code de haut niveau de la tâche, tous les événements GPIO sont implémentés dans des emballages spécifiques à l'architecture (par exemple, src/avr/gpio.c). Le code est compilé avec l'optimisation "-flto -fwhole-program" de gcc qui fait un excellent travail d'intégration des fonctions à travers les unités de compilation, ainsi la plupart de ces minuscules fonctions gpio sont intégrées à leurs appelants, et il n'y a aucun coût d'exécution à les utiliser.

Aperçu du code Klippy

Le code hôte (Klippy) est destiné à fonctionner sur un ordinateur à petit prix (tel qu'un Raspberry Pi) associé au microcontrôleur. Le code est principalement écrit en Python, cependant il utilise CFFI pour implémenter certaines fonctionnalités en code C.

L'exécution initiale commence dans klippy/klippy.py. Ceci lit les arguments de la ligne de commande, ouvre le fichier de configuration de l'imprimante, instancie les principaux objets de l'imprimante et lance la connexion série. L'exécution principale des commandes G-code se fait dans la méthode process_commands() de klippy/gcode.py. Ce code traduit les commandes G-code en appels d'objets d'imprimante fréquemment traduis en commandes d'actions à exécuter sur le micro-contrôleur (tel que déclaré via la macro DECL_COMMAND dans le code du micro-contrôleur).

Il y a quatre threads dans le code hôte de Klippy. Le thread principal gère les commandes gcode entrantes. Un deuxième thread (résidant entièrement dans le code C klippy/chelper/serialqueue.c) gère les entrées/sorties de bas niveau avec le port série. Le troisième thread est utilisé pour traiter les messages de réponse du micro-contrôleur dans le code Python (voir klippy/serialhdl.py). Le quatrième thread écrit les messages de débogage dans le journal (voir klippy/queuelogger.py) afin que les autres threads ne bloquent jamais les écritures dans le journal.

Flux du code d'une commande de déplacement

Un mouvement typique de l'imprimante commence lorsqu'une commande "G1" est envoyée à l'hôte Klippy et se termine lorsque les impulsions de pas correspondantes sont produites sur le micro-contrôleur. Cette section décrit le flux du code d'une commande de déplacement typique. Le document cinématiques fournit des informations supplémentaires sur la mécanique des mouvements.

  • Le traitement d'une commande de déplacement commence dans gcode.py. Le but de gcode.py est de traduire le G-code en appels internes. Une commande G1 invoquera cmd_G1() dans klippy/extras/gcode_move.py. Le code gcode_move.py gère les changements d'origine (par exemple, G92), les changements de positions relatives et absolues (par exemple, G90), et les changements d'unités (par exemple, F6000=100mm/s). Le chemin du code pour un déplacement est : _process_data() -> _process_commands() -> cmd_G1(). Finalement, la classe ToolHead est invoquée pour exécuter la demande réelle : cmd_G1() -> ToolHead.move()
  • La classe ToolHead (dans toolhead.py) gère le "look-ahead" et suit la temporisation des actions d'impression. Le chemin du code principal pour un déplacement est : ToolHead.move() -> MoveQueue.add_move() -> MoveQueue.flush() -> Move.set_junction() -> ToolHead._process_moves().
    • ToolHead.move() crée un objet Move() avec les paramètres du déplacement (dans l'espace cartésien et en unités de secondes et de millimètres).
    • La classe cinématique a la possibilité de vérifier chaque mouvement (ToolHead.move() -> kin.check_move()). Les classes cinématiques sont situées dans le répertoire klippy/kinematics/. Le code check_move() peut lever une erreur si le déplacement n'est pas valide. Si check_move() se termine avec succès, alors la cinématique sous-jacente doit être capable de gérer le déplacement.
    • MoveQueue.add_move() place l'objet move dans la file d'attente "look-ahead".
    • MoveQueue.flush() détermine les vitesses de début et de fin de chaque mouvement.
    • Move.set_junction() implémente le "générateur de trapèze" sur un mouvement. Le "générateur de trapèze" divise chaque mouvement en trois parties : une phase d'accélération constante, suivie d'une phase de vitesse constante, puis d'une phase de décélération constante. Chaque déplacement contient ces trois phases dans cet ordre, certaines phases pouvant être de durée nulle.
    • Lorsque ToolHead._process_moves() est appelé, tout ce qui concerne le déplacement est connu : sa position de départ, sa position d'arrivée, son accélération, sa vitesse de départ/de croisière/de fin, et la distance parcourue pendant l'accélération/la croisière/la décélération. Toutes ces informations sont stockées dans la classe Move() et sont dans l'espace cartésien en unités de millimètres et de secondes.
  • Klipper utilise un solveur itératif afin de générer les délais des pas pour chaque moteur pas à pas. Pour des raisons d'efficacité, les délais d'impulsion des pas sont générés en code C. Les mouvements sont d'abord placés dans une "file d'attente de mouvements trapézoïdaux" : ToolHead._process_moves() -> trapq_append() (dans klippy/chelper/trapq.c). Les délais de pas sont ensuite générés : ToolHead._process_moves() -> ToolHead._update_move_time() -> MCU_Stepper.generate_steps() -> itersolve_generate_steps() -> itersolve_gen_steps_range() (dans klippy/chelper/itersolve.c). Le but du solveur itératif est de trouver les délais de pas à partir d'une fonction calculant la position du moteur à partir du temps. Ceci est fait en "devinant" de manière répétée plusieurs délais jusqu'à ce que la formule de position du pas renvoie la position désirée du pas suivant sur le moteur. Le retour d'information produit par chaque estimation est utilisé afin d'améliorer les estimations futures, de sorte que le processus converge rapidement vers le délai souhaité. Les formules de position cinématique du moteur sont situées dans le répertoire klippy/chelper/ (par exemple, kin_cart.c, kin_corexy.c, kin_delta.c, kin_extruder.c).
  • Notez que l'extrudeuse est gérée dans sa propre classe cinématique : ToolHead._process_moves() -> PrinterExtruder.move(). Puisque la classe Move() spécifie le délai exact du mouvement et que les impulsions de pas sont envoyées au micro-contrôleur avec un timing spécifique, les mouvements du moteur produits par la classe de l'extrudeuse seront synchronisés avec le mouvement de la tête même si le code est séparé.
  • Après que le solveur itératif ait calculé les délais de pas, ils sont ajoutés à un tableau : itersolve_gen_steps_range() -> stepcompress_append() (dans klippy/chelper/stepcompress.c). Le tableau (struct stepcompress.queue) stocke les durées correspondantes du compteur d'horloge du micro-contrôleur pour chaque étape. Ici, la valeur du "compteur d'horloge du micro-contrôleur" correspond directement au compteur matériel du micro-contrôleur - elle est relative à la dernière mise sous tension du micro-contrôleur.
  • La prochaine étape majeure est de compresser les étapes : stepcompress_flush() -> compress_bisect_add() (dans klippy/chelper/stepcompress.c). Ce code génère et encode une série de commandes "queue_step" du micro-contrôleur correspondant à la liste des durées de pas du moteur construite à l'étape précédente. Ces commandes "queue_step" sont ensuite mises en file d'attente, hiérarchisées et envoyées au micro-contrôleur (via stepcompress.c:steppersync et serialqueue.c:serialqueue).
  • Le traitement des commandes queue_step sur le micro-contrôleur commence dans src/command.c qui analyse la commande et appelle command_queue_step(). Le code command_queue_step() (dans src/stepper.c) ajoute simplement les paramètres de chaque commande queue_step à une file d'attente par moteur. En fonctionnement normal, la commande queue_step est analysée et mise en file d'attente au moins 100ms avant le moment de son premier pas. Enfin, la génération des événements de pas du moteur est faite dans stepper_event(). Elle est appelée depuis l'interruption du compteur matériel à l'heure prévue du premier pas. Le code stepper_event() génère une impulsion de pas et se reprogramme ensuite pour s'exécuter au moment de l'impulsion de pas suivante pour les paramètres queue_step donnés. Les paramètres pour chaque commande queue_step sont "interval", "count", et "add". A un haut niveau, stepper_event() exécute la commande suivante, 'count' fois : do_step() ; next_wake_time = last_wake_time + interval ; interval += add;

Ce qui précède peut sembler beaucoup de complexité pour l'exécution d'un mouvement. Cependant, les seules parties vraiment intéressantes se trouvent dans les classes ToolHead et kinematic. C'est cette partie du code qui précise les mouvements et leurs durées. Les autres parties du traitement sont essentiellement de la communication et de la plomberie.

Ajout d'un module hôte

Le code hôte de Klippy a une capacité de chargement dynamique de modules. Si une section de configuration nommée "[mon_module]" est trouvée dans le fichier de configuration de l'imprimante, le logiciel tentera automatiquement de charger le module python klippy/extras/mon_module.py . Ce système de modules est la méthode préférée pour ajouter de nouvelles fonctionnalités à Klipper.

La façon la plus simple d'ajouter un nouveau module est d'utiliser un module existant comme référence - voir klippy/extras/servo.py comme exemple.

Les éléments suivants peuvent également être utiles :

  • L'exécution du module commence dans la fonction load_config() au niveau du module (pour les sections de configuration de la forme [mon_module]) ou dans load_config_prefix() (pour les sections de configuration de la forme [mon_module mon_nom]). On passe à cette fonction un objet "config" et elle doit retourner un nouvel "objet imprimante" associé à la section de configuration donnée.
  • Durant le processus d'instanciation d'un nouvel objet imprimante, l'objet config peut être utilisé pour lire les paramètres de la section config donnée. Ceci est fait en utilisant les méthodes config.get(), config.getfloat(), config.getint(), etc. Assurez-vous de lire toutes les valeurs de la configuration pendant la construction de l'objet imprimante - si l'utilisateur spécifie un paramètre de configuration qui n'est pas lu pendant cette phase, il sera supposé qu'il s'agit d'une faute de frappe dans la configuration et une erreur sera levée.
  • Utilisez la méthode config.get_printer() pour obtenir une référence à la classe principale "printer". Cette classe "printer" stocke les références de tous les "objets imprimante" instanciés. Utilisez la méthode printer.lookup_object() pour trouver des références à d'autres objets imprimante. Presque toutes les fonctionnalités (même les modules cinématiques de base) sont encapsulées dans un de ces objets imprimante. Notez, cependant, que lorsqu'un nouveau module est instancié, tous les autres objets d'impression n'auront pas encore été instanciés. Les modules "gcode" et "pins" seront toujours disponibles, mais pour les autres modules, il est préférable de différer la recherche.
  • Enregistrez les gestionnaires d'événements en utilisant la méthode printer.register_event_handler() si le code doit être appelé lors d'"événements" déclenchés par d'autres objets imprimante. Chaque nom d'événement est une chaîne de caractères, et par convention, c'est le nom du module source principal déclenchant l'événement avec un nom court pour l'action produite (par exemple, "klippy:connect"). Les paramètres passés à chaque gestionnaire d'événement sont spécifiques à l'événement en question (tout comme le traitement des exceptions et le contexte d'exécution). Deux événements de démarrage courants sont :
    • klippy:connect - Cet événement est généré après l'instanciation de tous les objets imprimante. Il est couramment utilisé pour rechercher d'autres objets imprimante, pour vérifier les paramètres de configuration et pour effectuer une première "prise de contact" avec le matériel de l'imprimante.
    • klippy:ready - Cet événement est généré après que tous les gestionnaires de connexion se soient terminés avec succès. Il indique que l'imprimante est en train de passer à l'état prêt à gérer les opérations normales. Ne déclenchez pas d'erreur dans ce rappel.
  • S'il y a une erreur dans la configuration de l'utilisateur, assurez-vous de la signaler pendant les phases load_config() ou "connect event". Utilisez soit raise config.error("mon erreur") soit raise printer.config_error("mon erreur") pour signaler l'erreur.
  • Utilisez le module "pins" pour configurer une broche sur un micro-contrôleur. Ceci est typiquement fait avec quelque chose de similaire à printer.lookup_object("pins").setup_pin("pwm", config.get("my_pin")). L'objet retourné peut alors être commandé au moment de l'exécution.
  • Si l'objet imprimante définit une méthode get_status(), le module peut exporter des informations d'état via des macros et via le serveur API. La méthode get_status() doit retourner un dictionnaire Python dont les clés sont des chaînes de caractères et les valeurs des entiers, des flottants, des chaînes de caractères, des listes, des dictionnaires, True, False ou None. Les tuples (et les tuples nommés) peuvent également être utilisés (ils apparaissent comme des listes lorsqu'on y accède via le serveur API). Les listes et les dictionnaires exportés doivent être traités comme "immuables" - si leur contenu change, un nouvel objet doit être renvoyé par get_status(), sinon le serveur API ne détectera pas ces changements.
  • Si le module doit accéder à la temporisation du système ou à des descripteurs de fichiers externes, utilisez printer.get_reactor() pour obtenir l'accès à la classe globale "event reactor". Cette classe de réacteur permet de programmer des temporisations, d'attendre des entrées sur des descripteurs de fichiers, et d'"endormir" le code hôte.
  • N'utilisez pas de variables globales. Tous les états doivent être stockés dans l'objet imprimante renvoyé par la fonction load_config(). Ceci est important car sinon la commande RESTART pourrait ne pas fonctionner comme prévu. De même, pour des raisons similaires, si des fichiers externes (ou des sockets) sont ouverts, assurez-vous d'enregistrer un gestionnaire d'événement "klippy:disconnect" et de les fermer à partir de ce rappel.
  • Évitez d'accéder aux variables membres internes (ou d'appeler les méthodes commençant par un trait de soulignement) d'autres objets imprimante. Le respect de cette convention facilite la gestion des modifications futures.
  • Il est recommandé d'attribuer une valeur à toutes les variables membres dans le constructeur Python des classes Python. (Et donc d'éviter d'utiliser la capacité de Python à créer dynamiquement de nouvelles variables membres.)
  • Si une variable Python doit stocker une valeur en virgule flottante, il est recommandé de toujours affecter et manipuler cette variable avec des constantes en virgule flottante (et de ne jamais utiliser de constantes entières). Par exemple, préférez self.speed = 1. à self.speed = 1, et préférez self.speed = 2. * x plutôt que self.speed = 2 * x. L'utilisation cohérente des valeurs à virgule flottante peut éviter des bizarreries difficiles à déboguer dans les conversions de types Python.
  • Si vous soumettez le module pour qu'il soit inclus dans le code principal de Klipper, veillez à placer un avis de copyright en haut du module. Consultez les modules existants pour connaître le format préféré.

Ajout de nouvelles cinématiques

Cette section fournit quelques conseils pour ajouter le support à Klipper de types supplémentaires de cinématiques d'imprimante. Ce type d'activité nécessite une excellente compréhension des formules mathématiques de la cinématique cible. Elle requiert également des compétences en matière de développement de logiciels, même s'il suffit de mettre à jour le logiciel hôte.

Les étapes utiles :

  1. Commencez par étudier la section "code de flux d'un mouvement" et le document cinématiques.
  2. Passez en revue les classes cinématiques existantes dans le répertoire klippy/kinematics/. Les classes cinématiques sont chargées de convertir le mouvement en coordonnées cartésiennes en un mouvement sur chaque moteur. On devrait être capable de copier un de ces fichiers comme point de départ.
  3. Implémentez les fonctions de position cinématique du moteur en C pour chaque moteur si elles ne sont pas déjà disponibles (voir kin_cart.c, kin_corexy.c, et kin_delta.c dans klippy/chelper/). La fonction doit appeler move_get_coord() pour convertir une durée de déplacement donnée (en secondes) en coordonnées cartésiennes (en millimètres), et ensuite calculer la position souhaitée du moteur (en millimètres) à partir de ces coordonnées cartésiennes.
  4. Implémentez la méthode calc_position() dans la nouvelle classe cinématique. Cette méthode calcule la position de la tête de l'outil en coordonnées cartésiennes à partir de la position de chaque moteur. Elle n'a pas besoin d'être efficace car elle n'est typiquement appelée que pendant les opérations de mise à l'origine et de palpage.
  5. Autres méthodes. Implémentez les méthodes check_move(), get_status(), get_steppers(), home(), et set_position(). Ces fonctions sont typiquement utilisées pour fournir des vérifications spécifiques à la cinématique. Cependant, au début du développement, on peut utiliser du code passe-partout ici.
  6. Implémenter des cas de test. Créez un fichier g-code avec une série de mouvements pouvant tester des cas importants pour la cinématique donnée. Suivez la documentation de débogage pour convertir ce fichier g-code en commandes de micro-contrôleur. Ceci est utile pour tester les cas extrêmes et vérifier les régressions.

Portage sur un nouveau microcontrôleur

Cette section fournit quelques conseils sur le portage du code du microcontrôleur de Klipper vers une nouvelle architecture. Ce type d'activité nécessite une bonne connaissance du développement embarqué et un accès pratique au microcontrôleur cible.

Les étapes utiles :

  1. Commencez par identifier toutes les bibliothèques tierces qui seront utilisées pendant le portage. Les exemples courants incluent les wrappers "CMSIS" et les bibliothèques "HAL" des fabricants. Tout code tiers doit être compatible avec la licence GNU GPLv3. Le code tiers doit être livré dans le répertoire Klipper lib/. Mettez à jour le fichier lib/README en indiquant où et quand la bibliothèque a été obtenue. Il est préférable de copier le code dans le dépôt Klipper sans modification, mais si des modifications sont nécessaires, elles doivent être indiquées explicitement dans le fichier lib/README.
  2. Créer un nouveau sous-répertoire d'architecture dans le répertoire src/ et ajouter le support initial de Kconfig et Makefile. Utilisez les architectures existantes comme guide. Le simulateur src/simulator fournit un exemple de base pour un point de départ minimal.
  3. La première tâche principale de codage est d'apporter le support de communication à la carte cible. C'est l'étape la plus difficile dans un nouveau portage. Une fois que la communication de base fonctionne, les autres étapes ont tendance à être beaucoup plus faciles. Il est typique d'utiliser un dispositif série de type UART pendant le développement initial car ces types de dispositifs matériels sont généralement plus faciles à activer et à contrôler. Pendant cette phase, faites un usage libéral du code d'aide du répertoire src/generic/ (vérifiez comment src/simulator/Makefile inclut le code C générique dans la compilation). Il est également nécessaire de définir timer_read_time() (qui renvoie l'horloge système actuelle) dans cette phase, mais il n'est pas nécessaire de supporter complètement la gestion des irq des timers.
  4. Familiarisez-vous avec l'outil console.py (comme décrit dans le document de débogage) et vérifiez la connectivité au microcontrôleur avec cet outil. Cet outil traduit le protocole de communication de bas niveau du microcontrôleur en une forme lisible par l'homme.
  5. Ajouter le support pour la distribution des timers à partir des interruptions matérielles. Voir Klipper commit 970831ee comme exemple des étapes 1-5 réalisées pour l'architecture LPC176x.
  6. Apporter le support de base des entrées et sorties GPIO. Voir Klipper commit c78b9076 comme exemple.
  7. Faire apparaître des périphériques supplémentaires - par exemple, voir Klipper commit 65613aed, c812a40a, et c381d03a.
  8. Créez un exemple de fichier de configuration Klipper dans le répertoire config/. Testez le micro-contrôleur avec le programme principal klippy.py.
  9. Pensez à ajouter des cas de test de construction dans le répertoire test/.

Conseils supplémentaires pour le codage :

  1. Évitez d'utiliser des "champs de bits C" pour accéder aux registres d'E/S ; préférez les opérations directes de lecture et d'écriture d'entiers de 32, 16 ou 8 bits. Les spécifications du langage C ne précisent pas clairement comment le compilateur doit implémenter les champs de bits C (par exemple, l'endianness et la disposition des bits), et il est difficile de déterminer quelles opérations d'E/S se produiront lors de la lecture ou de l'écriture d'un champ de bits C.
  2. Préférez l'écriture de valeurs explicites dans les registres d'E/S à l'utilisation d'opérations de lecture-modification-écriture. Autrement dit, si l'on met à jour un champ dans un registre d'E/S dont les autres champs ont des valeurs connues, il est préférable d'écrire explicitement le contenu complet du registre. Les écritures explicites produisent un code plus petit, plus rapide et plus facile à déboguer.

Les systèmes de coordonnées

En interne, Klipper suit principalement la position de la tête de l'outil en coordonnées cartésiennes relatives au système de coordonnées spécifié dans le fichier de configuration. C'est-à-dire que la plupart du code de Klipper ne connaîtra jamais de changement de système de coordonnées. Si l'utilisateur fait une demande pour changer l'origine (par exemple, une commande G92) alors cet effet est obtenu en traduisant les commandes futures vers le système de coordonnées primaires.

Cependant, dans certains cas, il est utile d'obtenir la position de la tête de l'outil dans un autre système de coordonnées et Klipper dispose de plusieurs outils pour faciliter cela. Vous pouvez le constater en exécutant la commande GET_POSITION. Par exemple :

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

La position "mcu" (stepper.get_mcu_position() dans le code) est le nombre total de pas que le micro-contrôleur a émis dans un sens positif moins le nombre de pas émis dans un sens négatif depuis la dernière réinitialisation du micro-contrôleur. Si le robot est en mouvement lorsque la requête est émise, la valeur rapportée inclut les mouvements mis en mémoire tampon dans le microcontrôleur, mais pas les mouvements de la file d'attente de surveillance.

La position "stepper" (stepper.get_commanded_position()) est la position du moteur donné telle qu'elle est suivie par le code cinématique. Cela correspond généralement à la position (en mm) du chariot le long de son rail, par rapport à la position_endstop indiquée dans le fichier de configuration. (Si le robot est en mouvement lorsque la requête est émise, la valeur rapportée inclut les mouvements mis en mémoire tampon sur le micro-contrôleur, mais n'inclut pas les mouvements de la file d'attente look-ahead. On peut utiliser les appels toolhead.flush_step_generation() ou toolhead.wait_moves() pour vider complètement les codes de look-ahead et de génération de pas.

La position "kinematic" (kin.calc_position()) est la position cartésienne de la tête de l'outil telle que dérivée des positions "pas à pas" et est relative au système de coordonnées spécifié dans le fichier de configuration. Cela peut différer de la position cartésienne demandée en raison de la granularité des moteurs pas à pas. Si le robot est en mouvement lorsque les positions "stepper" sont prises, la valeur rapportée inclut les mouvements mis en mémoire tampon sur le micro-contrôleur, mais n'inclut pas les mouvements de la file d'attente look-ahead. On peut utiliser les appels toolhead.flush_step_generation() ou toolhead.wait_moves() pour vider complètement le code de look-ahead et de génération de pas.

La position de la tête d'outil (toolhead.get_position()) est la dernière position demandée de la tête d'outil en coordonnées cartésiennes par rapport au système de coordonnées spécifié dans le fichier de configuration. Si le robot est en mouvement lorsque la requête est émise, la valeur rapportée inclut tous les déplacements demandés (même ceux dans les tampons en attente d'être émis vers les pilotes de moteurs pas à pas).

La position "gcode" est la dernière position demandée par une commande G1 (ou G0) en coordonnées cartésiennes par rapport au système de coordonnées spécifié dans le fichier de configuration. Elle peut différer de la position "toolhead" si une transformation g-code (par exemple, bed_mesh, bed_tilt, skew_correction) est en cours. Cela peut différer des coordonnées réelles spécifiées dans la dernière commande G1 si l'origine du gcode a été modifiée (par exemple, G92, SET_GCODE_OFFSET, M221). La commande M114 (gcode_move.get_status()['gcode_position']) indiquera la dernière position du g-code par rapport au système de coordonnées actuel du g-code.

La "base gcode" est l'emplacement de l'origine du g-code en coordonnées cartésiennes par rapport au système de coordonnées spécifié dans le fichier de configuration. Les commandes telles que G92, SET_GCODE_OFFSET, et M221 modifient cette valeur.

Le "gcode homing" est l'emplacement à utiliser pour l'origine du g-code (en coordonnées cartésiennes par rapport au système de coordonnées spécifié dans le fichier de configuration) après une commande de mise à l'origine G28. La commande SET_GCODE_OFFSET peut modifier cette valeur.

Temporisation

La manipulation des horloges, des durées et des horodatages est fondamentale pour le fonctionnement de Klipper. Celui-ci exécute des actions sur l'imprimante en programmant des événements qui se produiront dans un avenir proche. Par exemple, pour allumer un ventilateur, le code peut programmer un changement sur une broche GPIO dans 100 ms. Il est rare que le code tente d'effectuer une action instantanée. Ainsi, la gestion du temps dans Klipper est essentielle pour un fonctionnement correct.

Il existe trois types de temporisations suivis en interne dans le logiciel hôte Klipper :

  • Heure du système. L'heure système utilise l'horloge du système - c'est un nombre à virgule flottante stocké sous forme de secondes (généralement) relatif à la date du dernier démarrage de l'ordinateur hôte. Les temps système ont un usage limité dans le logiciel - ils sont principalement utilisés lors de l'interaction avec le système d'exploitation. Dans le code hôte, les heures système sont souvent stockées dans des variables nommées eventtime ou curtime.
  • Temps d'impression. Le temps d'impression est synchronisé avec l'horloge du micro-contrôleur principal (le micro-contrôleur défini dans la section "[mcu]" de la configuration). C'est un nombre à virgule flottante stocké en secondes relatif à la date du dernier redémarrage du micro-contrôleur principal. Il est possible de convertir un "temps d'impression" en horloge matérielle du micro-contrôleur principal en multipliant le temps d'impression par le taux de fréquence du mcu configuré statiquement. Le code hôte de haut niveau utilise les temps d'impression pour calculer presque toutes les actions physiques (par exemple, le mouvement de la tête, les changements de chauffage, etc.) Dans le code hôte, les temps d'impression sont généralement stockés dans des variables nommées print_time ou move_time.
  • Horloge MCU. Il s'agit du compteur d'horloge matériel de chaque micro-contrôleur. Il est stocké sous forme d'un nombre entier et son taux de mise à jour est relatif à la fréquence du micro-contrôleur donné. Le logiciel hôte convertit ses temps internes en horloges avant de les transmettre à l'unité centrale. Le code de l'unité centrale de traitement ne suit jamais le temps qu'en ticks d'horloge. Dans le code hôte, les valeurs d'horloge sont suivies sous forme d'entiers de 64 bits, alors que le code de l'unité centrale utilise des entiers de 32 bits. Dans le code hôte, les horloges sont généralement stockées dans des variables dont les noms contiennent clock ou ticks.

La conversion entre les différents formats de temps est principalement implémentée dans le code klippy/clocksync.py.

Quelques éléments à prendre en compte lors de la révision du code :

  • Horloges 32bit et 64bit : Pour réduire la bande passante et améliorer l'efficacité du micro-contrôleur, les horloges sur le micro-contrôleur sont suivies comme des entiers 32bit. Lors de la comparaison de deux horloges dans le code mcu, la fonction timer_is_before() doit toujours être utilisée pour s'assurer que les renversements d'entiers sont traités correctement. Le logiciel hôte convertit les horloges 32bit en horloges 64bit en ajoutant les bits de poids fort du dernier timestamp mcu qu'il a reçu - aucun message du mcu n'est jamais plus de 2^31 ticks d'horloge dans le futur ou le passé, donc cette conversion n'est jamais ambiguë. L'hôte convertit les horloges de 64 bits en horloges de 32 bits en tronquant simplement les bits de poids fort. Pour s'assurer qu'il n'y a pas d'ambiguïté dans cette conversion, le code klippy/chelper/serialqueue.c mettra en mémoire tampon les messages jusqu'à ce qu'ils soient dans les 2^31 ticks d'horloge de leur temps cible.
  • Microcontrôleurs multiples : Le logiciel hôte permet d'utiliser plusieurs microcontrôleurs sur une seule imprimante. Dans ce cas, l'"horloge MCU" de chaque microcontrôleur est suivie séparément. Le code clocksync.py gère la dérive de l'horloge entre les micro-contrôleurs en modifiant la façon dont il convertit le "temps d'impression" en "horloge MCU". Sur les mcus secondaires, la fréquence du mcu utilisée dans cette conversion est régulièrement mise à jour pour tenir compte de la dérive mesurée.
Retour en haut de la page