Développement de son propre noyau pour l'intégration dans un système de processeur basé sur FPGA



Ainsi, dans le premier article du cycle, il a été dit qu'il était préférable d'utiliser un système de processeur pour contrôler nos équipements mis en œuvre à l'aide de FPGA pour le complexe Redd, puis au cours des premier et deuxième articles, il a été montré comment fabriquer ce système. Eh bien, c'est fait, nous pouvons même choisir des noyaux prêts à l'emploi dans la liste pour les inclure, mais le but ultime est de gérer nos propres noyaux personnalisés. Le moment est venu de réfléchir à la manière d'inclure un noyau arbitraire dans le système de processeur.

Tous les articles du cycle:
Développement du «firmware» le plus simple pour les FPGA installés dans Redd, et débogage en utilisant le test de mémoire comme exemple
Développement du «firmware» le plus simple pour les FPGA installés dans Redd. Partie 2. Code de programme

Pour comprendre la théorie d'aujourd'hui, vous devez trouver et télécharger le document Spécifications de l'interface Avalon , car le bus Avalon est le bus de base du système NIOS II. Je me référerai aux sections, tableaux et figures pour la révision du document du 26 septembre 2018.

Nous ouvrons la section 3 consacrée aux interfaces mappées en mémoire, ou plutôt - 3.2. Le tableau 9 répertorie les signaux de bus. Veuillez noter que tous ces signaux sont facultatifs. Je n'ai trouvé aucun signal contenant «Oui» dans la colonne Obligatoire. Nous pouvons très bien ne pas transmettre tel ou tel signal à notre appareil. Par conséquent, dans le cas le plus simple, le bus est extrêmement simple à mettre en œuvre. Le début du tableau ressemble à ceci:



Comme vous pouvez le voir, tous les signaux sont très bien décrits (sauf que cela se fait en anglais). Voici les chronogrammes pour divers cas. Le cas le plus simple ne pose aucune question. Je vais maintenant prendre le chronogramme du document et couvrir certaines des lignes avec un remplissage translucide (elles sont toutes facultatives, nous avons le droit d'exclure toutes les considérations).



Effrayant Mais tout est simple: on nous donne l'adresse et le stroboscope de lecture , il faut régler les données sur le bus readdata. Et vice versa: on nous donne l'adresse, les données sur le bus de données écrites et le stroboscope d'écriture, et nous devons capturer les données. Ce n'est pas du tout effrayant, un bus synchrone typique.

Des lignes de seize tables cachées sont nécessaires dans le cas où l'accès à la mémoire n'est pas des mots de 32 bits. Ceci est extrêmement important lorsque nous concevons des noyaux universels. Mais lorsque nous concevons un noyau d'un jour, nous écrivons simplement dans le document à propos de ce noyau (je suis un opposant à la marque dans ma tête, mais quelqu'un peut le limiter à cela) que nous devons utiliser des mots de 32 bits et c'est tout. Et bien, et le signal de réponse , c'est très spécial, et ça ne nous intéresse pas en principe.

Parfois, il est important que lorsque l'équipement n'est pas prêt, il soit possible de retarder le fonctionnement du bus pendant plusieurs cycles d'horloge. Dans ce cas, le signal WaitRequest doit être ajouté. Le chronogramme changera comme suit:



Pendant que WaitRequest est armé, l'assistant sait que notre appareil est occupé. Soyez prudent si ce signal n'est pas réinitialisé, le système entier "gèlera" lors de la manipulation, donc seul un redémarrage du FPGA peut le réinitialiser. JTAG se bloque avec le système. La dernière fois que j'ai observé ce phénomène, c'était lors de la préparation de cet article, donc les souvenirs sont toujours vifs.

Plus loin dans le document de l'entreprise, des cas plus productifs de pipelining de données et de transactions par lots sont envisagés, mais la tâche de l'article n'est pas de considérer toutes les options possibles, mais de montrer au lecteur la façon de travailler, en soulignant que tout cela n'est pas du tout effrayant, nous nous limiterons donc à ces deux options simples.

Concevons un appareil simple qui deviendra périodiquement indisponible sur le bus. La première chose qui me vient à l'esprit est l'interface série. Pendant la transmission, nous ferons attendre le système. Et dans la vie, je déconseille fortement de faire cela: le processeur s'arrêtera jusqu'à la fin d'une transaction occupée, mais c'est un cas idéal pour un article, car le code d'implémentation sera compréhensible et peu encombrant. En général, nous fabriquons un émetteur série qui peut envoyer des données et des signaux de sélection de puce à deux appareils.



Commençons par l'option de pneu la plus simple. Faisons un port de sortie parallèle, qui forme les signaux du choix des cristaux.



Pour cela, je prendrai le projet obtenu dans l'article précédent, mais afin d'éviter toute confusion, je le mettrai dans le répertoire AVALON_DEMO. Je ne changerai pas les noms des autres fichiers. Dans ce répertoire, créez le répertoire my_cores . Le nom du répertoire peut être n'importe quoi. Nous y stockerons nos noyaux. Certes, aujourd'hui, il en sera un. Créez un fichier CrazySerial.sv avec le contenu suivant:
module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output reg [1:0] cs ); always @(posedge clk, posedge reset) begin if (reset == 1) begin cs <= 0; end else begin if (write) case (address) 2'h00: cs <= writedata [1:0]; default:; endcase end end endmodule 

Faisons les choses correctement. Tout d'abord, les lignes d'interface. clk et reset sont les lignes d'horloge et de réinitialisation. Les noms des adresses , des lignes d' écriture et d' écriture sont extraits du tableau avec la liste des signaux du document Interfaces Mappées en Mémoire .





En fait, je pourrais donner n'importe quel nom. La liaison de lignes logiques avec des lignes physiques sera effectuée ultérieurement. Mais si vous donnez les noms, comme dans le tableau, l'environnement de développement les connectera par lui-même. Par conséquent, il est préférable de prendre les noms de la table.

Eh bien, cs sont les lignes de sélection des cristaux qui sortiront de la puce.

L'implémentation elle-même est triviale. Lors de la réinitialisation, les sorties sont mises à zéro. Et donc - à chaque mesure, nous vérifions s'il y a un signal d' écriture . S'il existe une adresse égale à zéro, cliquez sur les données. Bien sûr, il serait possible d'ajouter un décodeur ici, ce qui empêchera le choix de deux appareils à la fois, mais ce qui est bien dans la vie surchargera l'article. L'article ne fournit que les étapes les plus nécessaires, cependant, il est à noter que dans la vie, tout peut être fait plus compliqué.

Super. Nous sommes prêts à introduire ce code dans le système de processeur. Nous allons à Platform Designer , sélectionnez comme fichier d'entrée le système que nous avons construit dans les expériences précédentes:



Nous attirons l'attention sur l' élément Nouveau composant dans le coin supérieur gauche:



Pour ajouter votre composant, cliquez sur cet élément. Dans la boîte de dialogue qui s'ouvre, remplissez les champs. Et pour l'article, remplissez uniquement le nom du composant:



Maintenant, allez dans l'onglet Fichiers et cliquez sur Ajouter un fichier :



Ajoutez le fichier créé précédemment, sélectionnez-le dans la liste et cliquez sur Analyser le fichier de synthèse :



Il n'y a aucune erreur lors de l'analyse de SystemVerilog , mais il existe plusieurs erreurs conceptuelles. Ils sont dus au fait que certaines lignes étaient mal connectées par l'environnement de développement. Nous allons dans l'onglet Signaux & Interfaces et faisons attention ici:



Les lignes cs ont été incorrectement affectées à l'interface avalon_slave0 , le signal readdata . Mais ensuite, toutes les autres lignes ont été reconnues correctement, grâce au fait que nous leur avons donné des noms à partir du tableau de documents. Mais que faire des lignes problématiques? Ils doivent être affectés à une interface comme un conduit . Pour ce faire, cliquez sur l'élément «ajouter une interface»



Dans le menu déroulant, sélectionnez conduit :



Nous obtenons une nouvelle interface:



Si vous le souhaitez, il peut être renommé. Certes, cela sera certainement nécessaire si nous voulons faire plusieurs interfaces externes. Dans le cadre de l'article, nous lui laisserons le nom conduit_end . Maintenant, nous accrochons la ligne cs avec la souris et la faisons glisser dans cette interface. Nous devons réussir à lancer un signal sous la ligne conduit_end , nous serons alors autorisés à le faire. Dans d'autres endroits, le curseur apparaîtra comme un cercle barré. En fin de compte, nous devrions avoir ceci:



Remplacez le type de signal par readdata avec, disons, chipselect . Image finale:



Mais les erreurs sont restées. Le bus Avalon ne reçoit pas de signal de réinitialisation. Nous sélectionnons avalon_slave_0 dans la liste et examinons ses propriétés.



Remplacer aucun avec réinitialisation . Dans le même temps, nous examinerons les autres propriétés de l'interface.



On peut voir que l'adressage est en mots. Eh bien, un certain nombre d'autres éléments de la documentation sont configurés ici. Les diagrammes de temps obtenus dans ce cas seront dessinés tout en bas des propriétés:



En fait, il n'y a plus d'erreurs. Vous pouvez cliquer sur Terminer . Notre module créé est apparu dans l'arborescence des appareils:



Ajoutez-le au système de processeur, connectez les signaux d'horloge et réinitialisez. Nous connectons le bus de données au processeur Data Master . Double-cliquez sur Conduit_end et donnez au signal externe un nom, par exemple, des lignes . Cela se révèle en quelque sorte comme ceci:



Il est important de ne pas oublier que puisque nous avons ajouté un bloc au système, nous devons nous assurer qu'il n'entre en conflit avec personne dans l'espace d'adressage. Dans ce cas particulier, il n'y a pas de conflits dans la figure, mais de toute façon, je sélectionnerai l'élément de menu Système-> Attribuer des adresses de base .

C’est tout. Le bloc est créé, configuré, ajouté au système. Cliquez sur le bouton Générer HDL , puis sur Terminer .

Nous faisons un brouillon du projet, après quoi nous allons au Pin Planner et assignons les jambes. Il s'est avéré comme ceci:



Ce qui correspond aux contacts B22 et C22 du connecteur d'interface.

Nous faisons l'assemblage final, chargeons le système de processeur dans le FPGA. Maintenant, nous devons affiner le code du programme. Lancez Eclipse.

Permettez-moi de vous rappeler que je travaille actuellement avec un projet qui se trouve dans un répertoire différent par rapport à mon dernier travail avec Redd. Afin de ne pas être confus, je supprimerai les anciens projets de l'arborescence (mais uniquement de l'arborescence, sans effacer les fichiers eux-mêmes).



Ensuite, je clique sur le bouton droit de la souris sur une arborescence vide et sélectionne Importer dans le menu:



Suivant - Général - > Projet existant dans l'espace de travail :



Et sélectionnez simplement le répertoire dans lequel les fichiers du projet sont stockés:





Les deux projets hérités des expériences passées se connecteront à l'environnement de développement.



Je mettrai en évidence l'élément suivant dans un cadre:
Après chaque modification de la configuration matérielle, sélectionnez à nouveau l'élément de menu Nios II -> Générer BSP pour le projet BSP.




En fait, après cette opération, un nouveau bloc est apparu dans le fichier \ AVALON_DEMO \ software \ SDRAMtest_bsp \ system.h :
 /* * CrazySerial_0 configuration * */ #define ALT_MODULE_CLASS_CrazySerial_0 CrazySerial #define CRAZYSERIAL_0_BASE 0x4011020 #define CRAZYSERIAL_0_IRQ -1 #define CRAZYSERIAL_0_IRQ_INTERRUPT_CONTROLLER_ID -1 #define CRAZYSERIAL_0_NAME "/dev/CrazySerial_0" #define CRAZYSERIAL_0_SPAN 16 #define CRAZYSERIAL_0_TYPE "CrazySerial" 

Tout d'abord, nous nous intéressons à la constante CRAZYSERIAL_0_BASE .

Ajoutez le code suivant à la fonction main () :
  while (true) { IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x00); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x01); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x02); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x03); } 

Nous commençons le débogage et regardons le contenu des lignes avec un oscilloscope. Il doit y avoir du code binaire incrémentiel. Il est là.



De plus, la fréquence d'accès aux ports est tout simplement magnifique:



Environ 25 MHz est la moitié de la fréquence du bus (2 cycles d'horloge). Parfois, le temps d'accès n'est pas de 2 cycles, mais plus long. Cela est dû à l'exécution d'opérations de branchement dans le programme. En général, l'accès le plus simple au bus fonctionne.

Il est temps d'ajouter par exemple la fonctionnalité du port série. Pour ce faire, ajoutez le signal d'interface de demande d'attente lié au bus et une paire de signaux de port série - sck et sdo . Au total, nous obtenons le fragment de code suivant sur systemverilog :



Même texte:
 module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output waitrequest, output reg [1:0] cs, output reg sck, output sdo ); 


Selon les règles de bonne forme, vous devez faire une machine simple qui transmettra des données. Malheureusement, la machine la plus simple de l'article sera très difficile. Mais en fait, si je n'augmente pas la fonctionnalité de la machine (et je ne vais pas le faire dans le cadre de l'article), alors elle n'aura que deux états: la transmission est en cours et la transmission n'est pas en cours. Par conséquent, je peux encoder l'état avec un seul signal:
envoi reg = 0;

Pendant la transmission, j'ai besoin d'un compteur de bits, d'un diviseur d'horloge (je fais un appareil délibérément lent) et d'un registre à décalage pour les données transmises. Ajoutez les registres appropriés:
  reg [2:0] bit_cnt = 0; reg [3:0] clk_div = 0; reg [7:0] shifter = 0; 

Je diviserai la fréquence par 10 (guidée par le principe "pourquoi pas?"). En conséquence, à la cinquième étape, j'armerai SCK, et à la dixième - supprimez cette ligne, après quoi - passez au bit de données suivant. Pour toutes les autres mesures, augmentez simplement le compteur du diviseur. Il est important de ne pas oublier que sur la quatrième mesure, vous devez également augmenter le compteur, et sur la neuvième - zéro. Si nous omettons la transition vers le bit suivant, la logique spécifiée ressemble à ceci:
  if (sending) begin case (clk_div) 4: begin sck <= 1; clk_div <= clk_div + 1; end 9: begin sck <= 0; clk_div <= 0; // <   > end default: clk_div <= clk_div + 1; endcase end else 

Passer au bit suivant est facile. Ils ont décalé le registre à décalage, puis, si le bit actuel est le septième, ils ont cessé de fonctionner en commutant l'état de la machine, sinon ils ont augmenté le compteur de bits.
  shifter <= {shifter[6:0],1'b0}; if (bit_cnt == 7) begin sending <= 0; end else begin bit_cnt <= bit_cnt + 1; end 

En fait, c'est tout. Le bit de sortie est toujours extrait du bit haut du registre à décalage:
  assign sdo = shifter [7]; 

Et la ligne la plus importante pour la révision actuelle. Le signal de demande d'attente est armé à l'unité toujours lorsque des données série sont transmises. C'est-à-dire que c'est une copie du signal d' envoi qui définit l'état de la machine:
  assign waitrequest = sending; 

Eh bien, et lors de l'écriture à l'adresse 1 (rappel, nous avons ici l'adressage en mots de 32 bits), nous encliquetons les données dans le registre à décalage, remettons à zéro les compteurs et commençons le processus de transfert:
  if (write) //... 2'h01: begin bit_cnt <= 0; clk_div <= 0; sending <= 1; shifter <= writedata [7:0]; end default:; endcase end 

Je vais maintenant donner tous les fragments décrits comme un seul texte:
 module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output waitrequest, output reg [1:0] cs, output reg sck, output sdo ); reg sending = 0; reg [2:0] bit_cnt = 0; reg [3:0] clk_div = 0; reg [7:0] shifter = 0; always @(posedge clk, posedge reset) begin if (reset == 1) begin cs <= 0; sck <= 0; sending <= 0; end else begin if (sending) begin case (clk_div) 4: begin sck <= 1; clk_div <= clk_div + 1; end 9: begin clk_div <= 0; shifter <= {shifter[6:0],1'b0}; sck <= 0; if (bit_cnt == 7) begin sending <= 0; end else begin bit_cnt <= bit_cnt + 1; end end default: clk_div <= clk_div + 1; endcase end else if (write) case (address) 2'h00: cs <= writedata [1:0]; 2'h01: begin bit_cnt <= 0; clk_div <= 0; sending <= 1; shifter <= writedata [7:0]; end default:; endcase end end assign sdo = shifter [7]; assign waitrequest = sending; endmodule 


Nous commençons à introduire un nouveau code dans le système. En fait, le chemin d'accès est le même que lors de la création du composant, mais certaines étapes peuvent déjà être omises. Nous allons maintenant nous familiariser avec le processus de raffinement. Accédez à Platform Designer . Si nous ne changions que le code verilog, il serait assez simple d'effectuer l'opération Generate HDL pour le système fini. Mais comme le module a de nouvelles lignes (c'est-à-dire que l'interface a changé), il doit être refait. Pour ce faire, sélectionnez-le dans l'arborescence, appuyez sur le bouton droit de la souris et sélectionnez Modifier .



Nous éditons un système existant. Il suffit donc d'aller dans l'onglet Fichiers et de cliquer sur Analyser les fichiers de sinthèse :



De manière prévisible, des erreurs se sont produites. Mais nous savons déjà que les mauvaises lignes sont à blâmer. Par conséquent, nous allons dans l'onglet Signaux et interfaces , faites glisser sck et sdo le long de la même ligne de l'interface avalon_slave_0 vers l'interface conduit_end :



Renommez également les champs Type de signal pour eux. Le résultat devrait être le suivant:



En fait, c'est tout. Cliquez sur Terminer , appelez Générer un fichier HDL pour le système de processeur, rédigez le projet dans Quartus, attribuez de nouvelles étapes:



Ce sont les contacts A21 et A22 du connecteur d'interface, nous faisons l'assemblage final, remplissez le «firmware» dans le FPGA.

Fer mis à jour. Maintenant, le programme. Allons à Eclipse. Que retenons-nous de faire là-bas? C'est vrai, n'oubliez pas de choisir Générer BSP .

En fait, c'est tout. Il reste à ajouter des fonctionnalités au programme. Transférons une paire d'octets vers le port série, mais nous enverrons le premier octet au périphérique sélectionné par la ligne cs [0] et le second - cs [1] .
  IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x01); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE+4,0x12); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x02); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE+4,0x34); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x00); 

Veuillez noter qu'il n'y a aucun contrôle de disponibilité. Les colis se succèdent. Néanmoins, sur l'oscilloscope, tout s'est avéré assez cohérent



Le rayon jaune est cs [0] , le rayon vert est sdo , le rayon violet est sck et le rayon bleu est cs [1] . On peut voir que le code 0x12 est allé au premier appareil, 0x34 au second.

La lecture se fait de la même manière, mais je ne peux tout simplement pas trouver un bel exemple, à l'exception de la lecture banale du contenu du pied du connecteur. Mais cet exemple est tellement dégénéré qu'il n'est même pas intéressant de le faire. Mais ici, il convient de noter que lors de la lecture de ce paramètre de bus peut être extrêmement important:



S'il y a une ligne de lecture , un chronogramme de lecture apparaîtra dans la boîte de dialogue des paramètres. Et cela montrera l'influence de ce paramètre. Lors de la lecture des jambes du connecteur, il ne sera toujours pas perceptible, mais lors de la lecture à partir du même FIFO ou RAM - complètement. La RAM peut être configurée pour émettre des données immédiatement après la soumission de l'adresse, ou elle peut être émise de manière synchrone. Dans le second cas, une latence est ajoutée. Après tout, le bus a réglé l'adresse, réglé le stroboscope ... Mais il n'y a pas de données sur le front le plus proche du signal d'horloge. Ils apparaîtront après ce front ... Autrement dit, le système a une latence à une latence. Et il suffit de le prendre en compte en définissant ce paramètre. En bref, si vous ne lisez pas ce qui était attendu, vérifiez d'abord si vous avez besoin de configurer la latence. Le reste - la lecture n'est pas différente de l'écriture.

Eh bien, permettez-moi de vous rappeler une fois de plus qu'il vaut mieux ne pas supprimer la disponibilité du bus pour les opérations à long terme, sinon il est tout à fait possible de réduire considérablement les performances du système. Le signal prêt est bon pour maintenir la transaction pendant quelques cycles d'horloge, et pas jusqu'à 80 cycles d'horloge, comme dans mon exemple. Mais premièrement, tout autre exemple serait gênant pour l'article, et deuxièmement, pour les noyaux d'un jour, c'est tout à fait acceptable. Vous serez pleinement conscient de vos actions et éviterez les situations où le bus est bloqué. Certes, si le noyau survit au temps qui lui est alloué, une telle hypothèse peut gâcher la vie à l'avenir, lorsque tout le monde l'oublie, et cela ralentit tout. Mais ce sera plus tard.

Néanmoins, nous avons appris à faire en sorte que le cœur du processeur contrôle nos cœurs. Tout est clair avec le monde adressable, il est maintenant temps de traiter avec le monde du streaming. Mais nous le ferons dans le prochain article, et peut-être même plusieurs articles.

Conclusion


L'article montre comment un noyau Verilog arbitraire peut être connecté pour contrôler le système de processeur Nios II. Les options pour la connexion la plus simple au bus Avalon, ainsi que la connexion dans laquelle le bus peut être dans un état occupé, sont affichées. Des liens vers la littérature sont fournis, à partir desquels vous pouvez découvrir d'autres modes de fonctionnement du bus Avalon en mode mappé en mémoire.

Le projet résultant peut être téléchargé ici .

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


All Articles