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





En las dos partes anteriores, hablé sobre cómo hice una GUI, comencé a controlar un motor paso a paso y a organizar el trabajo con archivos en una unidad flash USB.



Hoy escribiré sobre el proceso de impresión, la salida de las capas impresas a la pantalla de resaltado y las cosas restantes, no tan esenciales:



4. Salida de las imágenes de las capas a la pantalla de resaltado.

5. Todo, como controlar la iluminación y los ventiladores, cargar y guardar configuraciones, etc.

6. Funciones adicionales para comodidad y conveniencia.





4.



4.1 -



¿Cómo logró un microcontrolador, que no tiene periféricos especializados, hacer la imagen en una matriz de alta resolución a una velocidad de 74 millones de píxeles por segundo (resolución de 2560x1440, 20 cuadros por segundo) actualizada a través de la interfaz MIPI? Respuesta: usando una FPGA con una SDRAM de 16 MB conectada y dos chips de interfaz MIPI: SSD2828. Dos microcircuitos valen porque la pantalla está lógicamente dividida en dos mitades, cada una de las cuales es atendida por su propio canal separado, por lo que se obtienen dos pantallas en una.



La imagen para mostrar se almacena en uno de los 4 bancos de SDRAM, el chip FPGA es responsable de dar servicio a la SDRAM y enviar la imagen al SSD2828. FPGA genera señales de sincronización vertical y horizontal para SSD2828 y unidades

flujo continuo de valores de color de píxeles en 24 líneas (8R 8G 8B) en cada uno de los SSD2828. La velocidad de fotogramas resulta ser de unos 20 Hz.



La FPGA está conectada al microcontrolador con una interfaz serial (SPI) a través de la cual el microcontrolador puede transmitir una imagen. Se transmite en paquetes, cada uno de los cuales contiene una línea de la imagen (las líneas se cuentan a lo largo del lado corto de la pantalla: 1440 píxeles). Además de estos datos, el paquete también contiene el número de banco SDRAM, el número de línea y la suma de verificación - CRC16. La FPGA recibe este paquete, verifica la suma de verificación y, si todo está bien, guarda los datos en el área SDRAM correspondiente. Si el CRC no coincide, la FPGA pone una señal en uno de sus pines, también conectado al microcontrolador, según la cual el microcontrolador entiende que los datos no llegaron con normalidad y puede repetir el envío. Para obtener una imagen completa, el microcontrolador debe enviar 2560 paquetes de este tipo a la FPGA.



Los datos de imagen dentro del paquete se representan en formato de bits: 1 - píxel está iluminado, 0 - píxel está oscuro. Por desgracia, esto excluye por completo la posibilidad de organizar el desenfoque en escala de grises de los bordes de las capas impresas: suavizado. Para organizar esta forma de desenfoque, es necesario reescribir la configuración (firmware) de la FPGA, para lo cual aún no estoy preparado. Llevo demasiado tiempo y no mucho tiempo trabajando con FPGA, tendré que volver a masterizar prácticamente todo.



Además de los paquetes de datos, el microcontrolador también puede enviar un comando de control para indicar desde qué banco SDRAM leer datos para enviarlos a la pantalla y encender / apagar la salida de imagen.



Los chips SSD2828 también están conectados al microcontrolador a través de SPI. Esto es necesario para configurar sus registros cuando están encendidos, transferirlos al modo de suspensión o activo.

Hay varias líneas más entre el microcontrolador y el FPGA / SSD2828: la señal de reinicio y las señales de selección de chip activo (Chip Select) para cada uno de los microcircuitos.



En general, este esquema de trabajo está bastante lejos de ser óptimo, en mi opinión. Por ejemplo, sería más lógico conectar la FPGA al microcontrolador a través de una interfaz de memoria externa paralela, los datos se transferirían mucho más rápido que a través de SPI con un límite de frecuencia de 20 MHz (cuando la frecuencia aumenta, la FPGA deja de recibir datos normalmente). Además, la señal de reinicio no se enruta a la entrada física Reset FPGA, sino como una señal lógica normal, es decir, la FPGA no realiza un reinicio de hardware en ella. Y esto también jugó una broma cruel, que se discutirá a continuación.



Descubrí todo esto entendiendo los códigos fuente del fabricante. Transferí las funciones de trabajar con FPGA de su código fuente tal cual, todavía no entendía completamente cómo funciona todo. Afortunadamente, los chinos han comentado su código lo suficiente (en chino) para poder descifrarlo sin mucha dificultad.



4.2 Leer capas de un archivo de impresión



Ok, ya hemos calculado más o menos el resultado de la imagen terminada, ahora te contaré un poco sobre cómo se extraen estas imágenes de archivos preparados para imprimir. Los archivos .pws, .photons, .photon, .cbddlp son esencialmente un montón de imágenes de capas. Este formato vino, que yo sepa, de la empresa china Chitu, a la que se le ocurrió la idea de hacer placas con dicho circuito (microcontrolador - FPGA - SDRAM - SSD2828). Suponga que desea imprimir un modelo con una altura de 30 mm con cada capa de 0,05 mm de grosor. El programa de corte corta este modelo en capas del grosor especificado y para cada una de ellas forma su imagen.



Así, se obtienen 30 / 0.05 = 600 imágenes con una resolución de 1440x2560. Estas imágenes se empaquetan en un archivo de salida, el encabezado con todos los parámetros se ingresa allí y dicho archivo ya se envía a la impresora. Las imágenes de capa tienen una profundidad de 1 bit y el algoritmo RLE las comprime un byte a la vez; el bit más significativo indica el valor del color y los siete bits menos significativos representan el número de repeticiones. Este método le permite comprimir la imagen de capa de 460 KB a aproximadamente 30-50. La impresora lee la capa comprimida, la descomprime y la envía línea por línea a la FPGA.



El fabricante hace esto de la siguiente manera:



  1. — 1, 1, 0. , (1440), .
  2. , 1440 (180 ).
  3. FPGA .


Este es el método de tres pasos utilizado por los chinos. Resultó que esto se hizo para que la imagen de la capa pudiera mostrarse en forma reducida en la pantalla de la interfaz, mostrando al usuario lo que se está imprimiendo. Esta imagen se acaba de formar a partir de la matriz de bytes. Aunque no está claro qué impidió formarlo inmediatamente a partir de los bits decodificados. Y tampoco está claro qué impidió la formación de un mapa de bits para transferirlo a FPGA en el mismo ciclo.



Ahora uso el mismo método, aunque optimizado. Para aclarar cuál fue la optimización, necesito aclarar un punto más. Los datos de la línea de visualización no son una matriz sólida de carga útil. En el medio hay algunos píxeles "que no funcionan" adicionales debido al hecho de que dos controladores de pantalla están unidos en el lado corto, y cada uno de ellos tiene 24 píxeles "que no funcionan" en los bordes. Así, los datos reales transmitidos para una línea de la imagen constan de 3 partes: datos para la primera mitad (primer controlador), 48 píxeles intermedios "no operativos", datos para la segunda mitad (segundo controlador).



Entonces, los chinos, al formar la matriz de bytes dentro del bucle, verificaron si se alcanzó el final de la primera mitad, si no, el valor fue escrito por el puntero * p, de lo contrario mediante el puntero * (p + 48) . Esta verificación para cada uno de los 1440 valores, e incluso la modificación del puntero para la mitad de ellos, claramente no contribuyó a la velocidad del bucle. Divido este bucle en dos separados: en el primero, se llena la primera mitad de la matriz, después de este bucle, el puntero aumenta en 48 y el segundo bucle comienza para la segunda mitad de la matriz. En la versión original, la capa se leyó y se mostró en 1,9 segundos, esta modificación por sí sola redujo el tiempo de lectura y salida a 1,2 segundos.



Otro cambio se refería a la transferencia de datos a FPGA. En las fuentes originales, pasa por DMA, pero después del inicio de la transferencia a través de DMA, la función espera su finalización y solo después comienza a decodificar y formar una nueva línea de la imagen. Eliminé esta expectativa para que la siguiente línea se genere mientras se transfieren los datos de la línea anterior. Esto redujo el tiempo en otros 0,3 segundos, a 0,9 por capa. Y esto es cuando compila sin optimización, si compila con optimización completa, entonces el tiempo disminuye a aproximadamente 0.53 segundos, lo que ya es bastante aceptable. De estos 0,53 segundos, se necesitan aproximadamente 0,22 segundos para calcular CRC16 y aproximadamente 0,19 segundos para formar un mapa de bits a partir de una matriz de bytes antes de la transmisión. Pero la transferencia de todas las líneas a FPGA toma alrededor de 0.4 segundos y con esto, lo más probable es queno hay nada que hacer, todo aquí se basa en la limitación de la frecuencia máxima de SPI permitida para FPGA.



Si yo mismo escribiera la configuración FPGA, entonces podríamos darle la descompresión RLE, y esto podría acelerar la salida de la capa en un orden de magnitud, pero ¿cómo se hace esto?



Y sí, iba a escribir sobre la jamba asociada con el hecho de que la FPGA no se reinicia por hardware en una señal de reinicio del microcontrolador. Entonces, cuando ya aprendí a mostrar imágenes de capas, completé el proceso de impresión en sí, me encontré con un error incomprensible: una vez de 5 a 10, la impresión se inició con una pantalla completamente iluminada. Veo en el depurador que las capas se leen correctamente, los datos se envían a la FPGA según sea necesario, la FPGA confirma la corrección del CRC. Es decir, todo funciona y, en lugar de dibujar una capa, una pantalla completamente blanca. Claramente, la culpa es de FPGA o SSD2828. Una vez más, verifiqué dos veces la inicialización de SSD2828: todo está bien, todos los registros en ellos se inicializan con los valores requeridos, esto se puede ver durante la lectura de control de los valores de ellos. Entonces ya metí la mano en el tablero con un osciloscopio. Y descubrí que cuando ocurre tal falla, la FPGA no escribe ningún dato en SDRAM. NOSOTROS señal,permitiendo la escritura, se encuentra arraigado en el lugar en el nivel inactivo. Y probablemente habría luchado con este error durante mucho tiempo, si no fuera por un amigo que me recomendó que intentara darle a la FPGA un comando explícito para apagar la salida de la imagen antes de reiniciar, de modo que en el momento del reinicio no hay llamadas garantizadas desde FPGA a SDRAM ¡Lo intenté y funcionó! Este error nunca volvió a aparecer. Al final, llegamos a la conclusión de que el núcleo IP del controlador SDRAM dentro de la FPGA no está implementado del todo correctamente, el reinicio e inicialización del controlador SDRAM no ocurre normalmente en todos los casos. Algo impide el correcto reinicio si en este momento se accede a los datos en SDRAM. Me gusta esto…quien aconsejó intentar antes de reiniciar para darle a la FPGA un comando explícito para apagar la salida de imagen, de modo que en el momento de reiniciar no se garantice ninguna llamada desde la FPGA a SDRAM. ¡Lo intenté y funcionó! Este error nunca volvió a aparecer. Al final, llegamos a la conclusión de que el núcleo IP del controlador SDRAM dentro de la FPGA no se implementó del todo correctamente, el reinicio e inicialización del controlador SDRAM no ocurre normalmente en todos los casos. Algo interfiere con el restablecimiento correcto si se accede a los datos en SDRAM en este momento. Me gusta esto…quien recomendó intentar antes de reiniciar para darle a la FPGA un comando explícito para apagar la salida de imagen, de modo que en el momento de reiniciar no se garantice ninguna llamada desde la FPGA a la SDRAM. ¡Lo intenté y funcionó! Este error nunca volvió a aparecer. Al final, llegamos a la conclusión de que el núcleo IP del controlador SDRAM dentro de la FPGA no está implementado del todo correctamente, el reinicio e inicialización del controlador SDRAM no ocurre normalmente en todos los casos. Algo interfiere con el reinicio correcto si se accede a los datos en SDRAM en este momento. Me gusta esto…que el núcleo IP del controlador SDRAM dentro de la FPGA no está implementado correctamente, el reinicio e inicialización del controlador SDRAM no funciona normalmente en todos los casos. Algo impide el correcto reinicio si en este momento se accede a los datos en SDRAM. Me gusta esto…que el núcleo IP del controlador SDRAM dentro de la FPGA no está implementado correctamente, el reinicio e inicialización del controlador SDRAM no funciona normalmente en todos los casos. Algo interfiere con el restablecimiento correcto si se accede a los datos en SDRAM en este momento. Me gusta esto…



4.3 Interfaz de usuario durante la impresión de archivos



Una vez que el usuario ha seleccionado el archivo y ha comenzado a imprimirlo, aparece la siguiente pantalla:







Esta es una pantalla bastante estándar para este tipo de impresoras de fotopolímero.



El área más grande de la pantalla está ocupada por la imagen de la capa actualmente expuesta.

La visualización de esta imagen se sincroniza con la luz de fondo: cuando la luz de fondo está encendida, se muestra la imagen, cuando se apaga la luz de fondo, la imagen se borra. La imagen se forma como en la pantalla UV, a lo largo del lado corto de la imagen. No me apresuré con los punteros a lo largo de los desplazamientos de línea de esta imagen, pero justo antes de mostrarla le doy al controlador de pantalla un comando para cambiar la dirección de salida de los datos que se vierten, es decir el área de esta imagen resulta estar "volteada" de lado.



A continuación se muestra información sobre el progreso de la impresión: el tiempo de impresión transcurrido y estimado, la capa actual y el número total de capas, una barra de progreso con porcentajes a la derecha. También quiero agregar la altura actual en milímetros después del número de capas, solo para ser.



A la derecha están los botones de pausa, configuración e interrupción. Cuando presiona la pausa en el firmware, la bandera de pausa se establece y el comportamiento adicional depende del estado de la impresora en ese momento. Si la plataforma baja para la siguiente capa o la exposición de la capa ya ha comenzado, entonces el firmware completará la exposición y solo después de eso elevará la plataforma a la altura de pausa (que se establece en la configuración), donde esperará hasta que el usuario haga clic en el botón "Continuar":







El levantamiento de la plataforma para una pausa ocurre primero a la velocidad especificada en los parámetros del archivo, y después de la altura especificada en los mismos parámetros, la velocidad aumenta.



Cuando se interrumpe la impresión, aparecerá una ventana confirmando esta acción, y solo después de la confirmación se detendrá la impresión y la plataforma subirá a la altura máxima del eje. La velocidad de elevación, así como durante la pausa, es variable: primero lentamente para separar la capa de la película y luego aumenta al máximo.



El botón de configuración aún no es funcional, pero al hacer clic en él, el usuario será llevado a una pantalla con parámetros de impresión que se pueden cambiar: tiempo de exposición de la capa, altura y velocidad de elevación, etc. Ahora mismo lo estoy terminando. También existe una idea para dar la oportunidad de guardar los parámetros modificados en el archivo impreso.



5. Todo, como controlar la iluminación y los ventiladores, cargar y guardar configuraciones, etc.



La placa tiene 3 salidas MOSFET de alta potencia: una para LED UV para iluminación y dos para ventiladores (enfriar los diodos de iluminación y enfriar la pantalla, por ejemplo). No hay nada interesante aquí: las salidas del microcontrolador están conectadas a las puertas de estos transistores y controlarlos es tan fácil como hacer parpadear un LED. Para una alta precisión del tiempo de exposición, se enciende en el ciclo principal a través de la función que establece el tiempo de funcionamiento:



UVLED_TimerOn(l_info.light_time * 1000);

void		UVLED_TimerOn(uint32_t time)
{
	uvled_timer = time;
	UVLED_On();
}


Y se apaga a partir de la interrupción de milisegundos del temporizador cuando el contador de retroiluminación llega a cero:



...
	if (uvled_timer && uvled_timer != TIMER_DISABLE)
	{
		uvled_timer--;
		if (uvled_timer == 0)
			UVLED_Off();
	}
...


5.1 Configuración, carga desde archivo y guardado en EEPROM



Los ajustes se almacenan en la EEPROM incorporada en 24c16. Aquí, a diferencia de almacenar recursos en una gran memoria flash, todo es simple: para cada tipo de datos almacenados, el desplazamiento de dirección dentro de la EEPROM está codificado. En total, se almacenan tres bloques en él: configuración del eje Z, configuración general del sistema (idioma, sonido, etc.) y contadores para el tiempo de funcionamiento de los componentes principales de la impresora: luz, pantalla y ventilador.



Las estructuras de bloque almacenadas contienen la versión de firmware actual y una suma de comprobación primitiva, solo la suma de 16 bits de los valores de todos los bytes del bloque. Al leer los ajustes de la EPROM, se comprueba el CRC y si no corresponde al real, se asignan valores por defecto a los parámetros de este bloque, se calcula un nuevo CRC y el bloque se guarda en la EPROM en lugar del antiguo. Si el bloque de lectura no coincide con la versión actual, debe actualizarse a la versión actual y se guardará en una forma nueva en lugar de la anterior. Esto aún no se ha implementado, pero se hará en el futuro para actualizar correctamente el firmware.



Algunas configuraciones se pueden cambiar a través de la interfaz, pero la mayoría solo se pueden cambiar cargando un archivo de configuración. Aquí no cambié mis hábitos y escribí mi propio analizador para tales archivos.



La estructura de dicho archivo es estándar: nombre del parámetro + signo igual + valor del parámetro. Una línea, un parámetro. Los espacios y tabulaciones al principio de una línea y entre el signo igual y el nombre y el valor se ignoran. Las líneas en blanco y las líneas que comienzan con el carácter de almohadilla - "#" también se ignoran, este carácter define líneas con comentarios. El caso de las letras en los nombres de parámetros y secciones no importa.



Además de los parámetros, el archivo también contiene secciones cuyos nombres están entre corchetes. Después del nombre de la sección encontrado, el analizador espera que solo los parámetros que pertenecen a esta sección vayan más allá hasta que se encuentre otro nombre de sección. Honestamente, no sé por qué presenté estas secciones. Cuando hice esto, tuve algún tipo de pensamiento asociado con ellos, pero ahora no puedo recordarlo.



Para acortar las comparaciones del nombre del parámetro de lectura con nombres predefinidos, primero se analiza la primera letra del nombre de lectura y, a continuación, solo se comparan los nombres que comienzan con esta letra.



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




Cuando se selecciona un archivo de este tipo (con la extensión .acfg) en la lista de archivos, el firmware le preguntará al usuario si desea descargar y aplicar la configuración de este archivo y, tras la confirmación, comenzará a analizar este archivo.







Si se encuentra un error, se mostrará un mensaje indicando el tipo de error y el número de línea. Se manejan los siguientes errores:



  • nombre de partición desconocido
  • nombre de parámetro desconocido
  • valor de parámetro no válido: cuando, por ejemplo, se intenta asignar un valor de texto a un parámetro numérico


Si alguien está interesado, aquí hay una hoja completa de las tres funciones principales del analizador
void			_cfg_GetParamName(char *src, char *dest, uint16_t maxlen)
{
	if (src == NULL || dest == NULL)
		return;
	
	char *string = src;
	// skip spaces
	while (*string != 0 && maxlen > 0 && (*string == ' ' || *string == '\t' || *string == '\r'))
	{
		string++;
		maxlen--;
	}
	// until first space symbol
	while (maxlen > 0 && *string != 0 && *string != ' ' && *string != '\t' && *string != '\r' && *string != '\n' && *string != '=')
	{
		*dest = *string;
		dest++;
		string++;
		maxlen--;
	}
	
	if (maxlen == 0)
		dest--;
	
	*dest = 0;
	return;
}
//==============================================================================




void			_cfg_GetParamValue(char *src, PARAM_VALUE *val)
{
	val->type = PARAMVAL_NONE;
	val->float_val = 0;
	val->int_val = 0;
	val->uint_val = 0;
	val->char_val = (char*)"";
	
	if (src == NULL)
		return;
	if (val == NULL)
		return;
	
	char *string = src;
	// search '='
	while (*string > 0 && *string != '=')
		string++;
	if (*string == 0)
		return;
	
	// skip '='
	string++;
	// skip spaces
	while (*string != 0 && (*string == ' ' || *string == '\t' || *string == '\r'))
		string++;
	if (*string == 0)
		return;

	// check param if it numeric
	if ((*string > 47 && *string < 58) || *string == '.' || (*string == '-' && (*(string+1) > 47 && *(string+1) < 58) || *(string+1) == '.'))
	{
		val->type = PARAMVAL_NUMERIC;
		val->float_val = (float)atof(string);
		val->int_val = atoi(string);
		val->uint_val = strtoul(string, NULL, 10);
	}
	else
	{
		val->type = PARAMVAL_STRING;
		val->char_val = string;
	}
	
	return;
}
//==============================================================================




void			CFG_LoadFromFile(void *par1, void *par2)
{
	sprintf(msg, LANG_GetString(LSTR_MSG_CFGFILE_LOADING), cfgCFileName);
	TGUI_MessageBoxWait(LANG_GetString(LSTR_WAIT), msg);

	UTF8ToUnicode_Str(cfgTFileName, cfgCFileName, sizeof(cfgTFileName)/2);
	if (f_open(&ufile, cfgTFileName, FA_OPEN_EXISTING | FA_READ) != FR_OK)
	{
		if (tguiActiveScreen == (TG_SCREEN*)&tguiMsgBox)
			tguiActiveScreen = (TG_SCREEN*)((TG_MSGBOX*)tguiActiveScreen)->prevscreen;
		TGUI_MessageBoxOk(LANG_GetString(LSTR_ERROR), LANG_GetString(LSTR_MSG_FILE_OPEN_ERROR));
		BUZZ_TimerOn(cfgConfig.buzzer_msg);
		return;
	}

	uint16_t		cnt = 0;
	uint32_t		readed = 0, totalreaded = 0;
	char			*string = msg;
	char			lexem[128];
	PARAM_VALUE		pval;
	CFGREAD_STATE	rdstate = CFGR_GENERAL;
	int16_t			numstr = 0;
	
	while (1)
	{
		// read one string
		cnt = 0;
		readed = 0;
		string = msg;
		while (cnt < sizeof(msg))
		{
			if (f_read(&ufile, string, 1, &readed) != FR_OK || readed == 0 || *string == '\n')
			{
				*string = 0;
				break;
			}
			cnt++;
			string++;
			totalreaded += readed;
		}
		if (cnt == sizeof(msg))
		{
			string--;
			*string = 0;
		}
		numstr++;
		string = msg;
		
		// trim spaces/tabs at begin and end
		strtrim(string);
		
		// if string is empty
		if (*string == 0)
		{
			// if end of file
			if (readed == 0)
				break;
			else
				continue;
		}
		
		// skip comments
		if (*string == '#')
			continue;
		
		// upper all letters
		strupper_utf(string);
		
		// get parameter name
		_cfg_GetParamName(string, lexem, sizeof(lexem));
		
		// check if here section name
		if (*lexem == '[')
		{
			if (strcmp(lexem, (char*)"[ZMOTOR]") == 0)
			{
				rdstate = CFGR_ZMOTOR;
				continue;
			}
			else if (strcmp(lexem, (char*)"[GENERAL]") == 0)
			{
				rdstate = CFGR_GENERAL;
				continue;
			}
			else
			{
				rdstate = CFGR_ERROR;
				string = LANG_GetString(LSTR_MSG_UNKNOWN_SECTNAME_IN_CFG);
				sprintf(msg, string, numstr);
				break;
			}
		}
		
		// get parameter value
		_cfg_GetParamValue(string, &pval);
		if (pval.type == PARAMVAL_NONE)
		{
			rdstate = CFGR_ERROR;
			string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
			sprintf(msg, string, numstr);
			break;
		}
		
		// check and setup parameter
		switch (rdstate)
		{
			case CFGR_ZMOTOR:
				rdstate = CFGR_ERROR;
				if (*lexem == 'A')
				{
					if (strcmp(lexem, (char*)"ACCELERATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						cfgzMotor.acceleration = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'C')
				{
					if (strcmp(lexem, (char*)"CURRENT_HOLD_VREF") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val < 100)
							pval.uint_val = 100;
						if (pval.uint_val > 1000)
							pval.uint_val = 1000;
						cfgzMotor.current_hold_vref = pval.uint_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"CURRENT_VREF") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val < 100)
							pval.uint_val = 100;
						if (pval.uint_val > 1000)
							pval.uint_val = 1000;
						cfgzMotor.current_vref = pval.uint_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'F')
				{
					if (strcmp(lexem, (char*)"FEEDRATE") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						if (pval.float_val > 40)
							pval.float_val = 40;
						cfgzMotor.feedrate = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'H')
				{
					if (strcmp(lexem, (char*)"HOLD_TIME") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val == 0)
							pval.uint_val = TIMER_DISABLE;
						else if (pval.uint_val > 100000)
							pval.uint_val = 100000;
						cfgzMotor.hold_time = pval.uint_val * 1000;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOME_DIRECTION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val != -1.0 && pval.int_val != 1.0)
							pval.int_val = -1;
						cfgzMotor.home_dir = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOME_POS") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						cfgzMotor.home_pos = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOMING_FEEDRATE_FAST") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						if (pval.float_val > 40)
							pval.float_val = 40;
						cfgzMotor.homing_feedrate_fast = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"HOMING_FEEDRATE_SLOW") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						if (pval.float_val > 40)
							pval.float_val = 40;
						cfgzMotor.homing_feedrate_slow = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'I')
				{
					if (strcmp(lexem, (char*)"INVERT_DIR") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val < 0 || pval.int_val > 1)
							pval.int_val = 1;
						cfgzMotor.invert_dir = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'M')
				{
					if (strcmp(lexem, (char*)"MAX_ENDSTOP_INVERTING") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val < 0 || pval.int_val > 1)
							pval.int_val = 1;
						cfgzMotor.max_endstop_inverting = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"MAX_POS") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						cfgzMotor.max_pos = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"MIN_ENDSTOP_INVERTING") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.int_val < 0 || pval.int_val > 1)
							pval.int_val = 1;
						cfgzMotor.min_endstop_inverting = pval.int_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"MIN_POS") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						cfgzMotor.min_pos = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'O')
				{
					if (strcmp(lexem, (char*)"OFF_TIME") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 100000)
							pval.uint_val = 100000;
						else if (pval.uint_val < cfgzMotor.hold_time)
							pval.uint_val = cfgzMotor.hold_time + 1000;
						else if (pval.uint_val == 0)
							pval.uint_val = TIMER_DISABLE;
						cfgzMotor.off_time = pval.int_val * 60000;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'S')
				{
					if (strcmp(lexem, (char*)"STEPS_PER_MM") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val < 1)
							pval.uint_val = 1;
						if (pval.uint_val > 200000)
							pval.uint_val = 200000;
						cfgzMotor.steps_per_mm = pval.uint_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				} else
				if (*lexem == 'T')
				{
					if (strcmp(lexem, (char*)"TRAVEL_ACCELERATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						cfgzMotor.travel_acceleration = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
					if (strcmp(lexem, (char*)"TRAVEL_FEEDRATE") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.float_val < 0.1)
							pval.float_val = 0.1;
						cfgzMotor.travel_feedrate = pval.float_val;
						rdstate = CFGR_ZMOTOR;
						break;
					}
				}

				string = LANG_GetString(LSTR_MSG_UNKNOWN_PARAMNAME_IN_CFG);
				sprintf(msg, string, numstr);
				break;

			case CFGR_GENERAL:
				rdstate = CFGR_ERROR;
				if (*lexem == 'B')
				{
					if (strcmp(lexem, (char*)"BUZZER_MSG_DURATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 15000)
							pval.uint_val = 15000;
						cfgConfig.buzzer_msg = pval.uint_val;
						rdstate = CFGR_GENERAL;
						break;
					}
					if (strcmp(lexem, (char*)"BUZZER_TOUCH_DURATION") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 15000)
							pval.uint_val = 15000;
						cfgConfig.buzzer_touch = pval.uint_val;
						rdstate = CFGR_GENERAL;
						break;
					}
				} else
				if (*lexem == 'R')
				{
					if (strcmp(lexem, (char*)"ROTATE_DISPLAY") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 0)
						{
							cfgConfig.display_rotate = 1;
							LCD_WriteCmd(0x0036);
							LCD_WriteRAM(0x0078);
						}
						else
						{
							cfgConfig.display_rotate = 0;
							LCD_WriteCmd(0x0036);
							LCD_WriteRAM(0x00B8);
						}
						rdstate = CFGR_GENERAL;
						break;
					}
				} else
				if (*lexem == 'S')
				{
					if (strcmp(lexem, (char*)"SCREENSAVER_TIME") == 0)
					{
						if (pval.type != PARAMVAL_NUMERIC)
						{
							string = LANG_GetString(LSTR_MSG_INVALID_PARAMVAL_IN_CFG);
							sprintf(msg, string, numstr);
							break;
						}
						if (pval.uint_val > 15000)
							cfgConfig.screensaver_time = 15000 * 60000;
						else if (pval.uint_val == 0)
							pval.uint_val = TIMER_DISABLE;
						else
							cfgConfig.screensaver_time = pval.uint_val * 60000;
						rdstate = CFGR_GENERAL;
						break;
					}
				}

				string = LANG_GetString(LSTR_MSG_UNKNOWN_PARAMNAME_IN_CFG);
				sprintf(msg, string, numstr);
				break;

		}
		
		if (rdstate == CFGR_ERROR)
			break;
		
		
	}
	f_close(&ufile);
	
	
	if (tguiActiveScreen == (TG_SCREEN*)&tguiMsgBox)
	{
		tguiActiveScreen = (TG_SCREEN*)((TG_MSGBOX*)tguiActiveScreen)->prevscreen;
	}

	if (rdstate == CFGR_ERROR)
	{
		TGUI_MessageBoxOk(LANG_GetString(LSTR_ERROR), msg);
		BUZZ_TimerOn(cfgConfig.buzzer_msg);
	}
	else
	{
		CFG_SaveMotor();
		CFG_SaveConfig();
		TGUI_MessageBoxOk(LANG_GetString(LSTR_COMPLETED), LANG_GetString(LSTR_MSG_CFGFILE_LOADED));
	}
}
//==============================================================================




Después de analizar correctamente el archivo, la nueva configuración se aplica inmediatamente y se guarda en la EPROM.



Los contadores de horas de funcionamiento de los componentes de la impresora solo se actualizan en la EPROM cuando el archivo se imprime o se interrumpe.



6. Funciones adicionales para comodidad y conveniencia



6.1 Reloj con calendario



Bueno, solo para hacerlo. Por qué desperdiciar bondad: un reloj autónomo en tiempo real integrado en el microcontrolador, que puede funcionar con una batería de litio cuando la energía general está apagada y consumir tan poco que el CR2032, según los cálculos, debería ser suficiente para varios años. Además, el fabricante incluso proporcionó en la placa el cuarzo de 32 kHz requerido para este reloj. Solo queda pegar el soporte de la batería a la placa y soldar el cableado al menos común y al terminal especial del microcontrolador, lo que hice en mi casa.



La hora, el día y el mes se muestran en la parte superior izquierda de la pantalla principal:







El mismo reloj de tiempo real se utiliza para contar el tiempo de impresión y las horas de funcionamiento de los componentes. Y también se utilizan en el protector de pantalla, que se describe a continuación.



6.2 Bloquear la pantalla de clics accidentales durante la impresión



Esto se hizo a pedido de un conocido. Bueno, por qué no, puede ser útil en algunos casos. El bloqueo se habilita y deshabilita presionando prolongadamente (~ 2.5 seg) en el encabezado de la pantalla de impresión. Cuando el bloqueo está activo, se muestra un candado rojo en la esquina superior derecha. Al final de la impresión, el bloqueo se libera automáticamente.



6.3 Disminuya la corriente del motor en modo de espera, apague el motor en reposo



Hecho para reducir la acumulación de calor general dentro del cuerpo de la impresora. El motor se puede poner en modo de espera con corriente reducida después del tiempo sin movimiento configurado. Esta característica, por cierto, está muy extendida en los controladores de motor paso a paso para "adultos" del tipo TB6560. Además, en la configuración, puede establecer el tiempo después del cual, en ausencia de movimiento, el motor se desactivará por completo. Pero esto también conducirá al hecho de que la puesta a cero del eje, si se llevó a cabo, dejará de ser válida. Ambas funciones se pueden desactivar por completo en la misma configuración.



6.4 Salvapantallas



Como un reloj, solo porque puedo. En ausencia de presionar la pantalla después de la hora especificada en la configuración, la pantalla cambia al modo de emulación de un reloj de escritorio digital:







Además de la hora, también se muestra la fecha completa con el día de la semana. El firmware sale de este modo presionando en cualquier parte de la pantalla. Teniendo en cuenta que los números son bastante grandes y el consumo de electricidad cuando el motor está apagado es de menos de 2 vatios, una impresora con un protector de pantalla de este tipo puede servir como reloj de habitación :) Durante la impresión, el protector de pantalla también aparece después de un tiempo específico, pero con una adición: el progreso de la impresión en la parte inferior de la pantalla:







En la configuración, puede establecer el tiempo de respuesta del protector de pantalla o deshabilitarlo.



6.5 Comprobación de la retroiluminación y la pantalla







Se puede acceder a esta pantalla desde el menú "Servicio" y será útil para verificar los diodos de retroiluminación o la pantalla UV. En la parte superior, se selecciona una de las tres imágenes, que se mostrarán en la pantalla UV: marco, iluminación completa de toda la pantalla, rectángulos. En la parte inferior hay dos botones que encienden y apagan la luz de fondo y la pantalla. La luz incluida se apagará automáticamente después de 2 minutos, por lo general este tiempo es suficiente para cualquier prueba. Cuando salga de esta pantalla, tanto la luz de fondo como la pantalla se apagarán automáticamente.



6.6 Configuración







También se puede acceder a esta pantalla desde el menú Herramientas. Hay muy pocas configuraciones aquí y, para ser honesto, nunca se me ocurrió qué configuraciones serían tan solicitadas que tendría sentido ponerlas en la interfaz, y no solo en el archivo de configuración. Todavía se agregará para restablecer los contadores de uso de la impresora, bueno, cuanto más no sé :)



Por supuesto, puede configurar la hora y la fecha (ya que hay un reloj) para abrir una pantalla separada:







Puede ajustar la altura de la plataforma elevadora en pausa y encender y apagar sonido de los clics y mensajes de la pantalla. Al cambiar la configuración, los nuevos valores solo tendrán efecto hasta que se apague la alimentación y no se guardarán en la EPROM. Para guardarlos, después de cambiar los parámetros, presione el botón guardar en el menú (con un icono de disquete).



Los valores numéricos se ingresan en una pantalla especial:







Aquí he implementado todas las características que me faltaban en otras impresoras.



  1. Botones "±" y "." sólo funcionan si el parámetro editado puede ser negativo o fraccionario, respectivamente.
  2. Si, después de ingresar a esta pantalla, se presiona primero cualquier botón numérico, el valor anterior será reemplazado por el dígito correspondiente. Si el botón es ".", Será reemplazado por "0". Es decir, no es necesario borrar el valor anterior, puede comenzar a ingresar uno nuevo inmediatamente.
  3. Botón "", poniendo a cero el valor actual.



    Al presionar el botón Atrás no se aplicará el nuevo valor. Para aplicarlo, debe hacer clic en "Aceptar".


6.7 Finalmente - Pantalla de información de la impresora







Esta pantalla es accesible directamente desde el menú principal. Lo más importante aquí es la versión de firmware / FPGA y los contadores de tiempo de funcionamiento. En la parte inferior, todavía hay información sobre el autor de la interfaz y la dirección del repositorio en GitHub. El autor de la interfaz es la base del futuro. Si aún puedo configurar la interfaz a través de un archivo de texto simple, entonces tendré la oportunidad de especificar el nombre del autor.



el fin



Esta es la última parte de este proyecto favorito mío. El proyecto vive y se desarrolla, aunque no tan rápido como me gustaría, pero ya es bastante eficiente.



Probablemente debería haber puesto más código ... Pero no creo que haya partes en mi código de las que presumir. En mi opinión, es más importante describir cómo funciona y qué se ha hecho, y el código está ahí, está todo en GitHub, quien esté interesado, puedo verlo allí en su totalidad. Creo que sí.



Espero sus preguntas y comentarios, y gracias por su interés en estos artículos.



- 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. Salida de imágenes de capas a 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