Files
LA32R/Reference/设计方案.md
2025-06-18 20:18:42 +08:00

39 KiB
Raw Blame History

LA32R单周期CPU模型机课程设计详细实现方案

前言

本报告旨在为“LA32R架构模型机硬件系统”课程设计项目提供一份全面、详尽的总体设计与分步实现方案。报告将遵循专业的硬件设计流程从指令集体系结构ISA的深度分析入手逐步构建数据通路、设计控制逻辑并最终提出一套完整的、基于Verilog HDL的模块化实现与自动化验证策略。本方案不仅涵盖了课程设计指导书中提出的所有要求更融入了业界标准的设计思想与最佳实践旨在帮助设计者不仅完成项目更能深刻理解计算机体系结构的核心原理。


第一部分:体系结构基础与数据通路设计

在着手编写任何硬件描述语言HDL代码之前首要任务是深入理解目标处理器的体系结构并在此基础上构建一个能够支持所有指令执行的逻辑数据通路。这一阶段是整个设计的基石其正确性与完备性直接决定了后续工作的成败。

1.1 LA32R指令集体系结构ISA深度分析

对指令集体系结构ISA的分析是CPU设计的起点。LA32R指令集采用哈佛结构包含独立的指令存储器和数据存储器其设计的14条指令根据编码格式和功能可以进行系统性的解构。

1.1.1 指令格式分类与解析

根据指导书中的表214条指令可归纳为五种不同的格式。对这些格式的透彻理解是设计译码器和数据通路的前提。

  1. 1RI20型 (LUI12I.W): 用于将一个20位的立即数加载到寄存器的高位。其格式为 opcode (6) | 0 (1) | si20 (20) | rd (5)
  2. 3R型 (ADD.W, SUB.W, SLT, SLTU, NOR, AND, OR): 这是典型的寄存器-寄存器操作指令。其格式为 opcode (6) | 0000 (4) | func (2) | 00000 (5) | rk (5) | rj (5) | rd (5)。这类指令从两个源寄存器(rj, rk)读取操作数,并将结果写入目标寄存器(rd)。
  3. 2RI12型 (ADDI.W, LD.W, ST.W): 这是寄存器-立即数操作指令。其格式为 opcode (6) | func (4) | si12 (12) | rj (5) | rd (5)。这类指令使用一个源寄存器(rj和一个12位立即数si12)作为操作数。
  4. I26型 (B): 无条件跳转指令。其格式为 opcode (6) | offs[15:0] (16) | offs[25:16] (10)。它包含一个26位的偏移量。
  5. 2RI16型 (BEQ, BLT): 条件分支指令。其格式为 opcode (6) | offs[15:0] (16) | rj (5) | rd (5)。它使用两个寄存器(rj, rd进行比较并包含一个16位的偏移量。

1.1.2 指令格式对数据通路设计的启示

对指令格式的深入分析,能够揭示出许多隐含的设计简化信息,从而指导我们构建更高效、更简洁的数据通路。

一个关键的发现是指令中 寄存器地址域的高度规整性 。在所有需要读写寄存器的指令中3R型、2RI12型、2RI16型目标寄存器地址rd始终位于指令的[4:0]位,第一个源寄存器地址rj始终位于[9:5]位,第二个源寄存器地址rk始终位于[14:10]位。

这种设计并非偶然它极大地简化了寄存器堆Register File的连接。这意味着

  • 指令的[9:5]位可以直接连接到寄存器堆的第一个读地址端口(ReadRegister1)。
  • 指令的[4:0]位可以直接连接到寄存器堆的写地址端口(WriteRegister)。

然而对于第二个源操作数情况有所不同。对于3R型指令它来自rk字段(instr[14:10]),但对于ST.WBEQBLT等指令,它们的第二个源操作数来自rd字段(instr[4:0]。因此在寄存器堆的第二个读地址端口前需要一个多路选择器MUX根据控制信号srcReg进行选择,以确保读取正确的源寄存器。

与此相对, 立即数字段的位置和长度则呈现多样性LUI12I.W使用20位立即数si20ADDI.W等使用12位si12B使用26位offs26,而BEQ等使用16位offs16。这预示着**立即数扩展单元Sign/Immediate Extension Unit**将是一个不可忽视的关键组合逻辑模块。该模块需要根据指令的

opcode从指令寄存器IR的不同位置提取出正确长度的立即数字段并根据指令功能说明例如ADDI.W需要符号扩展,而LUI12I.W是逻辑拼接)进行相应的处理。这个单元是初学者设计中常见的错误来源,必须给予足够重视。

1.2 设计单周期数据通路

单周期CPU的特点是每条指令都在一个时钟周期内完成取指、译码、执行、访存和写回。这意味着数据通路必须包含一个时钟周期内执行任何一条指令所需的所有硬件单元和连接。我们将以指导书中的参考图图1为基础逐步构建并完善数据通路。

1.2.1 核心硬件组件

数据通路主要由以下功能模块通过导线和多路选择器连接而成:

  • 程序计数器 (Program Counter, PC): 32位寄存器存放当前待取指令的地址。
  • 指令存储器 (Instruction Memory): 根据PC提供的地址输出对应的32位指令。
  • 寄存器堆 (Register File): 包含32个32位通用寄存器GR。它具有两个异步读端口和一个同步写端口能够在一个周期内同时读取两个操作数并写入一个结果。
  • 算术逻辑单元 (Arithmetic Logic Unit, ALU): 执行算术(加、减)和逻辑(与、或、或非)运算,以及比较操作。
  • 数据存储器 (Data Memory): 用于LD.W(加载)和ST.W(存储)指令,与处理器进行数据交换。
  • 立即数扩展单元 (Immediate Extender): 根据指令类型,对指令中的立即数字段进行提取、拼接和符号扩展。
  • 控制单元 (Control Unit): 核心的译码部件,根据指令的opcode生成控制数据通路中所有选择器和功能单元的控制信号。
  • 多路选择器 (Multiplexers, MUX): 在数据通路的关键节点,根据控制信号选择正确的数据来源。

1.2.2 关键指令的数据流追踪

通过追踪几条代表性指令的数据流,可以清晰地展示数据通路各组件如何协同工作。

  1. R型指令 (以ADD.W rd, rj, rk为例):
    • 取指 (IF): PC的值送入指令存储器地址端口指令存储器输出ADD.W指令。PC更新为PC+4
    • 译码/读寄存器 (ID): 指令被送入控制单元和寄存器堆。控制单元进行译码。寄存器堆根据指令中的rjinstr[9:5])和rkinstr[14:10]地址读出两个32位操作数。
    • 执行 (EX): 两个操作数被送入ALU。控制单元生成的ALUOp信号指示ALU执行加法操作。
    • 写回 (WB): ALU的计算结果通过一个选择器MemtoReg MUX此时选择ALU结果送回寄存器堆的写数据端口。控制单元使能RegWrite信号,将结果写入由rdinstr[4:0])指定的寄存器。
  2. 加载指令 (LD.W rd, rj, si12):
    • IF & ID: 与R型指令类似但此时寄存器堆只读取rj的值。同时,指令中的si12字段被送入立即数扩展单元进行符号扩展。
    • EX: rj寄存器的值和符号扩展后的si12被送入ALU进行加法运算以计算内存地址Addr
    • 访存 (MEM): ALU计算出的地址Addr被送入数据存储器的地址端口。控制单元使能MemRead信号,数据存储器根据地址读出数据。
    • WB: 从数据存储器读出的数据通过MemtoReg MUX此时选择内存数据送回寄存器堆并写入rd寄存器。
  3. 条件分支指令 (BEQ rj, rd, offs16):
    • IF & ID: 与R型指令类似读取rjrd寄存器的值。同时,offs16字段被立即数扩展单元进行符号扩展并左移两位(因为指令地址是字节对齐的,而偏移量通常以字为单位)。
    • EX: rjrd的值被送入ALU进行减法操作。如果两者相等ALU的Zero标志位输出为1。
    • PC更新: 控制单元生成Branch信号。该信号与ALU的Zero标志位进行逻辑与运算。如果结果为1则表示分支条件成立一个专门用于PC更新的选择器会选择由PC+4和扩展后的偏移量相加得到的跳转目标地址来更新PC否则PC正常更新为PC+4

1.2.3 单周期设计的核心权衡:性能与简洁性

单周期设计的最大特点是其 简洁性 。每条指令的控制流程固定,没有复杂的状态转换,使得控制单元的设计相对直接。然而,这种简洁性是以牺牲性能为代价的。

在单周期模型中,时钟周期必须足够长,以容纳最长路径指令的完整执行。通过上述数据流追踪,我们可以确定LD.W(加载)指令是关键路径最长的指令之一。其完整执行需要经历以下串行延迟:

T_{cycle}=T_{PC_{read}}+T_{IMem_{access}}+T_{RegFile_{read}}+T_{ALU_{calc}}+T_{DMem_{access}}+T_{MUX}+T_{RegFile_{setup}}

相比之下,一条简单的ADD.W指令并不需要访问数据存储器,而一条B无条件跳转指令甚至不需要ALU进行数据运算。然而在单周期设计中这些快速指令必须等待与LD.W同样长的时钟周期,造成了巨大的时钟资源浪费。

在最终的设计报告中,对这一核心权衡的分析是必不可少的。它不仅展示了设计者对项目本身的完成度,更体现了对计算机体系结构基本设计原则的深刻理解。这为后续学习更高效的多周期和流水线处理器设计奠定了理论基础。


第二部分:核心功能模块设计 (Verilog)

将概念性的数据通路图转化为可综合的硬件描述语言代码,是项目的核心实践环节。采用自底向上的模块化设计方法,可以有效管理复杂性,提高代码的可重用性和可测试性。

2.1 程序计数器(PC)及PC更新模块

  • 功能说明: PC模块用于存储当前指令的地址并在每个时钟周期更新以获取下一条指令。在顺序执行时PC递增4在遇到跳转或分支指令时PC根据偏移量更新。
  • 接口信号:
    • 输入: clk (时钟), rst (复位), PCSrc (下一PC来源控制信号), branch_addr (分支目标地址)。
    • 输出: PC (当前指令地址)。
  • 内部实现: PC本质是一个32位寄存器在时钟上升沿根据PCSrc信号选择更新。PCSrc=0时,PC_next = PC + 4PCSrc=1时,PC_next = branch_addrbranch_addr由一个加法器计算得出,通常为PC + SignExt(offset << 2)

2.2 指令与数据存储器

  • 指令存储器 (Instruction Memory):
    • 功能: 只读存储器根据PC提供的地址输出32位指令。在FPGA上可利用分布式ROM或块RAMBlock RAM实现。
    • 实现: 在仿真中可使用Verilog的reg数组和$readmemh系统任务从外部.hex文件加载机器码,以方便测试程序的更换与管理。
  • 数据存储器 (Data Memory):
    • 功能: 支持字32位读写的随机存取存储器RAM用于LD.WST.W指令。
    • 实现: 可用同步双端口RAM实现。写操作在时钟上升沿根据MemWrite信号执行;读操作可设计为异步(组合逻辑读),即根据当前地址立即输出数据。地址索引需注意字节地址到字地址的转换(如addr[31:2])。

2.3 寄存器堆 (Register File)

寄存器堆是CPU中用于临时存储数据的高速存储单元。对于LA32R需要一个包含32个32位寄存器的阵列。

2.3.1 寄存器堆接口与行为

  • 接口:
    • input clk, rst; // 时钟和复位信号
    • input RegWrite; // 写使能信号
    • input [4:0] ReadRegister1, ReadRegister2; // 两个5位读地址
    • input [4:0] WriteRegister; // 一个5位写地址
    • input [31:0] WriteData; // 32位写数据
    • output [31:0] ReadData1, ReadData2; // 两个32位读数据
  • 行为:
    • 写操作: 同步的。当RegWrite为高电平,在时钟的上升沿,WriteData被写入WriteRegister指定的寄存器中。
    • 读操作: 异步(组合逻辑)的。ReadData1ReadData2的输出实时反映由ReadRegister1ReadRegister2地址指定的寄存器的内容。

2.3.2 零号寄存器 (R0) 的防御性设计

在许多RISC架构中零号寄存器R0被硬性规定为常数0。它不可被写入读取时永远返回0。实现这一特性需要“防御性”的设计策略。

一个简单的实现是在写操作时增加一个判断条件: if (RegWrite && (WriteRegister!= 5'd0)) registers <= WriteData; 这可以阻止对R0的写入。然而这种方法并非万无一失。例如如果系统复位不彻底或者存在某些设计缺陷

registers的物理存储单元可能并非为0。此时如果读逻辑仅仅是assign ReadData1 = registers;,那么当ReadRegister1为0时可能会读出一个非零值导致灾难性的错误。

一个更稳健、更具防御性的设计方法是在读写两端同时施加约束

  1. 写端防护: 如上所述阻止对地址0的任何写操作。
  2. 读端强制: 在读端口的输出逻辑中明确地处理地址0的情况。 assign ReadData1 = (ReadRegister1 == 5'd0)? 32'b0 : registers; assign ReadData2 = (ReadRegister2 == 5'd0)? 32'b0 : registers;

这种双重保险的设计确保了无论内部物理存储状态如何对R0的读取操作永远返回0。这体现了专业的硬件设计思想模块的接口行为应是明确且不受内部意外状态影响的。

2.3.3 寄存器堆Verilog代码框架

// 寄存器堆模块 (32个32位寄存器, 2个读端口, 1个写端口)
module register_file (
    input  wire        clk,          // 时钟
    input  wire        rst,          // 复位
    input  wire        reg_write_en, // 写使能
    input  wire [4:0]  read_addr1,   // 读地址1
    input  wire [4:0]  read_addr2,   // 读地址2
    input  wire [4:0]  write_addr,   // 写地址
    input  wire [31:0] write_data,   // 写数据
    output wire [31:0] read_data1,   // 读数据1
    output wire [31:0] read_data2    // 读数据2
);

    // 声明32个32位的寄存器阵列
    reg [31:0] registers[0:31];

    // 同步写操作 (时钟上升沿触发)
    always @(posedge clk) begin
        if (rst) begin
            // 复位时,将所有寄存器清零 (可选,但良好实践)
            for (integer i = 0; i < 32; i = i + 1) begin
                registers[i] <= 32'b0;
            end
        end else if (reg_write_en) begin
            // 写使能有效时,执行写操作
            // 防御性设计确保不写入0号寄存器
            if (write_addr!= 5'd0) begin
                registers[write_addr] <= write_data;
            end
        end
    end

    // 异步读操作1
    // 防御性设计确保读取0号寄存器时返回0
    assign read_data1 = (read_addr1 == 5'd0)? 32'b0 : registers[read_addr1];

    // 异步读操作2
    assign read_data2 = (read_addr2 == 5'd0)? 32'b0 : registers[read_addr2];

endmodule

2.4 算术逻辑单元 (ALU)

ALU是CPU的计算核心负责执行指令指定的算术和逻辑运算。

2.4.1 ALU模块接口与功能

  • 接口:

    • input [31:0] A, B; // 两个32位操作数输入
    • input [3:0] ALUOp; // 4位操作控制信号
    • output reg [31:0] Result; // 32位运算结果输出
    • output Zero; // 零标志位输出,用于分支指令
    • output Negative; // 负标志位用于BLT指令
  • 功能: 根据ALUOp信号,执行ADD.W, SUB.W, SLT, SLTU, NOR, AND, OR等操作。此外ALU也用于计算内存地址和分支比较。

  • 实现: 使用always @(*)块和case语句是实现ALU组合逻辑最清晰、最直接的方式。

    Zero标志位可以通过比较Result是否为全0来生成assign Zero = (Result == 32'h00000000);

    Negative标志位可由结果的最高位获得:assign negative = result[1];

2.4.2 SLTSLTU的稳健实现

ISA要求ALU同时支持有符号比较SLT)和无符号比较(SLTU^^。这为ALU的设计带来了一个有趣的挑战。

一种直接的方法是利用Verilog的语言特性。在比较时将输入操作数强制转换为signed类型:

Result = ($signed(A) < $signed(B))? 32'd1 : 32'd0; 对于无符号比较则使用默认的无符号行为。这种方法虽然简单但高度依赖于Verilog的类型系统可能会在不同仿真或综合工具间存在细微差异且未能体现底层硬件的实现原理。

一种更为稳健和体现硬件本质的方法,是基于减法运算的标志位来实现比较。

  1. 执行减法: 对于A < B的比较ALU首先计算S = A - B
  2. 无符号比较 (SLTU): 在无符号数中,A < B等价于A - B会产生借位。在二进制补码加法器中,这通常表现为最高位的进位输出(CarryOut为0。因此SLTU的逻辑可以实现为:如果A-B的结果的最高位进位为0A < B
  3. 有符号比较 (SLT): 在二进制补码中情况更为复杂需要考虑溢出Overflow。有符号数A < B的条件是: 符号标志位 (Sign Flag) 不等于 溢出标志位 (Overflow Flag) ,即 Result = (SignFlag ^ OverflowFlag)? 32'd1 : 32'd0;。其中,SignFlag是减法结果的最高位,OverflowFlag可以通过(A & ~B & ~S) | (~A & B & S)来计算。

在设计报告中,提供并比较这两种实现方式,并最终选择基于算术标志位的方法,能显著展示设计者对二进制补码运算和硬件实现的深刻理解。

2.4.3 ALU Verilog代码框架

// ALU模块
module alu (
    input  wire [31:0] a,         // 操作数A
    input  wire [31:0] b,         // 操作数B
    input  wire [3:0]  alu_op,    // ALU操作控制码
    output reg  [31:0] result,    // 运算结果
    output wire        zero,      // 零标志位
    output wire        negative   // 负标志位
);

    // 临时线网用于SLT/SLTU
    wire slt_result = ($signed(a) < $signed(b));
    wire sltu_result = (a < b);

    // 主组合逻辑
    always @(*) begin
        case (alu_op)
            // 具体操作码需根据控制单元设计确定
            4'b0000: result = a + b;           // ADD.W
            4'b0001: result = a - b;           // SUB.W
            4'b0010: result = a & b;           // AND
            4'b0011: result = a | b;           // OR
            4'b0100: result = ~(a | b);        // NOR
            4'b0101: result = {31'b0, slt_result};   // SLT
            4'b0110: result = {31'b0, sltu_result};  // SLTU
            default: result = 32'hxxxxxxxx; // 未定义操作,输出不定态
        endcase
    end

    // 标志位输出
    assign zero = (result == 32'h00000000);
    assign negative = result[1];

endmodule

2.5 立即数扩展单元 (Immediate Extender)

  • 功能说明: 根据指令类型将指令中的立即数字段如12位、16位、20位、26位转换为32位值供ALU或PC更新模块使用。处理方式包括符号扩展、零扩展和拼接。

  • 接口信号:

    • 输入: instr (32位指令), ImmType (扩展类型控制信号)。
    • 输出: imm_ext (32位扩展后立即数)。
  • 内部实现: 这是一个纯组合逻辑模块。使用case语句根据ImmType选择对应的处理逻辑。例如对12位有符号立即数si12,可使用位拼接实现符号扩展:imm_ext = {{20{si12}}, si12}。对于分支偏移量还需在扩展后左移两位即末尾补两个0

2.6 多路选择器 (Multiplexers)

多路选择器MUX在数据通路中扮演着关键的“交通枢纽”角色根据控制信号选择正确的数据来源。

  • ALU第二操作数选择 (ALUSrc MUX): 在ALU的B输入端根据ALUSrc信号选择数据来源:是来自寄存器堆的第二个读端口,还是来自立即数扩展单元的输出。
  • 写回数据选择 (MemtoReg MUX): 决定写入寄存器堆的数据来源。根据MemtoReg信号选择是来自ALU的计算结果还是来自数据存储器的读出值。
  • PC更新选择 (PCSrc MUX): 决定下一个PC的值。根据PCSrc信号选择:是顺序执行的PC+4,还是分支/跳转的目标地址。
  • 寄存器堆第二读地址选择 (srcReg MUX): 对于ST.W和条件分支指令,其第二个源操作数寄存器地址位于rd字段(instr[4:0]),而非rk字段。因此需要一个MUX根据srcReg信号,为寄存器堆的第二个读地址端口选择instr[14:10]rkinstr[4:0]rd

2.7 顶层CPU模块集成

顶层cpu_top模块是整个设计的骨架。它不包含复杂的逻辑,其主要任务是:

  1. 实例化所有子模块PC、存储器、寄存器堆、ALU、立即数扩展单元、控制单元以及所有需要的MUX。
  2. 连接这些模块。根据数据通路图将一个模块的输出连接到另一个模块的输入。例如将寄存器堆的读数据端口连接到ALU的输入将ALU的输出连接到数据存储器的地址端口或写回MUX的输入。
  3. 将控制单元生成的控制信号分发到数据通路中对应的MUX选择端和功能单元的使能端。

这个过程是细致且易错的,必须严格对照数据通路图进行。模块化的设计使得这一过程条理清晰,大大降低了出错的概率。


第三部分:指令级数据通路与控制信号分析

本部分详细分析LA32R指令集中五类指令在单周期CPU中的数据通路走向以及执行时各主要控制信号的取值情况。通过对比可以看出不同类型指令在数据通路中激活的部件各异但整个CPU架构需统一容纳这些数据流。

3.1 1RI20型指令 (LUI12I.W)

  • 功能: GR[rd] ← si20 | | 12b0将20位立即数加载到目标寄存器高位低12位补零。
  • 数据通路: 立即数扩展单元提取si20并拼接12个0生成32位常量。该常量通过ALUSrc MUX送入ALU另一输入可为0ALU执行加法等效于传递结果通过MemtoReg MUX写回rd寄存器。
  • 控制信号:
    控制信号 LU12I.W (1RI20)
    RegWrite 1 (写使能)
    ALUSrc 1 (ALU第二操作数选立即数)
    MemtoReg 0 (ALU结果写回)
    MemRead 0
    MemWrite 0
    PCSrc 0 (顺序执行)

3.2 3R型指令 (ADD.W, SUB.W, SLT, SLTU, NOR, AND, OR)

  • 功能: GR[rd] ← GR[rj] op GR[rk],执行两个寄存器间的算术或逻辑运算。
  • 数据通路: 寄存器堆读出rjrk的值送入ALU。ALU根据AluCtrl信号执行相应运算,结果通过MemtoReg MUX写回rd寄存器。
  • 控制信号:
    控制信号 3R类型指令
    RegWrite 1
    ALUSrc 0 (ALU第二操作数来自寄存器)
    MemtoReg 0
    MemRead 0
    MemWrite 0
    srcReg 0 (第二读地址选rk字段)
    PCSrc 0
    AluCtrl 根据func译码

3.3 2RI12型指令 (ADDI.W, LD.W, ST.W)

这类指令格式相同,但数据通路和控制信号有显著差异。

  • ADDI.W rd, rj, si12: GR[rd] ← GR[rj] + SignExtend(si12)。数据通路类似3R型但ALU的第二操作数来自符号扩展后的

    si12

  • LD.W rd, rj, si12: GR[rd] ← M + SignExtend(si12)]。ALU计算地址数据存储器根据该地址读出数据该数据通过

    MemtoReg MUX写回rd寄存器。

  • ST.W rd, rj, si12: M + SignExtend(si12)] ← GR[rd]。ALU计算地址寄存器堆读出

    rd的值作为待写数据,写入数据存储器。此指令不写回寄存器。

  • 控制信号:

控制信号 ADDI.W LD.W ST.W
RegWrite 1 1 0
ALUSrc 1 1 1
MemtoReg 0 1 X
MemRead 0 1 0
MemWrite 0 0 1
srcReg X X 1
AluCtrl ADD ADD ADD
PCSrc 0 0 0

3.4 I26型指令 (B)

  • 功能: PC ← PC + SignExtend(offs26 | | 2b0),无条件跳转。
  • 数据通路: 立即数扩展单元处理offs26得到跳转偏移与PC相加后得到目标地址。该地址通过PCSrc MUX更新到PC寄存器。数据通路的主要部分寄存器堆、ALU、数据存储器均不参与有效运算。
  • 控制信号: PCSrc置为1强制选择跳转地址。其他数据通路控制信号RegWrite, MemWrite均为0或无关X

3.5 2RI16型指令 (BEQ, BLT)

  • 功能: if (condition) PC ← PC + SignExtend(offs16 | | 2b0),条件分支。
  • 数据通路: 寄存器堆读出rjrd的值送入ALU进行减法比较。ALU的Zero用于BEQNegative用于BLT标志位与控制单元生成的Branch信号共同决定PCSrc的取值。若条件成立,PCSrc为1PC更新为跳转目标地址否则为0PC更新为PC+4
  • 控制信号:
    控制信号 BEQ / BLT
    RegWrite 0
    ALUSrc 0
    MemRead 0
    MemWrite 0
    srcReg 1
    AluCtrl SUB (减法比较)
    PCSrc Branch & (Zero/Negative)

第四部分:控制逻辑、验证与测试

如果说数据通路是CPU的“肌肉和骨骼”那么控制单元就是其“大脑和神经系统”。设计完备的控制逻辑并建立一套行之有效的验证体系是确保CPU功能正确性的关键。

4.1 控制单元设计

控制单元是一个有限状态机FSM但在单周期CPU中这个FSM只有一个状态。因此它退化为一个纯组合逻辑电路其输出完全由其输入即指令的opcodefunc字段)决定。

4.1.1 主控制单元与ALU控制单元

为了使设计更加模块化,控制逻辑通常被分为两部分:

  1. 主控制单元 (Main Control Unit):
    • 输入: 指令的opcode字段(instr[31:26])及其他功能位。
    • 输出: 数据通路中除ALU操作外的所有控制信号RegWriteALUSrcMemReadMemWriteMemtoRegBranch等。此外它还会生成一个2位的ALUOp信号用于粗略地指示ALU需要执行的操作类型。
    • 实现: 使用一个大的case语句或硬布线逻辑,根据opcode为每种指令类型生成一组固定的控制信号。
  2. ALU控制单元 (ALU Control Unit):
    • 输入: 主控制单元生成的ALUOp信号,以及指令的func字段对于R型指令
    • 输出: 最终驱动ALU的4位ALUOperation信号。
    • 实现: 这种两级控制结构使得主控制单元不必关心R型指令的具体运算只需告诉ALU控制单元“这是一条R型指令”。ALU控制单元再根据func字段来确定具体的运算。这遵循了“关注点分离”的设计原则。

4.1.2 控制信号真值表与硬布线逻辑实现

在编写控制单元的Verilog代码之前创建控制信号真值表是连接ISA规范和硬件实现的桥梁。第三部分的指令分析表即是此真值表的详细体现。基于此表可采用组合逻辑硬布线高效实现控制单元。

可以定义代表指令类型的中间变量,然后直接用逻辑表达式生成控制信号:

// 根据指令编码定义指令类型 (注:位字段需根据最终编码确认)
wire opR    = (opcode == 6'b000000 &&...); // 3R运算
wire opADDI = (opcode == 6'b000000 &&...); // ADDI.W
wire opLD   = (opcode == 6'b001010 &&...); // LD.W
wire opST   = (opcode == 6'b001010 &&...); // ST.W
wire opLUI  = (opcode == 6'b000101 &&...); // LUI12I.W
wire opB    = (opcode == 6'b010100);        // B
wire opBEQ  = (opcode == 6'b010110);        // BEQ
wire opBLT  = (opcode == 6'b011000);        // BLT

// 根据指令类型生成主控制信号
assign RegWrite = opR |
| opADDI |
| opLD |
| opLUI;
assign MemWrite = opST;
assign MemRead  = opLD;
assign ALUSrc   = opADDI |
| opLD |
| opST |
| opLUI;
assign MemToReg = opLD;
assign srcReg   = opST |
| opBEQ |
| opBLT; // ST/分支用rd字段作第二源

对于AluCtrl,可进一步用case语句根据func字段细分。这种方法逻辑清晰,易于实现和调试。

4.2 验证策略与测试程序开发

验证是硬件设计中耗时最长、也最重要的环节。一个结构化的验证策略和精心设计的测试程序是必不可少的。

  • 策略: 遵循“增量测试”和“覆盖关键路径”的原则。测试程序不应是随机指令的堆砌而应是一个逻辑清晰、能逐步验证CPU功能的序列。
  • 测试程序序列示例:
    1. 寄存器加载: 首先使用LUI12I.WADDI.W向多个寄存器如R1, R2, R3加载已知的初始值。这是后续测试的基础。
    2. 算术/逻辑运算: 依次执行所有3R型指令ADD.W, SUB.W, AND, OR, NOR, SLT, SLTU操作数来自已加载的寄存器。将结果存入新的寄存器如R4, R5等
    3. 内存写/读验证: 使用ST.W将某个寄存器如R4中的已知值存入数据存储器的某个地址。然后立即使用LD.W从同一地址将数据读回到一个不同的寄存器如R6。最后比较R4和R6的值是否相等。
    4. 分支逻辑测试:
      • 分支不跳转: 设计一条BEQ指令其比较的两个寄存器值不相等。验证PC是否正常更新为PC+4
      • 分支跳转: 设计一条BEQ指令其比较的两个寄存器值相等。验证PC是否正确跳转到目标地址。
      • BLT进行类似的正反测试,并特别注意使用正负数作为测试用例。
    5. 无条件跳转: 使用B指令跳转到一个特定的循环或结束位置。
    6. 测试结束标志: 程序最后应有一个明确的结束标志。例如,可以是一个无限循环(B.),或者向某个约定的寄存器或内存地址写入一个特殊的“魔法数字”(如0xCAFEBABE),以供测试平台判断程序是否已成功执行完毕。

4.3 实现自检查测试平台 (Self-Checking Testbench)

手动观察波形来验证CPU功能的做法效率低下且极易出错。一个专业的测试平台Testbench应该是自动化的、能够自行判断测试结果的。

4.3.1 测试平台架构

一个强大的自检查测试平台应包含以下组件:

  • DUT实例化: cpu uut (...)
  • 时钟与复位生成器: always #5 clk = ~clk; 和一个initial块来控制复位信号。
  • 指令存储器模型: 一个reg数组,使用$readmemh("test_program.hex", instruction_memory);从外部文件加载测试程序的机器码。
  • 数据存储器模型: 另一个reg数组,用于模拟数据存储器。
  • 结果验证逻辑: 这是自检查的核心。

4.3.2 基于“黄金参考模型”的自动化验证

自检查的关键在于“与谁比较?”。最强大的方法之一是使用一个“黄金参考模型”。对于本课程设计,这个模型可以简化为对最终寄存器状态的期望值。

  1. 生成黄金参考: 在编写测试程序后手动或通过一个简单的高级语言脚本如Python模拟执行该程序计算出当程序执行完毕时所有32个通用寄存器的 最终期望值
  2. 存储黄金参考: 将这些期望值存储在测试平台的一个“黄金”寄存器数组中:reg [31:0] golden_reg_file[0:31];。这些值可以硬编码在测试平台中,或从另一个文件中读入。
  3. 执行与比对:
    • 测试平台启动仿真让CPU运行足够长的时间以确保测试程序执行完毕。
    • 在仿真结束时(例如,通过$finish前的final块),测试平台执行一个for循环逐一比较DUT内部的寄存器堆需要通过层次化引用访问uut.reg_file_inst.registers[i])和golden_reg_file中的值。
    • 报告结果:
      • 如果所有寄存器的值都匹配,测试平台打印出"****** TEST PASSED ******"
      • 一旦发现不匹配,立即打印出详细的错误信息,如"ERROR: Register R%d mismatch! Expected: 0x%h, Got: 0x%h", i, golden_reg_file[i], uut.reg_file_inst.registers[i],并报告"****** TEST FAILED ******"

这种方法将验证过程从主观的波形观察,转变为客观、自动化的PASS/FAIL判断。这不仅极大地提高了验证效率和可靠性,也是工业界验证方法学的入门实践,能显著提升课程设计的专业水准。


第五部分:仿真、调试与报告撰写

这一部分关注于如何利用EDA工具Vivado进行实际的仿真调试并总结了设计过程中常见的陷阱以及如何撰写一份高质量的设计报告。

5.1 Vivado仿真与波形分析

在Vivado中运行仿真后波形查看器是主要的调试工具。为了高效地追踪问题应重点观察以下关键信号

  • 时钟与复位: clk, rst - 确保它们行为正常。
  • PC与指令: pc, instruction - 验证取指是否正确PC是否按预期更新。
  • 寄存器写操作: reg_write_en, write_addr, write_data - 检查写回阶段是否在正确的时机将正确的数据写入了正确的寄存器。
  • ALU操作: alu_input_a, alu_input_b, alu_op, alu_result - 验证ALU的操作数和操作类型是否正确结果是否符合预期。
  • 内存访问: mem_address, mem_write_data, mem_read_data, mem_write_en, mem_read_en - 调试LD.WST.W指令的关键。

通过将这些信号组织在一起,可以清晰地看到在每个时钟周期,数据如何在数据通路中流动,从而验证每条指令的执行过程。

5.2 主动调试与常见陷阱规避

对于初学者而言Verilog的某些特性可能会导致与直觉相悖的结果。了解并规避这些常见陷阱是成功的关键。

  1. 思维模型错位 (Mental Model Mismatch): 最根本的错误是把Verilog当作一种顺序执行的编程语言。必须牢记

    Verilog是硬件描述语言 。正确的思维方式是:“ 先画出电路图再用Verilog描述它 ”。你写的每一行代码都应对应一个具体的硬件结构(寄存器、选择器、逻辑门等)。

  2. 阻塞赋值 (=) vs. 非阻塞赋值 (<=): 这是Verilog中最常见也最致命的错误之一。

    • 铁律:
      • 时序逻辑 always @(posedge clk))中,永远使用 非阻塞赋值 (<=)
      • 组合逻辑 always @(*))中,永远使用 阻塞赋值 (=)
    • 原因: 违反此规则会导致仿真结果与综合后的硬件行为不匹配的风险,即所谓的“仿真-综合不匹配”这是非常难以调试的。非阻塞赋值模拟了时序电路中所有D触发器在时钟沿同时更新的并行行为。
  3. 意外推断出锁存器 (Inferred Latches): 在组合逻辑块中,如果某些条件下输出没有被赋值,综合器会认为你需要“记住”上一个值,从而推断出一个锁存器。锁存器会引入意外的时序路径,是设计中的大忌。

    • 两大元凶:
      • if语句没有else分支。
      • case语句没有覆盖所有可能情况,且没有default分支。
    • 解决方法: 确保在任何组合逻辑的always块中,所有输出信号在所有可能的代码路径中都被赋值。一个最佳实践是在always @(*)块的开头为所有输出赋一个默认值。
  4. 不完整的敏感列表 (Incomplete Sensitivity List): 在Verilog-1995标准中组合逻辑的always块需要手动列出所有输入信号。如果遗漏了某个输入,当该输入变化时,逻辑块不会重新计算,同样会推断出锁存器。

    • 解决方法: 始终使用always @(*) Verilog-2001标准。它能自动将块内所有读取的信号都包含进敏感列表从根本上杜绝此类错误。

5.3 进阶调试Vivado ILA硬件在线调试

仿真环境是理想的而实际硬件中可能会出现仿真无法复现的问题如时序问题。Vivado的**集成逻辑分析仪 (Integrated Logic Analyzer, ILA)**核是一个可以植入到设计中的“片上示波器”允许你在FPGA运行时实时捕获内部信号的状态。

  • 基本流程:
    1. 标记信号: 在综合后的网表视图中,右键点击你关心的内部信号(如pc, instruction选择“Mark Debug”。
    2. 设置ILA: 运行“Set Up Debug”向导Vivado会自动创建一个ILA核并将标记的信号连接到ILA的探针上。你可以设置采样深度和触发条件。
    3. 重新实现与生成比特流: 包含ILA核的新设计需要重新进行实现布局布线并生成.bit文件。
    4. 硬件调试: 在Vivado的Hardware Manager中连接开发板并下载比特流。之后你可以像使用逻辑分析仪一样设置复杂的触发条件例如当PC等于某个特定值时触发然后捕获并查看信号在硬件中运行时的真实波形。

向学生介绍ILA的使用哪怕只是基础层面也是将其从纯粹的模拟仿真引向了专业的硬件调试领域极大地提升了其实践技能和解决复杂问题的能力。

5.4 命名规范与报告撰写

5.4.1 命名规范建议

良好的命名规范可以提高代码的可读性和维护性。建议统一使用简洁的英文缩写或单词来命名,并遵循一致的风格(如驼峰式或下划线式)。

  • 模块命名: CpuTopcpu_top
  • 信号命名: pc_next, reg_write, mem_to_reg
  • 常量: 使用全大写,如 parameter ALU_ADD = 4'b0000;
  • 注释: 在模块定义和关键逻辑处添加注释,说明信号作用和设计意图。

5.4.2 最终课程设计报告结构

一份结构清晰、内容详实的报告是展示项目成果的关键。建议报告遵循以下结构:

  1. 引言:
    • 项目背景、目标与意义。
    • LA32R架构简介。
    • 报告结构概述。
  2. 总体设计描述:
    • CPU总体架构、模块划分、数据通路概览。
    • 附上CPU总体结构框图或数据通路图并引用说明。
  3. 模块设计说明:
    • 分小节详细描述每个核心Verilog模块PC、寄存器堆、ALU、控制单元等
    • 为每个模块提供接口表(信号名、位宽、方向、含义)。
    • 描述内部实现原理可附上关键Verilog代码片段和注释。
  4. 数据通路与控制分析:
    • 为五类指令分别绘制简化的数据通路图(可在总图上高亮)。
    • 提供一张完整的控制信号总表,对比所有指令的控制信号取值,并进行分析。
  5. 实现细节和仿真结果:
    • 附上关键模块的核心Verilog代码。
    • 展示关键指令执行时的仿真波形图,并结合数据通路进行分析,证明其正确性。尤其要展示分支跳转成功和失败的对比波形。
    • 附上自检查测试平台的最终PASS/FAIL测试结果截图和日志。
  6. 结论与展望:
    • 总结项目完成情况和达成的设计目标。
    • 再次审视单周期设计的优缺点,并对可能的改进方向(如多周期、流水线)进行展望。
    • 分享设计过程中的心得与体会。
  7. 附录:
    • 所有Verilog源代码。
    • 测试程序的汇编代码和机器码。

总结

本报告为LA32R单周期CPU模型机的设计与实现提供了一个系统化、工程化的完整方案。通过融合两种设计思路的精华本方案不仅详细阐述了从架构分析到模块化实现再到自动化验证的完整设计流程还深入探讨了硬件设计的核心思想、常见陷阱、高级调试技术以及专业报告的撰写规范。遵循此综合方案设计者不仅能够成功构建一个功能正确的CPU更能够在此过程中建立起坚实的计算机体系结构知识基础和专业的硬件工程实践能力。