TP Nanoprocesseur

Introduction

Au cours de ce TP, vous allez :

  • étudier l’architecture du processeur abordé dans la leçon sur le "nanoprocesseur",
  • concevoir le compteur de programme ("program counter" ou PC) du processeur,
  • concevoir la machine à états qui contrôle le processeur (CTR)
  • configurer un circuit logique programmable Altera afin qu’il réalise ce processeur,
  • faire exécuter un programme de test puis un programme musical à votre processeur sur la carte de test DE2,
  • et de manière libre : améliorer votre processeur en enrichissant son jeu d’instructions...

Conseils :

  • respectez scrupuleusement les noms proposés pour les composants et les entrées-sorties,
  • vérifiez toujours que le fichier que vous éditez est le bon (et pas une copie dans un autre répertoire...),
  • faites simple !

Architecture du système à base de microprocesseur

Bien que le sujet du TP concerne davantage le coeur d’exécution du microprocesseur que son environnement d’utilisation, nous présentons ici le système complet implanté sur la maquette de test. Le système proposé se compose :

 

  • du microprocesseur implanté dans un circuit logique programmable Altera Cyclone II EP2C35,
  • d'une mémoire RAM contenant le programme et les données (cette mémoire est, elle aussi, intégrée dans le FPGA),
  • d’un haut-parleur piloté par un port de sortie du microprocesseur.

Quelques autres périphériques ne sont pas représentés sur le schéma 1:

 

  • le générateur d’horloge sclk et la génération de la remise à zéro (reset_n),

  • les afficheurs pour le debug.

Fig. 1: Schéma global du processeur et de ses périphériques

 

Nom

Description

Entrée ou Sortie

(vue du processeur)

sclk

Horloge générale du processeur

Entrée

reset_n

Remise à zéro, asynchrone et active au niveau bas

Entrée

BZ

Port pouvant être positionné à ’1’ ou ’0’

Sortie

ram_addr[7..0]

Adresse de la RAM

Sorties

accu[7..0]

Sortie de l’ACCUmulateur. La valeur de ce bus sera mémorisée dans la RAM à l’adresse ram_addr si ram_write vaut 1 (accès à la RAM en écriture)

Sorties

ram_data[7..0]

Données issues de la RAM, à l’adresse ram_addr (accès à la RAM en lecture)

Entrées

ram_write

Signal demandant l'écriture à la mémoire

Sortie

Tab. 1 : Entrées-sorties du processeur

 

Remarque : il y a quelques différences par rapport à la leçon sur le nanoprocesseur, portant sur les noms des signaux :

  • le bus d’adresse de la RAM est appelé ram_addr[7:0]
  • le bus d’entrée de la RAM est appelé directement accu[7:0] (car c’est la sortie de l’accumulateur du processeur)
  • le bus de sortie de la RAM est appelé ram_data[7:0]

 

Fonctionnement et jeu d’instructions du processeur

Les caractéristiques principales de notre processeur sont les suivantes :

 

  • Le jeu d’instructions du processeur comporte 16 instructions
  • Chaque instruction est codée sur deux octets :
    • Le premier octet contient le code de l’opération proprement dite ;
    • Le deuxième octet contient un pointeur qui donne l’adresse en mémoire d’un éventuel opérande.
  • L’adresse de l’instruction courante en RAM est notée PC ("Program Counter").

  • La mémoire adressable par le processeur ne dépasse pas 256 octets (8 bits d’adresse)
  • Les données traitées sont des entiers naturels limités à l’intervalle 0...255 (8 bits de données)
  • L’opérateur de calcul du processeur se limite à quelques opérations logiques et arithmétiques simples (voir tableau). Le résultat de l’opération effectuée est stocké dans l’accumulateur.
  • Lors d’une modification de l’accumulateur, deux signaux supplémentaires sont générés et mémorisés :
    • Z si ce résultat est nul

    • C si une retenue sortante existe (dans le cas des opérations arithmétiques et de rotation)

Le tableau 2 résume le jeu d’instruction du microprocesseur.

 

Mnémonique

Instruction

Effet sur les données

Effet sur le PC

code (binaire 8bits)

code (décimal)

NOP

No OP: pas d'opération

A inchangé

PC ⇐ PC + 2

0

0

XOR

XOR bit à bit

A ⇐ A xor (AD)

PC ⇐ PC + 2

1

1

AND

AND bit à bit

A ⇐ A and (AD)

PC ⇐ PC + 2

10

2

OR

OR bit à bit

A ⇐ A or (AD)

PC ⇐ PC + 2

11

3

ADD

ADDition sans retenue

A ⇐ A + (AD)

PC ⇐ PC + 2

100

4

ADC

ADDition avec retenue

A ⇐ A + (AD) + C

PC ⇐ PC + 2

101

5

SUB

Soustraction sans retenue

A ⇐ A - (AD)

PC ⇐ PC + 2

110

6

SBC

Soustraction avec retenue

A ⇐ A - (AD) - C

PC ⇐ PC + 2

111

7

ROL

Rotation à gauche

A ⇐ A[6..0]A[7]

PC ⇐ PC + 2

1000

8

ROR

Rotation à droite

A ⇐ A[0]A[7..1]

PC ⇐ PC + 2

1001

9

LDA

LoaD Acc: charger l'accu.

depuis la mémoire

A ⇐ (AD)

PC ⇐ PC + 2

1010

10

STA

STore Acc: ranger l'accu.

dans la mémoire

(AD) ⇐ A

A inchangé

PC ⇐ PC + 2

1011

11

OUT

OUTput: sortie vers BZ

BZ ⇐ (AD)(0)

A inchangé

PC ⇐ PC + 2

1100

12

JMP

JuMP: saut inconditionnel

A inchangé

PC ⇐ AD

1101

13

JNC

Sauter si pas de retenue

A inchangé

PC ⇐ AD si C=0,

PC + 2 sinon

1110

14

JNZ

Sauter si non nul

A inchangé

PC ⇐ AD si Z=0,

PC + 2 sinon

1111

15

Tab. 2 : Jeu d’instructions du processeur

 

  • La notation (AD) représente le contenu de la case mémoire d’adresse AD
  • L’expression PC ⇐ PC + 2 indique que la valeur du pointeur programme, pour l’instruction suivante, sera la valeur de l'instruction courante incrémentée de 2.
  • L’expression A[6..0] indique les 7 bits de poids faibles du registre A.
  • De même, (AD)[0] indique le bit de poids 0 du contenu de la mémoire d’adresse AD.

Nous vous conseillons de vous familiariser avec le jeu d’instructions et avec les codes associés au processeur en étudiant le programme test.s qui servira de jeu de test de simulation pour le processeur.

 

Architecture du coeur du processeur

Le coeur, comme illustré à la figure 2 est composé de quelques registres, d’une unité de calcul, d’un compteur de programme et d’une unité de contrôle. Tous les registres du microprocesseur sont pilotés par l’horloge sclk (comme horloge système, system clock en anglais) et remis à zéro par l’état bas du signal reset_n.

 

Fig. 2 : Intérieur du processeur

4.1 Le compteur de programme (''PC'')

Le compteur de programme (8 bits) sert à stocker l’adresse de l’instruction courante.

  • Son mode "standard" de fonctionnement est de s’auto-incrémenter lors des phases IF et AF pour aller chercher en séquence les différents octets des instructions à exécuter. L’incrémentation se fait au moyen du signal inc_PC.

  • En cas d’instruction de saut, il n’est pas incrémenté, mais directement chargé (lors de la phase AF) avec le contenu du bus de donnée ram_data[7..0], grâce au signal load_PC.

Le signal load_PC est prioritaire par rapport au signal inc_PC.

4.2 La machine à états du contrôleur (CTR)

Cette unité contient la machine à états permettant de gérer les unités du processeur. Elle est donc en charge de séquencer les différents cycles d’une instruction (chargement d’une instruction, chargement de l’opérande, puis exécution) et de générer les signaux de contrôle des différents blocs, sans oublier, bien sûr, le signal ram_write de validation d’écriture en mémoire.

4.3 Les registres de mémorisation

Ce bloc regroupe en fait les quatre registres suivants :

  • le registre de sortie pour le buzzer.
  • le registre d’accumulation.
  • le registre d’instruction
  • le registre d’adresse

Tous ces registres fonctionnent de la même façon : la valeur présente à l’entrée du registre xxx est enregistrée sur le front montant de l’horloge, si le signal de validation (load_xxx) est à 1.

4.4 Le multiplexeur d’adresses (MUX_ADDR, en sortie du PC)

Ce multiplexeur permet de choisir qui, de l’adresse "pointeur programme" PC ou de l’adresse de données AD, doit être envoyé sur le bus d’adresse ram_addr de la mémoire. Le choix de l’un ou de l’autre dépend de l’état courant du microprocesseur. De manière générale, lorsque le microprocesseur va chercher une instruction en mémoire, PC est sélectionné, et lorsque le microprocesseur va traiter une donnée en mémoire, AD est sélectionné. Le choix est contrôlé par le signal sel_adr. La table de vérité de cette fonction est :

 

  • sel_adr

    ram_addr

    0

    PC

    1

    AD

 

4.5 L’ALU, ou "unité arithmétique et logique" (UAL)

Ce bloc prend comme opérandes le contenu de l’accumulateur et le bus de donnée de la RAM, et effectue l’opération indiquée par I. Selon le type d’opération:

  • une retenue entrante peut éventuellement être prise en compte (C)

  • deux bits sont générés
    • Z qui indique si le résultat de l’opération effectuée est nul

    • C qui indique si une éventuelle retenue sortante est générée.

 

Séquencement des opérations

  • Le signal reset_n permet de réinitialiser le fonctionnement du microprocesseur.
  • Au premier coup d’horloge qui suit le passage à 1 du signal reset_n, le microprocesseur va chercher la première instruction du programme, qui se trouve implicitement à l’adresse "0" de la mémoire.
  • L’exécution d’une instruction s’effectue systématiquement en trois cycles d’horloge.

Le graphe de la machine à états est rappelé dans la figure 3 (toutes les sorties ne sont pas indiquées).

Fig. 3 : Graphe d’état du contrôleur CTR

5.1. Premier cycle ''IF''

 

Pendant le premier cycle (IF pour "instruction fetch") deux opérations sont réalisées simultanément.

  • Le registre d’instruction I est chargé (load_I = 1) avec le premier octet de l’instruction. Ce premier octet est lu dans la mémoire à l’adresse donnée par le pointeur PC. A cet effet, PC est positionné sur le bus d’adresse ram_addr de la mémoire (sel_adr = 0).
  • La deuxième opération consiste à incrémenter le compteur de programme PC (inc_PC = 1). Cette opération permettra, pour le cycle suivant, de disposer de l’adresse du deuxième octet de l’instruction.

5.2. Deuxième cycle ''AF''

Pendant le deuxième cycle (AF pour "address fetch") deux opérations sont réalisées simultanément.

  • Le registre d’adresse AD est chargé (load_AD = 1) avec le deuxième octet de l’instruction. Ce deuxième octet est lu dans la mémoire à l’adresse donnée par le pointeur PC. A cet effet, PC est positionné sur le bus d’adresse ram_addr de la mémoire (sel_adr = 0).
  • La deuxième opération consiste à mettre à jour le compteur programme PC. Deux cas peuvent se présenter :
    • L’instruction n’est pas un saut. Il faut alors incrémenter le compteur de programme PC (inc_PC=1) de manière à disposer du pointeur du premier octet de l’instruction suivante.
    • L’instruction est un saut (instruction JMP, instruction JNZ avec Z=0, ou encore instruction JNC avec C=0). Dans ce cas, le pointeur programme PC reçoit la valeur de la donnée lue en mémoire à l’adresse PC (load_PC = 1).

Remarque : pour simplifier, inc_PC est systématiquement mis à 1. En cas de saut, le bloc PC considérera le signal load_PC comme prioritaire.

5.3. Troisième cycle "EX"

 

Le troisième cycle correspond à l’exécution proprement dite de l’instruction. L’opération réalisée dépend de l’instruction stockée dans le registre d’instruction I. Dans tous les cas, le contenu de la mémoire à l’adresse déterminée par le pointeur stocké dans AD au cycle précédent, est lu de la mémoire et placé sur le bus d’entrée ram_data (sel_adr = 1).

  • Dans le cas d’une instruction "non opératoire" (NOP, STA, OUT, JMP, JNC ou JNZ), le contenu de l’accumulateur n’est pas modifié (LOAD_AZC = 0). Dans le cas contraire, le résultat de l’opération est mémorisé dans l’accumulateur (LOAD_AZC = 1). Remarque : l’instruction LDA est considérée comme une instruction opératoire (accuram_data, donc LOAD_AZC = 1).
  • Dans le cas de l’instruction OUT, la valeur du bit de poids faible ram_data[0], est stockée dans le registre BZ (LOAD_BZ = 1).
  • Dans le cas de l’instruction STA, le contenu de l’accumulateur accu est stocké en mémoire (ram_write=1) à l’adresse mémoire déterminée par le pointeur stocké dans AD au cycle précédent.

Le chronograme 4 permet de résumer les différents cas.

 

Fig. 4 : Chronogrammes des cycles du processeur

Travail demandé

Vous venez de lire les spécifications d’un petit micro-processeur, appelé ici "Nanoprocesseur". Comme tout projet bien spécifié, il a été possible de partager le travail de conception entre différentes équipes qui ont pu travailler de façon indépendante à la conception des différents modules. Ainsi, la carte de circuit imprimé contenant les différents composants est prête, un programme de test est déjà écrit (en assembleur), le schéma global est terminé ainsi que les sous-blocs ALU, REG, MEMOIRE.

Hélas, lors de la phase finale d’intégration, le chef de projet réalise, tardivement il est vrai, que les deux équipes chargées des blocs PC et CTR ont fait un travail de tellement piètre qualité, qu’il juge préférable de les reconcevoir complètement.

Il se tourne vers vous et vous confie la tâche de concevoir ces deux blocs. A vous de jouer...

1. Conception du bloc ''PC''

  • Déterminez le fonctionnement exact de ce bloc à partir des spécifications globales, en faisant un schéma.
  • Puis traduisez ce schéma en System-vérilog, et complétez le fichier pc.sv (en respectant les noms déjà définis).

Nous vous recommandons d'utiliser la syntaxe case déjà employée dans les TP précédents.

  • Vérifiez le fonctionnement de PC en tapant la commande make simu_pc

  • Après un certain temps, le simulateur doit vous afficher : "le bloc PC fonctionne", et la fenêtre des chronogrammes doit avoir la même tête que la figure 5.

Fig. 5 : Chronogrammes du PC

Si le simulateur vous indique une erreur, c’est que votre code Verilog comporte une erreur de syntaxe. Fermez le simulateur, corrigez le code source, et relancez la simulation.

Si les chronogrammes ne sont pas assez grands, vous pouvez zoomer à l’aide des icônes . Vous pouvez aussi zoomer sur une partie précise avec l’icône .

2. Conception du bloc ''CTR''

Fig. 6 : Bloc CTR

De nouveau, il faut commencer par lire les spécifications pour en extraire le comportement de la machine à états, puis en déduire une architecture (en faisant un schéma), puis le coder en system-vérilog. Faites attention, la machine à état est un peu spéciale:

  • L'évolution des états ne dépend pas des entrées,
  • Les sorties dépendent de l'état courant et des entrées (machine de Mealy).

Complétez le fichier ctr.sv. Pour cela nous vous conseillons :

  1. D'utiliser les méthodes proposées dans le TP séquentiel : Codage des machines à états,

  2. De traiter séparément chaque signal de sortie (un always@(*) par signal de sortie) en utilisant ce complément de syntaxe sur les expressions booléennes en System Verilog.

2.1. Validation

Vous pouvez vérifier le bon fonctionnement de CTR en simulant directement le système complet (nanoprocesseur + mémoire) avec un programme de test. Ce programme, nommé verif_globale permet de tester l’exécution de toutes les instructions du nanoprocesseur.

Pour cela, vous avez deux options : la simulation (virtuelle) et l’exécution pas-à-pas sur la maquette.

2.2. Simulation

Les chronogrammes doivent ressembler à ceux de la figure 7.

Fig. 7 : Chronogrammes de la simulation du programme de test

Ces chronogrammes sont disponibles en version plus grande en cliquant ici.

Nous avons inclus dans ces chronogrammes les signaux nécessaires à la compréhension de ce qui se passe dans le processeur :

  • l’horloge et le reset,
  • les signaux d'échange avec la RAM,
  • le signal pilotant le buzzer,
  • les trois registres principaux du processeur
    • I : le registre d’instructions, sous forme hexadécimale et textuelle,
    • PC : le compteur de programme,
    • ACCU : l’accumulateur,
  • les entrées et sorties de l’ALU,
  • les entrées et sorties du bloc contrôleur CTR.

Pour suivre les chronogrammes, il est conseillé de se rapporter à la page du programme de test, et de suivre le déroulement du programme cycle par cycle.

Il est également possible de comparer vos chronogrammes à des chronogrammes de référence. Pour cela :

  • Dans Tools -> Waveform Compare -> Comparison Wizard,

  • Choisir vsim_ref.wlf dans le champs "Reference Dataset", puis "Next"
  • Sélectionnez "Specify Comparison by Region", puis "Next"
  • Choisir "simu" dans le champs "Reference Region", puis "Next"
  • Séléctionnez "Compute Differences Now"
  • Dans la fenêtre des chronogrammes, la liste des différences est affichée sous les signaux (avec une icône en forme de triangle jaune).

Par exemple, ces chronogrammes mettent en évidence un dysfonctionnement

2.3. Test réel

Lorsque la simulation est correcte, vous pouvez suivre le déroulement du programme "en vrai", sur la maquette de test.

  • Synthétisez le code correspondant : make verif

  • Si la synthèse s’est terminée sans erreur, programmez le FPGA : make prg_verif

Il y a deux modes d’exécution de ce programme de test :

  • pas-à-pas, si l’interrupteur SW0 est à 1 (en position vers le haut) C’est le bouton KEY3 qui donne alors l’horloge (un appui = un coup d’horloge)

  • automatique, si l’interrupteur SW0 est à 0 (en position vers le bas) L’horloge est alors générée automatiquement, et le programme s’exécute à vitesse normale (25MHz).

Vous pouvez ré-initialiser votre microprocesseur en appuyant sur le bouton KEY0.

Vous pouvez suivre l’état de certains signaux du processeur :

  • sur les deux afficheurs 7 segments de droite (HEX1,HEX0) : ram_addr[7:0]

  • sur les deux afficheurs 7 segments de gauche (HEX7,HEX6) : accu[7:0]

  • sur l'afficheur 7 segment du milieu (HEX4) : I[3:0]

2.4. Test musical

Pour finir le test de votre processeur, nous vous proposons de lui faire jouer un petit morceau de musique. Pour cela:

  1. Lancez la compilation du processeur en lui passant en argument le programme musical : make chicken.
  2. Appelez un enseignant qui branchera un haut-parleur sur votre maquette.
  3. Programmez le FPGA : make prg_chicken
  • Montrez que les instructions JNC, JNZ, JMP, ROL et ROR peuvent être exécutées en deux cycles.
  • Modifier le contrôle en conséquence.
  • Vérifiez le bon fonctionnement de votre processeur (simulation, synthèse, programmation...)

4. Questions subsidiaires

Suivant le temps disponible, traitez dans l'ordre indiqué les questions suivantes:

  1. Comment augmenter de 256 à 1024 la taille de la mémoire ?
  2. Peut on pipeliner le nano-processeur pour multiplier sa vitesse de fonctionnement par 3 ?