O artigo descreve o próximo processador e montador primitivo para ele.
Em vez do RISC / ISC usual, o processador não possui um conjunto de instruções, como tal, existe apenas uma instrução de cópia.
Processadores similares possuem a série Maxim MAXQ .
Primeiro, vamos descrever a ROM, a memória do 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 porta dupla para memória de dados
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
e o próprio processador
module cpu(clk, reset, port); parameter WIDTH = 8; parameter RAM_SIZE = WIDTH; parameter ROM_SIZE = WIDTH; input clk, reset; output [WIDTH-1 : 0] port;
No mínimo, ele precisa do registro do contador de comandos, bem como de um registro auxiliar e do registro da porta IO, para que haja algo a ser mostrado em nosso processador.
reg [WIDTH-1 : 0] reg_pc; reg [WIDTH-1 : 0] reg_reg; reg [WIDTH-1 : 0] reg_port; assign port = reg_port;
O contador de comandos será o endereço da memória do 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;
A memória do programa de largura duplicada contém dois endereços: onde e de onde copiar os dados para a memória de dados de porta dupla.
ram1r1w ram (clk, addr_w, data_w, addr_r, data_r); defparam ram.ADDR_WIDTH = RAM_SIZE; defparam ram.DATA_WIDTH = WIDTH;
Denotamos endereços especiais: um contador de comandos, um gerador constante, uma verificação de 0 (para saltos condicionais), operações de adição / subtração e uma porta de entrada e saída, neste caso, apenas saída.
parameter PC = 0; parameter CG = 1; parameter TST = 2; parameter ADD = 3; parameter SUB = 4; parameter PORT = 5;
Os barramentos de dados das duas portas de memória não são apenas interconectados, mas também através de multiplexadores, que servirão ao mesmo tempo como a ALU.
Um multiplexador - no barramento de dados da porta de leitura, para que, em vez da memória em determinados endereços, leia o contador de comandos (para transições relativas), E / S, etc.
O segundo está no barramento de dados da porta de gravação, para não apenas transferir dados na memória, mas também ao mudar para determinados endereços para alterá-los.
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;
O registro auxiliar reg_reg, usado para operações aritméticas, não é diretamente acessível, mas o resultado de cada instrução é copiado para ele.
Portanto, para adicionar dois valores da memória, você deve primeiro ler um deles em qualquer lugar, por exemplo, copiá-lo para si mesmo (e ao mesmo tempo em reg_reg), e o próximo comando de gravação no endereço do somador registrará a soma com o valor anterior.
O gerador constante grava o endereço, não o valor da memória, nesse endereço.
Para saltos incondicionais, basta copiar o endereço que você deseja reg_pc e, para saltos condicionais, reservar outro endereço TST, que transforma qualquer valor diferente de zero em 1 e, ao mesmo tempo, incrementar o contador de comandos em 2 em vez de 1 para pular o próximo comando, se o resultado não for 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
Esse é o processador inteiro.
Montador
Agora vamos escrever para ele um programa simples que simplesmente gera os valores para a porta sequencialmente e para em 5.
Era muito preguiçoso escrever você mesmo um assembler, mesmo que simples (toda a sintaxe A = B); portanto, a linguagem Lua pronta foi usada como base, o que é muito adequado para a criação de vários idiomas específicos de domínio com base nela, ao mesmo tempo em que obtemos um pré-processador Lua pronto de graça .
Primeiro, o anúncio de endereços especiais, a entrada na qual altera os dados e a variável do contador no endereço 7
require ("asm") PC = mem(0) CG = mem(1) TST = mem(2) ADD = mem(3) SUB = mem(4) PORT = mem(5) cnt = mem(7)
Em vez de macros, você pode usar as funções usuais Lua, embora, devido ao fato de a meta-tabela de ambiente _G ter sido alterada para capturar atribuições (veja abaixo), as variáveis globais tenham caído ao mesmo tempo: a declaração da variável não-local some_variable = 0xAA que nosso montador considera como seu e tenta analisar em vez disso, para declarar uma variável global de pré-processador, você precisará usar o rawset (_G, alguma_variável, 0xAA), que não toca nos metamétodos.
function jmp(l) CG = l PC = CG end
Os rótulos serão indicados pela palavra label e constantes da string; em Lua, no caso de um argumento de string único para a função, os colchetes podem ser omitidos.
label "start"
Zere o contador da porta e registre:
CG = 0 cnt = CG PORT = CG
No loop, carregue a constante 1, adicione-a à variável do contador e mostre-a na porta:
label "loop" CG = 1 ADD = cnt
Adicione o que está faltando ao overflow em 0 e, se não houver zero, vá para o início, pulando CG = "exit", caso contrário, terminamos em um loop infinito "exit".
CG = -5 ADD = ADD
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
E agora o assembler asm.lua em si, como deveria estar em 20 linhas:
Na função mem (para declarar endereços especiais), também se adiciona a atribuição automática do próximo endereço livre, se um não for especificado como argumento.
E para tags, você precisaria verificar se re-declara uma tag existente
local output = {} local labels = {} function mem(addr) return addr end function label(name) labels[name] = #output end
Lua não possui um método para atribuição, mas existem métodos para indexar valores existentes e adicionar novos, incluindo a tabela de ambiente global _G.
Como __newindex funciona apenas para valores que não existem na tabela, em vez de adicionar novos elementos a _G, você precisa ocultá-los em algum lugar sem adicionar a _G e, portanto, retirá-los quando acessados via __index.
Se o nome já existir, adicione esta instrução ao restante.
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 })
Bem, depois de executar o programa assembler, quando o coletor de lixo finalmente chega a um array com o nosso programa de saída, apenas o imprimimos, ao mesmo tempo substituindo os rótulos de texto pelos endereços corretos.
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))
Ao executar lua53 test.lua> rom.txt ( ou online ), obtemos um programa para o processador em códigos de máquina.
rom.txt 0100 0701 0501 0101 0307 0703 0503 01FB 0303 0103 0203 010D 0001 010D 0001
Para simular, faremos um teste simples que apenas libera a redefinição e puxa os fragmentos.
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
Tendo simulado usando iverilog -o test.vvp test.v, abra o test.vcd resultante no GTKWave:

a porta conta até cinco e, em seguida, o processador faz um loop.
Agora que existe um processador que funciona minimamente, você pode adicionar o restante de operações aritméticas e lógicas, multiplicação, divisão, ponto flutuante, trigonometria, registros para acesso indireto à memória, pilhas, ciclos de hardware, vários periféricos, conforme necessário, ... e começar a serrar back-end para llvm.