Otro procesador simple de Verilog

El artículo describe el siguiente procesador y ensamblador primitivo para él.
En lugar del RISC / ISC habitual, el procesador no tiene un conjunto de instrucciones como tal, solo hay una instrucción de copia.


Procesadores similares tienen la serie Maxim MAXQ .


Primero, describamos ROM, memoria de programa


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 de doble puerto para memoria de datos


 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 

y el procesador en sí


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

Como mínimo, necesita el registro del contador de comandos, así como un registro auxiliar, bueno, y el registro del puerto IO, para que haya algo que mostrar de nuestro procesador.


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

El contador de comandos será la dirección de la memoria del programa.


  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 memoria del programa de ancho doble contiene dos direcciones: dónde y desde dónde copiar los datos a la memoria de datos de doble puerto.


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

Denotamos direcciones especiales: un contador de comandos, un generador constante, una verificación de 0 (para saltos condicionales), operaciones de suma / resta, y un puerto de entrada-salida, en este caso, solo salida.


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

Los buses de datos de los dos puertos de memoria no solo están interconectados, sino a través de multiplexores, que al mismo tiempo servirán como ALU.


Un multiplexor: en el bus de datos del puerto de lectura para que, en lugar de la memoria en ciertas direcciones, lea el contador de comandos (para transiciones relativas), IO, etc.


El segundo está en el bus de datos del puerto de grabación, para no solo transferir datos en la memoria, sino también al cambiar a ciertas direcciones para cambiarlos.


  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; 

El registro auxiliar reg_reg, que se utiliza para operaciones aritméticas, no es accesible directamente, pero el resultado de cada instrucción se copia en él.


Por lo tanto, para agregar dos valores de la memoria, primero debe leer uno de ellos en cualquier lugar, por ejemplo, cópielo usted mismo (y al mismo tiempo en reg_reg), y el siguiente comando de escritura en la dirección del sumador registrará la suma con el valor anterior allí.


El generador constante escribe la dirección, no el valor de la memoria, en esta dirección.


Para los saltos incondicionales, solo necesita copiar la dirección deseada en reg_pc, y para los saltos condicionales reservamos otra dirección TST, que convierte cualquier valor distinto de cero en 1, y al mismo tiempo aumenta el contador de comandos en 2 en lugar de 1 para omitir el siguiente comando si el resultado no es 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 

Ese es todo el procesador.


Ensamblador


Ahora escribiremos para él un programa simple que simplemente envíe los valores al puerto secuencialmente y se detenga en 5.


Era demasiado flojo escribir el ensamblador usted mismo, incluso uno tan simple (toda la sintaxis A = B), por lo que, en cambio, se utilizó el lenguaje Lua terminado como base, que es muy adecuado para construir varios lenguajes específicos de dominio basados ​​en él, al mismo tiempo que obtenemos un preprocesador Lua listo para usar de forma gratuita .


Primero, el anuncio de direcciones especiales, la entrada en la que cambia los datos y la variable de contador en la dirección 7


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

En lugar de macros, puede usar las funciones habituales de Lua, aunque debido al hecho de que el entorno metatable _G se cambió para capturar asignaciones (ver a continuación), las variables globales se cayeron al mismo tiempo: la declaración de la variable no local some_variable = 0xAA nuestro ensamblador considera que es la suya y trata de analizarla en su lugar, para declarar una variable de preprocesador global, deberá usar el conjunto de elementos sin formato (_G, some_variable, 0xAA), que no toca los metamétodos.


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

Las etiquetas se denotarán por la palabra etiqueta y las constantes de cadena; en Lua, en el caso de un argumento de cadena única para la función, se pueden omitir los corchetes.


 label "start" 

Ponga a cero el contador de puertos y registre:


 CG = 0 cnt = CG PORT = CG 

En el bucle, cargue la constante 1, agréguela a la variable del contador y muéstrela en el puerto:


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

Agregue el que falta al desbordamiento en 0 y, si no hay cero, vaya al principio, omitiendo CG = "salida", de lo contrario terminaremos en un bucle infinito "salida".


 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" 

Y ahora el ensamblador asm.lua en sí, como debería estar en 20 líneas:


En la función mem (para declarar direcciones especiales), también se agregaría la asignación automática de la siguiente dirección libre, si no se especifica como argumento.
Y para las etiquetas, deberá verificar la re-declaración de una etiqueta existente


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

Lua no tiene un metamétodo para la asignación, pero existen metametodos para indexar valores existentes y para agregar valores nuevos, incluida la tabla de entorno global _G.
Dado que __newindex funciona solo para valores que no existen en la tabla, en lugar de agregar nuevos elementos a _G, debe ocultarlos en algún lugar sin agregarlos a _G y, en consecuencia, sacarlos cuando se accede a ellos a través de __index.


Si el nombre ya existe, agregue esta instrucción al resto.


 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 }) 

Bueno, después de ejecutar el programa ensamblador, cuando el recolector de basura finalmente obtiene una matriz con nuestro programa de salida, simplemente lo imprimimos, al mismo tiempo que reemplazamos las etiquetas de texto con las direcciones correctas.


 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 }) 

Al ejecutar lua53 test.lua> rom.txt ( o en línea ) obtenemos un programa para el procesador en códigos de máquina.


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

Para simular, haremos un banco de pruebas simple que solo libera el reinicio y tira de los fragmentos.


prueba.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 

Después de simular usando iverilog -o test.vvp test.v, abra el test.vcd resultante en GTKWave:

el puerto cuenta hasta cinco y luego el procesador realiza un bucle.


Ahora que hay un procesador que funciona mínimamente, puede agregar el resto de operaciones aritméticas, lógicas, multiplicación, división, coma flotante, trigonometría, registros para acceso indirecto a la memoria, pilas, ciclos de hardware, varios periféricos, según sea necesario ... y comenzar a cortar backend para llvm.

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


All Articles