Leçon 12 : La mémoire de données

Dans cette ultime leçon, vous allez instancier un composant mémoire RAM. Avec ISE c’est assez simple. Vous pourrez ensuite modifier votre FSM afin de supporter deux nouvelles instructions READ et WRITE. Ces deux instructions concernent les registres Ram et Rdm non encore utilisés.

Enfin le processeur S3 pour être complet doit vous permettre de gérer des appels de fonction avec ou sans passage de paramètres. Ici encore c’est au niveau transfert de registres, vers le compteur ordinal, que vous allez intervenir. Une pile d’adresses sera instanciée et vous permettra d’empiler et de dépiler les adresses de branchement lors d’un appel, comme d’ailleurs les données qui s’échangent entre appelant et appelé.

Ayant choisi une architecture de type Harvard au départ, votre processeur ne possède pas encore de mémoire de donnée. Pourtant tout est déjà là pour l’intégrer. Ensuite vous pourrez écrire des programmes sur de larges structures de données stockées dans cette mémoire. Enfin la dernière fonctionnalité que nous implanterons ensemble concerne l’appel de fonction. Dans le même esprit que les instructions de branchement, c’est un transfert de données entre le compteur ordinal et une pile de retour qui réalisera la sauvegarde et restauration de l’adresse de retour, voire les passages de paramètres de la fonction.

Connaissance requises

Adressage mémoire, notion de pile, appel de fonctions retour et passage de paramètres.

Objectifs

Dans cette ultime leçon, vous allez instancier un composant mémoire RAM. Avec ISE c’est assez simple. Vous pourrez ensuite modifier votre FSM afin de supporter deux nouvelles instructions READ et WRITE. Ces deux instructions concernent les registres Ram et Rdm non encore utilisés.

Enfin le processeur S3 pour être complet doit vous permettre de gérer des appels de fonction avec ou sans passage de paramètres. Ici encore c’est au niveau transfert de registres, vers le compteur ordinal, que vous allez intervenir. Une pile d’adresses sera instanciée et vous permettra d’empiler et de dépiler les adresses de branchement lors d’un appel, comme d’ailleurs les données qui s’échangent entre appelant et appelé.

La fonction mémoire

C’est ISE qui va vous créer un symbole mémoire, il vous suffira de le placer dans S3.sch ensuite.

Ouvrez le projet construit lors de la leçon 10. Vous allez créer une nouvelle source de type IPcore, appelez-le datamem.

Figure 128 Création IP core datamem

Choisissez une fois validé, le générateur Basic Elements>Memory Elements> Distributed Memory Generator. Next puis Finish vous ouvre l’écran de spécification de cet IP.

Figure 129 Génération de l'IP

Il vous suffit de choisir la taille de 2048 mots de 16 bits en mode Single Port RAM et de générer l’IP par Generate.

Figure 130 Spécification de datamem

Le symbole est maintenant disponible. Vous pouvez venir l’instancier à gauche du registre Ram. Renommez les bus de sortie de Ram et Rdm, ADRmem(15:0) et DATmem(15:0). Reliez l’entrée clk sur le bus clk, l’entrée d(15:0) sur DATmem(15 :0), l’entrée a(10:0) via un Bus Tap sur ADRmem (10:0). Placez un fil en entrée de we et nommez-le WE.

Figure 131 Connexion datamem

En ce qui concerne le registre Rdm, il peut être chargé depuis cette mémoire de données ou depuis le bus de donnée du processeur S3. Comme pour le chargement du registre CO, vous allez utiliser un mux2x16 dont la sortie O(15:0) sera connectée à l’entrée D(15:0) du registre Rdm. L’entrée D0 est connectée à la sortie spo du composant DATAmem et l’entrée D1 directement au bus de donnée bus_data du processeur S3. Il vous reste à prendre en compte les deux commandes de chargement de ce registre Rdm. La première vient du signal B2Rdm, la seconde d’un nouveau signal RE (Read Enable). Placez alors un OR2 sur ces deux signaux, sa sortie commande directement le CE de Rdm. Reste à piloter le mux2x16, c’est le signal B2Rdm qui sélectionne le multiplexeur via l’entrée S0.

Figure 132 La mémoire datamem complètement connectée

Les instructions READ/WRITE

La sémantique de ces deux instructions est assez triviale :

  • READ : Ram contient une adresse. READ déclenche une lecture dans datamem à cette adresse, le résultat est rangé dans Rdm.
  • WRITE : Ram contient une adresse, Rdm contient une donnée. WRITE déclenche l’écriture de cette donnée dans datamem à l’adresse spécifiée.

La mémoire étant interne au processeur, ces deux instructions ne prennent qu’un seul cycle processeur.

Il va vous falloir modifier votre FSM !

Comme depuis le début, il vous faut ajouter les ports de sortie qui transporteront les signaux de commande. Ici il y a deux nouveaux signaux WE et RE. Je vous propose d’anticiper et d’ajouter de suite deux autres signaux push et pop. Ajoutez également les 4 signaux internes associés WE_I, RE_i, push_i et pop_i.

Dans le process Next_output, vous pouvez initialiser les 4 signaux internes. Ensuite ajoutez une branche dans le IF de l’état chargement afin de décoder ces deux instructions. Je vous propose de choisir les codes 7000 pour READ et 7100 pour WRITE.

Il vous reste à compléter le process Synchro afin de produire les signaux sur les ports de sortie de votre FSM.

Une fois le fichier fsm.vhd sauvé, vous pouvez recréer le symbole associé à fsm.vhd et venir le mettre à jour dans S3.sch. Ajouter les quatre fils sur les nouveaux ports WE, RE, push et pop et nommez-les avec les même noms. Les autres sorties ne changent pas.

Figure 133 La fsm avec les nouveaux ports

Mettez à jour votre assembleur S3asm.bat afin de supporter ces deux nouvelles instructions. Il suffit d’ajouter ces deux lignes de commande après les codes ALU par exemple.

Un programme test

Je vous propose d’écrire le programme qui saisit 3 mots de 8 bits depuis les switches, les range dans la mémoire aux adresses 0, 1, 2 puis les affiche ensuite un à un dans le même ordre sur l’afficheur.

Avec l’assembleur vous pouvez produire le fichier testmem.coe, le placer dans insmem, générer insmem, mettre à jour dans S3, sauver S3.sch, générer le symbole correspondant et enfin le mettre à jour dans toplevel. Mais tout çà vous savez le faire maintenant… A vous de tester ce programme sur la carte une fois le toplevel.bit produit ou le simuler si vous n'avez pas encore acheté votre carte ...

Une pile autonome

Habituellement la pile est rangée dans la mémoire de données et un registre particulier sert de pointeur de pile sur une zone réservée de cette mémoire. Une instruction PUSH demande un transfert depuis ce registre vers Ram, un transfert d’un registre contenant la donnée à empiler vers Rdm puis un WRITE. Pour un POP je vous laisse imaginer les actions à entreprendre. Afin de garder une exécution en un seul cycle, je vous propose de construire une mini-pile de 4 emplacements en dehors de la mémoire de données et avec ses propres instructions d’accès.

Dans un premier temps vous allez construire un pointeur de pile qui peut s’incrémenter ou se décrémenter sur le front montant de l’horloge.

Vous aviez déjà construit un compteur lors de la leçon 4 (Figure 51). Ici il faut que le compteur ne fonctionne que lorsqu’il y a une commande d’incrément ou de décrément active et doit être synchrone avec l’horloge: vous utiliserez une bascule FDE au lieu de FD (lisez le Symbol Info). Voici le comportement de notre pointeur de pile à chaque front montant de l’horloge en fonction des entrées DEC et INC.

Créez une nouvelle source de type schematic et nommez-là incdec2. Instanciez deux bascules FDE. Créez un fil clk avec son I/O Marker relié aux deux entrées C des bascules. Puis deux fils avec I/O Marker inc et dec sont associés par une porte OR2 et la sortie de cette porte valide les CE des deux bascules FDE quand l’un des deux signaux est actif, vous venez de réaliser l’identité sur Q0, Q1 quand ni DEC ni INC n'est actif. Pour les deux cas DEC ou INC, les bascules seront activées par le signal E. Pour Q0, il suffit de l’inverser dans les deux cas. Connectez la sortie Q de la bascule de gauche sur son entrée D après avoir traversé un inverseur INV. Pour Q1 il suffit d’un porte XOR2 sur les deux sorties Q0 et Q1. Il suffit alors de prendre en entrée de la seconde bascule le résultat de ce XOR2 s’il s’agit d’un incrément, son inverse s’il s’agit d’un décrément. Ce IF est réalisé avec deux AND2, un INV et un OR2.

Placez deux fils Q0 et Q1 sur les deux sorties Q des bascules respectives et placez vos I/O Marker sur ces fils.

Figure 134 Compteur/décompteur synchrone 2 bits

Un fois sauvegardé, vous pouvez générer le symbole correspondant.

Vous allez maintenant créer une pile de profondeur 4. Créez une nouvelle source de type shematic nommée pile. Placez 4 registres 16 bits avec le symbole déjà utilisé pour les registres du processeur : FD16CE. Le même I/O marker nommé clk est connecté à toutes les entrées C. Un autre I/O Marker nommé a(15:0) est connecté à toutes les entrée D(15:0).

Le pointeur de pile pointe toujours sur le prochain emplacement libre, lors d’un push actif il faut écrire dans le registre associé à la valeur du pointeur de pile. Pour cela vous utilisez un décodeur D2_4E de la bibliothèque Decoder. En fonction des sorties Q0 et Q1 du pointeur de pile incdec2, un et un seul registre sera actif. Le décodeur est lui-même actif seulement si le signal push est actif. Placez trois fils en entrée, nommez Q0 le fil connecté à A0, Q1 celui connecté à A1 et push celui connecté à E. Les quatre sorties D0.. D3 sont quant à elles connectées à chaque CE du registre correspondant.

Placez maintenant sous le décodeur une instance de votre composant incdec2. Placez trois fils en entrée : clk sur clk, push sur inc et pop sur dec. Placez deux I/O Marker sur push et pop. Tirez deux fils nommés Q0 et Q1 sur les sorties. Voici l’état de votre construction à cet instant.

Figure 135 Push sur la pile

Pour la partie pop, il faut se souvenir que le pointeur de pile pointe sur la prochaine case vide de la pile. Il faut donc sélectionner la sortie d’un des quatre registres en prenant en compte ce décalage. Comme pour l’ALU vous allez construire un arbre de multiplexeur mux2x16, ici il n’en faut que trois, le dernier produisant la sortie b(15:0) sur laquelle vous placez un I/O Marker. La table de vérité de cet arbre de sélection est simple !

Avec un inverseur sur Q0 et un XOR2 sur Q0 et Q1 on s’en sort. Il suffit alors de piloter respectivement les multiplexeurs de premier niveau puis de celui du deuxième niveau.

Figure 136 La pile

Vous pouvez sauver et générer le symbole correspondant.

Intégration dans le processeur S3

La pile doit recevoir une donnée en entrée et donc sera connectée au bus de donnée après le dernier le registre (RI), elle doit aussi produire une donnée sur ce bus , il suffit de l’inclure dans la cascade de connecteur16. Je vous conseille de placer une instance de pile à droite du registre RI. Supprimez le bus de connexion entre les connecteur16 CCO et CRI. Placez un nouveau connecteur16 au-dessus de votre pile et nommez-le Cpile. Reliez Dout(15:0) de CCO avec Din(15:0) de Cpile. Reliez Dout(15:0) de Cpile avec Din(15:0) de CRI, la cascade est reconstruite.

Connectez la sortie de la pile b(15:0) avec l’entrée R(15:0)du connecteur Cpile. L’entrée de la pile a(15:0) est reliée au bus de sortie de Dout(15:0) du CRI. Il vous reste à ajouter un fil sur clk nommez clk, un fil sur push nommez push et un fil reliant pop et connect de Cpile nommé pop.

Figure 137 Intégration de la pile

La dernière étape concerne la prise en compte des nouvelles instructions pour la gestion de cette pile. Cela se passe au niveau de votre FSM. Vous avez déjà ajouté les ports et signaux internes nécessaires. Il ne reste qu’à décoder les instructions.

Je vous propose d’intégrer trois instructions : PUSH, POP et PUSHI, ces trois instructions reproduisent les mêmes codages que les MOV et MVI :

  • PUSH : empile un registre sur la pile. Je vous propose le code x80s0 où s est le numéro du registre source (1..F).
  • POP : dépile la valeur du sommet de pile vers un registre. Si le registre destination du POP est le registre CO, comme pour le MOV l’instruction suivante sera quand même exécutée avant la prise en compte du branchement (branchement retardé). Je vous propose le code x900d où d est le numéro du registre source (1..F).
  • PUSHI : empile un immédiat de 16 bits sur la pile. Comme pour le MVI cet immédiat est construit à partir des 8 bits de l’instruction RI(11:4) complétés à gauche par des zéro. Je vous propose le code xAvv0 où vv sont les bits de poids faible de l’immédiat.

Dans le process Next_output, il vous suffit de décoder les trois codes de ces instructions et de valider la source ou destination puis le signal push ou pop.

Pour finir sauvez le fichier et regénérer le symbole associé à fsm.vhd. Mettez à jour les niveaux hiérarchiques supérieurs (S3 et toplevel)

Votre processeur S3 est complet !!!

Il vous reste à modifier votre assembleur pour en tirer parti facilement. Voici trois lignes à ajouter, changer l'ordre si besoin ;-) Pour les PUSH ou PUSHI, les instructions sont toujours codées sont 16 bits, la simplicité de SED vous oblige à ajouter à la main les 4 bits de poids faible manquants, mettez n’importe quoi sur 4 bits ça marchera car il n’y a pas de registre destination, le plus simple étant d’y placer un zéro à droite. (Voir l’exemple suivant pour la syntaxe)

Voici un petit programme pour tester ces dernières instructions. Vous aviez écrit un programme qui faisait la somme de deux nombres de 8 bits saisis en 2 fois sur les switches. Voici un code add16.S3 qui saisit deux nombres de 16 bits en 4 fois et en fait la somme pour un affichage. Avec S3asm.bat générez le fichier add16.coe puis placez-le dans insmem comme fichier de configuration de cette mémoire. Après mise à jour des niveaux hiérarchiques supérieurs, vous pouvez resynthétiser le fichier toplevel.bit.

Pour le tester, saisir 4 mots de 8 bits séparés par des pressions sur le bouton pause. Il affiche le résultat de la somme.

Ce code exécute deux fois les mêmes suites d’instructions. Vous allez pouvoir tester une nouvelle version add16V2.S3 qui utilise la notion de fonction. Cette fonction saisit un nombre de 16 bits et le place dans le registre R1. En l’appelant deux fois et avec quelques transferts de registres vous pouvez réaliser l’addition et affichage.

Assemblez, rangez dans insmem, mettez à jour les niveaux de hiérarchies, synthétisez et exécutez sur la carte ou simulez avec Isim.

Le dernier code que je vous propose permet de passer la valeur de retour sur la pile elle-même. Il suffit de ranger cette valeur par un PUSH juste après le POP sur le CO. Dans le programme appelant, un POP sur la pile permet de récupérer la donnée pour en faire ce que l’on veut.

A vous de jouer

Question 1

Ajoutez une fonction qui affiche un nombre de 16 bits avec passage de ce nombre sur la pile. Utilisez cette fonction pour l'affichage du résultat de l'addition.

Question 2 (difficile)

Avec les fonctions DEC, MUL que vous avez développés lors de la leçon 9, et en ajoutant une nouvelle fonction RDM (RanDoM) dans l'ALU et dans votre assembleur qui renvoie un nombre aléatoire sur 16 bits (Linear Feedback Shift Registers), écrire un programme qui calcule le nombre PI pas une méthode monté Carlo.

Réponses