Assembly 用verilog实现简单微处理器

Assembly 用verilog实现简单微处理器,assembly,verilog,microprocessors,Assembly,Verilog,Microprocessors,我试图在verilog中制作一个简单的微处理器,以同时理解verilog和汇编 我不确定我对微处理器的理解是否实现得足够好,或者我是否完全错了。我是应该简化微处理器的概念,还是应该像使用真正的芯片制作微处理器那样对其进行编程。例如,我应该定义一个名为address的变量,并生成一个大的case语句,它接受汇编命令并处理内存和地址。到目前为止,我已经做了类似的事情 case (CMD_op) //NOP 4'b0000: nxt_addr = addr + 4'b0001 ;

我试图在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
是commands'case语句中的一个case

我之所以问这个问题,是因为我觉得我是在模拟一个微处理器,而不是在制作一个,我觉得我只是在模拟一个特定的汇编命令对微处理器内部寄存器的作用。我没有总线,但是如果我可以使用Verilog跳过它的使用,总线会做什么呢


我觉得缺少了一些东西,谢谢。

如评论中所述,主要是关于如何处理内存/总线的问题,以及关于如何跨模块实现的一些一般性问题。虽然SO不能很好地回答这些关于通用单周期处理器设计/实现的广泛问题,但我将在这里介绍一个非常基本的步骤,作为一个简短的教程,以澄清作者的一些观点

步骤1:ISA 首先,必须了解指令集体系结构,并指定每条指令的功能。ISA中的内容包括指令本身、系统中寄存器的数量、中断和异常的处理方式等。通常,工程师们会使用一个已有的指令集(x86、ARM、MIPS、Sparc、PowerPC、m68k等),而不是从头开始设计一个新的指令集,但出于学习的目的,我会设计自己的指令集。在这里我将展示的例子中,只有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中的一个常见功能,单个操作码可能具有额外的位,用于指定有关参数的更多详细信息

步骤2:设计 现在我们有了ISA,我们需要实现它。要做到这一点,我们需要勾勒出系统的基本构建块。从ISA中,我们知道该系统需要一个4x16位寄存器文件(
r0
-
r3
)和
pc
(程序计数器)寄存器,一个简单的ALU(算术逻辑单元,在我们的例子中,它只能添加),带有零状态寄存器(
Z
标志)和一组组合逻辑,以便将它们连接在一起(用于解码指令,确定下一个
pc
值等)。通常情况下,将其全部绘制出来是最好的方法,使其尽可能详细,以指定设计。以下是针对我们的简单处理器所做的一些详细说明:

请注意,设计是前面讨论过的一组构建块。还包括处理器中的所有数据线、控制信号和状态信号。在编写代码之前仔细考虑所有需要的内容是一个好主意,这样您可以更轻松地将设计模块化(每个块都可以是一个模块)我想指出的是,在执行过程中,我确实注意到了这个图上的一些错误/疏忽(主要是缺少细节),但重要的是要注意,这个图是一个模板,用于描述此时正在做的事情

步骤3:实施 现在,总体设计已经完成,我们需要实现它。由于之前已经详细地绘制了它,这就可以归结为一次构建一个模块的设计。首先,让我们实现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
看看我们如何将设计图中的每个大模块作为一个单独的模块来帮助模块化代码(字面意思!),从而将功能块分为系统的不同部分。还要注意,我试图尽量减少
always@(posedge clk)中的逻辑量
blocks。我这样做是因为理解什么是寄存器和什么是组合逻辑通常是一个好主意,因此在代码中分离它们有助于您理解您的设计和它背后的硬件,以及避免闩锁和合成工具在您到达该阶段时可能与您的设计有关的其他问题。否则,寄存器er文件不应该太奇怪,它只是一个“端口”,用于在指令运行后写回寄存器(如
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
在我上面提供的设计中,每个模块都有许多控制信号(如内存
dWE
,用于在数据端口上启用内存写入;
regSelIn
用于选择寄存器文件中要写入的寄存器;
aluOp
用于确定ALU的操作
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
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
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