Files
LA32R/Reference/设计方案.md
2025-06-17 00:35:40 +08:00

36 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)。
  • 指令的[14:10]位可以直接连接到寄存器堆的第二个读地址端口(ReadRegister2)。
  • 指令的[4:0]位可以直接连接到寄存器堆的写地址端口(WriteRegister)。

这样的设计避免了在寄存器堆的地址输入端使用复杂的选择器MUX将选择的复杂性转移到了数据通路的其他部分例如选择哪个数据写回寄存器堆。然而对于 ST、BEQ、BLT 等指令,它们的第二个源操作数来自rd字段,因此在寄存器堆的第二个读地址端口前仍需一个 MUX 进行选择。

与此相对, 立即数字段的位置和长度则呈现多样性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 的写入 11。然而这种方法并非万无一失。例如如果系统复位不彻底或者存在某些设计缺陷

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; // 零标志位输出,用于分支指令
  • 功能: 根据ALUOp信号,执行ADD.W, SUB.W, SLT, SLTU, NOR, AND, OR等操作。此外ALU 也用于计算内存地址和分支比较。
  • 实现: 使用always @(*)块和case语句是实现 ALU 组合逻辑最清晰、最直接的方式。 Zero标志位可以通过比较Result是否为全 0 来生成:assign Zero = (Result == 32'h00000000);

2.4.2 SLTSLTU的稳健实现

ISA 要求 ALU 同时支持有符号比较(SLT)和无符号比较(SLTU^3^。这为 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 规范和硬件实现的桥梁,其重要性不可低估。第三部分的指令分析表即是此真值表的详细体现。

  • 价值:
    1. 强制性分析: 迫使设计者在编码前就理清所有逻辑细节。
    2. 设计规范: 作为控制单元最精确、无歧义的设计规范。
    3. 调试黄金标准: 在后续仿真调试阶段,可将实际控制信号与表中期望值比对,快速定位问题。

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

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

  • 策略: 遵循“增量测试”和“覆盖关键路径”的原则。测试程序不应是随机指令的堆砌,而应是一个逻辑清晰、能逐步验证 CPU 功能的序列。
  • 测试程序序列示例:
    1. 寄存器加载: 首先使用LUI12I.WADDI.W向多个寄存器加载已知初始值。
    2. 算术/逻辑运算: 依次执行所有 3R 型指令,操作数来自已加载的寄存器,结果存入新寄存器。
    3. 内存写/读验证: 使用ST.W将已知值存入内存,再用LD.W读回并比较。
    4. 分支逻辑测试: 分别测试条件成立(跳转)和不成立(不跳转)两种情况。
    5. 无条件跳转: 使用B指令跳转到特定位置。
    6. 测试结束标志: 程序最后应有明确的结束标志,如无限循环或向特定寄存器/内存地址写入“魔法数字”,供测试平台判断。

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. 生成黄金参考: 手动或通过脚本模拟执行测试程序,计算出程序结束时所有寄存器的 最终期望值
  2. 存储黄金参考: 将期望值存储在测试平台的golden_reg_file数组中。
  3. 执行与比对: 仿真结束时,测试平台用for循环逐一比较 DUT 内部寄存器值和golden_reg_file中的值。
  4. 报告结果: 如果匹配,打印"TEST PASSED";否则,打印详细错误信息和"TEST FAILED"

这种方法将验证过程从主观的波形观察,转变为客观、自动化的PASS/FAIL判断,是工业界验证方法学的入门实践。


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

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

5.1 Vivado 仿真与波形分析

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

  • 时钟与复位: clk, rst
  • PC 与指令: pc, instruction
  • 寄存器写操作: reg_write_en, write_addr, write_data
  • ALU 操作: alu_input_a, alu_input_b, alu_op, alu_result
  • 内存访问: mem_address, mem_write_data, mem_read_data, mem_write_en, mem_read_en

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

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

  1. 思维模型错位 (Mental Model Mismatch): 最根本的错误是把 Verilog 当作一种顺序执行的编程语言。必须牢记: Verilog 是硬件描述语言 。正确的思维方式是:“ 先画出电路图,再用 Verilog 描述它 ”。
  2. 阻塞赋值 (=) vs. 非阻塞赋值 (<=): 这是 Verilog 中最常见也最致命的错误之一。
    • 铁律:
      • 时序逻辑 always @(posedge clk))中,永远使用 非阻塞赋值 (<=)
      • 组合逻辑 always @(*))中,永远使用 阻塞赋值 (=)
  3. 意外推断出锁存器 (Inferred Latches): 在组合逻辑块中,如果某些条件下输出没有被赋值(如if缺少elsecase缺少default),综合器会推断出锁存器。
    • 解决方法: 确保在组合逻辑的always块中,所有输出信号在所有代码路径中都被赋值,或在块开头赋默认值。
  4. 不完整的敏感列表 (Incomplete Sensitivity List): 在 Verilog-1995 标准中,遗漏敏感列表中的输入会推断出锁存器。
    • 解决方法: 始终使用always @(*) Verilog-2001 标准),它能自动推断敏感列表。

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

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

  • 基本流程:
    1. 标记信号: 在综合后的网表视图中右键点击关心的内部信号选择“Mark Debug”。
    2. 设置 ILA: 运行“Set Up Debug”向导Vivado 会自动创建 ILA 核并连接探针。
    3. 重新实现与生成比特流: 包含 ILA 核的新设计需要重新实现并生成.bit文件。
    4. 硬件调试: 在 Hardware Manager 中连接开发板,下载比特流,然后像使用逻辑分析仪一样设置触发条件并捕获波形。

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

总结

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