# LA32R 单周期 CPU 模型机课程设计详细实现方案 ## 前言 本报告旨在为“LA32R 架构模型机硬件系统”课程设计项目提供一份全面、详尽的总体设计与分步实现方案。报告将遵循专业的硬件设计流程,从指令集体系结构(ISA)的深度分析入手,逐步构建数据通路、设计控制逻辑,并最终提出一套完整的、基于 Verilog HDL 的模块化实现与自动化验证策略。本方案不仅涵盖了课程设计指导书中提出的所有要求,更融入了业界标准的设计思想与最佳实践,旨在帮助设计者不仅完成项目,更能深刻理解计算机体系结构的核心原理。 --- ## 第一部分:体系结构基础与数据通路设计 在着手编写任何硬件描述语言(HDL)代码之前,首要任务是深入理解目标处理器的体系结构,并在此基础上构建一个能够支持所有指令执行的逻辑数据通路。这一阶段是整个设计的基石,其正确性与完备性直接决定了后续工作的成败。 ### 1.1 LA32R 指令集体系结构(ISA)深度分析 对指令集体系结构(ISA)的分析是 CPU 设计的起点。LA32R 指令集采用哈佛结构,包含独立的指令存储器和数据存储器,其设计的 14 条指令根据编码格式和功能,可以进行系统性的解构。 #### 1.1.1 指令格式分类与解析 根据指导书中的表 2,14 条指令可归纳为五种不同的格式。对这些格式的透彻理解是设计译码器和数据通路的前提。 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 位立即数`si20`,`ADDI.W`等使用 12 位`si12`,`B`使用 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):** 指令被送入控制单元和寄存器堆。控制单元进行译码。寄存器堆根据指令中的`rj`(`instr[9:5]`)和`rk`(`instr[14:10]`)地址,读出两个 32 位操作数。 - **执行 (EX):** 两个操作数被送入 ALU。控制单元生成的`ALUOp`信号指示 ALU 执行加法操作。 - **写回 (WB):** ALU 的计算结果通过一个选择器(`MemtoReg` MUX,此时选择 ALU 结果)送回寄存器堆的写数据端口。控制单元使能`RegWrite`信号,将结果写入由`rd`(`instr[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 型指令类似,读取`rj`和`rd`寄存器的值。同时,`offs16`字段被立即数扩展单元进行符号扩展并左移两位(因为指令地址是字节对齐的,而偏移量通常以字为单位)。 - **EX:** `rj`和`rd`的值被送入 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_{r​ead}}​+T_{IMem_{a​ccess}}​+T_{RegFile_{r​ead}}​+T_{ALU_{c​alc}}​+T_{DMem_{a​ccess}}​+T_{MUX}​+T_{RegFile_{s​etup}}​$$ 相比之下,一条简单的`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 + 4`;`PCSrc=1`时,`PC_next = branch_addr`。`branch_addr`由一个加法器计算得出,通常为`PC + SignExt(offset << 2)`。 ### 2.2 指令与数据存储器 - **指令存储器 (Instruction Memory):** - **功能:** 只读存储器,根据 PC 提供的地址输出 32 位指令。在 FPGA 上可利用分布式 ROM 或块 RAM(Block RAM)实现。 - **实现:** 在仿真中,可使用 Verilog 的`reg`数组和`$readmemh`系统任务从外部`.hex`文件加载机器码,以方便测试程序的更换与管理。 - **数据存储器 (Data Memory):** - **功能:** 支持字(32 位)读写的随机存取存储器(RAM),用于`LD.W`和`ST.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`指定的寄存器中。 - **读操作:** 异步(组合逻辑)的。`ReadData1`和`ReadData2`的输出实时反映由`ReadRegister1`和`ReadRegister2`地址指定的寄存器的内容。 #### 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 代码框架 ```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 `SLT`与`SLTU`的稳健实现 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`的结果的最高位进位为 0,则`A < 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 代码框架 ```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]`(rk)或`instr[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 | | 12’b0`,将 20 位立即数加载到目标寄存器高位,低 12 位补零。 - **数据通路:** 立即数扩展单元提取`si20`并拼接 12 个 0,生成 32 位常量。该常量通过`ALUSrc` MUX 送入 ALU(另一输入可为 0),ALU 执行加法(等效于传递)后,结果通过`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]`,执行两个寄存器间的算术或逻辑运算。 - **数据通路:** 寄存器堆读出`rj`和`rk`的值,送入 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 | | 2’b0)`,无条件跳转。 - **数据通路:** 立即数扩展单元处理`offs26`得到跳转偏移,与 PC 相加后得到目标地址。该地址通过`PCSrc` MUX 更新到 PC 寄存器。数据通路的主要部分(寄存器堆、ALU、数据存储器)均不参与有效运算。 - **控制信号:** `PCSrc`置为 1,强制选择跳转地址。其他数据通路控制信号(`RegWrite`, `MemWrite`等)均为 0 或无关(X)。 ### 3.5 2RI16 型指令 (BEQ, BLT) - **功能:** `if (condition) PC ← PC + SignExtend(offs16 | | 2’b0)`,条件分支。 - **数据通路:** 寄存器堆读出`rj`和`rd`的值送入 ALU 进行减法比较。ALU 的`Zero`(用于 BEQ)或`Negative`(用于 BLT)标志位与控制单元生成的`Branch`信号共同决定`PCSrc`的取值。若条件成立,`PCSrc`为 1,PC 更新为跳转目标地址;否则为 0,PC 更新为`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 只有一个状态。因此,它退化为一个纯组合逻辑电路,其输出完全由其输入(即指令的`opcode`和`func`字段)决定。 #### 4.1.1 主控制单元与 ALU 控制单元 为了使设计更加模块化,控制逻辑通常被分为两部分: 1. **主控制单元 (Main Control Unit):** - **输入:** 指令的`opcode`字段(`instr[31:26]`)及其他功能位。 - **输出:** 数据通路中除 ALU 操作外的所有控制信号,如`RegWrite`、`ALUSrc`、`MemRead`、`MemWrite`、`MemtoReg`、`Branch`等。此外,它还会生成一个 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.W`和`ADDI.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`缺少`else`,`case`缺少`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 命名规范建议 良好的命名规范可以提高代码的可读性和维护性。建议统一使用简洁的英文缩写或单词来命名,并遵循一致的风格(如驼峰式或下划线式)。 - **模块命名:** `CpuTop` 或 `cpu_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,更能够在此过程中建立起坚实的计算机体系结构知识基础和专业的硬件工程实践能力。