Usando los controladores UDB PSoC de Cypress para reducir las interrupciones en una impresora 3D



En los comentarios sobre la traducción de documentación patentada en UDB, se observó correctamente que los hechos simples no contribuyen a la comprensión del material. Pero ese documento contiene precisamente los hechos secos. Para diluirlos con la práctica, tomemos un descanso de la traducción. Volteemos este bloque en nuestras manos y veamos qué y cómo se puede lograr en la práctica.

Larga introducción


Este artículo es la segunda parte de la trilogía concebida. La primera parte se encuentra aquí (control de LED RGB a través de la unidad de microcontrolador Cypress UDB PSoC).

Además de los controladores UDB PSoC de Cypress, donde se implementan ciertas interfaces en ellos, sería interesante comprobar cómo estos bloques pueden facilitar la vida de los programadores al descargar el procesador central de ciertas tareas que requieren muchos recursos. Pero para aclarar lo que voy a hacer, tengo que escribir un extenso prefacio.

En el otoño de 2015, compré una nueva impresora 3D MZ3D, y para la primavera de 2016 estaba cansada de cómo sacudían sus motores paso a paso. Los tiempos eran salvajes, sobrevivimos lo mejor que pudimos, por lo que la única solución era pasar del microstep 1/16 a 1/32. La correspondencia con la fábrica mostró que esto no es posible en Arduino. Al final resultó que había una restricción en el "firmware" de esos años, con una frecuencia de paso superior a 10 KHz, no se tomaron pasos virtuales, sino dos pasos virtuales, de lo contrario el sistema simplemente no tenía suficiente tiempo para procesar todas las interrupciones de "paso". Solo había una salida: arrastrar todo a la plataforma ARM. Fue una operación de arrastrar y soltar, no una descarga, ya que tampoco había soluciones ARM preparadas en ese momento. En un par de semanas transferí todo esto a STM32F4, el sonido de los motores se volvió más agradable, el problema se resolvió.

Luego, el desarrollo del sistema operativo comenzó en nuestra empresa, y en las reuniones tuve que demostrar durante mucho tiempo que el enfoque típico para las interrupciones de procesamiento no siempre es aceptable en términos de velocidad, apelando solo a ese caso típico, pero muy glotón. Las discusiones sobre este tema se publican en mi artículo sobre las interrupciones en el sistema operativo aquí (Descripción general de un RTOS ruso, parte 8. Trabajar con interrupciones). En general, un problema se ha resuelto en mi cabeza durante mucho tiempo: las interrupciones auxiliares frecuentes que sirven a un subsistema ralentizan todo lo demás. El refinamiento simple del procesador central, por supuesto, elimina el problema, pero no trae la Satisfacción Moral Profunda de que todo esté bien hecho.

Periódicamente, volví a esta pregunta en un sentido puramente teórico. Por ejemplo, un día se me ocurrió la idea de que en lugar de usar un controlador costoso, puede tomar tres STM32F103C8T6, en el que una placa de prueba ya hecha cuesta 110 rublos, teniendo en cuenta la entrega, y el chip en sí es aún más barato. En uno de ellos para sacar solo la función de control del motor. Permítale gastar toda su potencia informática en esta función. Un par de los otros (tal vez incluso uno) resuelve otras tareas (procesar comandos, trabajar con PWM, mantener la temperatura, etc.) en un ambiente tranquilo. Esta solución también tiene una gran ventaja adicional: el número total de pines para varios controladores es simplemente enorme. En un STM32, tuve que diseñar solitario durante mucho tiempo, a qué pierna asignar. Aunque las patas de las salidas del temporizador y las patas ADC de los BRAZOS se asignan de manera más flexible que los controladores antiguos (una salida de la unidad de hardware puede ir a una de las varias patas físicas), pero al desplegar el solitario, comprende que la flexibilidad puede no ser suficiente. Si hay muchos controladores, entonces la elección aumenta. En el que sirve a motores paso a paso, en general, simplemente asignamos todas las patas como salidas digitales. Los otros también tienen dónde dar la vuelta.

Un problema con este enfoque es cómo sincronizar estos controladores. En teoría, el MAX Max RTOS contiene todo lo que necesita. El controlador de comandos genera una lista de tareas para mover cabezas. Periódicamente, los modifica (coordinando aceleraciones con tareas recién llegadas). Por lo tanto, la memoria para el formador y el artista debe ser compartida. RTOS MAX contiene la funcionalidad para organizar dicha memoria compartida. Lo describí aquí (Descripción general de un RTOS ruso, parte 7. Medios de intercambio de datos entre tareas). Pero en la práctica, un matiz arruina todo: el mantenimiento de motores paso a paso es un tipo de tarea de tiempo crítico. El menor retraso, y obtenemos flujos de plástico para una impresora 3D, para otras máquinas CNC, bueno, por ejemplo, hilos incorrectamente roscados. Cualquier comunicación a través de interfaces seriales no es la más rápida. Además: tiempo de arbitraje y otras necesidades oficiales. Y resulta que todas las ganancias de la eliminación de la funcionalidad del procesador principal se destinan a gastos generales. Por supuesto, aproveché mi posición oficial: fui y discutí este problema con los desarrolladores de este subsistema. Por desgracia Dijeron que hay sincronización sin demasiada sobrecarga en el sistema operativo, pero para equipos que admiten los buses correspondientes. Ahora, si tomo la arquitectura TigerShark como base, el sistema operativo organizará todo por mí sin ningún gasto. Solo los controladores fabricados de acuerdo con esta arquitectura son varias veces más caros que toda la impresora 3D que quería poner en ella. En general, nuevamente inaceptable.

Nos acercamos al final de una introducción prolongada. Alguien dirá que por alguna razón todavía estoy buscando un príncipe en un caballo blanco. Puede tomar y hacer todo sin un sistema operativo, y aquí estoy considerando todo tipo de opciones ... Puede, pero puede, pero cuando surgió el problema práctico "Cansado de escuchar el bloqueo de la impresora", se solucionó rápidamente. Eso es todo. Ella ya no está. Además, desde entonces han aparecido nuevos controladores de motor paso a paso que generalmente resuelven el problema de una manera completamente diferente (obtienen un microstep de 1/16 y dan 1/256). Y en esta introducción, describo precisamente que "No hay una solución hermosa al problema de las interrupciones frecuentes". Hace mucho que se tomó una decisión fea. No quería perder el tiempo revisando otras decisiones feas. Simplemente se desplazaron en mi cabeza.

Pero cuando me ocupé de los bloques UDB, me pareció que el problema se puede resolver de manera hermosa y dramática. Simplemente puede llevar el procesamiento de interrupciones desde el software al nivel de firmware, dejando la parte informática en la conciencia del procesador principal. ¡No se necesitan controladores adicionales! ¡Todo se coloca en el mismo chip! Entonces, comencemos.

Caballo esférico en el vacío


En este artículo, trabajar con UDB estará a la vanguardia. Si hablara de estar atado a un "firmware" específico, podrían señalarme correctamente que estaba equivocado con el concentrador. ¿Qué es para GeekTimes? Por lo tanto, UDB es primario, y los motores paso a paso son solo una cosa hermosa para ilustrar. En esta parte, generalmente haré un caballo esférico en el vacío. Tendrá deficiencias prácticas, que eliminaré en la segunda parte. Pero repitiendo mis acciones, los lectores podrán dominar la metodología de desarrollo de firmware para UDB.

Entonces ¿Cómo funciona el mecanismo de control del motor paso a paso? Hay una tarea que pone en línea los segmentos que la cabeza debe pasar con velocidad lineal. Hasta ahora, pretenderé no recordar la aceleración al principio y al final del segmento. Solo la cabeza debería pasar. Nuevos segmentos se ponen en la cola de la cola. Basado en la grabación de la cabeza, una tarea separada envía señales STEP a todos los motores activos.

Deje que la impresora tenga una velocidad máxima del cabezal de 200 mm / s. Deje 200 pasos necesarios por 1 milímetro de movimiento (esta cifra corresponde a una impresora real MZ3D-256C con un micropaso 1/32). Luego, los pulsos se deben suministrar con una frecuencia de hasta 200 * 200 = 40,000 Hz = 40 KHz. Es con tal frecuencia que una tarea que envía pulsos de paso puede ser invocada. Debe formar los pulsos mediante programación, y también calcular cuánto tiempo después de lo cual debe llamarse la próxima interrupción que lo active.

Recuerdo una broma sobre Kolobok y los Tres Bogatyrs, donde Kolobok saludaba constantemente a los Bogatyrs, luego les hacía preguntas y recibía respuestas. Luego sucesivamente se despidió de ellos. Bueno, entonces se reunió con los Treinta y tres Caballeros. El procesador está en el papel de un bollo, y los motores paso a paso están en el papel de Bogatyrs. Está claro que en presencia de una gran cantidad de bloques UDB, es posible paralelizar el trabajo con los motores, haciendo que cada motor sea reparado a su bloque. Y dado que tenemos segmentos durante los cuales los motores se moverán de manera uniforme, intentemos que el equipo funcione con tales transacciones, y no con cada paso.

¿Qué información se requiere para que un caballo esférico atraviese una sección lineal en el vacío?

  • Numero de pasos.
  • El período de tiempo entre pasos.

Dos parámetros UDB solo tiene dos baterías y dos registros de parámetros D0 y D1. Parece que todo es realizable. Solo estimamos la profundidad de bits que deberían tener estos registros.

Primero, el número de pasos. Si hay 8 dígitos, entonces, en un ciclo de operación UDB, la impresora podrá mover el cabezal de la impresora cartesiana un poco más de 1 mm (200 micro pasos). No es suficiente Si la capacidad es de 16 bits, el número de pasos será 65536. Esto es 65536/200 = 327 milímetros. Aceptable para la mayoría de los modelos. Para Core, Delta y otros es necesario estimar, pero en su conjunto, para un trazo completo, el segmento se puede dividir en varias partes. No habrá tantos (dos, bueno, un máximo de tres).

Ahora el periodo. Deje que la frecuencia del reloj sea de 48 MHz. 48000000/65536 = 732. Es decir, la frecuencia mínima permitida que se puede obtener con un divisor de 16 bits es de 732 Hz. Demasiado En el firmware Marlin, el mínimo es de 120 Hz (que corresponde aproximadamente a 8 MHz dividido por la misma constante 65536). Tendremos que hacer los registros de 24 bits. Entonces la frecuencia mínima será igual a 48000000 / (2 ^ 24) = 48000000/16777216 = 2.861 Hz.

Bueno ¡Alto a la aburrida teoría! ¡Pasemos a practicar! Inicie PSoC Creator y seleccione Archivo-> Nuevo-> Proyecto:



Luego, seleccioné la placa de prueba que tengo, de la cual el entorno tomará información básica sobre el controlador utilizado y su configuración:



Ya me siento listo para crear un proyecto desde cero, así que selecciono Esquema vacío :



Dé al entorno de trabajo el nombre PSoC3DTest :



Y aquí está, ¡un proyecto terminado!



Lo primero que quiero hacer es crear mi propio componente basado en UDB. Por lo tanto, como ya se señaló en el último artículo, necesito cambiar a la pestaña Componentes :



Haga clic derecho en el proyecto y seleccione Agregar elemento componente :



Decimos que necesitamos agregar un documento UDB , cambiar el nombre a StepperController y hacer clic en Crear nuevo :



El componente apareció en el árbol, además, el editor de este componente abrió:



Coloque el bloque Datapath en el formulario:



Una vez seleccionado este bloque, vamos a sus propiedades y cambiamos la profundidad de bits de 8 a 24. Los parámetros restantes pueden dejarse sin cambios.



Para iniciar todos los bloques (para todos los motores) al mismo tiempo, iniciaré la señal de inicio desde el exterior (agregue la entrada de Inicio ). Salidas: Haré que Step salga directamente, para poder enviarlo al controlador del motor paso a paso, así como a Out_Idle . Según esta señal, el procesador podrá determinar que en el momento en que la unidad haya terminado su trabajo. Los nombres de los circuitos que coinciden con estas entradas y salidas son visibles en la figura.



Antes de hablar sobre la lógica del autómata, describiré otro problema puramente de ingeniería: establecer la duración del pulso Paso . La documentación del controlador DRV8825 requiere que el ancho de pulso sea de al menos 1.9 μs. Otros conductores son menos exigentes en su ancho. Como ya se señaló en la parte teórica, los registros existentes ya están ocupados estableciendo la duración del paso y el número de pasos. Nos guste o no, se debe colocar un contador de siete bits en el circuito. Lo llamamos one-shot, que establece el pulso de paso. A una frecuencia de 48 MHz, para garantizar una duración de 1.9 μs, este contador debe contar al menos 91.2 pasos. Redondea a 92. Cualquier valor que exceda esto no será menor. Resulta la siguiente configuración:



Nombre del contador SingleVibrator . Nunca se restablece, por lo que la entrada Restablecer siempre está conectada a cero, considera que cuando la máquina (descrita a continuación) está en el estado Uno, se carga en todos los demás estados (al principio seleccioné los estados específicos de la máquina, pero resultó que con un método tan complicado , se requieren muchos menos recursos PLD, pero el resultado es el mismo). El valor de carga es decimal 92. Es cierto, un buen editor reemplazará inmediatamente este valor con hexadecimal:



Cuando el contador se cuenta hasta cero, informará esto a la cadena con el nombre One_Finished . Con el mostrador, eso es todo.

¿Qué tipo de indicadores de estado utilizará nuestra máquina? Lo tengo así (le recuerdo que haga doble clic en la lista de salidas en Datapath para configurarlas):





Usaré la batería A0 como contador durante la duración del pulso, por lo que cuando su valor llegue a cero, se activará la bandera a la que le di el nombre Pulse_Finished . La batería A1 contará pulsos para mí. Por lo tanto, su puesta a cero activará la bandera Process_Finished .

Construimos el gráfico de transición del autómata:



La variable que establece su estado se llama Estado . Asigne inmediatamente esta variable al registro de dirección de la instrucción ALU. Al principio olvidé hacer esto, así que durante mucho tiempo no pude entender por qué mi máquina no funciona. Haga doble clic en el bloque de entradas en Datapath:



Y partido:



Comenzamos a tratar con el gráfico de transición y las instrucciones ALU asociadas con él.

Comencemos con el estado inactivo . Está bastante saturado en sus acciones.

En primer lugar, el valor de los registros de datos D0 y D1 se coloca constantemente en las baterías A0 y A1, respectivamente:



Desde esta entrada, el ojo entrenado verá todo lo que necesita. Como nuestros ojos aún no están establecidos, hacemos doble clic en la entrada y vemos lo mismo, pero con más detalle:



El valor principal aquí es llenar la batería A1, el contador de pulsos. Cuando el programa ingresa el valor D1, inmediatamente va a A1. El programa definitivamente no tendrá tiempo para comenzar el proceso hasta la próxima medida. Este valor se verifica para formar una condición para salir de este estado, es decir, no hay otro lugar para llenarlo.

Ahora veamos qué se hace en el nivel del gráfico de transición:



El disparador auxiliar Start_Prev le permite capturar un borde positivo en la entrada Start , organizando una línea de retardo para 1 ciclo. Siempre contendrá el estado de la entrada de Inicio , que estaba en la medida anterior. Alguien está más familiarizado con ver esto en Verilog:



Mismo texto
always @ (posedge clock) begin : Idle_state_logic case(State) Idle : begin Start_Prev <= (Start); IsIdle <= (1); if (( Start&(!Start_Prev)&(!Process_Finished) ) == 1'b1) begin State <= One ; end end 


En consecuencia, la condición de Inicio y (! Start_Prev) es verdadera solo cuando se produce una diferencia de línea de Inicio positiva entre las medidas .

Además, cuando la máquina está en este estado, la salida IsIdle se pone en un solo estado, informando al entorno externo que el bloque es pasivo. Con este enfoque, se gastan menos recursos PLD que si la construcción State == Idle se enviara a la salida.

Cuando la diferencia de la señal de Inicio proviene del entorno externo, y el acumulador A1 tiene un valor distinto de cero, la máquina saldrá del estado Inactivo . Si se ingresa cero en A1, entonces el motor no participa en el desarrollo de este segmento, por lo que se ignora la diferencia en la línea de Inicio . Esto se aplica a una extrusora no utilizada. Para algunas impresoras, el motor del eje Z también se usa raramente. Permítanme recordarles cómo se forma una condición que revela un valor cero en A1 (y su inversión no es cero):



Luego, la máquina ingresa al estado Uno :



En este estado, la salida de paso se establece en 1. Se aplica un impulso de paso al controlador. Además, se restablece el valor del desencadenador IsIdle . Se informa al entorno externo que la unidad está en la fase activa.

La señal One_Finished sale de este estado, que se elevará a uno cuando el contador de siete bits cuente a cero. Permítame recordarle que la señal One_Finished es generada por este contador en particular:



Mientras la máquina está en este estado, la ALU carga en la batería A0 (configurando la duración del pulso) el valor del registro D0. Déjame mostrarte solo una breve nota que dice esto:



El valor cargado se usará en el siguiente estado. Al estar en él, la máquina genera un retraso que establece la duración del pulso:



La salida del paso se restablece a cero. La batería A0 disminuye, como lo demuestra la siguiente breve entrada:



Y si hace doble clic en él, una entrada completa:



Cuando el valor de A0 llega a cero, se levantará el indicador Pules_Finished y la máquina pasará al estado Decremento :



En este estado, en ALU, el valor del acumulador A1 disminuye, lo que establece el número de pulsos



Versión completa del registro:



Dependiendo del resultado, se produce una transición al siguiente pulso o al estado inactivo . Haga doble clic en el estado para ver las transiciones teniendo en cuenta las prioridades:



En realidad, con UDB todo. Ahora hacemos el símbolo correspondiente. Para hacer esto, haga clic derecho en el editor y seleccione Generar símbolo :



Vamos al diagrama del proyecto:



Y presentamos un circuito en el que hay un cierto número de estos controladores. Elegí cinco (tres ejes más dos extrusoras). Las impresoras con una gran cantidad de extrusoras no se considerarán baratas. Puedes poner FPGA en ellos. En el camino, para ver la complejidad real, arrojé un bloque USB-UART (para recibir datos de una computadora o el mismo Raspberry Pi) y un UART real (proporcionará comunicación con un módulo Wi-Fi barato ESP8266 o, por ejemplo, una pantalla inteligente que puede enviar GCODE a través de UART). No agregué PWM, etc., ya que su complejidad es aproximadamente clara y el sistema real aún está muy lejos. Resultó de alguna manera así:



El registro de control genera una señal de disparo, que se dirige a todos los bloques simultáneamente. Además, deje que salgan señales, que son estáticas durante la formación del segmento. Recopilé todas las salidas inactivas por "Y" y apliqué a la entrada de interrupción. Nombré una interrupción en un frente positivo. Si al menos un motor arranca, la entrada de interrupción se restablecerá. Al final del último motor, se activará, lo que informará al procesador sobre la preparación para la conclusión del próximo segmento. Ahora ajuste las frecuencias haciendo doble clic en el elemento del árbol Clocks :



En la tabla que aparece, haga doble clic en el elemento PLL_OUT :



Completaremos la tabla de alguna manera (no he entendido bien las reglas para configurar esta tabla, por eso uso el término "Algo así"):



Ahora haga doble clic en la línea Clock_1 :



Configure la frecuencia de reloj de los bloques UDB a 48 MHz:



Como el proyecto es experimental, no tiene sentido hacer una API. Pero para consolidar el material estudiado en el artículo anterior, volvemos a la pestaña Componentes y para el proyecto StepperController, hacemos clic con el botón derecho en Agregar elemento componente primero agregamos el archivo de encabezado y luego el archivo de código fuente C:





Mostraré superficialmente las dos funciones de inicialización e inicio del segmento que agregué. El resto se puede ver en el ejemplo del artículo.

 void `$INSTANCE_NAME`_Start() { `$INSTANCE_NAME`_SingleVibrator_Start(); //"One" Generator start } void `$INSTANCE_NAME`_PrepareStep(int nSteps,int duration) { CY_SET_XTND_REG24(`$INSTANCE_NAME`_Datapath_1_D0_PTR, duration>92?duration-92:0); CY_SET_XTND_REG24(`$INSTANCE_NAME`_Datapath_1_D1_PTR, nSteps>1?nSteps-1:0); } 

Reemplacé el nombre de main.c por main.cpp para verificar que el entorno de desarrollo responderá normalmente a C ++, porque el firmware de Marlin está orientado a objetos. Errores predecibles de lluvia que fueron eliminados previsiblemente mediante la adición de algo regular:



Mismo texto
 extern "C" { #include "project.h" } 


Para el lanzamiento global de motores, hice una función de este tipo (es muy difícil, pero para los experimentos con un caballo esférico en el vacío, lo hará, en los experimentos el tiempo de desarrollo es más importante que la belleza):
 void StartSteppers() { Stepper_Control_Reg_Write (1); Stepper_Control_Reg_Write (1); Stepper_Control_Reg_Write (1); Stepper_Control_Reg_Write (0); } 

Ella inicia la señal de Inicio , por si acaso, inmediatamente por tres medidas, luego la vuelve a soltar.

Bueno, comencemos los experimentos. Primero, simplemente pase por encima de los motores X e Y (en el ejemplo, el primer grupo de llamadas inicializa todos los controladores, el segundo establece los controladores X e Y en el número requerido de pasos e inicia el proceso):

 int main(void) { CyGlobalIntEnable; /* Enable global interrupts. */ StepperController_X_Start(); StepperController_Y_Start(); StepperController_Z_Start(); StepperController_E0_Start(); StepperController_E1_Start(); StepperController_X_PrepareStep (10,1000); //    StepperController_Y_PrepareStep (50,500); StartSteppers(); //   for(;;) { } } 

Nos fijamos en el resultado:



Verifique la duración del pulso positivo:



Eso es correcto Finalmente, verificamos qué tan bien funciona la interrupción. Agregue una variable de contador global:

 static int nStep=0; 

Esta variable se asigna a uno en la función principal y aumenta en la función de manejo de interrupciones. El controlador de interrupciones se disparará solo una vez, solo para verificación. Lo hice así:

 extern "C" { CY_ISR(StepperFinished) { if (nStep == 1) { StepperController_X_PrepareStep (5,500); StartSteppers(); nStep += 1; } } } 

Y en la función principal , agregué literalmente dos líneas: la inclusión de interrupciones y la asignación de esta muy variable. Y ya asigno cuando las máquinas comenzaron. De lo contrario, llegó una solicitud de interrupción falsa. No hay ninguna razón particular para combatirlo ahora. El proyecto es experimental.



Mismo texto
 int main(void) { CyGlobalIntEnable; /* Enable global interrupts. */ isr_1_StartEx(StepperFinished); StepperController_X_Start(); StepperController_Y_Start(); StepperController_Z_Start(); StepperController_E0_Start(); StepperController_E1_Start(); /* Place your initialization/startup code here (eg MyInst_Start()) */ StepperController_X_PrepareStep (10,1000); StepperController_Y_PrepareStep (20,500); StartSteppers(); nStep = 1; for(;;) { } } 


Verificamos el resultado (en el segundo paso, solo el motor X debería funcionar, y los pasos deberían ser la mitad):



Eso es correcto

Conclusión


En general, ya está claro que los bloques UDB pueden usarse no solo para configurar funciones rápidas de hardware, sino también para mover la lógica del software al nivel de firmware. Desafortunadamente, el volumen del artículo resultó ser tan grande que parece imposible completar la revisión y obtener una respuesta inequívoca si las capacidades de UDB son suficientes para la solución final de la tarea. Hasta ahora, solo un caballo esférico está listo en el vacío, cuyas acciones en principio son muy similares a las requeridas, pero un lector molesto familiarizado con la teoría del control del motor paso a paso encontrará muchas deficiencias en él. La unidad presentada no es compatible con la aceleración, sin la cual el funcionamiento de un motor paso a paso real es imposible. Más bien, es compatible, pero en esta etapa se requerirá una alta tasa de interrupción, y todo fue concebido para evitar esto.

La precisión de establecer la frecuencia del bloque presentado está lejos de ser aceptable. En particular, proporcionará una frecuencia de pulso de 40,000 Hz con un divisor de 1200 y 39966 Hz con un divisor de 1201. Las frecuencias intermedias entre estos dos valores en esta unidad son inalcanzables.

Quizás hay algunas otras deficiencias en él. Pero trataremos con ellos en el próximo artículo para verificar si hay suficientes recursos de UDB.

Mientras tanto, los lectores han recibido, entre otras cosas, un ejemplo real de crear un bloque basado en UDB desde cero. El proyecto de prueba obtenido durante la redacción de este artículo se puede tomar aquí .

Source: https://habr.com/ru/post/es433800/


All Articles