Micrologiciel DIY pour imprimante 3D LCD photopolymère. Partie 3





Dans les deux parties précédentes, j'ai expliqué comment j'avais créé une interface graphique, commencé à contrôler un moteur pas à pas et à organiser le travail avec des fichiers sur une clé USB.



Aujourd'hui, j'écrirai sur le processus d'impression, la sortie des couches imprimées sur l'écran de surbrillance et les choses restantes, pas si essentielles:



4. Sortie des images des couches vers l'affichage de surbrillance.

5. Chaque petite chose, comme le contrôle de l'éclairage et des ventilateurs, le chargement et l'enregistrement des paramètres, etc.

6. Caractéristiques supplémentaires pour le confort et la commodité.





4.



4.1 -



Comment un microcontrôleur, qui ne dispose pas de périphériques spécialisés, a-t-il réussi à rendre l'image sur une matrice haute résolution à une vitesse de 74 millions de pixels par seconde (résolution 2560x1440, 20 images par seconde) rafraîchie via l'interface MIPI? Réponse: en utilisant un FPGA avec une SDRAM de 16 Mo connectée et deux puces d'interface MIPI - SSD2828. Deux microcircuits valent la peine car l'affichage est logiquement divisé en deux moitiés, chacune étant desservie par son propre canal séparé, il en résulte deux écrans en un.



L'image à afficher est stockée dans l'une des 4 banques SDRAM, la puce FPGA est responsable de l'entretien de la SDRAM et de la sortie de l'image vers le SSD2828. FPGA génère des signaux de synchronisation verticale et horizontale pour SSD2828 et disques

flux continu de valeurs de couleur de pixel sur 24 lignes (8R 8G 8B) dans chacun des SSD2828. La fréquence d'images s'avère être d'environ 20 Hz.



Le FPGA est connecté au microcontrôleur avec une interface série (SPI) à travers laquelle le microcontrôleur peut transmettre une image. Il est transmis par paquets, dont chacun contient une ligne de l'image (les lignes sont comptées le long du côté court de l'écran - 1440 pixels). En plus de ces données, le paquet contient également le numéro de banque SDRAM, le numéro de ligne et la somme de contrôle - CRC16. Le FPGA reçoit ce paquet, vérifie la somme de contrôle et, si tout va bien, enregistre les données dans la zone SDRAM appropriée. Si le CRC ne correspond pas, le FPGA expose un signal sur l'une de ses broches, également connectée au microcontrôleur, selon lequel le microcontrôleur comprend que les données ne sont pas arrivées normalement et peut répéter l'envoi. Pour une image complète, le microcontrôleur doit envoyer 2560 paquets de ce type au FPGA.



Les données d'image à l'intérieur du paquet sont représentées au format bit: 1 - pixel est allumé, 0 - pixel est sombre. Hélas, cela exclut complètement la possibilité d'organiser un flou en niveaux de gris des bords des couches imprimées - anti-aliasing. Pour organiser cette façon de flouter, il faut réécrire la configuration (firmware) du FPGA, pour laquelle je ne suis pas encore prêt. Depuis trop longtemps et pas très longtemps, je travaille avec FPGA, je vais devoir pratiquement tout re-maîtriser.



En plus des paquets de données, le microcontrôleur peut également envoyer une commande de contrôle dans laquelle indiquer à partir de quelle banque SDRAM lire les données pour la sortie vers l'affichage et activer / désactiver la sortie d'image.



Les puces SSD2828 sont également connectées au microcontrôleur via SPI. Ceci est nécessaire pour configurer leurs registres lorsqu'ils sont allumés, les transférer en mode veille ou en mode actif.

Il y a plusieurs autres lignes entre le microcontrôleur et le FPGA / SSD2828 - le signal de réinitialisation et les signaux de sélection de puce actifs (Chip Select) pour chacun des microcircuits.



En général, ce schéma de travail est plutôt loin d'être optimal, à mon avis. Par exemple, il serait plus logique de connecter le FPGA au microcontrôleur via une interface de mémoire externe parallèle, les données seraient transférées beaucoup plus rapidement que SPI avec une limite de fréquence de 20 MHz (lorsque la fréquence augmente, le FPGA cesse de recevoir des données normalement). De plus, le signal de réinitialisation n'est pas acheminé vers l'entrée FPGA de réinitialisation physique, mais en tant que signal logique normal, c'est-à-dire que le FPGA n'effectue pas de réinitialisation matérielle sur celui-ci. Et cela a également joué une blague cruelle, qui sera discutée ci-dessous.



J'ai découvert tout cela en comprenant les codes sources du fabricant. J'ai transféré les fonctions de travail avec FPGA à partir de leur code source tel quel, je ne comprenais toujours pas complètement comment tout cela fonctionne. Heureusement, les Chinois ont suffisamment commenté leur code (en chinois) pour pouvoir le comprendre sans trop de difficulté.



4.2 Lecture de couches à partir d'un fichier d'impression



Ok, nous avons plus ou moins compris la sortie de l'image finie, maintenant je vais vous dire un peu comment ces images sont extraites à partir de fichiers préparés pour l'impression. Les fichiers .Pws, .photons, .photon, .cbddlp sont essentiellement un ensemble d'images de couches. Ce format, à ma connaissance, est venu de la société chinoise Chitu, qui a eu l'idée de fabriquer des cartes avec un tel circuit (microcontrôleur - FPGA - SDRAM - SSD2828). Supposons que vous souhaitiez imprimer un modèle d'une hauteur de 30 mm avec chaque couche de 0,05 mm d'épaisseur. Le programme slicer coupe ce modèle en couches de l'épaisseur spécifiée et pour chacune d'elles forme son image.



Ainsi, 30 / 0,05 = 600 images avec une résolution de 1440x2560 sont obtenues. Ces images sont emballées dans un fichier de sortie, l'en-tête avec tous les paramètres y est entré et un tel fichier est déjà envoyé à l'imprimante. Les images de couche ont une profondeur de 1 bit et sont compressées par l'algorithme RLE un octet à la fois, le bit le plus significatif indiquant la valeur de couleur et les sept bits les moins significatifs représentant le nombre de répétitions. Cette méthode vous permet de compresser l'image du calque de 460 Ko à environ 30-50. L'imprimante lit la couche compressée, la décompresse et l'envoie ligne par ligne au FPGA.



Le fabricant procède comme suit:



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


C'est la méthode en trois étapes utilisée par les Chinois. Il s'est avéré que cela a été fait pour que l'image de la couche puisse être affichée sous une forme réduite sur l'affichage de l'interface, montrant à l'utilisateur ce qui est imprimé. Cette image est juste formée à partir du tableau d'octets. Bien que ce qui a empêché de le former immédiatement à partir des bits décodés ne soit pas clair. Et ce qui a empêché la formation d'une image bitmap pour le transfert vers FPGA dans le même cycle n'est pas non plus clair.



J'utilise maintenant la même méthode, bien qu'optimisée. Pour clarifier ce qu'était l'optimisation, je dois clarifier un autre point. Les données de la ligne d'affichage ne constituent pas un tableau solide de données utiles. Au milieu, il y a quelques pixels «non fonctionnels» supplémentaires en raison du fait que deux contrôleurs d'affichage sont joints sur le côté court, et chacun d'eux a 24 pixels «non fonctionnels» sur les bords. Ainsi, les données effectivement transmises pour une ligne de l'image se composent de 3 parties: données pour la première moitié (premier contrôleur), intermédiaires «non fonctionnels» 48 pixels, données pour la seconde moitié (deuxième contrôleur).



Ainsi, les Chinois, lors de la formation du tableau d'octets à l'intérieur de la boucle, ont vérifié si la fin de la première moitié était atteinte, sinon, la valeur a été écrite par le pointeur * p, sinon par le pointeur * (p + 48) . Cette vérification pour chacune des 1440 valeurs, et même la modification du pointeur pour la moitié d'entre elles, n'a clairement pas contribué à la vitesse de la boucle. J'ai divisé cette boucle en deux - dans la première, la première moitié du tableau est remplie, après cette boucle, le pointeur est augmenté de 48 et la deuxième boucle commence pour la seconde moitié du tableau. Dans la version originale, la couche était lue et affichée en 1,9 seconde, cette modification à elle seule réduisait le temps de lecture et de sortie à 1,2 seconde.



Un autre changement concernait le transfert de données vers FPGA. Dans les sources d'origine, cela se produit via DMA, mais après le début du transfert via DMA, la fonction attend son achèvement et seulement après cela, elle commence à décoder et à former une nouvelle ligne de l'image. J'ai supprimé cette attente afin que la ligne suivante soit générée pendant le transfert des données de la ligne précédente. Cela a réduit le temps de 0,3 seconde supplémentaire, à 0,9 par couche. Et c'est lors de la compilation sans optimisation, si vous compilez avec une optimisation complète, le temps diminue à environ 0,53 seconde, ce qui est déjà tout à fait acceptable. Sur ces 0,53 secondes, il faut environ 0,22 seconde pour calculer CRC16 et environ 0,19 seconde pour former une image bitmap à partir d'un tableau d'octets avant la transmission. Mais le transfert même de toutes les lignes vers FPGA prend environ 0,4 seconde et avec cela, très probablement,il n'y a rien à faire - tout ici repose sur la limitation de la fréquence SPI maximale autorisée pour FPGA.



Si je pouvais écrire moi-même la configuration FPGA, je pourrais lui donner la décompression RLE, et cela pourrait accélérer la sortie de la couche d'un ordre de grandeur, mais comment est-ce fait?



Et oui, j'allais écrire sur le montant associé au fait que le FPGA n'est pas réinitialisé par le matériel sur un signal de réinitialisation du microcontrôleur. Ainsi, lorsque j'ai déjà appris à afficher des images de calques, terminé le processus d'impression lui-même, j'ai rencontré un bug incompréhensible - une fois sur 5 à 10, l'impression a été lancée avec un écran entièrement éclairé. Je vois dans le débogueur que les couches sont lues correctement, les données sont envoyées au FPGA au besoin, le FPGA confirme l'exactitude du CRC. Autrement dit, tout fonctionne, et au lieu de dessiner un calque, un affichage complètement blanc. Il est clair que FPGA ou SSD2828 sont à blâmer. Une fois de plus, j'ai revérifié l'initialisation de SSD2828 - tout va bien, tous les registres qu'ils contiennent sont initialisés avec les valeurs requises, cela peut être vu lors de la lecture de contrôle de leurs valeurs. Ensuite, j'ai déjà atteint la carte avec un oscilloscope. Et j'ai découvert que lorsqu'un tel échec se produit, le FPGA n'écrit aucune donnée dans la SDRAM. NOUS signalons,permettant l'écriture, se tient enraciné à l'endroit dans le niveau inactif. Et j'aurais probablement combattu ce problème pendant longtemps, sinon pour un ami qui m'a conseillé d'essayer de donner au FPGA une commande explicite pour désactiver la sortie d'image avant la réinitialisation, de sorte qu'au moment de la réinitialisation, il n'y ait aucun appel du FPGA vers la SDRAM. Je l'ai essayé et cela a fonctionné! Ce bogue ne s'est plus jamais montré. En fin de compte, nous sommes arrivés à la conclusion que le cœur IP du contrôleur SDRAM à l'intérieur du FPGA n'est pas implémenté assez correctement, la réinitialisation et l'initialisation du contrôleur SDRAM ne se produisent pas normalement dans tous les cas. Quelque chose interfère avec la réinitialisation correcte si les données de la SDRAM sont accédées à ce moment. Comme ça…qui a conseillé d'essayer avant de réinitialiser pour donner au FPGA une commande explicite pour désactiver la sortie d'image, de sorte qu'au moment de la réinitialisation, il n'y ait aucun appel du FPGA à la SDRAM. Je l'ai essayé et cela a fonctionné! Ce bogue ne s'est plus jamais montré. En fin de compte, nous sommes arrivés à la conclusion que le cœur IP du contrôleur SDRAM à l'intérieur du FPGA n'était pas implémenté assez correctement, la réinitialisation et l'initialisation du contrôleur SDRAM ne se produisent pas normalement dans tous les cas. Quelque chose empêche la réinitialisation correcte si, à ce moment, les données de la SDRAM sont accédées. Comme ça…qui a conseillé d'essayer avant de réinitialiser pour donner au FPGA une commande explicite pour désactiver la sortie d'image, de sorte qu'au moment de la réinitialisation, il n'y ait aucun appel du FPGA à la SDRAM. Je l'ai essayé et cela a fonctionné! Ce bogue ne s'est plus jamais montré. En fin de compte, nous sommes arrivés à la conclusion que le cœur IP du contrôleur SDRAM à l'intérieur du FPGA n'est pas implémenté assez correctement, la réinitialisation et l'initialisation du contrôleur SDRAM ne se produisent pas normalement dans tous les cas. Quelque chose empêche la réinitialisation correcte si à ce moment les données de la SDRAM sont accédées. Comme ça…que le cœur IP du contrôleur SDRAM à l'intérieur du FPGA n'est pas implémenté correctement, la réinitialisation et l'initialisation du contrôleur SDRAM ne fonctionnent pas normalement dans tous les cas. Quelque chose interfère avec la réinitialisation correcte si les données de la SDRAM sont accédées à ce moment. Comme ça…que le cœur IP du contrôleur SDRAM à l'intérieur du FPGA n'est pas implémenté correctement, la réinitialisation et l'initialisation du contrôleur SDRAM ne fonctionnent pas normalement dans tous les cas. Quelque chose empêche la réinitialisation correcte si à ce moment les données de la SDRAM sont accédées. Comme ça…



4.3 Interface utilisateur lors de l'impression de fichiers



Une fois que l'utilisateur a sélectionné le fichier et commencé à l'imprimer, l'écran suivant apparaît:







Il s'agit d'un écran assez standard pour de telles imprimantes photopolymères.



La plus grande zone de l'écran est occupée par l'image du calque actuellement exposé.

L'affichage de cette image est synchronisé avec le rétroéclairage - lorsque le rétroéclairage est activé, l'image est affichée, lorsque le rétroéclairage est désactivé, l'image est effacée. L'image est formée comme pour l'affichage UV - le long du côté court de l'image. Je ne me suis pas précipité avec des pointeurs le long des décalages de ligne de cette image, mais juste avant de l'afficher, je donne au contrôleur d'affichage une commande pour changer le sens de sortie des données versées, c'est-à-dire la zone de cette image s'avère être "tournée" sur le côté.



Vous trouverez ci-dessous des informations sur la progression de l'impression - le temps d'impression écoulé et estimé, la couche actuelle et le nombre total de couches, une barre de progression avec des pourcentages à droite de celle-ci. Je veux également ajouter la hauteur actuelle en millimètres après le nombre de couches, juste pour être.



Sur la droite se trouvent les boutons de pause, de paramètres et d'interruption. Lorsque vous appuyez sur la pause dans le micrologiciel, l'indicateur de pause est défini et le comportement ultérieur dépend de l'état actuel de l'imprimante. Si la plate-forme descend pour la couche suivante ou l'exposition de la couche a déjà commencé, le micrologiciel terminera l'exposition et seulement après cela augmentera la plate-forme à la hauteur de pause (qui est définie dans les paramètres), où elle attendra que l'utilisateur clique sur le bouton "Continuer":







L'élévation de la plate-forme pour une pause se produit d'abord à la vitesse spécifiée dans les paramètres du fichier, et après la hauteur spécifiée dans les mêmes paramètres, la vitesse augmente.



Lorsque l'impression est interrompue, une fenêtre apparaîtra confirmant cette action, et seulement après confirmation, l'impression sera arrêtée et la plate-forme remontera jusqu'à la hauteur maximale de l'axe. La vitesse de levage, ainsi que pendant la pause, est variable - d'abord lentement pour détacher la couche du film, puis augmente au maximum.



Le bouton des paramètres n'est pas encore fonctionnel, mais lorsque vous cliquez dessus, l'utilisateur sera dirigé vers un écran avec des paramètres d'impression qui peuvent être modifiés - le temps d'exposition de la couche, la hauteur et la vitesse de levage, etc. En ce moment, je le termine. Il y a aussi une idée pour donner la possibilité de sauvegarder les paramètres modifiés dans le fichier imprimé.



5. Chaque petite chose, comme le contrôle de l'éclairage et des ventilateurs, le chargement et l'enregistrement des paramètres, etc.



La carte dispose de 3 sorties MOSFET haute puissance - une pour les LED UV pour l'éclairage et deux pour les ventilateurs (refroidissement des diodes d'éclairage et refroidissement de l'écran, par exemple). Il n'y a rien d'intéressant ici - les sorties du microcontrôleur sont connectées aux grilles de ces transistors et leur contrôle est aussi simple que de faire clignoter une LED. Pour une précision élevée du temps d'exposition, il est activé dans le cycle principal via la fonction qui règle le temps de fonctionnement:



UVLED_TimerOn(l_info.light_time * 1000);

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


Et il s'éteint à partir de l'interruption en millisecondes de la minuterie lorsque le compteur de rétroéclairage atteint zéro:



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


5.1 Paramètres, chargement à partir d'un fichier et enregistrement dans l'EEPROM



Les paramètres sont enregistrés dans l'EEPROM embarquée à 24c16. Ici, contrairement au stockage des ressources dans une grande mémoire flash, tout est simple - pour chaque type de données stockées, le décalage d'adresse à l'intérieur de l'EEPROM est codé en dur. Au total, il stocke trois blocs: les paramètres de l'axe Z, les paramètres généraux du système (langue, son, etc.) et les compteurs de temps pour les principaux composants de l'imprimante - éclairage, affichage et ventilateur.



Les structures de bloc stockées contiennent la version actuelle du firmware et une somme de contrôle primitive - juste la somme de 16 bits des valeurs de tous les octets du bloc. Lors de la lecture des paramètres de l'EPROM, le CRC est vérifié et s'il ne correspond pas au réel, les paramètres de ce bloc reçoivent des valeurs par défaut, un nouveau CRC est calculé et le bloc est enregistré dans l'EPROM au lieu de l'ancien. Si le bloc de lecture ne correspond pas à la version actuelle, il doit être mis à jour vers la version actuelle et il sera enregistré sous une nouvelle forme au lieu de l'ancienne. Cela n'a pas encore été implémenté, mais sera fait à l'avenir pour mettre à jour correctement le firmware.



Certains paramètres peuvent être modifiés via l'interface, mais la plupart ne peuvent être modifiés qu'en chargeant un fichier de configuration. Ici, je n'ai pas changé mes habitudes et j'ai écrit mon propre analyseur pour ces fichiers.



La structure d'un tel fichier est standard: nom du paramètre + signe égal + valeur du paramètre. Une ligne - un paramètre. Les espaces et les tabulations au début d'une ligne et entre le signe égal et le nom et la valeur sont ignorés. Les lignes vides et les lignes commençant par le caractère dièse - "#" sont également ignorées, ce caractère définit les lignes avec des commentaires. La casse des lettres dans les noms des paramètres et des sections n'a pas d'importance.



En plus des paramètres, le fichier contient également des sections dont les noms sont entre crochets. Après le nom de section rencontré, l'analyseur s'attend à ce que seuls les paramètres appartenant à cette section iront plus loin jusqu'à ce qu'un autre nom de section soit rencontré. Honnêtement, je ne sais pas pourquoi j'ai introduit ces articles. Quand j'ai fait cela, j'avais une sorte de pensée qui leur était associée, mais maintenant je ne m'en souviens plus.



Pour raccourcir les comparaisons du nom de paramètre de lecture avec des noms prédéfinis, la première lettre du nom de lecture est analysée en premier, puis seuls les noms commençant par cette lettre sont comparés.



Contenu du fichier de configuration
# 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




Lorsqu'un tel fichier (avec l'extension .acfg) est sélectionné dans la liste des fichiers, le firmware demandera si l'utilisateur souhaite télécharger et appliquer les paramètres de ce fichier et, après confirmation, commencera à analyser ce fichier.







Si une erreur est détectée, un message s'affiche indiquant le type d'erreur et le numéro de ligne. Les erreurs suivantes sont gérées:



  • nom de partition inconnu
  • nom de paramètre inconnu
  • valeur de paramètre non valide - lorsque, par exemple, un paramètre numérique est tenté d'attribuer une valeur de texte


Si quelqu'un est intéressé - voici une fiche complète des trois principales fonctions de l'analyseur
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));
	}
}
//==============================================================================




Une fois l'analyse du fichier réussie, les nouveaux paramètres sont immédiatement appliqués et enregistrés dans l'EPROM.



Les compteurs d'heures de fonctionnement des composants de l'imprimante ne sont mis à jour dans l'EPROM que lorsque le fichier est imprimé ou interrompu.



6. Fonctionnalités supplémentaires pour le confort et la commodité



6.1 Horloge avec calendrier



Eh bien, juste pour y arriver. Pourquoi gaspiller de la bonté - une horloge en temps réel autonome intégrée au microcontrôleur, qui peut fonctionner sur une pile au lithium lorsque l'alimentation générale est coupée et consomme si peu que le CR2032, selon les calculs, devrait suffire pendant plusieurs années. De plus, le constructeur a même fourni sur la carte le quartz 32 kHz requis pour cette montre. Il ne reste plus qu'à coller le support de batterie à la carte et à souder le câblage de celle-ci au moins commun et à la borne spéciale du microcontrôleur, ce que j'ai fait chez moi.



L'heure, le jour et le mois sont affichés en haut à gauche de l'écran principal:







La même horloge temps réel est utilisée pour compter le temps d'impression et les heures de fonctionnement des composants. Et ils sont également utilisés dans l'économiseur d'écran, qui est décrit ci-dessous.



6.2 Verrouillage de l'écran contre les clics accidentels lors de l'impression



Cela a été fait à la demande d'une connaissance. Eh bien, pourquoi pas, cela peut être utile dans certains cas. Le verrouillage est activé et désactivé en appuyant longuement (~ 2,5 s) sur l'en-tête de l'écran d'impression. Lorsque le verrou est actif, un verrou rouge s'affiche dans le coin supérieur droit. A la fin de l'impression, le verrou est automatiquement libéré.



6.3 Diminuer le courant du moteur en mode maintien, arrêter le moteur au ralenti



Conçu pour réduire l'accumulation de chaleur à l'intérieur du corps de l'imprimante. Le moteur peut être mis en mode de maintien avec un courant réduit après le temps d'arrêt configuré. Cette caractéristique est d'ailleurs largement répandue dans les pilotes de moteurs pas à pas "adultes" du type TB6560. De plus, dans les paramètres, vous pouvez définir le temps après lequel, en l'absence de mouvement, le moteur sera complètement hors tension. Mais cela conduira également au fait que la mise à zéro de l'axe, si elle a été effectuée, deviendra invalide. Ces deux fonctionnalités peuvent être complètement désactivées dans les mêmes paramètres.



6.4 Écran de veille



Comme une montre - juste parce que je peux. A défaut d'appuyer sur l'écran après l'heure spécifiée dans les paramètres, l'écran bascule vers le mode d'émulation d'une horloge numérique de bureau:







Outre l'heure, la date complète avec le jour de la semaine est également affichée. Le micrologiciel quitte ce mode en appuyant sur n'importe quelle partie de l'écran. Étant donné que les nombres sont assez importants et que la consommation d'électricité lorsque le moteur est éteint est inférieure à 2 watts, une imprimante avec un tel économiseur d'écran peut bien servir d'horloge de la salle :) Lors de l'impression, l'économiseur d'écran apparaît également après un temps spécifié, mais avec un ajout - la progression de l'impression en bas de l'écran:







Dans les paramètres, vous pouvez définir le temps de réponse de l'économiseur d'écran ou le désactiver.



6.5 Contrôle du rétroéclairage et de l'affichage







Cet écran est accessible depuis le menu «Service» et sera utile lors de la vérification des diodes de rétroéclairage ou de l'affichage UV. En haut, l'une des trois images est sélectionnée, qui sera affichée sur l'écran UV - cadre, éclairage complet de l'ensemble de l'écran, rectangles. En bas, il y a deux boutons qui activent et désactivent le rétroéclairage et l'affichage. La lumière incluse s'éteint automatiquement après 2 minutes, généralement cette durée est suffisante pour tout test. Lorsque vous quittez cet écran, le rétroéclairage et l'affichage seront automatiquement désactivés.



6.6 Paramètres







Cet écran est également accessible depuis le menu Outils. Il y a très peu de paramètres ici et, pour être honnête, je n'ai jamais proposé quels paramètres seraient si souvent demandés qu'il serait logique de les mettre dans l'interface, et pas seulement dans le fichier de configuration. Cela ajoutera également la possibilité de réinitialiser les compteurs de temps de fonctionnement des composants de l'imprimante, eh bien, je ne sais plus :)



Bien sûr, ici, vous pouvez régler l'heure et la date (car il y a une horloge) dans l'écran qui s'ouvre séparément:







vous pouvez régler la hauteur de levage de la plate-forme sur pause et l'allumer et l'éteindre le son des clics et des messages affichés. Lors de la modification des paramètres, les nouvelles valeurs ne prendront effet que jusqu'à ce que l'alimentation soit coupée et ne seront pas enregistrées dans l'EPROM. Pour les enregistrer, après avoir modifié les paramètres, appuyez sur le bouton Enregistrer dans le menu (avec une icône de disquette).



Les valeurs numériques sont saisies dans un écran spécial:







Ici, j'ai implémenté toutes les fonctionnalités qui me manquaient dans d'autres imprimantes.



  1. Boutons "±" et "." ne fonctionne que si le paramètre modifié peut être négatif ou fractionnaire, respectivement.
  2. Si, après avoir accédé à cet écran, une touche numérique est d'abord pressée, l'ancienne valeur sera remplacée par le chiffre correspondant. Si le bouton est ".", Il sera remplacé par "0". Autrement dit, il n'est pas nécessaire d'effacer l'ancienne valeur, vous pouvez immédiatement commencer à en saisir une nouvelle.
  3. Bouton "", mettant à zéro la valeur actuelle.



    Appuyer sur le bouton Retour n'appliquera pas la nouvelle valeur. Pour l'appliquer, vous devez cliquer sur "OK".


6.7 Enfin - Écran d'informations sur l'imprimante







Cet écran est accessible directement depuis le menu principal. Le plus important ici est la version du firmware / FPGA et les compteurs de temps de fonctionnement. En bas, il y a encore des informations sur l'auteur de l'interface et l'adresse du référentiel sur GitHub. L'auteur de l'interface est la base de l'avenir. Si je permet toujours de configurer l'interface via un simple fichier texte, alors il y aura une opportunité de spécifier le nom de l'auteur.



la fin



Ceci est la dernière partie de ce projet pour animaux de compagnie. Le projet vit et se développe, mais pas aussi vite que je le souhaiterais, mais il est déjà assez efficace.



J'aurais probablement dû mettre plus de code ... Mais je ne pense pas qu'il y ait des morceaux dans mon code pour me vanter. À mon avis, il est plus important de décrire comment cela fonctionne et ce qui a été fait, et le code est là, tout est sur GitHub, qui sera intéressé, je peux le regarder dans son intégralité. Je le pense.



J'attends avec impatience vos questions et commentaires, et merci de l'intérêt que vous portez à ces articles.



- Partie 1: 1. Interface utilisateur.

- Partie 2: 2. Travailler avec le système de fichiers sur une clé USB. 3. Commande de moteur pas à pas pour le mouvement de la plate-forme.

- Partie 3:4. Sortie d'images des calques sur l'affichage rétroéclairé. 5. Chaque petite chose, comme le contrôle de l'éclairage et des ventilateurs, le chargement et l'enregistrement des paramètres, etc. 6. Caractéristiques supplémentaires pour le confort et la commodité.



Liens



Kit MKS DLP sur Aliexpress Codes

sources du firmware d'origine du fabricant sur les

schémas GitHub du fabricant de deux versions de la carte sur GitHub

Mes sources sur GitHub



All Articles