Partie 2: Utilisation des contrôleurs UDB PSoC de Cypress pour réduire le nombre d'interruptions dans une imprimante 3D



La dernière fois, nous avons envisagé la possibilité de générer des impulsions pour les moteurs pas à pas, partiellement supprimées du logiciel au niveau du firmware. En cas de succès complet, cela promet l'absence de la nécessité de traiter les interruptions arrivant avec une fréquence allant jusqu'à 40 KHz. Mais cette option présente un certain nombre de défauts évidents. Premièrement, les accélérations n'y sont pas prises en charge. Deuxièmement, la granularité des fréquences de pas admissibles dans cette solution est de centaines de hertz (par exemple, il est possible de générer des fréquences de 40000 Hz et 39966 Hz, mais il est impossible de générer des fréquences d'une amplitude comprise entre ces deux valeurs).

Mise en œuvre de l'accélération


Est-il possible d'éliminer les inconvénients indiqués en utilisant les mêmes outils UDB sans compliquer le système? Faisons les choses correctement. Commençons par les plus difficiles - avec les accélérations. Des accélérations sont ajoutées au début et à la fin du chemin. Premièrement, si des impulsions haute fréquence sont appliquées immédiatement au moteur pas à pas, il faudra un courant plus important pour démarrer le fonctionnement. Le courant admissible élevé est le chauffage et le bruit, il est donc préférable de le limiter. Mais alors le moteur peut sauter des étapes au démarrage. Il vaut donc mieux accélérer le moteur en douceur. Deuxièmement, si une tête lourde s'arrête brusquement, elle subit des transitoires associés à l'inertie. Les vagues sont visibles sur du plastique. Par conséquent, il est nécessaire non seulement de se disperser, mais également d'arrêter la tête. Classiquement, un graphique du régime moteur est présenté sous la forme d'un trapèze. Voici un fragment du code source du firmware Marlin:



Je n'essaierai même pas de comprendre s'il est possible de l'implémenter en utilisant UDB. Cela est dû au fait qu'un autre type d'accélération est désormais à la mode: non pas trapézoïdal, mais S-Curve. Leur calendrier ressemble à ceci:



Ce n'est certainement pas pour UDB. Abandonner? Pas du tout! J'ai déjà noté que UDB n'implémente pas d'interface matérielle, mais vous permet simplement de transférer une partie du code du logiciel au niveau du firmware. Laissez le profil calculer le processeur central, et la formation d'impulsions de pas effectue toujours l'UDB. Le processeur central a beaucoup de temps pour les calculs. La tâche d'éliminer les interruptions fréquentes continuera d'être résolue avec élégance, et personne ne prévoyait d'amener complètement le processus au niveau du micrologiciel.

Bien sûr, le profil devra être préparé en mémoire et UDB récupérera les données à partir de là en utilisant DMA. Mais combien de mémoire est requise? Un millimètre nécessite 200 pas. Désormais, avec un codage 24 bits, cela représente 600 octets par 1 mm de mouvement de tête! Encore une fois, rappelez-vous des interruptions pas si fréquentes, mais toujours constantes pour tout transmettre en fragments? Pas vraiment! Le fait est que le mécanisme DMA de PSoC est basé sur des descripteurs. Après avoir exécuté la tâche à partir d'un descripteur, le contrôleur DMA passe au suivant. Et donc, le long de la chaîne, vous pouvez utiliser beaucoup de descripteurs. Nous illustrons cela avec quelques dessins de la documentation officielle:



En fait, ce mécanisme peut également être utilisé en construisant une chaîne de trois descripteurs:

Non.Explication
1De la mémoire au FIFO avec incrément d'adresse. Indique une section avec un profil d'accélération.
2De la mémoire au FIFO sans incrément d'adresse. Envoie tout le temps au même mot en mémoire pour une vitesse constante.
3De la mémoire au FIFO avec incrément d'adresse. Indique une section avec un profil de freinage.

Il s'avère que le chemin principal est décrit à l'étape 2 et que le même mot est physiquement utilisé, ce qui définit la vitesse constante. La consommation de mémoire n'est pas importante. En réalité, le deuxième descripteur peut être physiquement représenté par deux ou trois descripteurs. Cela est dû au fait que la longueur maximale de pompage, selon TRM, peut être de 64 kilo-octets (la modification sera plus faible). Soit 32 767 mots. Celui à 200 pas par millimètre correspondra à un trajet de 163 millimètres. Vous devrez peut-être faire un segment de deux ou trois parties, selon la distance maximale que le moteur peut parcourir à la fois.

Néanmoins, pour économiser de la mémoire (et aux dépens des blocs UDB), je propose d'abandonner les blocs DatapPath 24 bits pour passer à des blocs 16 bits plus économiques.

Alors. La première proposition de révision.

Les tableaux sont préparés en mémoire qui codent la durée des étapes. De plus, ces informations sont transmises à l'UDB via DMA. La section rectiligne est codée par un tableau d'un élément, le bloc DMA n'augmente pas l'adresse, choisissant le même élément tout le temps. Les sections d'accélération, rectilignes et de freinage sont connectées par les moyens disponibles dans le contrôleur DMA.

Réglage fin des médiums


Nous allons maintenant examiner comment surmonter le problème de la granularité de fréquence. Bien sûr, il ne sera pas possible de le définir exactement. Mais, en fait, le "firmware" d'origine ne peut pas non plus le faire. Au lieu de cela, ils utilisent l'algorithme de Bresenham. Un délai d'une mesure est ajouté à certaines étapes. En conséquence, la fréquence moyenne devient intermédiaire, entre une valeur plus petite et une valeur plus grande. En ajustant le rapport des périodes régulières et prolongées, vous pouvez modifier en douceur la fréquence moyenne. Si notre vitesse n'est plus définie via le registre de données, mais transmise via FIFO, et que le nombre d'impulsions est généralement défini par le nombre de mots transmis via DMA, les deux registres de données dans UDB sont libérés. De plus, l'une des batteries, qui compte le nombre d'impulsions, est également libérée. Ici, nous allons construire un certain PWM sur eux.

En règle générale, les ALU comparent et affectent des registres avec le même index. Lorsqu'un registre a un index de 0 et l'autre un 1, toutes les versions de l'opération ne peuvent pas être implémentées. Mais j'ai réussi à rassembler le solitaire à partir des registres sous lesquels PWM peut être fait. Il s'est avéré comme indiqué sur la figure.



Lorsque la condition A0 <D1 est remplie, nous ajouterons un battement supplémentaire à la longueur d'impulsion donnée. Lorsque la condition n'est pas remplie, nous ne le faisons pas.

Cheval sphérique dans des conditions normales


Nous commençons donc à modifier le bloc développé pour UDB, en tenant compte de la nouvelle architecture. Remplacez la profondeur de bits de Datapath:



Nous aurons besoin de beaucoup plus de sorties de Datapath que la dernière fois.



En double-cliquant dessus, nous voyons les détails:



Il y a plus de chiffres pour la variable State , n'oubliez pas de connecter la plus ancienne !!! Dans l'ancienne version, il y avait un 0 constant.



Le graphe de transition de l'automate que j'ai obtenu comme ceci:



Nous sommes en état de veille alors que FIFO1 est vide. Soit dit en passant, travailler avec FIFO1 et non FIFO0 est le résultat de la formation même du solitaire. Le registre A0 est utilisé pour implémenter PWM, donc la largeur d'impulsion est déterminée par le registre A1. Et je ne peux le télécharger qu'à partir de FIFO1 (il existe peut-être d'autres méthodes secrètes, mais elles ne me sont pas connues). Par conséquent, DMA télécharge exactement les données sur FIFO1, et c'est précisément l'état «Non vide» pour FIFO1 qui quitte l'état inactif .

ALU à l'état IDLE annule le registre A0:



Ceci est nécessaire pour qu'au début du fonctionnement PWM, il démarre toujours le travail depuis le début.
Mais les données sont entrées dans le FIFO. La machine passe à l'état LoadData :



Dans cet état, l'ALU charge le mot suivant de la FIFO dans le registre A1. En cours de route, afin de ne pas créer d'états inutiles, la valeur du compteur A0, utilisé pour travailler avec PWM, est augmentée:



Si le compteur A0 n'a pas encore atteint la valeur D0 (c'est-à-dire que la condition A0 <D0 est déclenchée, armant le drapeau NoNeedReloadA0 ), on passe à l'état One . Sinon, l'état est ClearA0 .

Dans l'état ClearA0, l' ALU remet simplement à zéro la valeur de A0, en commençant un nouveau cycle PWM:



après quoi la machine passe également dans un état, juste un temps plus tard.

L'une nous est familière de l'ancienne version de la machine. ALU n'y remplit aucune fonction.

Et donc - dans cet état, une unité est générée à la sortie de Out_Step (ici l'optimiseur fonctionnait mieux lorsque l'unité est produite par la condition, cela a été détecté empiriquement).



Nous sommes dans cet état jusqu'à ce que le compteur à sept bits que nous connaissons déjà soit remis à zéro. Mais si auparavant nous sortions de cet état le long d'un chemin, il peut maintenant y avoir deux chemins: direct et retardé au rythme.



Nous entrerons dans l'état ExtraTick si l'indicateur AddCycle est défini , qui est affecté pour remplir la condition A0 <D1. Dans cet état, l'ALU n'effectue aucune action bénéfique. C'est juste que le cycle prend 1 temps de plus. De plus, tous les chemins convergent en état de retard .

Cette condition mesure la durée de l'impulsion. Le registre A1 (chargé alors qu'il est encore à l'état Load ) est réduit jusqu'à ce qu'il atteigne zéro.



De plus, selon qu'il existe ou non des données supplémentaires dans FIFO, la machine ira au prochain lot à charger ou au ralenti . Voyons cela non pas dans la figure (il y a de longues flèches, tout sera petit), mais sous la forme d'un tableau, double-cliquez sur l'état Delay :



Quitte maintenant UDB. J'ai converti le drapeau de l'état inactif en comparaison asynchrone (dans la version précédente, il y avait un déclencheur qui s'armait et se réinitialisait dans divers états), car pour lui, l'optimiseur montrait le meilleur résultat. De plus, le drapeau Hungry a été ajouté, signalant à l'unité DMA qu'elle était prête à recevoir des données. Il est enroulé sur le drapeau «FIFO1 n'est pas bondé» . Puisqu'il n'est pas encombré, DMA peut y charger un autre mot de données.



Sur la partie automatique - c'est tout.

Ajoutez des blocs DMA au diagramme de projet principal. Pour l'instant, j'ai commencé à interrompre les drapeaux de terminaison DMA, mais pas le fait que ce soit correct. Lorsque le processus d'accès direct à la mémoire est terminé, vous pouvez démarrer un nouveau processus lié au même segment, mais vous ne pouvez pas commencer à remplir des informations sur le nouveau segment. Le FIFO comporte encore trois à quatre éléments. A ce moment, il est encore impossible de reprogrammer les registres D0 et D1 du bloc sur la base de l'UDB, ils sont encore nécessaires au fonctionnement. Par conséquent, il est possible que des interruptions basées sur les sorties Out_Idle soient ajoutées ultérieurement . Mais cette cuisine ne sera plus liée à la programmation de blocs UDB, nous ne le mentionnerons donc qu'en passant.



Expériences logicielles


Puisque maintenant tout n'est pas connu, nous n'écrirons aucune fonction spéciale. Tous les contrôles seront effectués "sur le front". Ensuite, sur la base d'expériences réussies, des fonctions API peuvent être écrites. Alors. Nous rendons la fonction main () minimale. Il configure simplement le système et appelle le test sélectionné.

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(); // TestShortSteps(); TestWithPacking (); for(;;) { } 

Essayons d'envoyer un paquet d'impulsions en appelant une fonction, vérifiant le fait d'insérer une impulsion supplémentaire. L'appel de fonction est simple:

 TestShortSteps(); 

Mais le corps a besoin d'explications.
Je vais d'abord donner toute la fonction
 void TestShortSteps() { //   ,   //      //   ,  DMA    !!! //    ,   !!! StepperController_X_SingleVibrator_WritePeriod (6); //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); //         . //         static const uint16 steps[] = { 0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001, 0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001,0x0001 }; //  DMA  ,      uint8 channel = DMA_X_DmaInitialize (sizeof(steps[0]),1,HI16(steps),HI16(StepperController_X_Datapath_1_F1_PTR)); CyDmaChRoundRobin (channel,true); //       ,       uint8 td = CyDmaTdAllocate(); //       .  ,    . CyDmaTdSetConfiguration(td, sizeof(steps), CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); //       CyDmaTdSetAddress(td, LO16((uint32)steps), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //      CyDmaChSetInitialTd(channel, td); //         CyDmaChEnable(channel, 1); } 


Considérons maintenant ses parties importantes.

Si la longueur de la partie positive de l'impulsion est égale à 92 cycles d'horloge, l'oscilloscope ne pourra pas discerner s'il y a ou non un insert à cycle unique dans la partie négative. L'échelle ne sera pas la même. Il est nécessaire de rendre la partie positive aussi courte que possible afin que l'impulsion totale soit comparable en échelle avec le battement inséré. Par conséquent, je change avec force la période du compteur qui définit la durée de la partie positive de l'impulsion:

  //   ,   //      //   ,  DMA    !!! //    ,   !!! StepperController_X_SingleVibrator_WritePeriod (6); 

Mais pourquoi six mesures entières? Pourquoi pas trois? Pourquoi pas deux? Pourquoi, après tout, pas un seul? C'est une triste histoire. Si l'impulsion positive est inférieure à 6 cycles, le système ne fonctionne pas. Un long débogage sur un oscilloscope avec la sortie de lignes de test vers l'extérieur a montré que le DMA n'est pas une chose rapide. Si la machine fonctionne pendant moins d'une certaine durée, alors au moment où elle quitte l' état Retard , FIFO est le plus souvent encore vide. Il ne peut pas encore être placé un seul nouveau mot de données! Et seulement lorsque la partie positive de l'impulsion a une durée de 6 cycles, FIFO est garanti d'avoir le temps de se charger ...

Digression de latence


Une autre idée fixe qui me vient à l'esprit est l'accélération matérielle de certaines fonctions du noyau de notre RTOS MAX. Mais hélas, toutes mes meilleures idées sont brisées sur ces mêmes latences.

Il y avait un cas, j'ai étudié le développement d'applications Bare Metal pour Cyclone V SoC. Mais il s'est avéré que travailler avec des registres FPGA uniques (en alternant en y écrivant, puis en les lisant) réduit les opérations de base des centaines (!!!) fois. Vous avez bien entendu. C'est en centaines. De plus, tout cela est mal documenté, mais au début, j'ai ressenti intérieurement, puis j'ai prouvé à partir de fragments de phrases de la documentation que les latences étaient coupables lors du passage des demandes à travers un tas de ponts. Si vous devez chasser une grande baie, il y aura également une latence, mais en termes d'un mot pompé, ce ne sera pas significatif. Lorsque les demandes sont uniques (et que l'accélération matérielle du noyau du système d'exploitation les implique uniquement), le ralentissement se produit exactement des centaines de fois. Il sera beaucoup plus rapide de tout faire de manière purement programmatique, lorsque le programme travaillera avec la mémoire principale via le cache à une vitesse effrénée.

Sur PSoC, j'avais également certains plans. En apparence, vous pouvez merveilleusement rechercher des données dans un tableau à l'aide de DMA et UDB. Qu'est-ce qui est vraiment là! En raison de la structure des descripteurs DMA, ces contrôleurs pourraient effectuer une recherche entièrement matérielle dans les listes chaînées! Mais après avoir reçu le plug décrit ci-dessus, je me suis rendu compte qu'il est également associé à la latence. Ici, cette latence est magnifiquement décrite dans la documentation. À la fois dans la famille TRM et dans un document distinct AN84810 - PSoC 3 et PSoC 5LP Advanced DMA Topics . La section 3.2 y est consacrée. La prochaine accélération matérielle est donc annulée. Dommage. Mais, comme l'a dit Semyon Semyonovich Gorbunkov: "Nous allons chercher."

Expériences logicielles continues


Ensuite, j'ai défini les paramètres de l'algorithme de Bresenham:

  //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); 

Eh bien, vient ensuite le code régulier qui transfère un tableau de mots via DMA vers FIFO1 de l'unité de commande du moteur X.

Le résultat nécessite quelques explications. Le voici:



La valeur du compteur A0 est indiquée en rouge lorsque la machine est à l'état Un . L'astérisque vert montre les cas où le retard est inséré en raison de la machine en état ExtraTick . Il y a aussi des barres où le retard est dû à l'état ClearA0 , elles sont marquées d'une grille bleue.

Comme vous pouvez le voir, lorsque vous entrez pour la première fois, le tout premier délai est perdu. Cela est dû au fait que A0 est réinitialisé lorsqu'il est en veille , mais augmente lorsqu'il entre dans LoadData . Par conséquent, au point d'analyse (sortie de l'état de Un ), elle est déjà égale à l'unité. Le compte commence avec elle. Mais en général, cela n'affectera pas la fréquence moyenne. Il faut juste garder cela à l'esprit. Comme il faut garder à l'esprit que lors de la réinitialisation de A0, l'horloge sera également insérée. Il doit être pris en compte lors du calcul de la fréquence moyenne.

Mais en général, le nombre d'impulsions est correct. Leur durée est également crédible.
Essayons de programmer une chaîne de descripteurs plus réelle,

consistant en une phase d'accélération, de mouvement linéaire et de freinage.
 void TestWithPacking(int countOnLinearStage) { //   ,   //     . //   ,  DMA    !!! //    ,   !!! StepperController_X_SingleVibrator_WritePeriod (6); //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); //    static const uint16 accelerate[] = {0x0010,0x0008,0x0004}; //    static const uint16 deccelerate[] = {0x004,0x0008,0x0010}; //  .    . static const uint16 steps[] = {0x0001}; //  DMA  ,      uint8 channel = DMA_X_DmaInitialize (sizeof(steps[0]),1,HI16(steps),HI16(StepperController_X_Datapath_1_F1_PTR)); CyDmaChRoundRobin (channel,true); //   uint8 tdDeccelerate = CyDmaTdAllocate(); CyDmaTdSetConfiguration(tdDeccelerate, sizeof(deccelerate), CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(tdDeccelerate, LO16((uint32)deccelerate), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //       uint8 tdSteps = CyDmaTdAllocate(); //   !!! //     !!! CyDmaTdSetConfiguration(tdSteps, countOnLinearStage, tdDeccelerate, /*TD_INC_SRC_ADR |*/ TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(tdSteps, LO16((uint32)steps), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //   //     !!! uint8 tdAccelerate = CyDmaTdAllocate(); CyDmaTdSetConfiguration(tdAccelerate, sizeof(accelerate), tdSteps, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); CyDmaTdSetAddress(tdAccelerate, LO16((uint32)accelerate), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //      CyDmaChSetInitialTd(channel, tdAccelerate); //         CyDmaChEnable(channel, 1); } 


Tout d'abord, appelez pour les mêmes dix étapes (dans DMA, 20 octets vont réellement):

 TestWithPacking (20); 

Le résultat est comme prévu. Au début, l'accélération est visible. Et la sortie vers IDLE (rayon bleu) se produit avec un grand retard par rapport à la dernière impulsion, c'est alors que la dernière étape a été complètement terminée, sa valeur est approximativement égale à la valeur de la première.



Vrai cheval dans des conditions normales


Lors du remodelage de l'équipement, je suis en quelque sorte passé d'une largeur d'impulsion de 24 bits à une tâche de 16 bits. Mais nous avons découvert que cela ne peut pas être fait: la fréquence d'impulsion minimale sera trop élevée. Je l'ai fait intentionnellement. Le fait est que la technique pour augmenter la capacité en bits d'un compteur 16 bits s'est avérée si compliquée que si j'avais commencé à la décrire avec la machine principale, elle aurait détourné toute l'attention. Par conséquent, nous le considérons séparément.

Nous avons une batterie 16 bits. J'ai décidé d'ajouter l'entité standard de compteur à sept bits aux bits élevés. Qu'est-ce que ce compteur à sept bits? C'est la conception qui est disponible dans chaque bloc UDB (le bloc UDB de base a une largeur de bits de tous les registres 8 bits, l'augmentation de la profondeur de bits est déterminée par la combinaison de blocs en groupes). Des mêmes ressources, des registres de contrôle / état peuvent être implémentés. Nous avons maintenant un compteur et non une seule paire Contrôle / État pour 16 bits de données. Donc, en ajoutant un autre compteur au système, nous ne retarderons pas les ressources supplémentaires. Nous prenons simplement ce qui nous est déjà attribué. C'est bien! Nous faisons l'octet haut du compteur de largeur d'impulsion grâce à ce mécanisme et obtenons la largeur totale du compteur de largeur d'impulsion égale à 23 bits.



Je vais d'abord dire ce que je pensais. Je pensais qu'après avoir quitté l'état Delay , je vérifierais l'achèvement du comptage de ce compteur supplémentaire. S'il n'a pas fini de compter, je vais réduire sa valeur et passer à nouveau à l'état de retard . Si vous avez compté, la logique restera la même, sans ajouter de cycles supplémentaires.

De plus, la documentation de ce compteur indique que j'ai raison. Il dit littéralement:
Période
Définit la valeur initiale du registre de période. Pour une période de N horloges, la valeur de période doit être réglée sur la valeur de N-1. Le compteur comptera de N-1 à 0, ce qui se traduira par une période de cycle d'horloge N. Une valeur de registre de période de 0 n'est pas prise en charge et entraînera le maintien de la sortie de comptage des bornes à un état élevé constant.
La vie a montré que tout est différent. J'ai déduit l'état de la ligne de comptage du terminal sur l'oscilloscope et j'ai observé sa valeur à un zéro préchargé pendant la période et pendant le chargement du programme. Hélas et ah. Il n'y avait pas d' état haut constant !

Par essais et erreurs, j'ai réussi à faire fonctionner le système correctement, mais pour que cela se produise, au moins une soustraction du compteur doit se produire! Le nouvel état de "soustraction" n'est pas de côté. Il devait être coincé dans le chemin requis. Il est situé en face de l'état Delay et s'appelle Next65536 .



ALU dans cet état n'effectue aucune action utile. En fait, seul un nouveau compteur réagit au fait d'être dans cet état. Le voici dans le schéma:



Voici ses propriétés plus en détail:



En général, compte tenu des articles précédents, l'essence de ce compteur est claire. Seule la ligne Enable souffre. Encore une fois, je ne comprends pas bien pourquoi il doit être allumé lorsque la machine est dans l'état LoadData (puis le compteur recharge la valeur de période). J'ai emprunté cette astuce aux propriétés du compteur qui contrôle les LED, prises par l'auteur anglais de l'unité de contrôle de ces LED. Sans cela, la valeur zéro de la période ne fonctionne pas. Elle travaille avec elle.

Dans le code API, nous ajoutons l'initialisation d'un nouveau compteur. Maintenant, la fonction de démarrage ressemble à ceci:

 void `$INSTANCE_NAME`_Start() { `$INSTANCE_NAME`_SingleVibrator_Start(); //"One" Generator start `$INSTANCE_NAME`_Plus65536_Start(); } 

Voyons le nouveau système. Voici le code de fonction pour tester

(en elle, seule la première ligne diffère de la déjà connue):
 void JustTest(int extra65536s) { //      65536  StepperController_X_Plus65536_WritePeriod((uint8) extra65536s); //     //    —   CY_SET_REG16(StepperController_X_Datapath_1_D0_PTR, 4); CY_SET_REG16(StepperController_X_Datapath_1_D1_PTR, 2); //         . //         static const uint16 steps[] = { 0x1000,0x1000,0x1000,0x1000 }; //  DMA  ,      uint8 channel = DMA_X_DmaInitialize (sizeof(steps[0]),1,HI16(steps),HI16(StepperController_X_Datapath_1_F1_PTR)); CyDmaChRoundRobin (channel,true); //       ,       uint8 td = CyDmaTdAllocate(); //       .  ,    . CyDmaTdSetConfiguration(td, sizeof(steps), CY_DMA_DISABLE_TD, TD_INC_SRC_ADR | TD_AUTO_EXEC_NEXT); //       CyDmaTdSetAddress(td, LO16((uint32)steps), LO16((uint32)StepperController_X_Datapath_1_F1_PTR)); //      CyDmaChSetInitialTd(channel, td); //         CyDmaChEnable(channel, 1); } 


Nous l'appelons comme ceci:

  JustTest(0); 

Sur l'oscilloscope, nous voyons ce qui suit (faisceau jaune - sortie STEP, bleu - valeur de la sortie du compteur TC pour le contrôle du processus). La durée d'impulsion est définie par un tableau d' étapes . A chaque étape, la durée est de 0x1000 mesures.



Basculez vers un autre scan pour qu'il y ait compatibilité entre les différents résultats:



Remplacez l'appel de fonction par ceci:

  JustTest(1); 

Le résultat est comme prévu. Tout d'abord, la sortie TC est nulle pour les cycles 0x1000, puis - une unité pour les cycles 0x10000 (65536d). La fréquence est approximativement égale à 700 hertz, nous l'avons découvert dans la dernière partie de l'article, donc tout va bien.



Eh bien, essayons un diable:

  JustTest(2); 

Nous obtenons:



C'est vrai. La sortie TC est inversée à une sur les derniers 65536 cycles d'horloge. Avant cela, il était à zéro pour les cycles 0x1000 + 0x10000.

Bien sûr, avec cette approche, toutes les impulsions doivent aller avec la même valeur du nouveau compteur. Il est impossible de faire une impulsion avec l'octet le plus élevé pendant l'accélération, disons 3, puis 1, puis 0. Mais en fait, à des fréquences aussi basses (moins de sept cents hertz), les accélérations n'ont pas de signification physique, donc ce problème peut être négligé. À cette fréquence, vous pouvez travailler avec le moteur de façon linéaire.

Voler dans la pommade


Le document TRM pour la famille PSoC5LP indique:
Chaque transaction peut aller de 1 à 64 Ko
Mais dans l'AN84810 déjà mentionné, il y a une telle phrase:
1. Comment pouvez-vous mettre en mémoire tampon plus de 4095 octets en utilisant DMA?
Le nombre maximal de transferts d'un TD est limité à 4095 octets. Si vous devez transférer plus de 4095 octets à l'aide d'un seul canal DMA, utilisez plusieurs TD et chaînez-les comme indiqué dans l'exemple 5.
Qui a raison? Si vous menez des expériences, les résultats pencheront en faveur du pire des énoncés, mais le comportement sera complètement incompréhensible. Toute la faute est cette vérification dans l'API:



Même texte.
 cystatus CyDmaTdSetConfiguration(uint8 tdHandle, uint16 transferCount, uint8 nextTd, uint8 configuration) \ { cystatus status = CYRET_BAD_PARAM; if((tdHandle < CY_DMA_NUMBEROF_TDS) && (0u == (0xF000u & transferCount))) { /* Set 12 bits transfer count. */ reg16 *convert = (reg16 *) &CY_DMA_TDMEM_STRUCT_PTR[tdHandle].TD0[0u]; CY_SET_REG16(convert, transferCount); /* Set Next TD pointer. */ CY_DMA_TDMEM_STRUCT_PTR[tdHandle].TD0[2u] = nextTd; /* Configure the TD */ CY_DMA_TDMEM_STRUCT_PTR[tdHandle].TD0[3u] = configuration; status = CYRET_SUCCESS; } return(status); } 


Si une transaction de plus de 4095 octets est spécifiée, le paramètre précédent sera utilisé. Oui, je n'ai pas pensé à vérifier les codes d'erreur ...

Les expériences ont montré que si vous supprimez cette vérification, la longueur réelle sera coupée à l'aide du masque 0xfff (4096 = 0x1000). Hélas et ah. Tous les espoirs d'un travail agréable se sont effondrés. Vous pouvez bien sûr créer des chaînes de descripteurs associés en 4K. Mais disons que 64K, c'est 16 chaînes. Trois moteurs actifs (les extrudeuses auront moins d'étapes) - 48 chaînes. Exactement autant devrait être rempli dans le pire des cas, avant chaque segment. C'est peut-être acceptable à temps. Au minimum, 127 descripteurs sont disponibles, donc il y aura certainement assez de mémoire.

Vous pouvez envoyer les données manquantes au besoin. Une interruption est survenue lorsque la chaîne DMA a terminé ses travaux, nous y transférons un autre segment. Dans ce cas, aucun calcul n'est requis, le segment est déjà formé, tout sera rapide. Et il n'y a aucune exigence de performances: lorsqu'une demande d'interruption est émise, il y aura 4 autres éléments dans FIFO qui seront desservis chacun pendant plusieurs centaines, voire des milliers de cycles d'horloge. Autrement dit, tout est réel. Une stratégie spécifique sera plus facile à choisir lors de travaux réels. Mais une erreur dans la documentation (TRM) a gâché toute l'ambiance. Si cela avait été connu à l'avance, je n'aurais peut-être pas vérifié la méthodologie.

Conclusion


En apparence, l'outil de micrologiciel auxiliaire développé est devenu acceptable, de sorte qu'il serait possible, sur sa base, de créer une version du «micrologiciel», disons Marlin, qui n'est pas constamment dans le gestionnaire d'interruption pour les moteurs pas à pas. À ma connaissance, cela est particulièrement vrai pour les imprimantes Delta, où la demande de ressources informatiques est assez élevée. Peut-être que cela éliminera l'afflux qui se produit sur mon Delta aux endroits où la tête s'arrête. Sur le MZ3D à ces mêmes endroits, aucun afflux n'est observé. Que ce soit vrai ou non, le temps nous le dira, et le rapport à ce sujet devra être publié dans une branche complètement différente.

En attendant, nous avons vu que sur le bloc UDB, pour toute sa simplicité, il est tout à fait possible d'implémenter un coprocesseur fonctionnant en tandem avec le processeur principal et permettant son déchargement. Et quand il y a beaucoup de ces unités, les coprocesseurs peuvent fonctionner en parallèle.

Une erreur dans la documentation du contrôleur DMA a brouillé le résultat. Des interruptions sont néanmoins nécessaires, mais pas à la même fréquence et pas avec la criticité dans le temps qui était dans la version originale. L'ambiance est donc gâtée, mais l'utilisation d'un «coprocesseur» basé sur UDB donne encore un gain considérable par rapport au travail purement logiciel.

En cours de route, il a été révélé que le DMA fonctionne à une vitesse assez faible. À la suite de cela, certaines mesures ont été effectuées à la fois sur le PSoC5LP et sur le STM32. Les résultats tirent un autre article. Peut-être que je le ferai un jour si le sujet s'avère intéressant.

À la suite des expériences, deux projets de test ont été obtenus à la fois. Le premier est plus facile à comprendre. Vous pouvez le prendre ici . Le second est hérité du premier, mais confus lors de l'ajout d'un compteur à sept bits et de la logique associée. Vous pouvez le prendre ici . Bien sûr, ces exemples ne sont que des tests. Il n'y a pas encore de temps libre pour l'intégration dans le véritable «firmware». Mais dans le cadre de ces articles, il est plus important de s'entraîner à travailler avec UDB.

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


All Articles