Πώς μπορείτε να χρησιμοποιήσετε το Python για να δημιουργήσετε τον δικό σας ελεγκτή CNC και τρισδιάστατο εκτυπωτή

Αυτό το άρθρο ασχολείται με τη διαδικασία που χρησιμοποίησα για να δημιουργήσω την πρώτη υλοποίηση ελεγκτή μηχανών CNC σε καθαρό Python.

Οι ελεγκτές μηχανών αριθμητικού ελέγχου υπολογιστή (CNC) εφαρμόζονται συνήθως χρησιμοποιώντας τη γλώσσα προγραμματισμού C ή C ++. Λειτουργούν σε λειτουργικά συστήματα χωρίς λειτουργικό σύστημα ή σε πραγματικό χρόνο με απλούς μικροελεγκτές.

Σε αυτό το άρθρο, θα περιγράψω πώς να φτιάξετε έναν ελεγκτή CNC - ειδικότερα έναν 3D εκτυπωτή - χρησιμοποιώντας σύγχρονες πλακέτες ARM (Raspberry Pi) με μια σύγχρονη γλώσσα υψηλού επιπέδου (Python).

Μια τέτοια σύγχρονη προσέγγιση ανοίγει ένα ευρύ φάσμα επιλογών ολοκλήρωσης με άλλες τεχνολογίες αιχμής, λύσεις και υποδομές. Αυτό καθιστά ολόκληρο το έργο φιλικό για προγραμματιστές.

Σχετικά με το Έργο

Οι σύγχρονοι πίνακες ARM χρησιμοποιούν συνήθως το Linux ως λειτουργικό σύστημα αναφοράς. Αυτό μας δίνει πρόσβαση σε ολόκληρη την υποδομή Linux με όλα τα πακέτα λογισμικού Linux. Μπορούμε να φιλοξενήσουμε έναν διακομιστή ιστού σε έναν πίνακα, να χρησιμοποιήσουμε τη σύνδεση Bluetooth, να χρησιμοποιήσουμε το OpenCV για αναγνώριση εικόνας και να δημιουργήσουμε ένα σύμπλεγμα πινάκων, μεταξύ άλλων.

Αυτές είναι γνωστές εργασίες που μπορούν να εφαρμοστούν σε πίνακες ARM και μπορούν να είναι πραγματικά χρήσιμες για προσαρμοσμένες μηχανές CNC. Για παράδειγμα, η αυτόματη τοποθέτηση μέσω υπολογιστή μπορεί να είναι πολύ βολική για ορισμένα μηχανήματα.

Το Linux δεν είναι λειτουργικό σύστημα σε πραγματικό χρόνο. Αυτό σημαίνει ότι δεν μπορούμε να παράγουμε παλμούς με τους απαιτούμενους χρονισμούς για να ελέγξουμε τους κινητήρες stepper απευθείας από τις καρφίτσες πλακέτας με τρέχον λογισμικό, ακόμη και ως μονάδα πυρήνα. Λοιπόν, πώς μπορούμε να χρησιμοποιήσουμε steppers και λειτουργίες Linux υψηλού επιπέδου; Μπορούμε να χρησιμοποιήσουμε δύο μάρκες - έναν μικροελεγκτή με κλασική εφαρμογή CNC και μια πλακέτα ARM συνδεδεμένη σε αυτόν τον μικροελεγκτή μέσω UART (καθολικός ασύγχρονος δέκτης-πομπός).

Τι γίνεται αν δεν υπάρχουν κατάλληλες δυνατότητες υλικολογισμικού για αυτόν τον μικροελεγκτή; Τι γίνεται αν χρειαστεί να ελέγξουμε επιπλέον άξονες που δεν εφαρμόζονται στον μικροελεγκτή; Τυχόν τροποποιήσεις στο υπάρχον υλικολογισμικό C / C ++ θα απαιτούν άφθονο χρόνο και προσπάθειες ανάπτυξης. Ας δούμε αν μπορούμε να το κάνουμε πιο εύκολο και ακόμη και να εξοικονομήσουμε χρήματα σε μικροελεγκτές απλώς αφαιρώντας τα.

PyCNC

Το PyCNC είναι ένας δωρεάν διερμηνέας υψηλής απόδοσης ανοιχτού κώδικα G-code και ένας ελεγκτής CNC / 3D-εκτυπωτή. Μπορεί να τρέξει σε διάφορους πίνακες που βασίζονται σε Linux, βασισμένες σε ARM, όπως Raspberry Pi, Odroid, Beaglebone και άλλα. Αυτό σας δίνει την ευελιξία να διαλέξετε οποιονδήποτε πίνακα και να χρησιμοποιήσετε όλα όσα προσφέρει το Linux. Και μπορείτε να διατηρήσετε ολόκληρο τον χρόνο εκτέλεσης του κώδικα G σε έναν πίνακα χωρίς να απαιτείται ξεχωριστός μικροελεγκτής για λειτουργία σε πραγματικό χρόνο.

Η επιλογή της Python ως κύρια γλώσσα προγραμματισμού μειώνει σημαντικά τη βάση κώδικα σε σύγκριση με τα έργα C / C ++. Επίσης, μειώνει το boilerplate και τον ειδικό κωδικό μικροελεγκτή και καθιστά το έργο προσβάσιμο σε ένα ευρύτερο κοινό.

Πως δουλεύει

Το έργο χρησιμοποιεί DMA (Direct Memory Access) στη μονάδα υλικού chip. Αντιγράφει απλώς την κατάσταση buffer GPIO (General Purpose Input Output) που έχει εκχωρηθεί σε RAM στους πραγματικούς καταχωρητές GPIO. Αυτή η διαδικασία αντιγραφής συγχρονίζεται από το ρολόι συστήματος και λειτουργεί εντελώς ανεξάρτητα από τους πυρήνες της CPU. Έτσι, μια ακολουθία παλμών για τον άξονα του βηματικού κινητήρα δημιουργείται στη μνήμη και στη συνέχεια το DMA τους στέλνει ακριβώς.

Ας σκάψουμε βαθύτερα στον κώδικα για να κατανοήσουμε τα βασικά και πώς να αποκτήσουμε πρόσβαση σε μονάδες υλικού από την Python.

GPIO

Μια μονάδα εξόδου γενικού σκοπού ελέγχει τις καταστάσεις ακίδων Κάθε ακίδα μπορεί να έχει χαμηλή ή υψηλή κατάσταση. Όταν προγραμματίζουμε τον μικροελεγκτή, συνήθως χρησιμοποιούμε SDK (κιτ ανάπτυξης λογισμικού) καθορισμένες μεταβλητές για να γράψουμε σε αυτόν τον πείρο. Για παράδειγμα, για να ενεργοποιήσετε μια υψηλή κατάσταση για τις ακίδες 1 και 3:

PORTA = (1 << PIN1) | (1 << PIN3)

Αν κοιτάξετε στο SDK, θα βρείτε τη δήλωση αυτής της μεταβλητής και θα μοιάζει με:

#define PORTA (*(volatile uint8_t *)(0x12345678))

Είναι απλά ένας δείκτης. Δεν δείχνει τη θέση στη μνήμη RAM, αλλά τη διεύθυνση του φυσικού επεξεργαστή. Η πραγματική μονάδα GPIO βρίσκεται σε αυτήν τη διεύθυνση.

Για τη διαχείριση των καρφιτσών, μπορούμε να γράφουμε και να διαβάζουμε δεδομένα. Ο επεξεργαστής ARM του Raspberry Pi δεν αποτελεί εξαίρεση και έχει την ίδια μονάδα. Για τον έλεγχο των καρφιτσών, μπορούμε να γράψουμε / διαβάσουμε δεδομένα. Μπορούμε να βρούμε τις διευθύνσεις και τις δομές δεδομένων στην επίσημη τεκμηρίωση για περιφερειακά επεξεργαστών.

Όταν εκτελούμε μια διαδικασία στο χρόνο εκτέλεσης του χρήστη, η διαδικασία ξεκινά στον εικονικό χώρο διευθύνσεων. Το πραγματικό περιφερειακό είναι προσβάσιμο απευθείας. Ωστόσο, μπορούμε να έχουμε πρόσβαση σε πραγματικές φυσικές διευθύνσεις με ‘/dev/mem’συσκευή.

Ακολουθεί ένας απλός κώδικας στο Python που ελέγχει την κατάσταση ενός pin χρησιμοποιώντας αυτήν την προσέγγιση:

Ας το σπάσουμε ανά γραμμή:

Γραμμές 1–6 : κεφαλίδες, εισαγωγές.

Γραμμή 7 : ανοιχτή ‘/dev/mem’ πρόσβαση συσκευής στη φυσική διεύθυνση.

Γραμμή 8 : χρησιμοποιούμε την κλήση συστήματος mmap για να χαρτογραφήσουμε ένα αρχείο (αν και στην περίπτωσή μας, αυτό το αρχείο αντιπροσωπεύει φυσική μνήμη) στην εικονική μνήμη της διαδικασίας. Καθορίζουμε το μήκος και την μετατόπιση της περιοχής του χάρτη. Για το μήκος, παίρνουμε το μέγεθος της σελίδας. Και το όφσετ είναι 0x3F200000.

Η τεκμηρίωση αναφέρει ότι η διεύθυνση του διαύλου0x7E200000 περιέχει καταχωρητές GPIO και πρέπει να προσδιορίσουμε τη φυσική διεύθυνση. Η τεκμηρίωση λέει (σελίδα 6, παράγραφος 1.2.3) ότι η 0x7E000000διεύθυνση του διαύλου αντιστοιχεί στη 0x20000000φυσική διεύθυνση, αλλά αυτή η τεκμηρίωση προορίζεται για το Raspberry 1.

Λάβετε υπόψη ότι όλες οι διευθύνσεις διαύλου μονάδας είναι ίδιες για το Raspberry Pi 1-3, αλλά αυτός ο χάρτης άλλαξε σε 0x3F000000RPi 2 και 3. Έτσι, η διεύθυνση εδώ είναι 0x3F200000. Για το Raspberry Pi 1, αλλάξτε το σε 0x20200000.

Μετά από αυτό, μπορούμε να γράψουμε στην εικονική μνήμη της διαδικασίας μας, αλλά γράφει στην ενότητα GPIO.

Γραμμή 9 : Κλείστε τη λαβή του αρχείου, καθώς δεν χρειάζεται να την αποθηκεύσουμε.

Γραμμές 11–14 : διαβάζουμε και γράφουμε στον χάρτη μας με την 0x08αντιστάθμιση. Σύμφωνα με την τεκμηρίωση, είναι ο καταχωρητής GPFSEL2 GPIO Function Select 2. Και αυτός ο καταχωρητής ελέγχει τις λειτουργίες καρφιτσών.

Ρυθμίζουμε (διαγραφή όλων, μετά ρυθμίστε με τον χειριστή Ή) 3 bit με το 3ο bit ρυθμισμένο σε 001. Αυτή η τιμή σημαίνει ότι ο πείρος λειτουργεί ως έξοδος. Υπάρχουν πολλές καρφίτσες και πιθανές λειτουργίες για αυτούς. Αυτός είναι ο λόγος για τον οποίο ο καταχωρητής λειτουργιών χωρίζεται σε διάφορους καταχωρητές, όπου ο καθένας περιέχει τους τρόπους λειτουργίας για 10 ακίδες.

Γραμμές 16 και 22 : ρυθμίστε το χειριστή διακοπής «Ctrl + C».

Γραμμή 17 : άπειρος βρόχος.

Γραμμή 18 : ρυθμίστε τον πείρο στην υψηλή κατάσταση γράφοντας στον καταχωρητή GPSET0.

Λάβετε υπόψη ότι το Raspberry Pi δεν έχει καταχωρητές όπως το PORTA (AVR μικροελεγκτές). Δεν μπορούμε να γράψουμε ολόκληρη την κατάσταση GPIO όλων των καρφιτσών. Εκεί ακριβώς που και σαφείς μητρώα τα οποία χρησιμοποιούνται για την ομάδα και να καθαρίσει καθορίζονται με καρφίτσες μάσκα bitwise.

Γραμμές 19 και 21 : καθυστέρηση

Γραμμή 20 : ορίστε τον πείρο σε χαμηλή κατάσταση με τον καταχωρητή GPCLR0.

Γραμμές 25 και 26 : εναλλαγή του πείρου στην προεπιλεγμένη κατάσταση εισόδου. Κλείστε το χάρτη μνήμης.

Αυτός ο κωδικός πρέπει να εκτελείται με δικαιώματα superuser. Ονομάστε το αρχείο ‘gpio.py’ και εκτελέστε το ‘sudo python gpio.py’. Εάν έχετε συνδέσει ένα LED στον ακροδέκτη 21, θα αναβοσβήνει.

DMA

Η άμεση πρόσβαση μνήμης είναι μια ειδική μονάδα που έχει σχεδιαστεί για να αντιγράφει μπλοκ μνήμης από τη μία περιοχή στην άλλη. Θα αντιγράψουμε δεδομένα από το buffer μνήμης στη μονάδα GPIO. Πρώτα απ 'όλα, χρειαζόμαστε μια σταθερή περιοχή στη φυσική μνήμη RAM που θα αντιγραφεί.

Υπάρχουν μερικές πιθανές λύσεις:

  1. Μπορούμε να δημιουργήσουμε ένα απλό πρόγραμμα οδήγησης πυρήνα που θα εκχωρήσει, θα κλειδώσει και θα μας αναφέρει τη διεύθυνση αυτής της μνήμης.
  2. Σε ορισμένες εφαρμογές, η εικονική μνήμη εκχωρείται και χρησιμοποιεί ‘/proc/self/pagemap’για τη μετατροπή της διεύθυνσης σε φυσική. Δεν θα συνιστούσα αυτήν την προσέγγιση, ειδικά όταν πρέπει να διαθέσουμε μεγάλη έκταση. Οποιαδήποτε σχεδόν κατανεμημένη μνήμη (ακόμη και κλειδωμένη, δείτε την τεκμηρίωση του πυρήνα) μπορεί να μετακινηθεί στη φυσική περιοχή.
  3. Όλα τα Raspberry Pi διαθέτουν μια ‘/dev/vcio’συσκευή, η οποία αποτελεί μέρος του προγράμματος οδήγησης γραφικών και μπορεί να διαθέσει φυσική μνήμη για εμάς. Ένα επίσημο παράδειγμα δείχνει πώς να το κάνουμε. Και μπορούμε να το χρησιμοποιήσουμε αντί να δημιουργήσουμε το δικό μας.

Η ίδια η μονάδα DMA είναι απλώς ένα σύνολο καταχωρητών που βρίσκονται κάπου σε μια φυσική διεύθυνση. Μπορούμε να ελέγξουμε αυτήν την ενότητα μέσω αυτών των καταχωρητών. Βασικά, υπάρχουν καταχωρητές πηγών, προορισμού και ελέγχου. Ας δούμε έναν απλό κώδικα που δείχνει πώς να χρησιμοποιήσετε τις μονάδες DMA για τη διαχείριση του GPIO.

Δεδομένου ότι απαιτείται πρόσθετος κώδικας για την εκχώρηση φυσικής μνήμης ‘/dev/vcio’, θα χρησιμοποιήσουμε ένα αρχείο με υπάρχουσα υλοποίηση κλάσης CMA PhysicalMemory. Θα χρησιμοποιήσουμε επίσης την κλάση PhysicalMemory, η οποία εκτελεί το κόλπο με memap από το προηγούμενο δείγμα.

Ας το σπάσουμε ανά γραμμή:

Γραμμές 1-3 : κεφαλίδες, εισαγωγές.

Γραμμές 5–6 : σταθερές με τον αριθμό καναλιού DMA και την καρφίτσα GPIO που θα χρησιμοποιήσουμε.

Γραμμές 8–15 : αρχικοποιήστε τον καθορισμένο ακροδέκτη GPIO ως έξοδο και ανάψτε το για μισό δευτερόλεπτο για οπτικό έλεγχο. Στην πραγματικότητα, είναι το ίδιο πράγμα που κάναμε στο προηγούμενο παράδειγμα, γραμμένο με πιο πυθικό τρόπο.

Γραμμή 17 : εκχωρεί 64byte στη φυσική μνήμη.

Line 18: creates special structures — control blocks for the DMA module. The following lines break the structure of this block. Each field has a length of 32 bit.

Line 19: transfers information flags. You can find a full description of each flag on page 50 of the official documentation.

Line 20: source address. This address must be a bus address, so we call get_bus_address(). The DMA control block must be aligned by 32 bytes, but the size of this block is 24 bytes. So we have 8 bytes, which we use as storage.

Line 21: destination address. In our case, it’s the address of the SET register of the GPIO module.

Line 22: transmission length — 4 bytes.

Line 23: stride. We do not use this feature, set 0.

Line 24: address of the next control block, in our case, next 32 bytes.

Line 25: padding. But since we used this address as a data source, put a bit, which should trigger GPIO.

Line 26: padding.

Lines 28–37: fill in the second DMA control block. The difference is that we write to CLEAR GPIO register and set our first block as a next control block to loop the transmission.

Lines 38–39: write control blocks to physical memory.

Line 41: get the DMA module object with the selected channel.

Lines 42–43: reset the DMA module.

Line 44: specify the address of the first block.

Line 45: run the DMA module.

Lines 49–52: clean up. Stop the DMA module and switch the GPIO pin to the default state.

Let’s connect the oscilloscope to the specified pin and run this application (do not forget about sudo privileges). We will observe ~1.5 MHz square pulses:

DMA challenges

There are several things that you should take into consideration before building a real CNC machine.

First, the size of the DMA buffer can be hundreds of megabytes.

Second, the DMA module is designed for a fast data copying. If several DMA channels are working, we can go beyond the memory bandwidth, and buffer will be copied with delays that can cause jitters in the output pulses. So, it’s better to have some synchronization mechanism.

To overcome this, I created a special design for control blocks:

The oscillogram at the top of the image shows the desired GPIO states. The blocks below represent the DMA control blocks that generate this waveform. “Delay 1” specifies the pulse length, and “Delay 2” is the pause length between pulses. With this approach, the buffer size depends only on the number of pulses.

For example, for a machine with 200mm travel length and 400 pulses per mm, each pulse would take 128 bytes (4 control blocks per 32 bytes), and the total size will be ~9.8MB. We would have more than one axis, but most of the pulses would occur at the same time. And it would be dozens of megabytes, not hundreds.

I solved the second challenge, related to synchronization, by introducing temporary delays through the control blocks. The DMA module has a special feature: it can wait for a special ready signal from the module where it writes data. The most suitable module for us is the PWM (pulse width modulation) module, which will also help us with synchronization.

The PWM module can serialize the data and send it with fixed speed. In this mode, it generates a ready signal for the FIFO (first in, first out) buffer of the PWM module. So, let’s write data to the PWM module and use it only for synchronization.

Basically, we would need to enable a special flag in the perceptual mapping of the transfer information flag, and then run the PWM module with the desired frequency. The implementation is quite long — you can study it yourself.

Instead, let’s create some simple code that can use the existing module to generate precise pulses.

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)

The code is pretty simple, and there is no need to break it down. If you run this code and connect an oscilloscope, you will see:

And now we can create real G-code interpreter and control stepper motors. But wait! It is already implemented here. You can use this project, as it’s distributed under the MIT license.

Hardware

The Python project can be adopted for your purposes. But in order to inspire you, I will describe the original hardware implementation of this project — a 3D printer. It basically contains the following components:

  1. Raspberry Pi 3
  2. RAMPSv1.4 board
  3. 4 A4988 or DRV8825 module
  4. RepRap Prusa i3 frame with equipment (end-stops, motors, heaters, and sensors)
  5. 12V 15A power supply unit
  6. LM2596S DC-DC step down converter module
  7. MAX4420 chip
  8. ADS1115 analog to digital converter module
  9. UDMA133 IDE ribbon cable
  10. Acrylic glass
  11. PCB stands
  12. Set of connectors with 2.54mm step

The 40-pin IDE ribbon cable is suitable for the Raspberry Pi 40 pins connector, but the opposite end requires some work. Cut off the existing connector from the opposite end and crimp connectors to the cable wires.

The RAMPSv1.4 board was originally designed for connection to the Arduino Mega connector, so there is no easy way to connect this board to the Raspberry Pi. The following method allows you to simplify the boards connection. You will need to connect less than 40 wires.

I hope this connection diagram is fairly simple and easily duplicated. It’s better to connect some pins (2nd extruder, servos) for future use, even if they are not currently needed.

You might be wondering — why do we need the MAX4420 chip? The Raspberry Pi pins provide 3.3V for the GPIO outputs, and the pins can provide very small current. It’s not enough to switch the MOSFET (Metal Oxide Semiconductor Field Effect Transistor) gate. In addition, one of the MOSFETs works under the 10A load of a bed heater. As a result, with a direct connection to a Raspberry Pi, this transistor will overheat. Therefore, it is better to connect a special MOSFET driver between the highly loaded MOSFET and Raspberry Pi. It can switch the MOSFET in a an efficient way and reduce its heating.

The ADS1115 is an Analog to Digital Converter (ADC). Since Raspberry Pi doesn’t have an embedded ADC module, I used an external one to measure the temperature from the 100k Ohm thermistors. The RAMPSv1.4 module already has a voltage divider for the thermistors. The LM2596S step down converter must be adjusted to a 5V output, and it is used to power the Raspberry Pi board itself.

Now it can be mounted on the 3D printer frame and the RAMPSv1.4 board should be connected to the equipped frame.

That’s it. The 3D printer is assembled, and you can copy the source code to the Raspberry Pi and run it. sudo ./pycnc will run it in an interactive G-Code shell. sudo ./pycnc filename.gcode will run a G Code file. Check the ready config for Slic3r.

And in this video, you can see how it actually works.

Εάν βρήκατε αυτό το άρθρο χρήσιμο, παρακαλώ δώστε μου χειροκροτήματα, ώστε να το βλέπουν περισσότεροι. Ευχαριστώ!

Το IoT αφορά τα πρωτότυπα ιδέες γρήγορα. Για να το καταστήσουμε δυνατό, αναπτύξαμε το DeviceHive, μια πλατφόρμα IoT / M2M ανοιχτού κώδικα. Το DeviceHive παρέχει ένα σταθερό θεμέλιο και δομικά στοιχεία για τη δημιουργία οποιασδήποτε λύσης IoT / M2M, γεφυρώνοντας το χάσμα μεταξύ ενσωματωμένης ανάπτυξης, πλατφορμών cloud, μεγάλων δεδομένων και εφαρμογών πελατών.