TP : Le NanoProcesseur

Vous allez construire un microprocesseur 8 bits simple mais efficace, dont l'architecture est très proche de celle étudiée dans la séance précédente:

 Vous pourrez (devrez..) vous appuyer sur les résultats des travaux effectués dans la séance "Nanoprocesseur" pour concevoir les différents codes demandés.  Cette séance sera, de plus, l'occasion, de découvrir un nouvel outil, le simulateur logique.

 Le système proposé se compose :

  • Du microprocesseur implanté dans un circuit logique programmable Altera Cyclone V -SoC.
  • 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 blocs ne sont pas représentés sur ce schéma : 

  • le générateur d’horloge sclk et la génération de la remise à zéro (reset_n)
  • les afficheurs pour le debug
  • d'éventuels autres périphériques...

Au cours de cette séance vous allez :

  • Étudier l’architecture du microprocesseur abordé dans la leçon sur le processeur,
  • Concevoir le compteur de programme (PC) du microprocesseur,
  • Concevoir l'automate qui contrôle le processeur (CTR)
  • Concevoir l'ALU qui effectue les opérations arithmétiques du processeur
  • Configurer un circuit logique programmable Altera afin qu’il réalise ce microprocesseur,
  • Faire exécuter un programme de test puis un programme musical à votre microprocesseur sur la carte FPGA,
  • Et enfin améliorer votre microprocesseur 

Le rôle de chaque signal du schéma précédent sont indiqués dans le tableau 1.1.

Tableau 1.1: Les signaux du "système" à base de NanoProcesseur
Nom Description Type (vu du processeur)

sclk

Horloge générale du processeur

Entrée

reset_n

Remise à zéro asynchrone et active à ’0’

Entrée

out

Port dont les bits peuvent être positionnés à ’1’ ou ’0’ et branché ainsi :

  • bit 0 : sur codec audio (et sur haut-parleur externe)
  • bits 7 à 1 : leds rouges

Sortie

ram_addr[7:0]

Adresse de l’accès à la RAM

Sortie

ram_data_write[7:0]

Sortie de l’accumulateur. Ce mot est mémorisé dans la RAM à l’adresseram_addr si write vaut 1 (accès à la RAM en écriture)

Sortie

ram_data_read[7:0]

Mot lu dans la RAM à l’adresse ram_addr (accès à la RAM en lecture)

Entrée

ram_write

Signal indiquant le type d'échange effectué avec la mémoire :

  • 1 pour une écriture
  • 0 pour une lecture

Sortie

0. Architecture du Nanoprocesseur

Prenez le temps de vous remémorer l'architecture du coeur. Étudiez le rôle de chacun des blocs et les différents signaux. Attention cette architecture peut être légèrement différente de celle créée en séance de TD.

1. Mise en place de l'environnement de travail

Nous avons préparé un squelette d'architecture dans lequel  les blocs ALU,  PC et CTR sont des boites "vides".

Tous les modules nécessaires sont déjà préparés, dans le sous-répertoire "src" . Vous n'aurez qu'à compléter leur contenu, sans modifier les entrées/sorties déjà déclarées.

2. Codage et test du compteur de programme

Il s'agit du bloc PC de votre NanoProcesseur. Un simple compteur respectant le cahier des charges décrit dans  l'architecture du coeur devrait suffire...

  • Complétez le code SystemVerilog du bloc PC

Le test se fait par simulation :

  1. Ouvrez une fenêtre de terminal
  2. Dans cette fenêtre, placez vous dans le répertoire de plus haut niveau (../../nanoprocesseur)  
  3. Tapez la commande  make compile_pc
  4. Tant que vous obtenez des messages d'erreur c'est que votre code n'est pas syntaxiquement correct.., lisez les messages et corrigez en conséquence
  5. Une fois le code corrigé, lancez une simulation en tapant la commande make simu_pc
  6. Examinez les chronogrammes ainsi que les messages dans la fenêtre "transcript" (au bas de l'écran) et prenez les dispositions nécessaires...

ATTENTION : Pensez à fermer systématiquement le simulateur en cours avant de lancer une commande de type "make simu..." , vous risquez sinon d'avoir plusieurs programmes de simulation en parallèle, puis un bloquage de votre poste...

3. Codage et test de l'unité arithmétique et logique

Il s'agit du bloc ALU dont le détail est décrit ici. Le codage de ce bloc est relativement direct : Il s'agit de choisir une opération à effectuer en fonction du code de l'instruction en entrée...

  1. Complétez le code SystemVerilog du bloc ALU
  2. Vérifiez la syntaxe de votre ALU par la commande make compile_alu
  3. Pour la simulation tapez la commande  make simu_alu
  4. Cette simulation ne génère pas de chronogrammes. Examinez les messages d'erreur et modifiez votre code en conséquence

4. Codage de l'automate de contrôle, et test du nanoprocesseur

Il s'agit du bloc CTR qui génère, en fonction de l'instruction en cours et des flags Z et C, les signaux d'enable des registres et de commande des multiplexeurs. Ce bloc pourra être codé de la façon suivante :

  • Un compteur permettant de définir l'enchaînement des états.
  • Un processus combinatoire pour chaque  signal de contrôle à générer. 

La table précise des instructions à coder se trouve ici.

  1. Complétez le code SystemVerilog du bloc CTR
  2. Vérifiez la syntaxe de votre contrôleur par la commande make compile_ctr
  3. Dans la fenêtre de terminal, tapez la commande  make simu

Il n'y a pas de test du contrôleur en simulation, mais un test global du microprocesseur :

La fenêtre de chronogrammes présente 

  • Dans la partie haute, l'ensemble des signaux de votre nanoprocesseur
  • Dans la partie basse, une comparaison avec les chronogrammes d'un NanoProcessuer de référence.

S'il n'y a pas de zones hachurées en rouge c'est que votre microprocesseur fonctionne correctement. 

Sinon :

  • Examinez la première erreur
  • Déterminez l' instructions fautive en vous aidant éventuellement du source du programme exécuté (test.s)
  • Corrigez votre erreur
  • Itérez jusqu'à épuisement des erreurs...

ATTENTION : pensez à fermer systématiquement le simulateur en cours avant de lancer une commande de type "make simu..." , vous risquez sinon d'avoir plusieurs programmes de simulation en parallèle, puis un bloquage de votre poste... 

5. Synthèse et test sur la maquette DE1-SoC

Si les chronogrammes sont sans erreur, alors nous pouvons passer au test sur la maquette.

  1. Synthèsez  l'architecture (c'est un peu long et il y a plein de messages)  : make chicken
  2. Branchez et allumez la maquette...
  3. Programmez la maquette : make prg_chicken

Attention: la maquette peut être contrôlée de la façon suivante : 

  • Si SW0 est à 0, l'horloge est la bonne (1MHz), et on entend la musique ...
  • Si SW0 est à 1, alors c'est KEY3 qui joue le rôle d'horloge manuelle. On peut alors suivre le déroulement du programme sur les afficheurs (mais on n'entendra rien) :
    • L'afficheur HEX4 et HEX5  présentent le contenu de l'accumulateur
    • Les afficheurs HEX2 et HEX 3  présentent l'instruction traitée
    • L'afficheur HEX0 et HEX1  présentent le l'adresse présentée à la RAM.

6. K2000 en code assembleur

Maintenant que vous disposez d'un microprocesseur opérationnel, vous allez vous exercer à écrire du code en assembleur. Nous allons de nouveau prendre K2000 pour exemple.

L'instruction "OUT" du nanoprocesseur permet non seulement d'envoyer le bit de poids faible de la donnée addressée en lecture  vers le buzzer, mais aussi de de stocker les 7 autres bits dans un registre connecté aux leds rouges de la maquette (ledr[6:0]

 

  1. Nous voulons réaliser un K2000 en modifiant l'affichage des 7 leds de manière régulière à l'aide de l'instruction "OUT".   Pour cela, éditez le fichier "moncode.s" se trouvant dans le répertoire "soft", et tenez compte des commentaires suivants:
    • L'horloge du processeur sera réglée à environ 1KHz.
    • Une instruction de déroule en 3 cycles d'horloge.
    • Le bit de poids faible de l'accumulateur sera maintenu à 0 pour éviter de générer des sons intempestifs...
  2. Une fois votre code écrit, compilez le en executant la commande "make moncode". Corrigez les éventuelles erreurs et recompilez le code jusqu'à ce qu'aucune erreur ne soit signalée.
  3. Positionnez SW1 à 1 pour sélectionner une horloge de 1KHz
  4. Programmez le FPGA par la commande "make prg_moncode". Cela peut prendre un peu de temps car cela nécessite la mise à jour du fichier de programmation a partir de votre code
  5. Et vérifiez le bon fonctionnement, ou non de votre programme.
  6. N'oubliez pas que vous pouvez faire fonctionner le microprocesseur pas à pas en plaçant l'interrupteur SW0 à 1 et en utilisant le bouton KEY3.

 

 

 

 

Tentez de modifier le NanoProcesseur pour en améliorer les performances (nombre de cycles variable selon l'instruction).

 

Fichier attachéTaille
Fichier nanoprocesseur.tgz27.79 Ko

Architecture du coeur

Le cœur, comme illustré à la figure ci-dessous 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.

 

 

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.
 

L'automate de contrôle (CTR)

Cette unité contient un automate basique en charge de séquencer les différents cycles d’une instruction :

  1. chargement d’une instruction, 
  2. chargement de l’opérande, 
  3. 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 permettant d'écrire en mémoire.

Les registres de mémorisation

Ce bloc regroupe en fait les quatre registres suivants :

  • le registre de sortie pour le buzzer : BZ
  • le registre d’accumulation : ACC (+Z et C)
  • le registre d’instruction : I
  • le registre d’adresse : ADDR

Tous ces registres fonctionnent de la même façon : l’entrée du registre xxx est mémorisée sur le front montant de l’horloge si le signal d'enable correspondant (load_xxx) est à 1.

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 de fonctionnement 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 == 0 implique ram_addr ← PC
  • sel_adr == 1 implique ram_addr ← AD

L’ALU, unité arithmétique et logique

Ce bloc prend comme opérandes le contenu de l’accumulateur et le bus de donnée de la RAM, et effectue dessus l’opération indiquée par I. Selon le type d’opération, une retenue entrante peut éventuellement être prise en compte (C_out).

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.

 

Programme de test

;==============================================================
;== TP5 : Programme test.s
;==============================================================
;Il s'agit ici d'un fichier commenté du programme test.s
;
; La première zone correspond à la zone "programme"
; Le début de chaque ligne correspond à la zone programme. Elle comporte :
;       Code assembleur : colonnes 1,2,3 (label, code, operande)
;La fin de chaque ligne est un commentaire comportant
;       Le PC corresponsant à l'adresse de l'instruction en cours : colonne 4
;       L'adresse en RAM de l'éventuelle opérande : colonne 5
;       Le registre I (instruction en cours) : colonne 6
;       Accumulateur, Z et C après exécution de l'instruction en cours : colonnes 7,8 et 9
;       Commentaire global : colonne 10
;
;Les constantes indiquées sous forme 0xxx sont en hexadécimal.
;
;La deuxieme zone correspond a la zone des données. De même, elle comporte
;       Code assembleur : colonne 1,2,3 (nom variable, valeur, adresse)
;       Valeur variable : colonne 4,5,6 (décimal, binaire et hexadécimal)
;

;=================================================================
;== Zone programme
;=================================================================

;label  code operande;   PC       I     AD        accu   C  Z  Commentaire

:DEBUT  NOP  ZERO    ;  0x00      0x0    0x39        0x00    0  0
        LDA  DATA0   ;  0x02      0xA    0x30        0xFF    0  0    (DATA0 : donnée à l'adresse 0x30)
        XOR  DATA1   ;  0x04      0x1    0x31        0x3A    0  0    (A = 0xFF XOR 0xC5)
        AND  DATA2   ;  0x06      0x2    0x32        0x10    0  0    (A = 0x3A AND 0x10)
        OR   DATA3   ;  0x08      0x3    0x33        0x31    0  0    (A = 0x10 OR  0x21)
        ADD  DATA4   ;  0x0A      0x4    0x34        0x00    1  1    (A = 0x31  +  0xCF)
        ADC  DATA5   ;  0x0C      0x5    0x35        0x01    0  0    (A = 0x00  +  0x00  + 1)
        SUB  DATA6   ;  0x0E      0x6    0x36        0xFF    1  0    (A = 0x01  -  0x02)
        SBC  DATA7   ;  0x10      0x7    0x37        0x00    0  1    (A = 0xFF  -  0xFD  - 1)
        XOR  DATA1   ;  0x12      0x1    0x31        0xC5    0  0    (A = 0x00 XOR 0xC5)
        ROL  ZERO    ;  0x14      0x8    0x39        0x8A    0  0    (A = ROL(0xC5))
        ROR  ZERO    ;  0x16      0x9    0x39        0xC5    0  0    (A = ROR(0x8A))
        STA  RESULT  ;  0x18      0xB    0x3B        0xC5    0  0    (Memoire[0x3B] <- 0xC5)
        LDA  ZERO    ;  0x1A      0xA    0x39        0x00    0  1    (A <- Memoire[0x39])
        LDA  RESULT  ;  0x1C      0xA    0x3B        0xC5    0  0    (A <- Memoire[0x3B])
        LDA  COUNT   ;  0x1E      0xA    0x38        0x03    0  0    (A <- Memoire[0x38])
        OUT  UN      ;  0x20      0xC    0x3A        0x03    0  0    (BZ <- Memoire[0x3A][0])
:COMPT1 SUB  UN      ;  0x22      0x6    0x3A        0x02    0  0    (A = 0x03  -  0x01)
        JNC  COMPT1  ;  0x24      0xE    0x22        0x02    X  0    Retour à COMPT1 3 fois
        LDA  COUNT   ;  0x26      0xA    0x38        0x03    0  0    (A <- Memoire[0x38])
        OUT  ZERO    ;  0x28      0xC    0x39        0x03    0  0    (BZ <- Memoire[0x39][0])
:COMPT2 SUB  UN      ;  0x2A      0x6    0x3A        0x02    0  0    (A = 0x03  -  0x01)
        JNZ  COMPT2  ;  0x2C      0xF    0x2A        0x02    0  0    Retour à COMPT2 2 fois
        JMP  DEBUT   ;  0x2E      0xD    0x00        0x00    0  1    Retour à debut
;===================================================================
;== Zone donnees
;===================================================================
;Nom variable    Valeur    Ad   Décimal  Binaire     Hexadécimal
:DATA0    .db   255     ; 0x30    255     11111111      0xFF
:DATA1    .db   197     ; 0x31    197     11000101      0xC5
:DATA2    .db   16      ; 0x32     16     00010000      0x10
:DATA3    .db   33      ; 0x33     33     00100001      0x21
:DATA4    .db   207     ; 0x34    207     11001111      0xCF
:DATA5    .db   0       ; 0x35      0     00000000      0x00
:DATA6    .db   2       ; 0x36      2     00000010      0x02
:DATA7    .db   254     ; 0x37    254     11111110      0xFD
:COUNT    .db   3       ; 0x38      3     00000011      0x03
:ZERO     .db   0       ; 0x39      0     00000000      0x00
:UN       .db   1       ; 0x3A      1     00000001      0x01
:RESULT   .db   0       ; 0x3B      0     00000000      0x00
Fichier attachéTaille
Binary Data Pour télécharger le programme de test4.33 Ko

L'unité arithmétique et logique

1. Description de l'ALU

L'un des blocs majeurs de ce processeur est l'Unité Arithmétique et Logique (ALU en anglais), qui se charge d'effectuer les opérations arithmétiques et logiques (d'où le nom) sur deux nombres.

Pour nous, l'ALU est un bloc combinatoire, ses entrées-sorties  sont: 

  • Une entrée qui spécifie le calcul à effectuer : I[3:0]
  • Les données sur lesquelles on va effectuer les calculs : A[7:0]B[7:0] et Cin (retenue entrante). Remarque : en fonction du calcul à effectuer, toutes les entrées ne seront pas forcément utilisées...
  • Une sortie S[7:0],
  • Une retenue sortante Cout,
  • Et un signal Z indiquant si le résultat de l'ALU est nul (Z=1 si S==0).

Les entrées A et B de l'ALU sont respectivement connectées aux signaux accu et ram_data du nanoprocesseur.

Le tableau suivant décrit  les opérations que doit pouvoir réaliser l'ALU. 

I[3:0] Mnémonique Instruction Sortie S Sortie Cout
1 XOR XOR bit à bit de A et B S ← A xor B 0
2 AND AND bit à bit de A et B S ← A and B 0
3 OR OR bit à bit de A et B S ← A or B 0
4 ADD Addition sans retenue entrante S ← A + B retenue sortante
5 ADC Addition avec retenue entrante S ← A + B + Cin
 
retenue sortante
6 SUB Soustraction sans retenue entrante S ← A - B retenue sortante
7 SBC Soustraction avec retenue entrante S ← A - B - Cin
 
retenue sortante
8 ROL Rotation à gauche S[7:0] ← {A[6:0], Cin} A[7]
9 ROR Rotation à droite S[7:0] ← {Cin, A[7:1]} A[0]
autres ... ... S ← B 0

 

Jeu d'instructions du NanoProcesseur

  • Le jeu d’instructions du microprocesseur se limite à 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’accès courant en RAM est notée PC ("Program Counter")
  • La mémoire adressable par le microprocesseur 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 microprocesseur se limite à quelques opérations logiques et arithmétiques simples (voir tableau ci-dessous). 
  • 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 suivant résume le jeu d’instruction du microprocesseur:

  • représente la valeur de l'accumulateur avant exécution de l'instruction.
  • PC représente la valeur du PC courant.
  • L’expression [AD] représente le contenu de la mémoire à l’adresse AD fournie par le deuxième octet de l’instruction
  • L’expression A[6..0] indique les 7 bits de poids faibles de l'accumulateur.
  • De même, [AD][0] correspond au bit de poids 0 du contenu de la mémoire à l’adresse AD.
Code (décimal) Mnémonique Description Valeur de l'accumulateur A  après exécution de l'instruction Valeur du PC après exécution de l'instruction

0

NOP

ne fait rien !

rien

PC + 2

1

XOR

XOR

A ← A xor [AD]

PC + 2

2

AND

AND

A ← A and [AD]

PC + 2

3

OR

OR

A ← A or [AD]

 PC + 2

4

ADD

Addition sans retenue entrante

A ← A + [AD]

PC + 2

5

ADC

Addition avec retenue entrante

A ← A + [AD] + C_in

PC + 2

6

SUB

soustraction avec retenue entrante

A ← A - [AD]

PC + 2

7

SBC

soustraction avec retenue entrante

A ← A - [AD] - C_in

PC + 2

8

ROL

rotation sur la gauche

{C_out, A} ← {A[7:0], C_in}

PC + 2

9

ROR

rotation sur la droite

{C_out, A} ← {A[0], C_in, A[7:1]};

PC + 2

10

LDA

Load accumulateur from memory

A ← [AD]

PC + 2

11

STA

Store accumulateur to memory

[AD] ← A

PC + 2

12

OUT

positionne le port de sortie

OUT ← [AD]

PC + 2

13

JMP

saut inconditionnel

rien

AD

14

JNC

saut si C = 0

rien

AD si C=0 sinon PC + 2

15

JNZ

saut si Z = 0

rien

AD si Z=0 sinon PC + 2