Un autre processeur Verilog simple

L'article décrit le prochain processeur et assembleur primitif pour celui-ci.
Au lieu du RISC / CISC habituel, le processeur n'a pas un ensemble d'instructions en soi, il n'y a qu'une seule instruction de copie.


Des processeurs similaires ont la série Maxim MAXQ .


Décrivons d'abord la ROM, la mémoire du programme


module rom1r(addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input [ADDR_WIDTH - 1 : 0] addr_r; output [DATA_WIDTH - 1 : 0] data_r; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; initial $readmemh("rom.txt", mem, 0, (1<<ADDR_WIDTH) - 1); assign data_r = mem[addr_r]; endmodule 

RAM double port pour mémoire de données


 module ram1r1w(clk_wr, addr_w, data_w, addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input clk_wr; input [ADDR_WIDTH - 1 : 0] addr_r, addr_w; output [DATA_WIDTH - 1 : 0] data_r; input [DATA_WIDTH - 1 : 0] data_w; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; assign data_r = mem[addr_r]; always @ (posedge clk_wr) mem[addr_w] <= data_w; endmodule 

et le processeur lui-même


 module cpu(clk, reset, port); parameter WIDTH = 8; parameter RAM_SIZE = WIDTH; parameter ROM_SIZE = WIDTH; input clk, reset; output [WIDTH-1 : 0] port; 

Au minimum, il a besoin du registre du compteur de commandes, ainsi que d'un registre auxiliaire, et du registre de port IO, pour qu'il y ait quelque chose à montrer de notre processeur.


  reg [WIDTH-1 : 0] reg_pc; reg [WIDTH-1 : 0] reg_reg; reg [WIDTH-1 : 0] reg_port; assign port = reg_port; 

Le compteur de commandes sera l'adresse de la mémoire du programme.


  wire [WIDTH-1 : 0] addr_w, addr_r, data_r, data_w, data; rom1r rom (reg_pc, {addr_w, addr_r}); defparam rom.ADDR_WIDTH = ROM_SIZE; defparam rom.DATA_WIDTH = RAM_SIZE * 2; 

La mémoire de programme de largeur double contient deux adresses: où et d'où copier les données dans la mémoire de données à double port.


  ram1r1w ram (clk, addr_w, data_w, addr_r, data_r); defparam ram.ADDR_WIDTH = RAM_SIZE; defparam ram.DATA_WIDTH = WIDTH; 

Nous désignons des adresses spéciales: un compteur de commandes, un générateur de constantes, une vérification de 0 (pour les sauts conditionnels), des opérations d'addition / soustraction et un port d'entrée-sortie, dans ce cas, uniquement une sortie.


  parameter PC = 0; parameter CG = 1; parameter TST = 2; parameter ADD = 3; parameter SUB = 4; parameter PORT = 5; 

Les bus de données des deux ports mémoire ne sont pas seulement interconnectés, mais via des multiplexeurs, qui serviront en même temps d'ALU.


Un multiplexeur - sur le bus de données du port de lecture, afin de lire le compteur de commandes (pour les transitions relatives), IO, etc. au lieu de la mémoire à des adresses spécifiques


Le second se trouve sur le bus de données du port d'enregistrement, de manière non seulement à transférer des données en mémoire, mais aussi lors du passage à certaines adresses pour les changer.


  assign data = (addr_r == PC) ? reg_pc : (addr_r == PORT) ? reg_port : data_r; assign data_w = (addr_w == CG) ? addr_r : (addr_w == TST) ? |data : (addr_w == ADD) ? data + reg_reg : (addr_w == SUB) ? data - reg_reg : data; 

Le registre auxiliaire reg_reg, qui est utilisé pour les opérations arithmétiques, n'est pas directement accessible, mais le résultat de chaque instruction y est copié.


Ainsi, pour ajouter deux valeurs de la mémoire, vous devez d'abord lire l'une d'entre elles n'importe où, par exemple, la copier sur vous-même (et en même temps dans reg_reg), et la prochaine commande d'écriture à l'adresse de l'additionneur y inscrira la somme avec la valeur précédente.


Le générateur de constante écrit l'adresse, et non la valeur de la mémoire, dans cette adresse.


Pour les sauts inconditionnels, il vous suffit de copier l'adresse que vous souhaitez reg_pc, et pour les sauts conditionnels, réservez une autre adresse TST, qui transforme toute valeur non nulle en 1, et en même temps incrémentez le compteur de commandes de 2 au lieu de 1 pour ignorer la commande suivante si le résultat n'est pas 0.


  always @ (posedge clk) begin if (reset) begin reg_pc <= 0; end else begin reg_reg <= data_w; if (addr_w == PC) begin reg_pc <= data_w; end else begin reg_pc <= reg_pc + (((addr_w == TST) && data_w[0]) ? 2 : 1); case (addr_w) PORT: reg_port <= data_w; endcase end end end endmodule 

cpu.v
 module rom1r(addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input [ADDR_WIDTH - 1 : 0] addr_r; output [DATA_WIDTH - 1 : 0] data_r; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; initial $readmemh("rom.txt", mem, 0, (1<<ADDR_WIDTH) - 1); assign data_r = mem[addr_r]; endmodule module ram1r1w(write, addr_w, data_w, addr_r, data_r); parameter ADDR_WIDTH = 8; parameter DATA_WIDTH = 8; input write; input [ADDR_WIDTH - 1 : 0] addr_r, addr_w; output [DATA_WIDTH - 1 : 0] data_r; input [DATA_WIDTH - 1 : 0] data_w; reg [DATA_WIDTH - 1 : 0] mem [0 : (1<<ADDR_WIDTH) - 1]; assign data_r = mem[addr_r]; always @ (posedge write) mem[addr_w] <= data_w; endmodule module cpu(clk, reset, port); parameter WIDTH = 8; parameter RAM_SIZE = 8; parameter ROM_SIZE = 8; parameter PC = 0; parameter CG = 1; parameter TST = 2; parameter ADD = 3; parameter SUB = 4; parameter PORT = 5; input clk, reset; output [WIDTH-1 : 0] port; wire [WIDTH-1 : 0] addr_r, addr_w, data_r, data_w, data; reg [WIDTH-1 : 0] reg_pc; reg [WIDTH-1 : 0] reg_reg; reg [WIDTH-1 : 0] reg_port; assign port = reg_port; rom1r rom(reg_pc, {addr_w, addr_r}); defparam rom.ADDR_WIDTH = ROM_SIZE; defparam rom.DATA_WIDTH = RAM_SIZE * 2; ram1r1w ram (clk, addr_w, data_w, addr_r, data_r); defparam ram.ADDR_WIDTH = RAM_SIZE; defparam ram.DATA_WIDTH = WIDTH; assign data = (addr_r == PC) ? reg_pc : (addr_r == PORT) ? reg_port : data_r; assign data_w = (addr_w == CG) ? addr_r : (addr_w == TST) ? |data : (addr_w == ADD) ? data + reg_reg : (addr_w == SUB) ? data - reg_reg : data; always @ (posedge clk) begin if (reset) begin reg_pc <= 0; end else begin reg_reg <= data_w; if (addr_w == PC) begin reg_pc <= data_w; end else begin reg_pc <= reg_pc + (((addr_w == TST) && data_w[0]) ? 2 : 1); case (addr_w) PORT: reg_port <= data_w; endcase end end end endmodule 

C'est tout le processeur.


Assembleur


Maintenant, nous allons lui écrire un programme simple qui sort simplement les valeurs dans le port séquentiellement et s'arrête à 5.


C'était trop paresseux pour écrire l'assembleur vous-même, même si simple (la syntaxe entière est A = B), donc nous avons plutôt pris le langage Lua prêt à l'emploi comme base, ce qui est très bien adapté pour construire divers langages spécifiques au domaine basés sur lui, en même temps nous obtenons gratuitement un préprocesseur Lua prêt à l'emploi .


Tout d'abord, l'annonce d'adresses spéciales, l'entrée dans laquelle modifie les données et la variable de compteur à l'adresse 7


 require ("asm") PC = mem(0) CG = mem(1) TST = mem(2) ADD = mem(3) SUB = mem(4) PORT = mem(5) cnt = mem(7) 

Au lieu de macros, vous pouvez utiliser les fonctions Lua habituelles, bien que du fait que l'environnement métatable _G ait été modifié pour attraper les affectations (voir ci-dessous), les variables globales sont tombées en même temps: la déclaration de la variable non locale some_variable = 0xAA que notre assembleur considère comme la sienne et tente d'analyser à la place, pour déclarer une variable globale de préprocesseur, vous devrez utiliser rawset (_G, some_variable, 0xAA), qui ne touche pas les métaméthodes.


 function jmp(l) CG = l PC = CG end 

Les étiquettes seront désignées par le mot label et les constantes de chaîne; dans Lua, dans le cas d'un seul argument de chaîne pour la fonction, les crochets peuvent être omis.


 label "start" 

Remettez le compteur de ports à zéro et enregistrez-vous:


 CG = 0 cnt = CG PORT = CG 

Dans la boucle, chargez la constante 1, ajoutez-la à la variable compteur et affichez-la dans le port:


 label "loop" CG = 1 ADD = cnt -- add = cnt + 1 cnt = ADD PORT = ADD 

Ajoutez celui qui manque au débordement à 0 et, s'il n'y a pas zéro, retournez au début en sautant CG = "exit", sinon on termine dans une boucle infinie "exit".


 CG = -5 ADD = ADD --add = add + 251 CG = "loop" TST = ADD --skip "exit" if not 0 CG = "exit" PC = CG label "exit" jmp "exit" 

test.lua
 require ("asm") PC = mem(0) CG = mem(1) TST = mem(2) ADD = mem(3) SUB = mem(4) PORT = mem(5) cnt = mem(7) function jmp(l) CG = l PC = CG end label "start" CG = 0 cnt = CG PORT = CG label "loop" CG = 1 ADD = cnt -- add = cnt + 1 cnt = ADD PORT = ADD CG = -5 ADD = ADD --add = add + 256 - 5 CG = "loop" TST = ADD --skip "exit" if not 0 CG = "exit" PC = CG label "exit" jmp "exit" 

Et maintenant l'assembleur asm.lua lui-même, comme il se doit en 20 lignes:


Dans la fonction mem (pour déclarer des adresses spéciales), on ajouterait également l'affectation automatique de la prochaine adresse libre, si on n'est pas spécifié comme argument.
Et pour les balises, vous devrez vérifier la nouvelle déclaration d'une balise existante


 local output = {} local labels = {} function mem(addr) return addr end function label(name) labels[name] = #output end 

Lua n'a pas de métaméthode d'affectation, mais il existe des métaméthodes pour indexer les valeurs existantes et pour en ajouter de nouvelles, y compris la table d'environnement global _G.
Étant donné que __newindex ne fonctionne que pour les valeurs qui n'existent pas dans la table, au lieu d'ajouter de nouveaux éléments à _G, vous devez les masquer quelque part sans ajouter à _G et, par conséquent, les extraire lorsqu'ils sont accessibles via __index.


Si le nom existe déjà, ajoutez cette instruction au reste.


 local g = {} setmetatable(_G, { __index = function(t, k, v) return g[k] end, __newindex = function(t, k, v) if g[k] then table.insert(output, {g[k], v}) else g[k]=v end end }) 

Eh bien, après avoir exécuté le programme assembleur, lorsque le garbage collector arrive enfin pour le tableau avec notre programme de sortie, nous l'imprimons, tout en remplaçant les étiquettes de texte par les adresses correctes.


 setmetatable(output, { __gc = function(o) for i,v in ipairs(o) do if type(v[2]) == "string" then v[2] = labels[v[2]] or print("error: ", v[2]) end print(string.format("%02X%02X", v[1] & 0xFF, v[2] & 0xFF)) end end }) 

asm.lua
 local output = {} local labels = {} function mem(addr) return addr end function label(name) labels[name] = #output end local g = {} setmetatable(_G, { __index = function(t, k, v) return g[k] end, __newindex = function(t, k, v) if g[k] then table.insert(output, {g[k], v}) else g[k]=v end end }) setmetatable(output, { __gc = function(o) for i,v in ipairs(o) do if type(v[2]) == "string" then v[2] = labels[v[2]] or print("error: ", v[2]) end print(string.format("%02X%02X", v[1] & 0xFF, v[2] & 0xFF)) --FIX for WIDTH > 8 end end }) 

En exécutant lua53 test.lua> rom.txt ( ou en ligne ), nous obtenons un programme pour le processeur dans les codes machine.


rom.txt
 0100 0701 0501 0101 0307 0703 0503 01FB 0303 0103 0203 010D 0001 010D 0001 

Pour simuler, nous allons créer un banc de test simple qui ne relâche que la réinitialisation et tire les blocs.


test.v
 `include "cpu.v" module test(); reg clk; reg reset; wire [7:0] port; cpu c(clk, reset, port); initial begin $dumpfile("test.vcd"); reset <= 1; clk <= 0; #4 reset <= 0; #150 $finish; end always #1 clk <= !clk; endmodule 

Après avoir simulé en utilisant iverilog -o test.vvp test.v, ouvrez le test.vcd résultant dans GTKWave:

le port compte jusqu'à cinq, puis le processeur boucle.


Maintenant qu'il existe un processeur fonctionnant au minimum, vous pouvez ajouter le reste des opérations arithmétiques, logiques, la multiplication, la division, la virgule flottante, la trigonométrie, les registres d'accès indirect à la mémoire, les piles, les cycles matériels, divers périphériques, selon les besoins, ... et commencer à scier backend pour llvm.

Source: https://habr.com/ru/post/fr433342/


All Articles