
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 exempleDéveloppement du «firmware» le plus simple pour les FPGA installés dans Redd. Partie 2. Code de programmePour 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 .