Cómo puede usar Python para construir su propio controlador CNC e impresora 3D

Este artículo analiza el proceso que utilicé para construir la primera implementación de controlador de máquina CNC en Python puro.

Los controladores de máquina de control numérico por computadora (CNC) se implementan típicamente usando el lenguaje de programación C o C ++. Se ejecutan en sistemas operativos sin SO o en tiempo real con microcontroladores simples.

En este artículo, describiré cómo construir un controlador CNC, una impresora 3D en particular, utilizando placas ARM modernas (Raspberry Pi) con un lenguaje moderno de alto nivel (Python).

Este enfoque moderno abre una amplia gama de opciones de integración con otras tecnologías, soluciones e infraestructuras de vanguardia. Esto hace que todo el proyecto sea fácil de desarrollar.

Sobre el proyecto

Las placas ARM modernas suelen utilizar Linux como sistema operativo de referencia. Esto nos da acceso a toda la infraestructura de Linux con todos los paquetes de software de Linux. Podemos alojar un servidor web en una placa, usar conectividad Bluetooth, usar OpenCV para el reconocimiento de imágenes y construir un grupo de placas, entre otras cosas.

Estas son tareas bien conocidas que se pueden implementar en placas ARM y pueden ser realmente útiles para máquinas CNC personalizadas. Por ejemplo, el posicionamiento automático mediante compuvision puede ser muy útil para algunas máquinas.

Linux no es un sistema operativo en tiempo real. Esto significa que no podemos generar pulsos con los tiempos requeridos para controlar motores paso a paso directamente desde los pines de la placa con software en ejecución, incluso como un módulo de kernel. Entonces, ¿cómo podemos usar steppers y funciones de Linux de alto nivel? Podemos utilizar dos chips: un microcontrolador con una implementación clásica de CNC y una placa ARM conectada a este microcontrolador mediante UART (receptor-transmisor asíncrono universal).

¿Qué pasa si no hay funciones de firmware adecuadas para este microcontrolador? ¿Qué pasa si necesitamos controlar ejes adicionales que no están implementados en el microcontrolador? Cualquier modificación al firmware C / C ++ existente requerirá mucho tiempo y esfuerzo de desarrollo. Veamos si podemos hacerlo más fácil e incluso ahorrar dinero en microcontroladores simplemente eliminándolos.

PyCNC

PyCNC es un intérprete de código G de alto rendimiento de código abierto gratuito y un controlador de impresora CNC / 3D. Se puede ejecutar en varias placas basadas en ARM con tecnología Linux, como Raspberry Pi, Odroid, Beaglebone y otras. Esto le brinda la flexibilidad de elegir cualquier placa y usar todo lo que ofrece Linux. Y puede mantener todo el tiempo de ejecución del código G en una placa sin la necesidad de un microcontrolador separado para la operación en tiempo real.

La elección de Python como el lenguaje de programación principal reduce significativamente la base de código en comparación con los proyectos C / C ++. También reduce el código repetitivo y específico del microcontrolador, y hace que el proyecto sea accesible para una audiencia más amplia.

Cómo funciona

El proyecto utiliza DMA (acceso directo a memoria) en el módulo de hardware del chip. Simplemente copia el búfer de estados GPIO (entrada y salida de propósito general) asignado en la RAM a los registros GPIO reales. Este proceso de copia está sincronizado por el reloj del sistema y funciona de forma completamente independiente de los núcleos de la CPU. Por lo tanto, se genera una secuencia de pulsos para el eje del motor paso a paso en la memoria y luego el DMA los envía con precisión.

Profundicemos en el código para comprender los conceptos básicos y cómo acceder a los módulos de hardware desde Python.

GPIO

Un módulo de entrada y salida de uso general controla los estados de los pines. Cada pin puede tener un estado bajo o alto. Cuando programamos el microcontrolador, usualmente usamos variables definidas por SDK (kit de desarrollo de software) para escribir en ese pin. Por ejemplo, para habilitar un estado alto para los pines 1 y 3:

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

Si busca en el SDK, encontrará la declaración de esta variable, y tendrá un aspecto similar a:

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

Es solo un puntero. No apunta a la ubicación en la RAM, sino a la dirección del procesador físico. El módulo GPIO real se encuentra en esta dirección.

Para administrar los pines, podemos escribir y leer datos. El procesador ARM de Raspberry Pi no es una excepción y tiene el mismo módulo. Para controlar los pines, podemos escribir / leer datos. Podemos encontrar las direcciones y estructuras de datos en la documentación oficial de los periféricos del procesador.

Cuando ejecutamos un proceso en el tiempo de ejecución del usuario, el proceso comienza en el espacio de direcciones virtuales. El periférico real es accesible directamente. Pero aún podemos acceder a direcciones físicas reales con el ‘/dev/mem’dispositivo.

Aquí hay un código simple en Python que controla el estado de un pin usando este enfoque:

Vamos a dividirlo línea por línea:

Líneas 1 a 6 : encabezados, importaciones.

Línea 7 : abre el ‘/dev/mem’ acceso del dispositivo a la dirección física.

Línea 8 : usamos la llamada al sistema mmap para mapear un archivo (aunque en nuestro caso, este archivo representa la memoria física) en la memoria virtual del proceso. Especificamos la longitud y el desplazamiento del área del mapa. Para la longitud, tomamos el tamaño de la página. Y la compensación es 0x3F200000.

La documentación dice que la dirección del bus0x7E200000 contiene registros GPIO y necesitamos especificar la dirección física . La documentación dice (página 6, párrafo 1.2.3) que la 0x7E000000dirección del bus está asignada a la 0x20000000dirección física, pero esta documentación es para Raspberry 1.

Tenga en cuenta que todas las direcciones de bus del módulo son las mismas para Raspberry Pi 1-3, pero este mapa se cambió a 0x3F000000RPi 2 y 3. Por lo tanto, la dirección aquí es 0x3F200000. Para Raspberry Pi 1, cámbielo a 0x20200000.

Después de esto, podemos escribir en la memoria virtual de nuestro proceso, pero en realidad escribe en el módulo GPIO.

Línea 9 : cierre el identificador del archivo, ya que no necesitamos almacenarlo.

Líneas 11-14 : leemos y escribimos en nuestro mapa con el 0x08desplazamiento. Según la documentación, es el registro GPFSEL2 GPIO Function Select 2. Y este registro controla las funciones de los pines.

Establecemos (borramos todo, luego establecemos con el operador OR) 3 bits con el tercer bit establecido en 001. Este valor significa que el pin funciona como salida. Hay muchos pines y modos posibles para ellos. Es por eso que el registro de modos se divide en varios registros, donde cada uno contiene los modos para 10 pines.

Líneas 16 y 22 : configure el controlador de interrupciones 'Ctrl + C'.

Línea 17 : bucle infinito.

Línea 18 : establezca el pin en el estado alto escribiendo en el registro GPSET0.

Tenga en cuenta que Raspberry Pi no tiene registros como los de PORTA (microcontroladores AVR). No podemos escribir todo el estado GPIO de todos los pines. Solo hay registros de configuración y borrado que se utilizan para configurar y borrar especificados con pines de máscara bit a bit.

Líneas 19 y 21 : retraso

Línea 20 : establezca el pin en estado bajo con el registro GPCLR0.

Líneas 25 y 26 : cambie el pin al estado de entrada predeterminado. Cierre el mapa de memoria.

Este código debe ejecutarse con privilegios de superusuario. Nombra el archivo ‘gpio.py’ y ejecútalo con ‘sudo python gpio.py’. Si tiene un LED conectado al pin 21, parpadeará.

DMA

Direct Memory Access es un módulo especial diseñado para copiar bloques de memoria de un área a otra. Copiaremos los datos del búfer de memoria al módulo GPIO. En primer lugar, necesitamos un área sólida en la RAM física que se copiará.

Hay pocas soluciones posibles:

  1. Podemos crear un controlador de kernel simple que nos asignará, bloqueará y nos informará la dirección de esta memoria.
  2. In some implementations, virtual memory is allocated and uses ‘/proc/self/pagemap’ to convert the address to the physical one. I wouldn't recommend this approach, especially when we need to allocate big area. Any virtually allocated memory (even locked, see the kernel documentation) can be moved to the physical area.
  3. All Raspberry Pi have a ‘/dev/vcio’ device, which is a part of the graphic driver and can allocate physical memory for us. An official example shows how to do it. And we can use it instead of creating our own.

The DMA module itself is just a set of registers that are located somewhere at a physical address. We can control this module via these registers. Basically, there are source, destination, and control registers. Let’s check some simple code that shows how to use the DMA modules to manage the 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:

  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.

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.