![]() |
Projet PAF : Processeur RISC |
L’objectif de ce projet est de développer un processeur utilisant l’architecture DLX ainsi que son assembleur.
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 nommée DLX créée par John Hennessy et David Patterson à des fins d’enseignement, mais très proche de l’architecture MIPS utilisée par de nombreux processeurs embarqués.
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.
Le processeur DLX 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 du DLX sont elles-mêmes codées sur 32 bits.
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. Le processeur DLX contient 32 registres (numérotés de R0
à R31
) 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, parmi les 32 registres du processeur DLX, deux ont un comportement particulier :
R0
: 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éeR31
: Link Register (voir plus loin)Toutes les instructions sont codées sur 32 bits. Il existe trois codages différents pour les instructions (en fonction du type d’instruction).
Format | I[31:26] | I[25:21] | I[20:16] | I[15:11] | I[10:6] | I[5:0] |
---|---|---|---|---|---|---|
R | 0x0 | Rs1 | Rs2 | Rd | Inutilisé | Opcode |
I | Opcode | Rs1 | Rd | immediate | immediate | immediate |
J | Opcode | value | value | value | value | value |
Rd
, Rs1
et Rs2
sont des numéros de registres (sur 5 bits pour pouvoir adresser l’un des 32 registres du processeur) qui indiquent le registre où sera stocké le resultat de l’opération et les registres qui contiennent les opérandes (voir tableau ci-dessous).immediate
est une valeur sur 16 bits qui peut servir d’opérande dans certaine des instructions (voir tableau ci-dessous).value
est une valeur sur 26 bits et qui sert dans les instructions de saut.Voici le sous-ensemble des instructions du processeur DLX que vous allez implémenter.
Instruction | Description | Format | Opcode | Opération (expression à la C) |
---|---|---|---|---|
ADD | add | R | 0x20 | Rd = Rs1 + Rs2 |
ADDI | add immediate | I | 0x08 | Rd = Rs1 + sign_extend(immediate) |
AND | and | R | 0x24 | Rd = Rs1 & Rs2 |
ANDI | and immediate | I | 0x0c | Rd = Rs1 & extend(immediate) |
BEQZ | branch if equal to zero | I | 0x04 | PC += (Rs1 == 0 ? sign_extend(immediate) : 0) |
BNEZ | branch if not equal to zero | I | 0x05 | PC += (Rs1 != 0 ? sign_extend(immediate) : 0) |
J | jump | J | 0x02 | PC += sign_extend(value) |
JAL | jump and link | J | 0x03 | R31 = PC + 4 ; PC += sign_extend(value) |
JALR | jump and link register | I | 0x13 | R31 = PC + 4 ; PC = Rs1 |
JR | jump register | I | 0x12 | PC = Rs1 |
LHI | load high bits | I | 0x0f | Rd = extend(immediate) << 16 |
LW | load word | I | 0x23 | Rd = MEM[Rs1 + sign_extend(immediate)] |
OR | or | R | 0x25 | Rd = Rs1 | Rs2 |
ORI | or immediate | I | 0x0d | Rd = Rs1 | extend(immediate) |
SEQ | set if equal | R | 0x28 | Rd = (Rs1 == Rs2 ? 1 : 0) |
SEQI | set if equal to immediate | I | 0x18 | Rd = (Rs1 == sign_extend(immediate) ? 1 : 0) |
SLE | set if less than or equal | R | 0x2c | Rd = (Rs1 <= Rs2 ? 1 : 0) |
SLEI | set if less than or equal to immediate | I | 0x1c | Rd = (Rs1 <= sign_extend(immediate) ? 1 : 0) |
SLL | shift left logical | R | 0x04 | Rd = Rs1 << (Rs2 % 8) |
SLLI | shift left logical immediate | I | 0x14 | Rd = Rs1 << (extend(immediate) % 8) |
SLT | set if less than | R | 0x2a | Rd = (Rs1 < Rs2 ? 1 : 0) |
SLTI | set if less than immediate | I | 0x1a | Rd = (Rs1 < sign_extend(immediate) ? 1 : 0) |
SNE | set if not equal | R | 0x29 | Rd = (Rs1 != Rs2 ? 1 : 0) |
SNEI | set if not equal to immediate | I | 0x19 | Rd = (Rs1 != sign_extend(immediate) ? 1 : 0) |
SRA | shift right arithmetic | R | 0x07 | Rd = Rs1 >>> (Rs2 % 8) |
SRAI | shift right arithmetic immediate | I | 0x17 | Rd = Rs1 >>> (extend(immediate) % 8) |
SRL | shift right logical | R | 0x06 | Rd = Rs1 >> (Rs2 % 8) |
SRLI | shift right logical immediate | I | 0x16 | Rd = Rs1 >> (extend(immediate) % 8) |
SUB | subtract | R | 0x22 | Rd = Rs1 - Rs2 |
SUBI | subtract immediate | I | 0x0a | Rd = Rs1 - sign_extend(immediate) |
SW | store word | I | 0x2b | MEM[Rs1 + sign_extend(immediate)] = Rd |
XOR | exclusive or | R | 0x26 | Rd = Rs1 ^ Rs2 |
XORI | exclusive or immediate | I | 0x0e | Rd = Rs1 ^ extend(immediate) |
Quelques remarques sur ces instructions :
SW
. La valeur écrite en mémoire est contenue dans Rd
(qui n’est donc pas un registre de “destination”).MEM[x]
représente la donnée stockée à l’adresse x
dans la mémoire.PC
représente l’adresse de l’instruction en cours d’exécution. Comme les instructions font 32 bits, PC+4
représente l’instruction qui suit l’instruction qui est actuellement exécutée.L’architecture DLX est prévue pour être pipelinée. L’exécution d’une instruction est décomposée en 5 étapes :
IF
(Instruction Fetch) : On présente 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 DLX 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 $1,$0,1 # R1 = R0 | 1 (= 1)
OR $2,$0,$1 # R2 = R0 | R1 (= R1 = 1)
ORI $3,$0,10 # R3 = R0 | 10 (= 10)
boucle: ADD $4,$1,$2 # R4 = R1 + R2
OR $1,$0,$2 # R1 = R2
OR $2,$0,$4 # R2 = R4
ADDI $3,$3,-1 # R3 = R3 + (-1)
BNEZ $3,boucle # Si R3 != 0, on saute à boucle
fin: J fin # Boucle inifinie à la fin
$x
correspond au registre numéro x
($1
correspond au registre 1)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).