Comment utiliser Python pour créer votre propre contrôleur CNC et imprimante 3D
par Nikolaï Khabarov
Cet article traite du processus que j'ai utilisé pour créer la toute première implémentation d'un contrôleur de machine CNC sur Python pur.
Les contrôleurs de machines à commande numérique par ordinateur (CNC) sont généralement implémentés à l'aide du langage de programmation C ou C++. Ils fonctionnent sur des systèmes d'exploitation sans système d'exploitation ou en temps réel avec de simples microcontrôleurs.
Dans cet article, je vais décrire comment construire un contrôleur CNC - une imprimante 3D en particulier - en utilisant des cartes ARM modernes (Raspberry Pi) avec un langage moderne de haut niveau (Python).
Une telle approche moderne ouvre un large éventail d’options d’intégration avec d’autres technologies, solutions et infrastructures de pointe. Cela rend l’ensemble du projet convivial pour les développeurs.
À propos du projet
Les cartes ARM modernes utilisent généralement Linux comme système d'exploitation de référence. Cela nous donne accès à toute l'infrastructure Linux avec tous les progiciels Linux. Nous pouvons, entre autres, héberger un serveur Web sur une carte, utiliser la connectivité Bluetooth, utiliser OpenCV pour la reconnaissance d'images et créer un cluster de cartes.
Ce sont des tâches bien connues qui peuvent être implémentées sur les cartes ARM et qui peuvent être très utiles pour les machines CNC personnalisées. Par exemple, le positionnement automatique à l’aide de Compuvision peut s’avérer très pratique pour certaines machines.
Linux n'est pas un système d'exploitation temps réel. Cela signifie que nous ne pouvons pas générer d'impulsions avec les timings requis pour contrôler les moteurs pas à pas directement à partir des broches de la carte avec un logiciel en cours d'exécution, même en tant que module du noyau. Alors, comment pouvons-nous utiliser les steppers et les fonctionnalités Linux de haut niveau ? Nous pouvons utiliser deux puces : un microcontrôleur avec une implémentation CNC classique et une carte ARM connectée à ce microcontrôleur via UART (récepteur-émetteur asynchrone universel).
Que se passe-t-il s'il n'existe aucune fonctionnalité de micrologiciel adaptée à ce microcontrôleur ? Que se passe-t-il si nous devons contrôler des axes supplémentaires qui ne sont pas implémentés dans le microcontrôleur ? Toute modification du micrologiciel C/C++ existant nécessitera beaucoup de temps et d'efforts de développement. Voyons si nous pouvons faciliter les choses et même économiser de l'argent sur les microcontrôleurs en les supprimant simplement.
PyCNC
PyCNC est un interpréteur de code G haute performance open source gratuit et un contrôleur d'imprimante CNC/3D. Il peut fonctionner sur diverses cartes ARM alimentées par Linux, telles que Raspberry Pi, Odroid, Beaglebone et autres. Cela vous donne la possibilité de choisir n'importe quelle carte et d'utiliser tout ce que propose Linux. Et vous pouvez conserver l’intégralité du temps d’exécution du G-code sur une seule carte sans avoir besoin d’un microcontrôleur séparé pour un fonctionnement en temps réel.
Choisir Python comme langage de programmation principal réduit considérablement la base de code par rapport aux projets C/C++. Il réduit également le code standard et spécifique au microcontrôleur et rend le projet accessible à un public plus large.
Comment ça fonctionne
Le projet utilise le DMA (Direct Memory Access) sur le module matériel de la puce. Il copie simplement le tampon d'états GPIO (General Purpose Input Output) alloué dans la RAM aux registres GPIO réels. Ce processus de copie est synchronisé par l'horloge système et fonctionne de manière totalement indépendante des cœurs du processeur. Ainsi, une séquence d’impulsions pour l’axe du moteur pas à pas est générée en mémoire, puis le DMA les envoie avec précision.
Approfondissons le code pour comprendre les bases et comment accéder aux modules matériels à partir de Python.
GPIO
Un module d'entrée-sortie à usage général contrôle les états des broches. Chaque broche peut avoir un état bas ou haut. Lorsque nous programmons le microcontrôleur, nous utilisons généralement des variables définies par le SDK (kit de développement logiciel) pour écrire sur cette broche. Par exemple, pour activer un état haut pour les broches 1 et 3 :
PORTA = (1 << PIN1) | (1 << PIN3)
Si vous regardez dans le SDK, vous trouverez la déclaration de cette variable, et elle ressemblera à :
#define PORTA (*(volatile uint8_t *)(0x12345678))
C'est juste un pointeur. Il ne pointe pas vers l’emplacement dans la RAM, mais vers l’adresse du processeur physique. Le module GPIO actuel se trouve à cette adresse.
Pour gérer les broches, nous pouvons écrire et lire des données. Le processeur ARM du Raspberry Pi ne fait pas exception et possède le même module. Pour contrôler les broches, nous pouvons écrire/lire des données. Nous pouvons trouver les adresses et les structures de données dans la documentation officielle des périphériques du processeur.
Lorsque nous exécutons un processus dans le runtime de l'utilisateur, le processus démarre dans l'espace d'adressage virtuel. Le périphérique lui-même est accessible directement. Mais nous pouvons toujours accéder à de vraies adresses physiques avec le périphérique '/dev/mem'
.
Voici un code simple en Python qui contrôle l’état d’une broche en utilisant cette approche :
Décomposons-le ligne par ligne :
Lignes 1 à 6 : en-têtes, importations.
Ligne 7 : ouvrez l'accès '/dev/mem'
de l'appareil à l'adresse physique.
Ligne 8 : nous utilisons l'appel système mmap pour mapper un fichier (bien que dans notre cas, ce fichier représente la mémoire physique) dans la mémoire virtuelle du processus. Nous spécifions la longueur et le décalage de la zone de la carte. Pour la longueur, on prend le format de la page. Et le décalage est 0x3F200000
.
La documentation indique que l'adresse bus 0x7E200000
contient des registres GPIO et que nous devons spécifier l'adresse physique. La documentation indique (page 6, paragraphe 1.2.3) que l'adresse du bus 0x7E000000
est mappée à l'adresse physique 0x20000000
, mais cette documentation concerne Raspberry 1.
Veuillez noter que toutes les adresses de bus de module sont les mêmes pour Raspberry Pi 1-3, mais cette carte a été modifiée en 0x3F000000
pour les RPi 2 et 3. L'adresse ici est donc 0x3F200000.
. Pour Raspberry Pi 1, remplacez-le par 0x20200000
.
Après cela, nous pouvons écrire dans la mémoire virtuelle de notre processus, mais celui-ci écrit en réalité dans le module GPIO.
Ligne 9 : fermez le descripteur de fichier, car nous n'avons pas besoin de le stocker.
Lignes 11 à 14 : nous lisons et écrivons sur notre carte avec le décalage 0x08
. Selon la documentation, il s'agit du registre GPFSEL2 GPIO Function Select 2. Et ce registre contrôle les fonctions des broches.
Nous définissons (tout effacer, puis définir avec l'opérateur OR) 3 bits avec le 3ème bit défini sur 001
. Cette valeur signifie que la broche fonctionne comme une sortie. Il existe de nombreuses broches et modes possibles pour celles-ci. C'est pourquoi le registre des modes est divisé en plusieurs registres, chacun contenant les modes pour 10 broches.
Lignes 16 et 22 : configurez le gestionnaire d'interruption « Ctrl+C ».
Ligne 17 : boucle infinie.
Ligne 18 : placez la broche à l'état haut en écrivant dans le registre GPSET0.
Veuillez noter que Raspberry Pi n'a pas de registres comme PORTA (microcontrôleurs AVR). Nous ne pouvons pas écrire l’intégralité de l’état GPIO de toutes les broches. Il n'y a que des registres set et clear qui sont utilisés pour définir et effacer les broches de masque au niveau du bit spécifiées.
Lignes 19 et 21 : retard
Ligne 20 : mettez la broche à l'état bas avec le registre GPCLR0.
Lignes 25 et 26 : basculer la broche par défaut, état d'entrée. Fermez la carte mémoire.
Ce code doit être exécuté avec les privilèges de superutilisateur. Nommez le fichier 'gpio.py'
et exécutez-le avec 'sudo python gpio.py'
. Si une LED est connectée à la broche 21, elle clignotera.
DMLA
Direct Memory Access est un module spécial conçu pour copier des blocs de mémoire d'une zone à une autre. Nous copierons les données de la mémoire tampon vers le module GPIO. Tout d’abord, nous avons besoin d’une zone solide dans la RAM physique qui sera copiée.
Il existe peu de solutions possibles :
- Nous pouvons créer un pilote de noyau simple qui allouera, verrouillera et nous signalera l'adresse de cette mémoire.
- Dans certaines implémentations, la mémoire virtuelle est allouée et utilise
'/proc/self/pagemap'
pour convertir l'adresse en adresse physique. Je ne recommanderais pas cette approche, surtout lorsque nous devons allouer une grande surface. Toute mémoire virtuellement allouée (même verrouillée, voir la documentation du noyau) peut être déplacée vers la zone physique. - Tous les Raspberry Pi ont un périphérique
'/dev/vcio'
, qui fait partie du pilote graphique et peut nous allouer de la mémoire physique. Un exemple officiel montre comment procéder. Et nous pouvons l'utiliser au lieu de créer le nôtre.
Le module DMA lui-même n'est qu'un ensemble de registres situés quelque part à une adresse physique. Nous pouvons contrôler ce module via ces registres. Fondamentalement, il existe des registres source, destination et contrôle. Vérifions un code simple qui montre comment utiliser les modules DMA pour gérer le GPIO.
Étant donné qu'un code supplémentaire est requis pour allouer de la mémoire physique avec '/dev/vcio'
, nous utiliserons un fichier avec une implémentation de classe CMA PhysicalMemory existante. Nous utiliserons également la classe PhysicalMemory, qui effectue l'astuce avec le memap de l'exemple précédent.
Décomposons-le ligne par ligne :
Lignes 1 à 3 : en-têtes, importations.
Lignes 5 à 6 : constantes avec le numéro DMA du canal et la broche GPIO que nous utiliserons.
Lignes 8 à 15 : initialisez la broche GPIO spécifiée en tant que sortie et allumez-la pendant une demi-seconde pour un contrôle visuel. En fait, c’est la même chose que nous avons fait dans l’exemple précédent, écrit de manière plus pythonique.
Ligne 17 : alloue 64
octets dans la mémoire physique.
Ligne 18 : crée des structures spéciales — des blocs de contrôle pour le module DMA. Les lignes suivantes cassent la structure de ce bloc. Chaque champ a une longueur de 32
bits.
Ligne 19 : transfère les drapeaux d'information. Vous pouvez trouver une description complète de chaque drapeau à la page 50 de la documentation officielle.
Ligne 20 : adresse source. Cette adresse doit être une adresse de bus, nous appelons donc get_bus_address()
. Le bloc de contrôle DMA doit être aligné sur 32 octets, mais la taille de ce bloc est de 24
octets. Nous disposons donc de 8 octets que nous utilisons comme stockage.
Ligne 21 : adresse de destination. Dans notre cas, il s’agit de l’adresse du registre SET du module GPIO.
Ligne 22 : longueur de transmission — 4
octets.
Ligne 23 : foulée. Nous n'utilisons pas cette fonctionnalité, définissez 0
.
Ligne 24 : adresse du prochain bloc de contrôle, dans notre cas, les 32 octets suivants.
Ligne 25 : remplissage. Mais puisque nous avons utilisé cette adresse comme source de données, mettez-en un peu, ce qui devrait déclencher le GPIO.
Ligne 26 : remplissage.
Lignes 28 à 37 : remplissez le deuxième bloc de contrôle DMA. La différence est que nous écrivons dans le registre CLEAR GPIO et définissons notre premier bloc comme prochain bloc de contrôle pour boucler la transmission.
Lignes 38 à 39 : écriture des blocs de contrôle dans la mémoire physique.
Ligne 41 : récupère l'objet module DMA avec le canal sélectionné.
Lignes 42-43 : réinitialisez le module DMA.
Ligne 44 : précisez l'adresse du premier bloc.
Ligne 45 : exécutez le module DMA.
Lignes 49 à 52 : nettoyer. Arrêtez le module DMA et basculez la broche GPIO à l'état par défaut.
Connectons l'oscilloscope à la broche spécifiée et exécutons cette application (n'oubliez pas les privilèges sudo). Nous observerons des impulsions carrées de ~1,5 MHz :
Les défis du DMA
Il y a plusieurs choses que vous devez prendre en considération avant de construire une véritable machine CNC.
Premièrement, la taille du tampon DMA peut atteindre des centaines de mégaoctets.
Deuxièmement, le module DMA est conçu pour une copie rapide des données. Si plusieurs canaux DMA fonctionnent, nous pouvons dépasser la bande passante mémoire et le tampon sera copié avec des retards pouvant provoquer des tremblements dans les impulsions de sortie. Il est donc préférable d’avoir un mécanisme de synchronisation.
Pour surmonter ce problème, j'ai créé une conception spéciale pour les blocs de contrôle :
L'oscillogramme en haut de l'image montre les états GPIO souhaités. Les blocs ci-dessous représentent les blocs de contrôle DMA qui génèrent cette forme d'onde. « Delay 1 » spécifie la durée de l'impulsion et « Delay 2 » est la durée de la pause entre les impulsions. Avec cette approche, la taille du tampon dépend uniquement du nombre d’impulsions.
Par exemple, pour une machine avec une course de 200 mm et 400 impulsions par mm, chaque impulsion prendrait 128 octets (4 blocs de contrôle pour 32 octets) et la taille totale serait d'environ 9,8 Mo. Nous aurions plus d’un axe, mais la plupart des impulsions se produiraient en même temps. Et ce serait des dizaines de mégaoctets, pas des centaines.
J'ai résolu le deuxième défi, lié à la synchronisation, en introduisant des délais temporaires via les blocs de contrôle. Le module DMA a une particularité : il peut attendre un signal spécial prêt du module sur lequel il écrit les données. Le module le plus approprié pour nous est le module PWM (modulation de largeur d'impulsion), qui nous aidera également à la synchronisation.
Le module PWM peut sérialiser les données et les envoyer à vitesse fixe. Dans ce mode, il génère un signal prêt pour le tampon FIFO (premier entré, premier sorti) du module PWM. Alors, écrivons les données dans le module PWM et utilisons-les uniquement pour la synchronisation.
Fondamentalement, nous aurions besoin d'activer un indicateur spécial dans le mappage perceptuel de l'indicateur d'informations de transfert, puis d'exécuter le module PWM avec la fréquence souhaitée. La mise en œuvre est assez longue — vous pouvez l'étudier vous-même.
Créons plutôt un code simple qui peut utiliser le module existant pour générer des impulsions précises.
import rpgpio
PIN=21PINMASK = 1 << PINPULSE_LENGTH_US = 1000PULSE_DELAY_US = 1000DELAY_US = 2000 g = rpgpio.GPIO()g.init(PIN, rpgpio.GPIO.MODE_OUTPUT) dma = rpgpio.DMAGPIO()for i in range(1, 6): for i in range(0, i): dma.add_pulse(PINMASK, PULSE_LENGTH_US) dma.add_delay(PULSE_DELAY_US) dma.add_delay(DELAY_US)dma.run(True) raw_input(“Press Enter to stop”)dma.stop()g.init(PIN, rpgpio.GPIO.MODE_INPUT_NOPULL)
Le code est assez simple et il n’est pas nécessaire de le décomposer. Si vous exécutez ce code et connectez un oscilloscope, vous verrez :
Et maintenant, nous pouvons créer un véritable interpréteur de code G et contrôler des moteurs pas à pas. Mais attendez! C'est déjà implémenté ici. Vous pouvez utiliser ce projet, car il est distribué sous licence MIT.
Matériel
Le projet Python peut être adopté pour vos besoins. Mais afin de vous inspirer, je vais décrire la mise en œuvre matérielle originale de ce projet : une imprimante 3D. Il contient essentiellement les composants suivants :
- Framboise Pi 3
- Carte RAMPSv1.4
- 4 modules A4988 ou DRV8825
- Cadre RepRap Prusa i3 avec équipement (butées de fin de course, moteurs, chauffages et capteurs)
- Bloc d'alimentation 12V 15A
- Module convertisseur abaisseur DC-DC LM2596S
- Puce MAX4420
- Module convertisseur analogique-numérique ADS1115
- Câble ruban IDE UDMA133
- Verre acrylique
- Supports pour PCB
- Jeu de connecteurs au pas de 2,54 mm
Le câble ruban IDE 40 broches convient au connecteur Raspberry Pi 40 broches, mais l'extrémité opposée nécessite un peu de travail. Coupez le connecteur existant de l'extrémité opposée et sertissez les connecteurs sur les fils du câble.
La carte RAMPSv1.4 a été initialement conçue pour être connectée au connecteur Arduino Mega, il n'existe donc pas de moyen simple de connecter cette carte au Raspberry Pi. La méthode suivante vous permet de simplifier la connexion des cartes. Vous devrez connecter moins de 40 fils.
J'espère que ce schéma de connexion est assez simple et facile à reproduire. Il est préférable de connecter quelques broches (2ème extrudeuse, servos) pour une utilisation future, même si elles ne sont pas nécessaires actuellement.
Vous vous demandez peut-être pourquoi avons-nous besoin de la puce MAX4420 ? Les broches du Raspberry Pi fournissent 3,3 V pour les sorties GPIO et les broches peuvent fournir un très faible courant. Il ne suffit pas de changer la porte du MOSFET (Metal Oxide Semiconductor Field Effect Transistor). De plus, l'un des MOSFET fonctionne sous la charge de 10 A d'un radiateur de lit. De ce fait, avec une connexion directe à un Raspberry Pi, ce transistor va surchauffer. Par conséquent, il est préférable de connecter un pilote MOSFET spécial entre le MOSFET hautement chargé et le Raspberry Pi. Il peut commuter le MOSFET de manière efficace et réduire son échauffement.
L'ADS1115 est un convertisseur analogique-numérique (ADC). Étant donné que Raspberry Pi n'a pas de module ADC intégré, j'en ai utilisé un externe pour mesurer la température des thermistances de 100 000 Ohms. Le module RAMPSv1.4 dispose déjà d'un diviseur de tension pour les thermistances. Le convertisseur abaisseur LM2596S doit être réglé sur une sortie de 5 V et il est utilisé pour alimenter la carte Raspberry Pi elle-même.
Il peut maintenant être monté sur le cadre de l'imprimante 3D et la carte RAMPSv1.4 doit être connectée au cadre équipé.
C'est ça. L'imprimante 3D est assemblée et vous pouvez copier le code source sur le Raspberry Pi et l'exécuter. sudo ./pycnc
l'exécutera dans un shell G-Code interactif. sudo ./pycnc filename.gcode
exécutera un fichier G Code. Vérifiez la configuration prête pour Slic3r.
Et dans cette vidéo, vous pouvez voir comment cela fonctionne réellement.
Si vous avez trouvé cet article utile, applaudissez-moi pour que davantage de personnes le voient. Merci!
L'IoT consiste à prototyper rapidement des idées. Pour rendre cela possible, nous avons développé DeviceHive, une plateforme IoT/M2M open source. DeviceHive fournit une base solide et des éléments de base pour créer n'importe quelle solution IoT/M2M, comblant le fossé entre le développement intégré, les plates-formes cloud, le big data et les applications client.