另一个简单的Verilog处理器

本文介绍了下一个原始处理器和汇编器。
代替通常的RISC /ISC,处理器没有这样的一组指令,只有一条复制指令。


类似的处理器具有Maxim MAXQ系列。


首先,我们来描述ROM,程序存储器


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用于数据存储


 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 

和处理器本身


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

至少,他需要命令计数器的寄存器以及一个辅助寄存器和IO端口寄存器,以便从我们的处理器中显示出一些东西。


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

命令计数器将是程序存储器的地址。


  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; 

宽度加倍的程序存储器包含两个地址:将数据复制到双端口数据存储器的位置和位置。


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

我们表示特殊的地址:命令计数器,常量生成器,检查0(用于条件跳转),加法/减法操作以及输入输出端口(在这种情况下,仅输出)。


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

两个存储器端口的数据总线不仅互连,还通过多路复用器互连,这些复用器将同时用作ALU。


一个多路复用器-位于读取端口的数据总线上,以便代替某些地址的存储器,而读取命令计数器(用于相对转换),IO等。


第二个在记录端口的数据总线上,以便不仅在内存中传输数据,而且在更改为某些地址以更改它们时也是如此。


  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; 

不能直接访问用于算术运算的辅助寄存器reg_reg,但是会将每条指令的结果复制到该寄存器。


因此,要从内存中添加两个值,您必须首先在任何地方读取其中一个值,例如将其复制到自己(同时在reg_reg中复制),并且加法器地址处的下一个写命令将在此处记录具有前一个值的总和。


常量生成器将地址而不是存储器值写入该地址。


对于无条件跳转,您只需要将所需的地址复制到reg_pc中,对于有条件跳转,我们保留另一个TST地址,该地址将任何非零值都变为1,并且如果结果不为0,则同时将命令计数器增加2而不是1来跳过下一个命令。


  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 

中央处理器
 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 

这就是整个处理器。


组装工


现在,我们为他编写一个简单的程序,该程序将值依次顺序输出到端口,并在5处停止。


自己编写汇编程序实在是太懒惰了,即使是这样简单的汇编程序(整个语法A = B),因此,使用现成的Lua语言作为基础,非常适合基于它来构建各种领域特定的语言,同时我们免费提供现成的Lua预处理程序。 。


首先,宣告特殊地址,更改数据的条目以及地址7处的计数器变量


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

可以使用通常的Lua函数来代替宏,尽管由于更改了环境元表_G以捕获分配(请参见下文),全局变量同时下降了:非局部变量some​​_variable = 0xAA的声明被我们的汇编程序视为自己的变量并尝试解析相反,要声明全局预处理程序变量,您将必须使用不涉及元方法的rawset(_G,some_variable,0xAA)。


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

标签将由单词label和字符串常量表示,在Lua中,如果使用单个字符串参数,则可以省略括号的功能。


 label "start" 

将端口计数器清零并注册:


 CG = 0 cnt = CG PORT = CG 

在循环中,加载常数1,将其添加到计数器变量中并在端口中显示:


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

将丢失的1加到溢出的0处,如果不为零,则从头开始,跳过CG =“ exit”,否则我们以无限循环“ 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" 

现在,汇编程序asm.lua本身应该是20行:


在mem函数(用于声明特殊地址)中,如果未将一个空闲地址指定为自变量,则还会添加下一个空闲地址的自动分配。
对于标签,您需要检查是否重新声明现有标签


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

Lua没有用于分配的元方法,但是存在用于索引现有值和添加新值的元方法,包括_G全局环境表。
因为__newindex仅适用于表中不存在的值,而不是向_G添加新元素,所以您需要将它们隐藏在不添加到_G的某个位置,因此,当通过__index访问它们时将其删除。


如果名称已经存在,则将该指令添加到其余的指令中。


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

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

通过运行lua53 test.lua> rom.txt( 或在线 ),我们获得了机器代码的处理器程序。


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

为了模拟,我们将制作一个简单的测试台,仅释放复位并拉动切丝。


测试版
 `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 

使用iverilog -o test.vvp test.v模拟后,在GTKWave中打开生成的test.vcd:

端口计数到五个,然后处理器循环。


现在,当有一个最低限度工作的处理器时,您可以根据需要添加其余的算术,逻辑运算,乘法,除法,浮点数,三角函数,用于间接存储器访问的寄存器,堆栈,硬件周期,各种外设...并开始锯切llvm的后端。

Source: https://habr.com/ru/post/zh-CN433342/


All Articles