Projet P2 - CPU
Ce projet est décomposé en 5 parties, dans l'ordre logique, arithmétique, ALU, mémoire et CPU. L'objectif final est de créer le schéma (en porte logique) d'un petit processeur mono-cœur mono-cycle. Chacune des parties fournies correspond à un fichier de test, il vous est demandé de compléter les fichiers pour que tous les tests passent. Notez qu'il n'y a pas de tests pour le CPU complet, ce sera à vous de le fournir. À chaque partie vous pouvez tester vos programmes avec dune runtest ; cela lance tous les tests, c'est donc normal que ça échoue sur ce que vous n'avez pas implémenté.
Partie Logique
Dans cette partie vous devez compléter le fichier logique.ml. Les 5 premiers opérateurs travaillent avec des fils simples, et les 5 suivants avec des bus de 16 fils (l'aspect petit ou grand boutisme n'a pas d'impact).
Partie Arithmétique
Dans
cette partie vous devez compléter le fichier arithmetique.ml. Les opérations à implémenter sont :
- un half-adder, qui est un circuit qui prend deux bits a et b en entrée et renvoie leur somme et leur retenue. Par convention on renverra la paire (retenue, somme) ;
- un full-adder qui est un circuit qui prend trois bits et renvoie leur somme et la retenue. Par convention on renverra la paire (retenue, somme) ;
- une somme de deux nombres de 16 bits, par convention les nombres de 16 bits sont stockés dans des tableaux de 16 fils avec le bit de poids faible dans la case 0 et le bit de poids fort dans la case 15. On ne gère pas le débordement ;
- increment a/decrement a qui calculent a+1 et a-1 ;
- difference qui calcule la différence ;
- est_nul/est_negatif/est_positif qui prend un nombre de 16 bits et renvoie un bit qui vaut 1 quand le nombre représenté est 0 (resp. est négatif, c'est à dire avec le bit de poids fort à 1, resp. est positif, c'est à dire avec le bit de poids fort à 0) ;
Partie ALU
Dans cette partie vous devez implémenter une ALU. Dans l'ALU on prend un nombre opcode de 3 bits décrivant l'opération à effecteur et deux nombres de 16 bits. Les opérations à effectuer sont selon opcode, x et y sont :
- opcode=0 : x+y
- opcode=1 : ~x (inverse logique de x)
- opcode=2 : x&y (et logique)
- opcode=3 : x+1
- opcode=4 : x-y
- opcode=5 : x xor y
- opcode=6 : x|y (ou logique)
- opcode=7 : x-1
Partie Mémoire
Dans mémoire il faut implémenter 5 opérations :
- bit_registre est une sorte de registre à bascule amélioré, il prend un bit pour savoir s'il faut écrire ou non et la valeur à écrire le cas échéant ; il renvoie la valeur stockée
- word_registre marche un peu comme bit_registre mais prend un bit (s'il faut écrire) et un nombre 16 bits (la valeur à écrire) et renvoie la valeur stockée
- memoire correspond à un tableau de mémoire, memoire doit prendre 6 arguments : 1/ la taille en nombre de bits des adresses (en effet nous allons faire des mémoires qui ont un nombre de cases plus petit que 2^16), 2/ un bit pour savoir s'il faut écrire, 3/ une première adresse dont on veut lire le contenu, 4/ une deuxième adresse dont on veut lire le contenu, 5/ une adresse à écrire (le cas écheant) et 6/ la valeur à écrire (le cas écheant). Memoire doit renvoyer deux nombres, les deux nombres lus. Vous devez coder memoire récursivement en distinguant les cas où la taille des adresses est 0 et le cas général.
- ROM correspond un peu à mémoire mais il s'agit d'une mémoire en lecture seule. Contratement à memoire qui est une sorte de RAM initialisée à 0, ROM doit avoir une valeur initiale car on ne peut pas la modifier ensuite, rom prend donc 3 arguments, 2 adresses qui sont des nombres 16 bits pour les adresses à lire et un tableau correspondant aux valeurs initiales.
- Enfin RAM_ROM est simplement une RAM et une ROM collée ; cela servira pour le CPU, attention ram_rom n'est pas testé par les tests de mémoire ! Faites vos propres tests !
Partie CPU
Vous devez compléter cpu.ml et run_cpu.ml. Dans cpu.ml vous décrirez le circuit de votre propre CPU, ce sera un CPU monocycle (c'est à dire qu'à chaque instant une instruction est exécutée) qui manipulera des nombres 16 bits. Le CPU travaillera avec une mémoire de 512 registres : 256 en ROM et 256 en RAM (avec la RAM dans les adresses 256..511 tandis que la ROM est aux adresses 0..255). Il y aura 16 registres généraux (qui peuvent être passés en argument de toutes les commandes plus 1 registre pc (correspondant au Program Counter c'est à dire à l'adresse de l'instruction en cours). Les instructions CPU seront codées de la façon suivante :
- 4 bits pour l'opcode
- 4 bits pour r1
- 4 bits pour r2
- 4 bits pour r3
Pour la plupart des instructions, r1 décrit l'adresse registre modifié, r2 et r3 décrivent les adresses des registres lus. Pour les opcode de 8 à 15 on utilisera l'ALU pour faire le calcul (avec opcode=opcode-8). Voilà la description des autres opcodes :
- les opcode 6 et 7 correspondent au chargement de constantes dans le registre décrit par r1. Pour l'opcode 6 on chargera les bits de poids faibles et pour 7 ceux de poids forts (les 8 bits chargés sont les 4 bits de r2 concaténés avec ceux de r3) ;
- l'opcode 5 effectue une lecture en RAM, on utilise la valeur du registre r2 pour obtenir une adresse et on met dans r1 la valeur pointée par la valeur de r2 ;
- pour l'opcode 4, on note v la valeur correspondant au nombre de 8 bits décrit par la concaténation de r2 et r3 et on stocke dans le registre r1 la valeur de pc+v (v est entendu comme un nombre relatif) ;
- pour l'opcode 3, on écrit la valeur du registre r3 dans la mémoire à l'adresse correspondant à la valeur r2 décalée d'un offset de r1 interprété comme nombre sur 4 bits (offset de -8 à 7) ;
- pour l'opcode 2 il s'agit d'un jump (branchement inconditionnel) relatif à la destionation pc+offset avec offset qui vaut le nombre relatif à 10 bits constitué des 2 derniers bits de r1 concaténés à r2 et à r3 ;
- pour l'opcode 1 il s'agit d'un branchement si la valeur du registre r2 est négative ; pour l'opcode 0 il s'agit d'un branchement si la valeur du registre r2 est nulle. Pour les opcodes 0 et 1 on doit aussi gérer :
- si r1[1] est à 1 on inverse la condition du branchement
- si r1[0] est à 0 le branchement est absolu et donc la destination est la valeur stockée dans r3
- si r1[0] est à 1 le branchement est relatif et donc la destinaton est la valeur du nombre relatif sur 6 bits constitué des deux derniers bits de r1 et de r3.
En extension on pourra faire en sorte que le registre 15 est systématiquement modifié par un jump (jump and link) et on pourra faire en sorte que le registre 0 vaille toujours 0.
La fonction CPU prend un argument program qui est un tableau d'instructions correspond au contenu de la ROM (donc au plus 256 registres). La fonction CPU doit construire le circuit correspond au CPU décrit ci-haut. Pour pouvoir visualiser ce que fait le CPU on renverra la valeur de pc, l'op code de l'instruction courante ainsi que les valeurs lues dans les registres r2 et r3.
Le fichier run_cpu.ml est là pour que vous simuliez le CPU que vous venez d'écrire. Vous pouvez utiliser les fonctions du fichier run_cpu.ml qui calculent les codes des instructions (si vous avez respecté les conventions décrites ci-haut).
Addendum :
Vous trouverez dans les fichiers attachés deux fichiers, template_cpu.ml et un fichier template_run_cpu.ml qui peuvent vous aider. Pour compiler run_cpu vous pouvez rajouter
(promote (until-clean))
dans la phrase de build de run_cpu (et donc il faut avoir
(executable
(name run_cpu)
(modules run_cpu)
(promote (until-clean))
(libraries circuit logique arithmetique alu memory cpu)
)
Au lieu de
(executable
(name run_cpu)
(modules run_cpu)
(libraries circuit logique arithmetique alu memory cpu)
)
dans le fichier dune
- 26 septembre 2023, 20:54
- 25 septembre 2023, 19:00
- 4 octobre 2023, 14:57
- 4 octobre 2023, 14:57
- 26 septembre 2023, 14:48