我正在尝试在verilog中制作一个简单的微处理器,以同时了解verilog和汇编。
我不确定我是否能很好地实现我对微处理器的想法,或者我完全错了。我应该简化微处理器的概念,还是在使用真实芯片制造微处理器时对其进行编程。例如,我应该定义一个名为address的变量,并执行一个大的case
语句,该语句采用汇编命令并处理内存和地址。到目前为止,我已经做了类似的事情。
case (CMD_op)
//NOP
4'b0000: nxt_addr = addr + 4'b0001 ;
//ADD
4'b0001: begin
op3_r = op1_r + op2_r;
nxt_addr = addr + 4'b0001;
end
CMD_op是一个4位输入,它引用我在上面添加的case语句内的一组预定义的16条命令,这只是前两种情况,我为每条命令以及它们如何篡改地址做了一个案例。我有一个16位x 16位的数组,应该包含主程序。每行的前4位引用汇编命令,后12位引用命令的参数。
例如,这里是无条件跳转命令JMP
//JMP
4'b0101: nxt_addr = op1_r ;
4'b0101
是命令的case语句中的一个case。
之所以问这个问题,是因为我觉得自己是在仿真微处理器而不是在制造微处理器。我觉得我只是在仿真特定的汇编命令对微处理器内部寄存器的作用。我没有公共汽车,但是如果我可以使用Verilog跳过其用途,公共汽车会做什么。
我感觉有些东西丢失了,谢谢。
答案 0 :(得分:2)
如评论中所详细描述的,似乎在如何处理内存/总线以及如何在模块之间实现事物方面存在一些一般性问题,这似乎主要是令人困惑的。虽然SO的设计不能很好地回答通用单周期处理器的设计/实现这些广泛的问题,但在这里,我将通过一个非常基础的步骤作为简要教程,以阐明作者的观点。
首先,必须知道指令集体系结构并指定每个指令的功能。 ISA中的内容包括指令本身,系统中的寄存器数量,中断和异常的处理方式等。通常,工程师将使用预先存在的指令集(x86,ARM,MIPS,Sparc,PowerPC,68000等),而不是从头开始设计新的指令集,但是出于学习目的,我会自行设计。在这里我将展示的情况下,只有4条基本指令:LD
(将数据从内存加载到寄存器中),ST
(将数据从内存存储到寄存器中),ADD
(将寄存器加在一起)和BRZ
(如果最后一个操作等于零,则分支)。将有4个通用寄存器和一个程序计数器。处理器将以16位(即16位字)完成所有操作。每条指令将按如下方式分解:
[15 OPCODE 14] | [13 SPECIFIC 0] -- Opcode is always in the top two bits, the rest of the instruction depends on the type it is
ADD: add rd, rs1, rs2 -- rd = rs1 + rs2; z = (rd == 0)
[15 2'b00 14] | [13 rd 12] | [11 rs1 10] | [9 rs2 8] | [7 RESERVED 0]
LD: ld rd, rs -- rd = MEM[ra]
[15 2'b01 14] | [13 rd 12] | [11 ra 10] | [9 RESERVED 1] | [0 1'b1 0]
ld rd, $addr -- rd = MEM[$addr]
[15 2'b01 14] | [13 rd 12] | [11 $addr 1] | [0 1'b0 0]
ST: st rs, ra -- MEM[ra] = rs
[15 2'b10 14] | [13 RESERVED 12] | [11 ra 10] | [9 rs 8] | [7 RESERVED 1] | [0 1'b1 0]
st rs, $addr -- MEM[$addr] = rs
[15 2'b10 14] | [13 $addr[10:7] 10] | [9 rs 8 ] | [7 $addr[6:0] 1] | [0 1'b0 0]
BRZ: brz ra -- if (z): pc = ra
[15 2'b11 14] | [13 RESERVED 12] | [11 ra 10] | [9 RESERVED 1] | [0 1'b1 0]
brz $addr -- if (z): pc = pc + $addr
[15 2'b11 14] | [13 RESERVED 12] | [11 $addr 1] | [0 1'b0 0]
请注意,由于使用不同的寻址存储器方式(LD
/ ST
都允许寄存器寻址和绝对寻址),许多指令具有不同的风格;这是大多数ISA中的常见功能,单个操作码可能具有附加位,用于指定有关参数的更多详细信息。
现在我们有了ISA,我们需要实现它。为此,我们将需要勾勒出系统的基本组成部分。从ISA中,我们知道该系统需要一个4x16位寄存器文件(r0
-r3
)并为pc
(程序计数器)注册一个简单的ALU(算术逻辑单元)。在我们的情况下,它只能加上零状态寄存器(Z
标志)和一束组合逻辑以将所有逻辑绑定在一起(用于解码指令,确定pc
的下一个值,等等)。通常,实际将其全部画出是最好的方法,并根据需要指定设计细节。这是我们的简单处理器的详细信息:
请注意,设计是前面讨论过的一堆构建基块。还包括处理器中的所有数据线,控制信号和状态信号。在编写代码之前仔细考虑所需的所有内容是一个好主意,因此您可以更轻松地将设计模块化(每个块可以是一个模块),并事先查看所有主要挑战。我想指出的是,在实施过程中我确实注意到了该图上的一些错误/疏漏(主要是缺少细节),但重要的是要注意,该图是此时所做内容的模板。
现在总体设计已经完成,我们需要实施它。由于事先已将其详细画出,因此可以一次构建一个模块。首先,让ALU实现起来非常简单:
module ALU(input clk, // Note we need a clock and reset for the Z register
input rst,
input [15:0] in1,
input [15:0] in2,
input op, // Adding more functions to the system means adding bits to this
output reg [15:0] out,
output reg zFlag);
reg zFlagNext;
// Z flag register
always @(posedge clk, posedge rst) begin
if (rst) begin
zFlag <= 1'b0;
end
else begin
zFlag <= zFlagNext;
end
end
// ALU Logic
always @(*) begin
// Defaults -- I do this to: 1) make sure there are no latches, 2) list all variables set by this block
out = 16'd0;
zFlagNext = zFlag; // Note, according to our ISA, the z flag only changes when an ADD is performed, otherwise it should retain its value
case (op)
// Note aluOp == 0 is not mapped to anything, it could be mapped to more operations later, but for now theres no logic needed behind it
// ADD
1: begin
out = in1 + in2;
zFlagNext = (out == 16'd0);
end
endcase
end
endmodule
解决您对行为Verilog的担忧;是的,您正在编写更高级别的代码,可能看起来像是仿真。但是,在进行Verilog时,您实际上是在实现硬件设计。因此,尽管您可能编写类似out = in1 + in2
的行,但要意识到您实际上是在设计中实例化加法器。
现在,让我们实现注册文件:
module registerFile(input clk,
input rst,
input [15:0] in, // Data for write back register
input [1:0] inSel, // Register number to write back to
input inEn, // Dont actually write back unless asserted
input [1:0] outSel1, // Register number for out1
input [1:0] outSel2, // Register number for out2
output [15:0] out1,
output [15:0] out2);
reg [15:0] regs[3:0];
// Actual register file storage
always @(posedge clk, posedge rst) begin
if (rst) begin
regs[3] <= 16'd0;
regs[2] <= 16'd0;
regs[1] <= 16'd0;
regs[0] <= 16'd0;
end
else begin
if (inEn) begin // Only write back when inEn is asserted, not all instructions write to the register file!
regs[inSel] <= in;
end
end
end
// Output registers
assign out1 = regs[outSel1];
assign out2 = regs[outSel2];
endmodule
了解如何将设计图中的每个大块都视为一个单独的模块,以帮助模块化代码(字面意义上!),从而将功能块分为系统的不同部分。还要注意,我尝试最小化always @(posedge clk)
块内的逻辑量。我这样做通常是理解什么是寄存器和什么是组合逻辑的一个好主意,因此将它们分离为代码可帮助您了解设计和其背后的硬件,并避免闩锁和综合工具可能对您造成的其他问题到达那个阶段时进行设计。否则,寄存器文件不会太令人惊讶,只是一个用于在指令运行后写回寄存器的“端口”(如LD
或ADD
)和两个用于提取寄存器“参数”的“端口” 。”
下一个是内存:
module memory(input clk,
input [15:0] iAddr, // These next two signals form the instruction port
output [15:0] iDataOut,
input [15:0] dAddr, // These next four signals form the data port
input dWE,
input [15:0] dDataIn,
output [15:0] dDataOut);
reg [15:0] memArray [1023:0]; // Notice that Im not filling in all of memory with the memory array, ie, addresses can only from $0000 to $03ff
initial begin
// Load in the program/initial memory state into the memory module
$readmemh("program.hex", memArray);
end
always @(posedge clk) begin
if (dWE) begin // When the WE line is asserted, write into memory at the given address
memArray[dAddr[9:0]] <= dDataIn; // Limit the range of the addresses
end
end
assign dDataOut = memArray[dAddr[9:0]];
assign iDataOut = memArray[iAddr[9:0]];
endmodule
这里要注意几件事。首先,我有点作弊,并允许组合内存读取(最后两个assign
语句),即,与大多数实际硬件(此设计)一样,内存阵列的地址和数据线上没有寄存器。在FPGA上可能会很昂贵)。重要的是要了解您的设计将被集成到哪种硬件中,以避免冗长的组合链或不切实际的记忆。还要注意,内存不会填满整个2 ^ 16可能的地址空间。在计算机系统中,拥有尽可能多的物理内存(通常不超出地址空间允许的数量)是不常见的。这将为外设和其他内存映射的IO打开那些内存地址。通常,这就是您所称的系统总线,内存,CPU和任何其他外围设备之间的互连。 CPU通过其指令读端口和数据读/写端口访问总线。在该系统中,用于存储指令和数据的内存是相同的,因此被称为冯·诺依曼架构。如果我将指令存储器与数据存储器(即两个独立的存储器模块)分开,那将是哈佛体系结构。
最后一个子模块,指令解码器:
module decoder(input [15:0] instruction,
input zFlag,
output reg [1:0] nextPCSel,
output reg regInSource,
output [1:0] regInSel,
output reg regInEn,
output [1:0] regOutSel1,
output [1:0] regOutSel2,
output reg aluOp,
output reg dWE,
output reg dAddrSel,
output reg [15:0] addr);
// Notice all instructions are designed in such a way that the instruction can be parsed to get the registers out, even if a given instruction does not use that register. The rest of the control signals will ensure nothing goes wrong
assign regInSel = instruction[13:12];
assign regOutSel1 = instruction[11:10];
assign regOutSel2 = instruction[9:8];
always @(*) begin
// Defaults
nextPCSel = 2'b0;
regInSource = 1'b0;
regInEn = 1'b0;
aluOp = 1'b0;
dAddrSel = 1'b0;
dWE = 1'b0;
addr = 16'd0;
// Decode the instruction and assert the relevant control signals
case (instruction[15:14])
// ADD
2'b00: begin
aluOp = 1'b1; // Make sure ALU is instructed to add
regInSource = 1'b0; // Source the write back register data from the ALU
regInEn = 1'b1; // Assert write back enabled
end
// LD
2'b01: begin
// LD has 2 versions, register addressing and absolute addressing, case on that here
case (instruction[0])
// Absolute
1'b0: begin
dAddrSel = 1'b0; // Choose to use addr as dAddr
dWE = 1'b0; // Read from memory
regInSource = 1'b1; // Source the write back register data from memory
regInEn = 1'b1; // Assert write back enabled
addr = {6'b0, instruction[11:1]}; // Zero fill addr to get full address
end
// Register
1'b1: begin
dAddrSel = 1'b1; // Choose to use value from register file as dAddr
dWE = 1'b0; // Read from memory
regInSource = 1'b1; // Source the write back register data from memory
regInEn = 1'b1; // Assert write back enabled
end
endcase
end
// ST
2'b10: begin
// ST has 2 versions, register addressing and absolute addressing, case on that here
case (instruction[0])
// Absolute
1'b0: begin
dAddrSel = 1'b0; // Choose to use addr as dAddr
dWE = 1'b1; // Write to memory
addr = {6'b0, instruction[13:10], instruction[7:1]}; // Zero fill addr to get full address
end
// Register
1'b1: begin
dAddrSel = 1'b1; // Choose to use value from register file as dAddr
dWE = 1'b1; // Write to memory
end
endcase
end
// BRZ
2'b11: begin
// Instruction does nothing if zFlag isnt set
if (zFlag) begin
// BRZ has 2 versions, register addressing and relative addressing, case on that here
case (instruction[0])
// Relative
1'b0: begin
nextPCSel = 2'b01; // Select to add the addr field to PC
addr = {{6{instruction[11]}}, instruction[11:1]}; // sign extend the addr field of the instruction
end
// Register
1'b1: begin
nextPCSel = 2'b1x; // Select to use register value
end
endcase
end
end
endcase
end
endmodule
在上面提供的设计中,每个模块都有许多控制信号(例如存储器dWE
用于使能数据端口上的存储器写入; regSelIn
用于选择寄存器文件中的寄存器以用于写入aluOp
以确定ALU应该执行的操作)和许多状态信号(在我们的设计中,仅zFlag
)。解码器的工作是将指令拆开,并根据指令要执行的操作来断言所需的控制信号,有时需要状态信号的帮助(例如BRZ
需要zFlag
的帮助)。有时,指令本身直接对这些信号进行编码(例如如何将regInSel
,regOutSel1
和regOutSel2
从指令字本身中拉出),但有时这些控制信号并不直接映射(像regInEn
并不真正映射到指令字中的任何单个位)。
在您的设计中,您似乎在解码器本身内部做了很多指令的实际工作,有时候还可以,但这通常会导致大量额外的硬件(即,类似的指令将不会共享硬件,例如增量指令和加法指令,通常不会以您的编码样式共享加法器,但在实际设计中应该如此)。将系统分为控制路径和数据路径,其中控制路径声明控制信号以指示数据路径如何处理数据,而数据路径执行实际工作并返回状态信号以指示重要内容。
最后一步是将所有内容整合在一起,并添加不能整齐地放入一个不错的盒子中的硬件部分(例如程序计数器,别忘了!):
module processor(input clk,
input rst);
wire [15:0] dAddr;
wire [15:0] dDataOut;
wire dWE;
wire dAddrSel;
wire [15:0] addr;
wire [15:0] regIn;
wire [1:0] regInSel;
wire regInEn;
wire regInSource;
wire [1:0] regOutSel1;
wire [1:0] regOutSel2;
wire [15:0] regOut1;
wire [15:0] regOut2;
wire aluOp;
wire zFlag;
wire [15:0] aluOut;
wire [1:0] nextPCSel;
reg [15:0] PC;
reg [15:0] nextPC;
wire [15:0] instruction;
// Instatiate all of our components
memory mem(.clk(clk),
.iAddr(PC), // The instruction port uses the PC as its address and outputs the current instruction, so connect these directly
.iDataOut(instruction),
.dAddr(dAddr),
.dWE(dWE),
.dDataIn(regOut2), // In all instructions, only source register 2 is ever written to memory, so make this connection direct
.dDataOut(dDataOut));
registerFile regFile(.clk(clk),
.rst(rst),
.in(regIn),
.inSel(regInSel),
.inEn(regInEn),
.outSel1(regOutSel1),
.outSel2(regOutSel2),
.out1(regOut1),
.out2(regOut2));
ALU alu(.clk(clk),
.rst(rst),
.in1(regOut1),
.in2(regOut2),
.op(aluOp),
.out(aluOut),
.zFlag(zFlag));
decoder decode(.instruction(instruction),
.zFlag(zFlag),
.nextPCSel(nextPCSel),
.regInSource(regInSource),
.regInSel(regInSel),
.regInEn(regInEn),
.regOutSel1(regOutSel1),
.regOutSel2(regOutSel2),
.aluOp(aluOp),
.dWE(dWE),
.dAddrSel(dAddrSel),
.addr(addr));
// PC Logic
always @(*) begin
nextPC = 16'd0;
case (nextPCSel)
// From register file
2'b1x: begin
nextPC = regOut1;
end
// From instruction relative
2'b01: begin
nextPC = PC + addr;
end
// Regular operation, increment
default: begin
nextPC = PC + 16'd1;
end
endcase
end
// PC Register
always @(posedge clk, posedge rst) begin
if (rst) begin
PC <= 16'd0;
end
else begin
PC <= nextPC;
end
end
// Extra logic
assign regIn = (regInSource) ? dDataOut : aluOut;
assign dAddr = (dAddrSel) ? regOut1 : addr;
endmodule
看到我的处理器现在只是一堆模块实例,还有一些额外的寄存器和复用器将它们链接在一起。但是,这些确实会给我们的设计增加一些额外的控制信号,因此请确保将其视为整个系统设计的一部分。然而,返回并向解码器添加这些新信号并不是一个大细节,但是您可能已经意识到此时您需要它们了!要注意的另一件事是,在处理器本身中包括内存并不常见。如前所述,内存与CPU分开,并且这两个通常在处理器本身外部连接在一起(因此,应在处理器模块外部完成);但这是一个快速而简单的介绍,因此我将其全部放在此处,以避免必须使用另一个包含处理器和内存的模块并将它们连接在一起。
希望这个实际示例向您展示了所有步骤,所有主要组件以及如何实现它们。请注意,我没有完全验证该设计,因此有可能在代码中犯了一些错误(尽管我确实进行了一些测试,所以应该可以:))。同样,这种事情对SO而言不是最好的,您应该提出一些具体问题,因为通常会很快关闭广泛的主题问题。还要注意,这是一个简短而超级简单的介绍,您可以在网上找到更多信息,并且比这有更多的计算机体系结构深度。流水线,中断/异常,缓存都是下一个主题。而且,这种架构甚至没有任何类型的内存停顿,没有多字获取指令的好处,甚至在最小的处理器中也有很多常见的东西。