
Ovaj članak raspravlja o procesu koji sam koristio za izgradnju prve implementacije CNC strojnog kontrolera na čistom Pythonu.
Kontroleri računala s numeričkim upravljanjem (CNC) obično se implementiraju pomoću programskog jezika C ili C ++. Rade na operativnim sustavima bez OS-a ili u stvarnom vremenu s jednostavnim mikrokontrolerom.
U ovom ću članku opisati kako izraditi CNC kontroler - posebno 3D printer - koristeći moderne ARM ploče (Raspberry Pi) s modernim jezikom visoke razine (Python).
Takav moderan pristup otvara širok spektar mogućnosti integracije s ostalim vrhunskim tehnologijama, rješenjima i infrastrukturama. To čini cijeli projekt pogodnim za programere.
O projektu
Moderne ARM ploče obično koriste Linux kao referentni operativni sustav. To nam daje pristup cjelokupnoj Linux infrastrukturi sa svim Linuxovim softverskim paketima. Možemo ugostiti web poslužitelj na ploči, koristiti Bluetooth vezu, koristiti OpenCV za prepoznavanje slika i, između ostalog, izgraditi skup ploča.
To su dobro poznati zadaci koji se mogu implementirati na ARM ploče, a mogu biti stvarno korisni za prilagođene CNC strojeve. Na primjer, automatsko pozicioniranje pomoću Compuvision-a može biti vrlo korisno za neke strojeve.
Linux nije operativni sustav u stvarnom vremenu. To znači da ne možemo generirati impulse s potrebnim vremenskim intervalima za upravljanje koračnim motorima izravno s pinova s upravljačkim softverom, čak ni kao modul jezgre. Pa, kako možemo koristiti stepere i značajke Linuxa na visokoj razini? Možemo koristiti dva čipa - jedan mikrokontroler s klasičnom CNC implementacijom i ARM ploču spojenu na ovaj mikrokontroler putem UART-a (univerzalni asinkroni prijemnik-odašiljač).
Što ako za ovaj mikrokontroler nema prikladnih značajki firmvera? Što ako trebamo kontrolirati dodatne osi koje nisu implementirane u mikrokontroler? Sve izmjene postojećeg C / C ++ firmwarea zahtijevat će puno vremena za razvoj i napore. Pogledajmo možemo li to olakšati, pa čak i uštedjeti novac na mikrokontrolerima jednostavnim uklanjanjem.
PyCNC
PyCNC je besplatni prevoditelj G-kodova visokih performansi otvorenog koda i kontroler CNC / 3D pisača. Može se pokretati na raznim pločama koje se temelje na ARM-u, kao što su Raspberry Pi, Odroid, Beaglebone i druge. To vam daje fleksibilnost da odaberete bilo koju ploču i koristite sve što Linux nudi. A možete držati cijelo vrijeme izvođenja G-koda na jednoj ploči bez potrebe za zasebnim mikrokontrolerom za rad u stvarnom vremenu.
Odabir Pythona kao glavnog programskog jezika značajno smanjuje bazu koda u odnosu na C / C ++ projekte. Također smanjuje šifru specifičnu za tipku i mikrokontrolere, a projekt čini dostupnim široj publici.
Kako radi
Projekt koristi DMA (Direct Memory Access) na hardverskom modulu čipa. Jednostavno kopira međuspremnik stanja GPIO (General Purpose Input Output) dodijeljen u RAM-u u stvarne GPIO registre. Ovaj postupak kopiranja sinkronizira sistemski sat i radi potpuno neovisno od jezgri procesora. Dakle, u memoriji se generira slijed impulsa za os koračnog motora, a zatim ih DMA precizno šalje.
Za dublje istražimo kod kako bismo razumjeli osnove i kako pristupiti hardverskim modulima iz Pythona.
GPIO
Ulazni izlazni modul opće namjene kontrolira stanja pinova. Svaki pin može imati nisko ili visoko stanje. Kada programiramo mikrokontroler, obično koristimo SDK (komplet za razvoj softvera) definirane varijable za pisanje na taj pin. Na primjer, da biste omogućili visoko stanje za pinove 1 i 3:
PORTA = (1 << PIN1) | (1 << PIN3)
Ako pogledate SDK, pronaći ćete deklaraciju ove varijable, a ona će izgledati slično:
#define PORTA (*(volatile uint8_t *)(0x12345678))
To je samo pokazivač. Ne ukazuje na mjesto u RAM-u, već na adresu fizičkog procesora. Stvarni GPIO modul nalazi se na ovoj adresi.
Da bismo upravljali iglama, možemo pisati i čitati podatke. ARM procesor Raspberry Pi nije iznimka i ima isti modul. Za kontrolu pinova možemo pisati / čitati podatke. Adrese i strukture podataka možemo pronaći u službenoj dokumentaciji za periferne uređaje procesora.
Kada postupak pokrenemo u korisničkom izvođenju, postupak započinje u virtualnom adresnom prostoru. Stvarni periferni uređaj dostupan je izravno. Ali stvarnim fizičkim adresama i dalje možemo pristupiti pomoću ‘/dev/mem’
uređaja.
Evo nekoliko jednostavnih kodova u Pythonu koji kontroliraju stanje pina pomoću ovog pristupa:
Podijelimo to red po red:
Redci 1–6 : zaglavlja, uvoz.
Redak 7 : otvorite ‘/dev/mem’
pristup uređaju fizičkoj adresi.
Redak 8 : koristimo sistemski poziv mmap za mapiranje datoteke (iako u našem slučaju ova datoteka predstavlja fizičku memoriju) u virtualnu memoriju procesa. Određujemo duljinu i pomak područja karte. Za duljinu uzimamo veličinu stranice. A pomak je 0x3F200000
.
Dokumentacija kaže da adresa sabirnice0x7E200000
sadrži GPIO registre, a mi moramo navesti fizičku adresu. Dokumentacija kaže (stranica 6, paragraf 1.2.3) da je 0x7E000000
adresa sabirnice preslikana na 0x20000000
fizičku adresu, ali ova je dokumentacija za Raspberry 1.
Napominjemo da su sve adrese sabirnice modula iste za Raspberry Pi 1–3, ali ova je karta promijenjena u 0x3F000000
za RPi 2 i 3. Dakle, ovdje je adresa 0x3F200000
. Za Raspberry Pi 1 promijenite je u 0x20200000
.
Nakon toga možemo pisati u virtualnu memoriju našeg procesa, ali zapravo zapisuje u GPIO modul.
Redak 9 : zatvorite ručicu datoteke, jer je ne trebamo spremati.
Redci 11–14 : čitamo i upisujemo na našu kartu s 0x08
pomakom. Prema dokumentaciji, to je registar GPFSEL2 GPIO Function Select 2. I ovaj registar kontrolira pin funkcije.
Postavljamo (brišemo sve, a zatim postavljamo pomoću operatora OR) 3 bita s trećim bitom postavljenim na 001
. Ova vrijednost znači da pin radi kao izlaz. Postoji mnogo pribadača i mogućih načina rada za njih. Zbog toga je registar za načine rada podijeljen u nekoliko registara, gdje svaki sadrži načine za 10 pinova.
Linije 16 i 22 : postavite obrađivač prekida 'Ctrl + C'.
Linija 17 : beskonačna petlja.
Redak 18 : postavite pin u visoko stanje upisivanjem u registar GPSET0.
Napominjemo, Raspberry Pi nema registre kao što ih ima PORTA (AVR mikrokontroleri). Ne možemo napisati cijelo GPIO stanje svih pinova. Postoje samo postavljeni i jasni registri koji se koriste za postavljanje i brisanje navedenih s bitovnim maskim iglama.
Linije 19 i 21 : kašnjenje
Linija 20 : postavite pin u nisko stanje s registrom GPCLR0.
Linije 25 i 26 : prebacite pin na zadano, ulazno stanje. Zatvorite mapu memorije.
Ovaj bi se kôd trebao pokretati s privilegijama superusera. Nazovite datoteku ‘gpio.py’
i pokrenite je s ‘sudo python gpio.py’
. Ako ste na pin 21 spojili LED, on će treptati.
DMA
Izravni pristup memoriji je poseban modul koji je dizajniran za kopiranje memorijskih blokova iz jednog područja u drugo. Kopirat ćemo podatke iz međuspremnika memorije u GPIO modul. Prije svega, trebamo solidno područje u fizičkom RAM-u koje će se kopirati.
Postoji nekoliko mogućih rješenja:
- Možemo stvoriti jednostavan pokretački program jezgre koji će nam dodijeliti, zaključati i prijaviti adresu ove memorije.
- U nekim se implementacijama virtualna memorija dodjeljuje i koristi
‘/proc/self/pagemap’
za pretvaranje adrese u fizičku. Ne bih preporučio takav pristup, pogotovo kada moramo dodijeliti veliku površinu. Bilo koja virtualno dodijeljena memorija (čak i zaključana, pogledajte dokumentaciju jezgre) može se premjestiti u fizičko područje. - Svi Raspberry Pi imaju
‘/dev/vcio’
uređaj koji je dio grafičkog upravljačkog programa i može nam dodijeliti fizičku memoriju. Službeni primjer pokazuje kako se to radi. A možemo ga koristiti umjesto da stvaramo vlastiti.
DMA modul sam je samo skup registara koji se nalaze negdje na fizičkoj adresi. Ovim modulom možemo upravljati putem ovih registara. U osnovi postoje izvorišni, odredišni i kontrolni registri. Provjerimo nekoliko jednostavnih kodova koji pokazuju kako koristiti DMA module za upravljanje GPIO.
Since additional code is required to allocate physical memory with ‘/dev/vcio’
, we will use a file with an existing CMA PhysicalMemory class implementation. We will also use the PhysicalMemory class, which performs the trick with memap from the previous sample.
Let’s break it down line by line:
Lines 1–3: headers, imports.
Lines 5–6: constants with the channel DMA number and GPIO pin that we will use.
Lines 8–15: initialize the specified GPIO pin as an output, and light it up for a half second for visual control. In fact, it’s the same thing we did in the previous example, written in a more pythonic way.
Line 17: allocates 64
bytes in physical memory.
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:
- Raspberry Pi 3
- RAMPSv1.4 board
- 4 A4988 or DRV8825 module
- RepRap Prusa i3 frame with equipment (end-stops, motors, heaters, and sensors)
- 12V 15A power supply unit
- LM2596S DC-DC step down converter module
- MAX4420 chip
- ADS1115 analog to digital converter module
- UDMA133 IDE ribbon cable
- Acrylic glass
- PCB stands
- 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.
If you found this article useful, please give me some claps so more people see it. Thanks!

IoT is all about prototyping ideas quickly. To make it possible we developed DeviceHive, an open source IoT/M2M platform. DeviceHive provides a solid foundation and building blocks to create any IoT/M2M solution, bridging the gap between embedded development, cloud platforms, big data & client apps.
