使用verilog实现简单微处理器

Ado*_*bII 4 assembly verilog microprocessors

我试图在 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
Run Code Online (Sandbox Code Playgroud)

CMD_op 是一个 4 位输入,它指的是我在上面添加的 case 语句中的一组预定义的 16 个命令,这只是前两种情况,我为每个命令以及它如何篡改地址做了一个 case。我有一个 16 位 x 16 位的数组来保存主程序。每行的前 4 位指的是汇编命令,接下来的 12 位指的是命令的参数。

例如这里是无条件跳转命令 JMP

  //JMP
  4'b0101: nxt_addr = op1_r ;
Run Code Online (Sandbox Code Playgroud)

4'b0101是命令的 case 语句中的一个 case。

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

感觉少了点什么,谢谢。

Unn*_*Unn 11

正如评论中详述的那样,似乎主要是关于如何处理内存/总线以及关于如何跨模块实现事物的一些一般问题存在混淆。虽然 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] 
Run Code Online (Sandbox Code Playgroud)

请注意,由于寻址内存的方式不同(LD/ST都允许寄存器寻址和绝对寻址),因此许多指令具有不同的风格;这是大多数 ISA 中的一个常见功能,单个操作码可能具有额外的位,用于指定有关参数的更多详细信息。

第 2 步:设计

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

简单的单周期cpu

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

第 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
Run Code Online (Sandbox Code Playgroud)

解决您对行为 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
Run Code Online (Sandbox Code Playgroud)

看看我们如何将设计图中的每个大块视为一个单独的模块,以帮助模块化代码(字面意思!),从而将功能块分成系统的不同部分。另请注意,我尝试尽量减少always @(posedge clk)块内的逻辑量。我这样做是因为理解什么是寄存器和什么是组合逻辑通常是一个好主意,因此在代码中将它们分开可以帮助您了解您的设计及其背后的硬件,并避免锁存器和其他综合工具可能会遇到的问题当你到达那个阶段时设计。否则,寄存器文件不应该太令人惊讶,只是一个在指令运行后写回寄存器的“端口”(如LDADD)和两个用于拉出寄存器“参数”的“端口”。

接下来是内存:

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
Run Code Online (Sandbox Code Playgroud)

这里有几点需要注意。首先,我有点作弊并允许组合内存读取(最后两个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
Run Code Online (Sandbox Code Playgroud)

在我上面提供的设计中,每个模块都有许多控制信号(比如内存dWE,用于在数据端口上启用内存写入;regSelIn选择寄存器文件中的寄存器进行写入;aluOp确定 ALU 应该执行什么操作)和许多状态信号(在我们的设计中,仅此而已zFlag)。解码器的工作是将指令拆开,并根据指令尝试执行的操作断言所需的控制信号,有时借助状态信号(例如如何BRZ需要zFlag)。有时,指令本身直接对这些信号进行编码(例如 how regInSelregOutSel1并且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
Run Code Online (Sandbox Code Playgroud)

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

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