Firmware de bricolaje para impresora 3D LCD de fotopolímero. Parte 2





Continuación del artículo sobre cómo escribir su propio firmware para una impresora 3D LCD de fotopolímero.

En esta parte, continuaré describiendo las etapas de mi proyecto:

2. Trabajar con una unidad flash USB y archivos en ella

3. Controlar un motor paso a paso para mover la plataforma.





- Parte 1: 1. Interfaz de usuario.

- Parte 2: 2. Trabajar con el sistema de archivos en una unidad flash USB. 3. Control de motor paso a paso para el movimiento de la plataforma.

- Parte 3: 4. Visualización de imágenes de capas en la pantalla retroiluminada. 5. Todo, como controlar la iluminación y los ventiladores, cargar y guardar configuraciones, etc. 6. Funciones adicionales para comodidad y conveniencia.



2. Trabajar con una unidad flash USB y archivos en ella



Nunca antes había trabajado con un host USB en microcontroladores. Como dispositivo USB, hice firmware tanto con la clase CDC (emulación de puerto COM) como con la clase HID, pero no funcionó con el host. Por tanto, para acelerar el proceso, creé toda la inicialización de este periférico en STM32CUBE. Como resultado, obtuve un host funcionando en modo USB FS que admite dispositivos de almacenamiento masivo. En el mismo cubo, conecté inmediatamente la biblioteca FatFS para trabajar con el sistema de archivos y los archivos. Luego, solo tenía que copiar las fuentes resultantes en su proyecto y descubrir cómo trabajar con ellas. Resultó fácil y no hay mucho que describir aquí. El archivo usb_host.c de Cuba tiene una variable global Appli_state de tipo ApplicationTypeDef:

typedef enum {
  APPLICATION_IDLE = 0,
  APPLICATION_START,
  APPLICATION_READY,
  APPLICATION_DISCONNECT
}ApplicationTypeDef;


En varios eventos del periférico del host USB (en interrupciones), esta variable puede tomar uno de los estados enumerados, lo que indica el estado actual del host. En el ciclo principal del programa, queda simplemente rastrear los cambios en esta variable y reaccionar en consecuencia. Por ejemplo, si su valor ha cambiado a APPLICATION_READY, entonces la unidad flash o el lector de tarjetas está conectado e inicializado correctamente, puede leer archivos de ellos.



Tampoco hay dificultades con FatFS: el Cube ya lo configura completamente y se "conecta" al host USB, por lo que inmediatamente después de conectar la unidad flash, puede acceder a las funciones de esta biblioteca para trabajar con archivos. Es cierto que el cubo recién actualizado incluye la biblioteca de la versión anterior. Después de actualizar sus archivos a una nueva versión, tuve que corregir los nombres de las definiciones de la configuración de FatFS en algunos lugares del código fuente cubano, porque han cambiado en la nueva versión. Pero la actualización no trajo ningún problema en particular, todo fue rápido y fácil.



Pero para que FatFS funcione con Cyrillic en los nombres de archivos y directorios, tuve que modificar un poco. Para que FatFS lea correctamente los nombres cirílicos, es necesario habilitar Unicode en su configuración, y después de eso, todas las cadenas asociadas con FatFS deben estar solo en esta codificación: nombres de disco, nombres de archivo, etc. Al mismo tiempo, el editor de texto en el IDE y FatFS admite Unicode con diferentes posiciones del byte alto, una con Little Endian y la otra con Big Endian, por lo que es imposible escribir fuentes simplemente con textos Unicode. Y no quiero, para ser honesto. Fue entonces cuando tuve que escribir convertidores de ANSI y UTF-8 a Unicode y viceversa, además de varias funciones para trabajar con cadenas de diferentes codificaciones en diferentes combinaciones. Por ejemplo, copie una cadena UTF-8 en una cadena Unicode o agregue una cadena ANSI a una cadena Unicode. Sin embargo, las cadenas ANSI parecen serno queda ningún lugar, todas las fuentes se convierten completamente a la codificación UTF-8.

Entonces, abrir un archivo con un nombre de pila ahora se ve así:

tstrcpy(u_tfname, UsbPath);	//    (Unicode)    (Unicode)
tstrcat_utf(u_tfname, SDIR_IMAGES);	//    (Unicode)   (UTF-8)
tstrcat_utf(u_tfname, (char*)"\\");	//    (Unicode)  (UTF-8)
tstrcat(u_tfname, fname);	//    (Unicode)   (Unicode)


Cuando todo funcionó rápidamente, quise verificar la velocidad de lectura de archivos desde una unidad flash. La lectura de un archivo de 10 MB en bloques de 4 KB mostró una velocidad de aproximadamente 9 Mbps, lo que, en general, era bastante bueno y me convenía.



Traté de estudiar el tema de la transferencia de este caso a DMA, pero resultó que los periféricos del host USB simplemente no tienen acceso a DMA. Bueno, o no lo encontré :) Por lo tanto, parecía lógico organizar todos los búferes de lectura / escritura para archivos USB en CCM (memoria acoplada central), un área de 64 KB de RAM que tampoco tiene salida DMA. En la misma área de memoria, tiene sentido colocar otras variables / matrices que no funcionan con DMA, solo para dejar más memoria en la RAM normal. Por cierto, me pareció que el núcleo en sí trabaja con esta memoria un poco más rápido que con la memoria ordinaria.



2.1 Interfaz de usuario de archivos



La impresora Anycubic Photon S que tengo muestra una lista de archivos como iconos de vista previa, 4 por pantalla. Y, en principio, es bastante conveniente: puede ver el nombre del archivo, en la imagen de vista previa puede ver aproximadamente qué tipo de modelo. Por lo tanto, seguí el mismo camino: los archivos se muestran 4 piezas por página en forma de imágenes de vista previa con el nombre del archivo.



La conocida carpeta amarilla está dibujada en los íconos del directorio y un engranaje en los archivos de configuración. Solo se muestran aquellos archivos para los que la extensión cae dentro de uno de los conocidos. Actualmente, estos son archivos .pws (archivos preparados por la cortadora para imprimir) y archivos .acfg (archivos de texto con configuraciones de impresora).



Dado que el firmware también funciona con directorios que el usuario puede ingresar, coloqué una línea encima de la lista de archivos en los que está escrita la ruta actual. Los botones para salir del directorio actual o desplazarse hacia abajo y hacia arriba aparecen solo cuando tienen sentido, es decir, cuando puede salir del directorio actual o desplazarse hacia abajo o hacia arriba en la lista.





Mi amigo, a quien le mostré todo esto mientras se escribía el firmware, sugirió otra opción para generar archivos, en forma de lista, tabla. En primer lugar, caben más archivos en la página y, en segundo lugar, la lista se muestra mucho más rápido, ya que no es necesario leer las imágenes de vista previa de los archivos y dibujarlas con escala en la pantalla, y en tercer lugar, en forma tabular, también puede mostrar además del nombre y la hora en que se modificó por última vez el archivo, lo que a veces es muy conveniente. Es un pecado rechazar una buena idea, así que agregué una lista de tablas y, al mismo tiempo, un botón para cambiar entre las vistas de "icono" y "tabla". Los directorios en forma de tabla están resaltados con un fondo amarillo y la línea "DIR" está escrita en lugar de la fecha y la hora:





Por cierto, no hay intriga sobre las imágenes de vista previa que se dibujan para archivos en el modo de icono. El firmware no analiza todo el archivo para construir una imagen a partir de un modelo 3D, como algunos piensan :) Esta imagen es guardada en el archivo de impresión por la propia cortadora, en un formato similar a BMP - una matriz de valores de color de píxeles de 16 bits. El tamaño de la imagen de vista previa se almacena en campos especiales dentro del archivo. Entonces todo es muy simple.



Lo único que tiene que esforzarse el firmware es escalar la imagen del archivo al tamaño del icono en la pantalla. El firmware realiza el escalado de una manera muy simple: se calcula el factor de escala k(número fraccionario): el ancho de la imagen original se divide por el ancho del área de visualización en la pantalla (el coeficiente de altura también se calcula y el mayor de los dos valores se toma en funcionamiento) y luego se toman píxeles y líneas de la imagen original para mostrar en la pantalla con un paso de k .



De esta forma, puede escalar tanto en más como en menos. La calidad del resultado escalado, por supuesto, deja mucho que desear, ya que no se realiza interpolación, pero en una pantalla tan pequeña y no de alta calidad es imperceptible, pero la velocidad de dicho algoritmo es bastante alta.



Cuando hace clic en el icono o la línea del archivo .pws, se abre una pantalla para ver información sobre el archivo con la posibilidad de comenzar a imprimirlo. Si se hace clic en el archivo .acfg, se le pedirá al usuario que cargue la configuración de este archivo. Bueno, si se presiona un directorio, se vuelve actual y se actualiza la lista de archivos.



2.2 Visualización de la información del archivo antes de imprimir



Como señaló correctamente en los comentarios a la parte anterior, Anycubic no tiene ninguna información sobre el archivo cuando está seleccionado. Simplemente aparecen los botones para comenzar a imprimir y eliminarlo. Y esto es muy inconveniente: para averiguar el tiempo de impresión estimado, la cantidad de capas u otros parámetros de este archivo, debe comenzar a imprimirlo. Decidí no repetir este defecto, y al hacer clic en el archivo cortado, se abre una pantalla con la información más completa al respecto:





nombre del archivo, tamaño, hora de la última modificación y casi todos los parámetros de impresión. Aquí, sin embargo, el hecho de que la pantalla MKS DLP tiene una resolución de 480x320 jugó en mis manos, mientras que los Enikubiks tienen una más pequeña, 320x240, en esta no se puede balancear con un montón de texto.



2.2.1 Escribiré por separado sobre el cálculo del tiempo de impresión.

Este indicador no se almacena en el archivo, a diferencia de todos los demás parámetros. Su impresora debe calcular de forma independiente, basándose en la información que conoce. El mismo Anycubic Photon S tiene la costumbre de sobrepasarse con este cálculo y hacia abajo; por ejemplo, promete 5 horas de impresión, mientras que en realidad imprime 6 horas. Y Longer Orange 30 en general durante la impresión cambia esta vez de un lado a otro casi dos veces. Decidí abordar este punto con el mayor cuidado posible. ¿En qué consiste este tiempo?

  1. El tiempo que tarda la plataforma en descender a una velocidad determinada hasta la altura de la siguiente capa.
  2. El tiempo de pausa antes del inicio de la exposición.
  3. Tiempo de exposición de la capa.
  4. El tiempo que tarda la plataforma en elevarse a una altura determinada a una velocidad determinada después de que se expone la capa.




Estos 4 parámetros se suman, se multiplican por el número de capas y se obtiene el tiempo total de impresión. Si todo es elemental con tiempos de pausa y flare, se mantienen con precisión de milisegundos, pero con el movimiento de la plataforma, ya todo es un poco más complicado.



La plataforma no recupera la velocidad establecida al instante, tiene cierta aceleración, que se establece en la configuración. Además, al imprimir, esta es una aceleración bastante pequeña, ya que la plataforma debería comenzar a subir muy suavemente para que la última capa curada se desprenda sin dolor de la película en el fondo del baño (sí, el polímero también se pega a la película, desafortunadamente)



Resulta que el movimiento de la plataforma consta de tres componentes: aceleración hasta que se alcanza una velocidad determinada, movimiento uniforme a una velocidad determinada y desaceleración hasta una parada completa. Y aquí es donde comienzan las opciones: por ejemplo, la aceleración y la altura de elevación especificadas no permiten que la plataforma alcance la velocidad especificada, todavía está acelerando en el momento en que ya necesita comenzar a desacelerar para detenerse a la altura especificada. O la aceleración y la altitud son suficientes para que la plataforma acelere a la velocidad establecida y recorra una parte del camino en movimiento constante antes de comenzar a desacelerar. Necesitamos comprobar todo esto, calcular los tiempos y distancias para cada componente.



Para ser honesto, mi cabeza daba vueltas cuando escribí la función para calcular el tiempo de impresión :) Y como resultado, todavía recibí un pequeño error. Por ejemplo, el tiempo de impresión real es 07:43:30 en lugar del estimado 07:34:32.





O 05:48:43 en lugar de las 05:43:23 calculadas.





Pero, en principio, este error me vino bien. Traté de encontrar un error en los cálculos, pero todo parece estar correcto allí. Lo más probable es que la aceleración real no se corresponda ligeramente con la especificada debido a las peculiaridades del control del motor paso a paso. Así que sin problemas llegamos a la siguiente etapa :)



3. Control de motor paso a paso para el movimiento de la plataforma.



Al principio tuve la idea de escribir mi propio control de motor paso a paso. No es nada difícil tener un controlador normal en la placa: establezca la dirección de rotación en un pin y conduzca los pulsos de los pasos al otro pin. Debe rotar rápidamente: aumenta la frecuencia del pulso, debe hacerlo lentamente, la disminuye.



Pero cuando comencé a abordar este problema de manera más específica, me di cuenta de que su simplicidad es engañosa. No, puedes escribir el tuyo propio y funcionará, pero escribir para que funcione bien es una tarea bastante grande. A los motores paso a paso no les gustan mucho las irregularidades en los pasos, por lo tanto, es necesario garantizar una buena uniformidad de los pulsos de paso en un rango de frecuencia bastante amplio, desde unos pocos hercios hasta decenas de kilohercios. Es necesario asegurar un aumento y una disminución suaves de la frecuencia de los pulsos para la aceleración y la desaceleración. Es necesario contar con precisión los impulsos generados para tener la seguridad de saber en qué posición se encuentra ahora la plataforma. Es necesario calcular el número de pulsos y el período de su cambio de frecuencia en un período de tiempo estrictamente definido para proporcionar la aceleración requerida.



En definitiva, la tarea, aunque factible, es muy, muy voluminosa, lo que me llevaría más de un día. Así que decidí quitarle las funciones de gestión del motor a Marlin . Pensé que sería fácil ...



Primero, tomé el archivo stepper.cpp de las fuentes de Marlin, controlando directamente el motor paso a paso. Sin embargo, resultó que su trabajo depende en gran medida del planificador de movimiento del archivo planner.cpp, así que tuve que tomarlo también. Bueno, al montón, también tomé el archivo endstops.cpp desde allí, procesando los interruptores de límite de eje, ya que todavía necesitaba procesar eventos de ellos, y aquí el programador y el control del motor ya estaban asociados con este archivo para interruptores de límite.



Pasé mucho tiempo tratando de eliminar todo lo innecesario de estos archivos y desatarlos del resto del ecosistema Marlin. El hecho es que Marlin se afila bajo el control de 6 o 7 steppers al mismo tiempo, mientras que su funcionamiento puede depender de la temperatura de varios calentadores, de los parámetros del plástico, etc. El sistema es realmente complicado allí. Tuve que rehacer mucho, principalmente eliminando ejes y extrusores innecesarios y deshaciéndome de un montón de macros que eran útiles en la versión original, pero muy perturbadoras en la mía. Solo para entender: el tamaño de las fuentes que tomé de Marlin se ha reducido de 346 a 121 KB. Y cada línea tenía que borrarse con precaución.



Naturalmente, en el proceso de esta dura poda, profundicé un poco más en el trabajo de todo el sistema, cómo funciona. Para mover el eje, la posición de destino del eje se transfiere al planificador a través de una de sus funciones (el planificador almacena la posición actual). El programador calcula el número de pasos y sus parámetros para la aceleración, el movimiento en línea recta y la desaceleración y forma a partir de estos datos un paquete de datos especial para la función de control directo del motor (paso a paso). Puede haber varios de estos paquetes, el planificador calcula y crea un nuevo paquete siguiente para cada nueva tarea.



Stepper, trabajando en una interrupción de temporizador, en un estado libre, solicita el siguiente paquete de datos del programador. Si el planificador tiene un paquete preparado, lo regala y lo considera terminado. Stepper lleva el paquete recibido al trabajo y comienza a trabajar los pasos del motor de acuerdo con los datos del mismo. Hasta que lo complete, no se solicita el siguiente paquete.



Lo que curiosamente se implementa en stepper es que a bajas velocidades emite un pulso de paso en cada interrupción, ajustando el temporizador para que la siguiente interrupción ocurra después del período de tiempo requerido. Cuando la velocidad de paso requerida excede un cierto valor, el paso a paso comienza a emitir varios pasos en cada interrupción. Al mismo tiempo, todos los tiempos están tan bien elegidos que la uniformidad de los pasos es muy buena, por curiosidad miré el osciloscopio.



El planificador también sabe cómo "unir" paquetes adyacentes. Qué significa esto: si el programador ya tiene un paquete preparado para el stepper y luego le llega una nueva tarea, entonces forma el siguiente paquete y cambia el anterior para que, como resultado del procesamiento secuencial de estos dos paquetes por el stepper, se obtenga un movimiento suave.



Dejame explicarte con un ejemplo. El planificador está libre, recibe una tarea para mover el eje hacia adelante 20 mm a una velocidad de 30 mm / s. El planificador genera el primer paquete, en el que describe la aceleración de cero a 30 mm / s, el movimiento en línea recta a esta velocidad y la desaceleración de esta velocidad a cero. Si, antes de que el paso a paso tome este paquete del programador, el programador recibe una nueva tarea para mover este eje otros 50 mm hacia adelante, pero ya a una velocidad de 40 mm / s, entonces el programador no solo creará un nuevo paquete con aceleración desde cero, sino que cambiará el primer paquete. eliminando la desaceleración y extendiendo el movimiento en línea recta por su distancia, y en el segundo paquete creado, la aceleración comenzará no desde cero, sino desde la velocidad del paquete anterior.



El resultado es un movimiento en el que el eje se acelerará a 30 mm / s, se desplazará 20 mm, luego volverá a acelerar a 40 mm / sy se desplazará otros 50 mm, reduciendo la velocidad a cero al final. Pero esto es solo si el stepper aún no ha logrado recoger el paquete anterior, de lo contrario estas dos tareas se procesarán como dos movimientos separados con cero velocidades inicial y final en cada uno de ellos. Por lo tanto, por cierto, en las impresoras con control de plataforma manual, si presiona el elevador varias veces seguidas en incrementos de 10 mm, la plataforma se detendrá después del primer elevador de 10 mm y luego continuará moviéndose sin detenerse hasta la altura completa en la que hace clic el botón.



En la nueva versión de Marlin, ya ha aparecido un remedio contra un movimiento tan "entrecortado": ahora el planificador no da un paquete paso a paso durante un cierto tiempo después de su formación si este paquete es el único listo. Este tiempo se reserva para esperar: ¿llegará la próxima tarea para que pueda acoplarla a la existente?



3.1 Interfaz de control de movimiento de la plataforma







Aquí, en general, todo es estándar y habitual para las impresoras de fotopolímero. En la parte superior está la selección del paso del movimiento del eje, a la derecha están los botones para mover el eje hacia arriba o hacia abajo con el paso seleccionado.

El botón "Inicio" se utiliza para poner a cero la plataforma (estacionamiento, casa), cuando se presiona, la plataforma comienza a moverse hacia el interruptor de límite de "casa". Una vez alcanzado, la plataforma se detiene, retrocede un poco y de nuevo lentamente (para mayor precisión) se topa con el final de carrera. Después de eso, el firmware definitivamente conoce la altura de elevación actual exacta de la plataforma.



Botón de ajuste Z = 0 ”se utiliza para calibrar la altura de la plataforma sobre la pantalla. Un sistema de calibración de este tipo se utiliza, por ejemplo, en impresoras Anycubic, cuando el punto cero de la plataforma (su altura óptima por encima de la pantalla) está 1-2 mm por debajo de la activación del interruptor de límite de "inicio". Y este sistema de calibración me parece más correcto que los sistemas que se han popularizado recientemente, cuando la altura de actuación del final de carrera es al mismo tiempo la altura cero de la plataforma.



Y el último botón es "¡Alto!" Es una parada incondicional e inmediata del movimiento de la plataforma. Por cierto, mientras la plataforma está en movimiento, no puedes salir de esta pantalla, el botón "Atrás" no funcionará. Esto se hace para que mientras la plataforma se está moviendo, el botón "Parar" esté disponible instantáneamente.



3.2 Otros puntos sobre el movimiento de la plataforma



Hay varias cosas que me molestan terriblemente en Anycubic Photon.



La primera es ¿por qué el movimiento manual de la plataforma se produce con la misma aceleración de caracol que en el modo de impresión? Al escribir, una aceleración tan pequeña es útil, pero cuando se controla manualmente el eje, acelera durante 2 segundos; es solo una pesadilla. Y la velocidad del movimiento es regular.



El segundo punto: ¿por qué, cuando la impresión está en pausa, la plataforma se eleva a la altura de la pausa a la velocidad especificada en los parámetros de impresión? Demonios, esperar 15 segundos a que la plataforma se eleve dos (solo) centímetros está más que bien. Pero gracias por levantarte. En Orange 30, una pausa no implica en absoluto un levantamiento de la plataforma ni siquiera un milímetro, por lo que ni siquiera está claro por qué está allí.



Y el tercer momento, que simplemente enfurece: después del final de la impresión, la plataforma se eleva a la cima. A la misma velocidad que se especificó en los parámetros de impresión: 1 mm / seg. ¡Se necesitan 100 segundos para subir desde una altura de 5 cm!



Por lo tanto, en mi firmware, hice velocidades y aceleraciones ajustables por separado para el modo de impresión y por separado para el control manual de la plataforma. Pero con dos limitaciones:

  1. Hasta que el eje se reinicie con el botón Inicio, la velocidad de desplazamiento se reducirá tres veces. Esto se debe a que, si bien la impresora no conoce la altura exacta actual de la plataforma, existe el peligro de aplastar la pantalla sin detenerse a alta velocidad (inercia, por lo que) o dañar el tope del eje superior. Después de poner a cero el eje, la impresora ya conoce exactamente la posición de la plataforma y entran en vigor los límites de altura del software, que también se establecen en los ajustes.
  2. A una altura de menos de 30 mm, la velocidad también se reduce en tres veces, independientemente de si el eje está a cero o no. Esto es para evitar que el fotopolímero salpique de la bañera cuando la plataforma se baja demasiado rápido. O cuando salga demasiado rápido.




Por supuesto, hay otros parámetros de eje estándar en la configuración: el número de pasos por 1 mm, la dirección del movimiento, el trabajo de los interruptores de límite, etc. Si alguien está interesado, debajo del spoiler hay un archivo de configuración de texto con todos los parámetros admitidos. Dicho archivo con la extensión .acfg es consumido por el firmware directamente de la lista de archivos, cargando los parámetros, guardándolos en la EPROM y aplicándolos inmediatamente, sin reiniciar:

Contenido del archivo de configuración
# Stepper motor Z axis settings

[ZMotor]



# .

# : 0 1. : 1.

# .

invert_dir = 1



# .

# : -1 1. : -1.

# -1,

# , . 1

# .

home_direction = -1



# Z . ,

# 0, - .

home_pos = 0.0



# .

# : -32000.0 32000.0.

# : -3.0

# .

# , .

min_pos = -3.0



# .

# : -32000.0 32000.0.

# : 180.0

# .

# , .

max_pos = 180.0



# .

# : 0 1. : 1.

# ,

# 1, - 0.

min_endstop_inverting = 1



# .

# : 0 1. : 1.

# ,

# 1, - 0.

max_endstop_inverting = 1



# 1 .

steps_per_mm = 1600



# ,

# , /. : 6.0.

homing_feedrate_fast = 6.0



# ,

# , /. : 1.0.

homing_feedrate_slow = 1.0



# , /2.

acceleration = 0.7



# , /.

feedrate = 5.0



# ( ,

# ..), /2.

travel_acceleration = 25.0



# ( ,

# ..), /. 30

# ,

# 5 /.

travel_feedrate = 25.0



# , .

current_vref = 800.0



# , .

current_hold_vref = 300.0



# ,

# . . 0

# .

hold_time = 30.0



# ,

# . .

# hold_time. 0 .

# , .

off_time = 10.0



# General settings

[General]



# (0.001 )

# .

# : 0 15000. : 700 (0.7 ).

buzzer_msg_duration = 700



# (0.001 )

# , .

# : 0 15000. : 70 (0.07 ).

buzzer_touch_duration = 70



# 180 .

# .

# : 0 1. : 0.

rotate_display = 0



# , .

# LCD-. -

# .

# : 0 15000. : 10. 0 .

screensaver_time = 10







Y con eso terminaré esta parte, y ya hay demasiado texto :)

Como antes, estaré encantado de responder preguntas y aceptar comentarios.



- Parte 1: 1. Interfaz de usuario.

- Parte 2: 2. Trabajar con el sistema de archivos en una unidad flash USB. 3. Control de motor paso a paso para el movimiento de la plataforma.

- Parte 3: 4. Visualización de imágenes de capas en la pantalla retroiluminada. 5. Todo, como controlar la iluminación y los ventiladores, cargar y guardar configuraciones, etc. 6. Funciones adicionales para comodidad y conveniencia.



Enlaces



Kit MKS DLP en Aliexpress

Fuentes de firmware originales del fabricante en GitHub

Schemes del fabricante de dos versiones de la placa en GitHub

Mis fuentes en GitHub



All Articles