使用verilog实现简单的微处理器

Implementation of simple microprocessor using verilog

我正在尝试用 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 跳过它的使用,公交车会做什么。

感觉少了点什么,谢谢

正如评论中所详述的,似乎主要是关于如何处理 memory/bus 以及关于如何跨模块实现事物的一些一般性问题的混淆。虽然 SO 的设计并不能很好地回答 design/implementation 通用单周期处理器的这些广泛问题,但我将在这里通过一个非常基本的步骤作为一个简短的教程来阐明作者的一些观点。

第 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 的下一个值等)。通常,最好的方法是实际将其全部绘制出来,使其尽可能详细以指定设计。这是我们简单处理器的一些细节:

请注意,该设计是一堆之前讨论过的构建块。还包括处理器中的所有数据线、控制信号和状态信号。在开始编码之前仔细考虑您需要的一切是一个好主意,这样您可以更轻松地将您的设计模块化(每个块可以是一个模块)并事先看到任何重大挑战。我想指出的是,在实施过程中我确实注意到了该图上的一些 mistakes/oversights(主要是遗漏的细节),但重要的是要注意该图是此时正在制作的模板。

第 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

解决您对行为 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) 块内的逻辑量。我这样做是因为它通常是了解什么是寄存器和什么是组合逻辑的好主意,因此在代码中将它们分开有助于您了解您的设计及其背后的硬件,并避免锁存器和合成工具可能与您的其他问题当你到达那个阶段时进行设计。否则,寄存器文件应该不会太令人惊讶,只是一个用于在指令后写回寄存器的“端口”是运行(如LDADD)和两个用于拉取的“端口” out 注册“参数”。

接下来是内存:

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 [=13=]00 to ff
        
        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 个可能的地址 space。在计算机系统中,拥有地址 space 所允许的尽可能多的物理内存并不常见。这为外围设备和其他内存映射 IO 打开了那些内存地址。这通常就是您所说的系统总线、内存、CPU 和任何其他外围设备之间的互连。 CPU 通过其指令读取端口和数据 read/write 端口访问总线。在这个系统中,用于存储指令和数据的内存是一样的,所以称为冯·诺依曼体系结构。如果我把指令内存和数据内存分开(即两个独立的内存模块),那将是哈佛架构。

进入最后的子模块,指令解码器:

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到select在要写入的寄存器文件中注册;aluOp 以确定 ALU 应该执行什么操作)和一些状态信号(在我们的设计中,就是 zFlag)。解码器的工作是将指令分开并根据指令尝试执行的操作断言所需的控制信号,有时需要状态信号的帮助(例如 BRZ 需要 zFlag)。有时,指令本身会直接对这些信号进行编码(例如 regInSelregOutSel1regOutSel2 可以从指令字本身中提取出来),但有时这些控制信号不会直接映射(就像 regInEn 并没有真正映射到指令字中的任何一位)。

在您的设计中,似乎您在解码器本身内部完成了指令的大量实际工作,有时这很好,但通常会导致一堆额外的硬件(即,类似的指令会不共享硬件,例如递增指令和加法指令通常不会在您的编码风格中共享加法器,但它们应该在实际设计中)。将系统分为控制路径和数据路径,其中控制路径断言控制信号以指示数据路径如何处理数据,而数据路径执行实际工作和 returns 状态信号以指示任何重要内容。

最后的步骤是将它们组合在一起,并添加不能整齐地放入一个漂亮盒子中的硬件部分(比如程序计数器,不要忘记!):

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

看到我的处理器现在只是一堆模块实例化和一些额外的寄存器和多路复用器 link 它们在一起。不过,这些确实为我们的设计添加了一些额外的控制信号,因此请确保您将其作为整体系统设计的一部分加以考虑。然而,返回并将这些新信号添加到解码器并不是一个大细节,但此时您可能已经意识到您需要它们!另一件需要注意的事情是,在处理器本身中包含内存并不常见。如前所述,内存与 CPU 是分开的,这两者通常在处理器本身之外连接在一起(因此,应该在处理器模块之外完成);但这是一个快速而简单的介绍,所以我把它全部放在这里是为了避免必须有另一个包含处理器和内存的模块并将它们连接在一起。

希望这个实际示例向您展示了所有步骤和所有主要组件以及如何实现它们。请注意,我没有完全验证此设计,所以我可能在代码中犯了一些错误(尽管我做了 运行 一些测试,所以应该没问题:))。同样,这种事情对 SO 来说不是最好的,您应该提出具体问题,因为广泛的主题问题通常很快就会结束。另请注意,这是一个简短且超级简单的介绍,您可以在网上找到更多信息,而且计算机体系结构比这更深入;流水线、interrupts/exceptions、缓存都是下一个主题。而且这种架构甚至没有任何类型的内存停顿,没有指令的多字提取以及即使在最小的处理器中也能找到的许多更常见的东西。