![]() |
Projet PAF : Processeur RISC |
L’objectif de ce projet est de développer un processeur utilisant l’architecture de jeu d’instruction (Instruction Set Architecture, ISA) libre et ouverte RISC-V.
Durant les cours de PAN (ELECINF102), vous avez manipulé un petit processeur baptisé NanoProcesseur. Ce processeur était extrêmement limité. Le premier objectif de ce projet est de réaliser un processeur plus complet et réaliste, en se basant sur une architecture de jeu d’instruction nommée RISC-V, initialement créée à l’Université de Californie à Berkeley et maintenue par une fondation regroupant de très nombreuses entreprises et centres de recherche. Cette architecture est libre et ouverte, les spécifications complètes sont disponibles et chacun peut créer un processeur se basant sur ce jeu d’instruction.
Dans un premier temps, vous décrirez en SystemVerilog une version simplifiée du processeur (reprenant un sous-ensemble du jeu d’instruction et sans pipeline) que vous simulerez (pour vérifier son bon fonctionnement puis que testerez sur la carte FPGA DE1-SoC (celle que vous auriez du utiliser en TP d’ELECINF102). Puis, dans un second temps, vous enrichirez ce premier prototype en ajoutant les instructions manquantes, en implémentant le pipeline et en implémentant quelques périphériques.
Le second objectif de ce projet est, pour pouvoir utiliser le processeur ainsi développé, de programmer un assembleur, c’est-à-dire un programme prenant en entrée un programme écrit en langage assembleur et produisant le code machine correspondant, prêt à être exécuté par le processeur.
Nous nous concentrerons sur la variante RV32I (RISC-V, 32 bits, instructions de calcul sur les entiers) de l’ISA. Il existe d’autres variantes proposant des tailles différentes (64 ou 128 bits), une version adaptée au monde de l’embarqué (E), et toute une série d’extensions, certaines standardisées, d’autres laissées libres aux constructeurs.
Avec cette variante, notre processeur est un processeur 32 bits. Il travaille donc nativement avec des données codées sur 32 bits (c’est-à-dire que les opérandes et les résultats des calculs arithmériques et logiques sont représentés sur 32 bits). De plus, toutes les instructions utilisées sont elles-mêmes codées sur 32 bits.
Les spécifications du RISC-V sont disponibles ici.
Dans l’UE ELECINF102, le nanoprocesseur possédait un accumulateur qui servait à stocker le résultat de la dernière opérations effectuée et dont le contenu était utilisé comme un des opérandes par l’ALU.
Dans les processeurs actuels, le concept d’accumulateur est étendu et la notion de registre apparaît. Votre processeur contient 32 registres (numérotés de x0
à x31
) pouvant chacun contenir une donnée sur 32 bits. Toutes les opérations arithmétiques et logiques travaillent sur les registres (c’est-à-dire que les opérandes de l’opération proviennent de deux registres et le résultat de l’opération est stocké dans un registre). Seules quelques instructions particulières (baptisées load/store) permettent d’effectuer des transferts entre les registres et la mémoire.
Pour des raisons pratiques, le registre x0
contient toujours la valeur 0. Une lecture depuis ce registre renvoie toujours la valeur 0 et une écriture dans ce registre est tout simplement ignorée.
Il existe enfin un registre supplémentaire, pc
qui contient l’adresse de l’instruction en cours d’exécution.
Pour plus de détails, référez-vous à la section 2.1 (pp. 13 à 15 des spécifications).
Toutes les instructions sont codées sur 32 bits. Il existe plusieurs codages différents. En effet, en fonction du type d’instruction, il faut, parmi ces 32 bits, spécifier :
opcode
, et des champs funct3
et funct7
rs1
, rs2
)rd
)imm
)Pour plus de détails, référez-vous à la section 2.2 (pp. 15 à 17 des spécifications). Les valeurs du champ opcode
sont données page 130.
La liste des instructions pour la variante RV32I est donnée dans les pages 17 à 30 des spécifications.
Dans un premier temps, nous ne nous intéresserons pas aux instructions FENCE
, SYSTEM
(ECALL
et EBREAK
), ni aux différents HINT
. Nous supposerons également que les accès mémoires seront alignés.
Cette ISA essaie de ne pas faire d’hypothèses sur la manière dont elle est implémentée (sans pipeline, pipeline à 3 étages, pipeline à 5 étages, etc.). À des fins pédagogiques, nous étudierons un pipeline à 5 étages et donc nous décomposerons dès le début l’exécution d’une instruction en 5 étapes :
IF
(Instruction Fetch) : On présente à la mémoire l’adresse de l’instruction à exécuter (pc
) et on gère ensuite son incrémentation (pc <= pc + 4
)ID
(Instruction Decode) : On décode l’instruction lue depuis la mémoire et on présente au banc de registre (module qui gère les registres) les numéros des registres concernés par l’instructionEX
(Execute) : À partir des valeurs des registres, l’ALU effectue les opérations arithmétiques ou logiquesMEM
(Data memory access) : Les éventuelles lectures / écritures en mémoire sont effectuées lors de ce cycleWB
(Write-Back) : On écrit le résultat dans le banc de registreLe processeur est un module avec des entrées et des sorties. Dans un premier temps, voici la description de ces signaux :
Nom | Direction | Taille | Description |
---|---|---|---|
clk | Entrée | 1 | L’horloge du processeur |
reset_n | Entrée | 1 | Signal de remise à zéro asynchrone actif à l’état bas |
i_address | Sortie | 32 | Adresse pour la mémoire d’instruction |
i_data_read | Entrée | 32 | Donnée (instruction) lue depuis la mémoire d’instructions |
i_data_valid | Entrée | 1 | La donnée présentée sur i_data_read est valide |
d_address | Sortie | 32 | Adresse pour la mémoire de données |
d_data_read | Entrée | 32 | Donnée lue depuis la mémoire de données |
d_data_write | Sortie | 32 | Donnée à écrire dans la mémoire de données |
d_write_enable | Sortie | 1 | Signal d’écriture en mémoire de données |
d_data_valid | Entrée | 1 | Dans le cas d’une lecture (d_write_enable==0), indique que la donnée présentée sur d_data_read est valide. Dans le cas d’une écriture (d_write_enable==1), indique que la donnée a bien été écrite. |
Le fonctionnement des deux mémoires (données et instructions) est synchrone : l’adresse présentée à la mémoire est échantillonnée sur un front montant d’horloge et la donnée à cette adresse est présentée en sortie de la mémoire peu de temps après (temps de propagation).
On supposera dans un premier temps que les mémoires répondent toujours en un cycle et donc que les signaux i_data_valid
et d_data_valid
sont toujours à 1 et donc qu’il n’y a pas besoin d’en tenir compte.
Le langage assembleur est un langage, lisible par un être humain, qui a la particularité d’être très proche du code machine car il y a normalement une correspondance 1 à 1 entre une instruction assembleur et une instruction machine.
Voici un exemple de code assembleur que votre programme d’assemblage devra être capable de traiter. Par la suite, plus de fonctionnalités seront ajoutées.
# Petit programme calculant les 10 premiers termes de fibo
debut: ori x1,x0,1 # x1 = x0 | 1 (= 1)
or x2,x0,x1 # x2 = x0 | x1 (= x1 = 1)
ori x3,x0,10 # x3 = x0 | 10 (= 10)
boucle: add x4,x1,x2 # x4 = x1 + x2
or x1,x0,x2 # x1 = x2
or x2,x0,x4 # x2 = x4
addi x3,x3,-1 # x3 = x3 + (-1)
bne x3,x0,boucle # Si x3 != 0, on saute à boucle
fin: jal x0, fin # Boucle inifinie à la fin
0b
, elles sont exprimées en binaire et si elles commencent par 0x
, elles sont exprimées en hexadécimal.© Copyright 2021 Guillaume Duc. Le contenu de cette page est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International (à l'exception des exemples de code tirés du noyau Linux et qui sont distribués sous leurs licences d'origine).