ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从全加器到MIPS指令执行:计算机组成原理实验全链路解析

从全加器到MIPS指令执行:计算机组成原理实验全链路解析 简介这是杭州电子科技大学计算机组成原理课程的系统性实验合集覆盖全加器、超前进位加法器、多功能ALU、寄存器堆、存储器设计以及MIPS汇编器与模拟器、取指令与译码和R型指令实现从数字电路基础到处理器指令执行层层递进适合计算机专业本科生对照课程进度动手实践。压缩包共1519个文件大小34.06MB其中vhd/v为硬件描述与仿真源码c/sv为程序与验证代码prj/xise为工程文件tcl/do/bat为编译仿真脚本txt/docx为说明文档另有大量测试与综合报告结构按实验模块组织便于查找。已有75人学习下载。资源不仅给出各实验的完整设计源码还包含MIPS模拟器实现、存储器初始化mif/coe文件、综合实现后的bit/ncd等产物并附说明文档与附赠资料可帮助读者从寄存器传输到指令周期建立整体认知适合课程设计、实验复习或考研复试准备。1. 计算机组成原理实验闭环从一位全加器到 R 型指令计算机组成原理这门课理论考试刷题是一回事真正把实验从全加器一路做到 MIPS 指令执行是另一回事。杭电这套实验资源的价值在于它不是零散的几个参考代码而是把组合逻辑、时序逻辑、数据通路、指令译码、汇编器与模拟器串成了一条完整的链路全加器是起点超前进位加法器教你优化进位链ALU 和寄存器堆构成运算核心存储器补上数据通路最后用 MIPS 汇编器和模拟器让指令真正跑起来。适合正在修这门课、准备考研复试上机、或者想补硬件设计基本功的软件方向同学。我拆完这套资源最大的感受是八个实验按它的顺序做是在用一条主线把计算机组成原理重新学一遍不是机械地填实验报告。2. 全加器与超前进位加法器用进位生成和传播砍掉逐级延迟2.1 全加器从真值表到可综合 Verilog全加器是三输入两输出的组合逻辑输入端是 a、b 和来自低位的进位 cin输出端是本位和 sum 以及向高位的进位 cout。很多同学第一次写全加器会下意识照着真值表枚举八个用例那样固然能过仿真但可读性和参数化能力都差。更好的做法是先把逻辑式归纳出来再直接写进 assign 语句。module full_adder( input wire a, b, cin, output wire sum, cout ); assign sum a ^ b ^ cin; assign cout (a b) | (cin (a ^ b)); endmodulesum 用三个输入的异或表达cout 的表达式拆成两部分理解a b是 ab 都为 1 时必然产生进位cin (a ^ b)是 a、b 中只有一个为 1 时进位来自低位的 cin。形式上等价于a b | a cin | b cin但写成带异或的形式在超前进位加法器里可以直接复用 a^b 这一路信号节省一部分逻辑门。仿真验证时常规做法是写一个遍历 8 种输入组合的 testbench把 sum 和 cout 与逻辑仿真器算出的真值表比对。这里有一个容易被忽略的点全加器的进位输出是否被正确接到下一级的 cin。单独测一个全加器看不出问题一旦串成多位加法器进位链断裂会表现为中间某些位的结果完全错误而且错误位与进位断开的位置高度相关。调试时优先检查每一位 cout 到高一位 cin 的连线而不是去怀疑逻辑式。2.2 超前进位加法器进位提前算延迟不再随位宽线性增长行波进位加法器之所以慢是因为第 i 位的进位必须等第 i-1 位的进位算完才能确定32 位加法在最坏情况下要串过 32 级进位逻辑。超前进位加法器的思路是引入两个中间信号进位生成信号 g 和进位传播信号 p。module cla4( input wire [3:0] a, b, input wire cin, output wire [3:0] sum, output wire cout ); wire [3:0] g, p, c; assign g a b; assign p a ^ b; assign c[0] cin; assign c[1] g[0] | (p[0] c[0]); assign c[2] g[1] | (p[1] c[1]); assign c[3] g[2] | (p[2] c[2]); assign cout g[3] | (p[3] c[3]); assign sum p ^ c; endmoduleg 表示当前位本身就能产生进位p 表示当前位能把低位的进位传播上去。c[1] 到 c[3] 看起来还是逐级代入但综合工具会把这组等式展开成与或式等价于直接写出 c3 g2 | (p2 g1) | (p2 p1 g0) | (p2 p1 p0 cin)所有位都不再等待前一级进位。这就是超前进位的本质逻辑。代价是高位进位的与门扇入会随位宽增长所以工程上很少做 32 位单级 CLA常见做法是 4 位或 8 位一组做 CLA组间再用行波或者组间也做超前进位形成两级 CLA。用这个逻辑设计进位链实验报告里最该写清楚的参数是延迟对比行波进位加法器的关键路径延迟大致随位宽线性增长4 位 CLA 的关键路径与位宽无关只取决于单个进位表达式的门级深度。仿真时把两个模块放到同一个 testbench 里输入相同的随机激励比对 sum 是否一致这就是一个最直接的验证手段。3. 多功能 ALU 与寄存器堆数据通路的两个核心组件3.1 多功能 ALU操作码决定运算类型零标志位为分支指令做准备ALU 是处理器执行算术和逻辑运算的部件这个实验的核心是把加、减、与、或、异或、或非、比较大小这些操作统一到一个模块里由 alu_op 信号选择。设计时有个经验先把功能表列全再写代码避免后续加指令时翻工。alu_op功能说明0000out a b加法0001out a - b减法0010out a b按位与0011out a | b按位或0100out a ^ b按位异或0101out ~(a | b)按位或非0110out (a b) ? 1 : 0带符号比较配合 slt 指令module alu #(parameter WIDTH 32) ( input wire [WIDTH-1:0] a, b, input wire [3:0] alu_op, output reg [WIDTH-1:0] result, output reg zero ); always (*) begin case (alu_op) 4b0000: result a b; 4b0001: result a - b; 4b0010: result a b; 4b0011: result a | b; 4b0100: result a ^ b; 4b0101: result ~(a | b); 4b0110: result ($signed(a) $signed(b)) ? 32d1 : 32d0; default: result {WIDTH{1b0}}; endcase zero (result {WIDTH{1b0}}); end endmodule注意几个参数细节。WIDTH 参数化后模块可以同时被 32 位的数据通路和 8 位的验证环境复用实验报告里建议写明默认参数 WIDTH32。slt 那一路必须用$signed(a) $signed(b)如果直接写成a b综合工具按无符号数比较负数会被误判成很大的正数。zero 标志要在整个 always 块内被赋值不能只在某个分支里赋值否则仿真会出锁存器警告。default 分支兜底所有未定义操作码保证 case 语句完备。多周期 CPU 设计里ALU 操作码通常不是直接来自指令的 funct 字段而是经过译码器映射。这个资源里没有强制要求实现映射逻辑但建议在 testbench 里加一层检查给定 alu_op遍历所有边界值比如 a0x80000000, b0x00000001 的减法这种边界最容易暴露符号扩展问题。3.2 寄存器堆三端口设计、异步读与写零寄存器的边界寄存器堆是处理器里的临时存储阵列要求两个读端口和一个写端口支持同时读出两个源操作数、写入一个结果。MIPS 的 32 个通用寄存器里R0 恒为 0这一点必须在硬件层面保证。module regfile #(parameter WIDTH 32, DEPTH 32) ( input wire clk, input wire we, input wire [4:0] raddr1, raddr2, waddr, input wire [WIDTH-1:0] wdata, output wire [WIDTH-1:0] rdata1, rdata2 ); reg [WIDTH-1:0] regs [0:DEPTH-1]; integer i; initial begin for (i 0; i DEPTH; i i 1) regs[i] 0; end assign rdata1 (raddr1 5d0) ? {WIDTH{1b0}} : regs[raddr1]; assign rdata2 (raddr2 5d0) ? {WIDTH{1b0}} : regs[raddr2]; always (posedge clk) begin if (we waddr ! 5d0) regs[waddr] wdata; end endmodule读端口是组合逻辑不依赖时钟取指后只要地址稳定源操作数就可以立刻被 ALU 使用。写端口是同步写时钟上升沿且 we 有效时写入。两个关键边界处理一是读写同一地址时同步写保证读到的是旧值除非设计成写优先二是 R0 不能通过写入改变这里在写使能条件里显式排除 waddr 等于 0 的情况同时在读路径上强制返回 0双保险。initial 循环初始化不是可综合风格但在仿真环境里非常实用否则所有寄存器初始值都是 X 态一上来跑指令全部出错排查起来极难。如果以后需要上板再把这部分换成外部复位信号即可。寄存器堆的端口参数表在实验报告里可以这样列读端口 2 个、写端口 1 个、字宽 32 位、深度 32 个、写使能高有效、读使能低有效。这个设计在 MIPS 流水线里会被频繁读写题目中涉及的寄存器堆实验和后续 R 型指令实现高度相关寄存器堆没写对后面的 CPU 实验基本没法正常跑。4. 存储器与 MIPS 汇编器RAM 读写时序与指令到二进制的转换4.1 单口 RAM 设计读优先还是写优先时序里藏着大坑存储器实验的重点是同步 RAM 的读写时序。单口 RAM 共用一份地址读和写不能同时进行工程上有读优先和写优先两种策略。读优先模式下即使写使能有效读数据线仍然输出存储单元的旧值写优先模式下读数据线直接输出写入的新值。module ram_single #(parameter WIDTH 32, DEPTH 1024) ( input wire clk, input wire en, input wire we, input wire [$clog2(DEPTH)-1:0] addr, input wire [WIDTH-1:0] wdata, output reg [WIDTH-1:0] rdata ); reg [WIDTH-1:0] mem [0:DEPTH-1]; always (posedge clk) begin if (en) begin rdata mem[addr]; if (we) mem[addr] wdata; end end endmodule这段代码的 rdata 更新用非阻塞赋值并且先读后写因此仿真表现是读优先rdata 输出的是地址对应单元的旧值。如果实验要求写优先需要把rdata mem[addr]改成rdata wdata并放在 we 分支里。这个区别很隐蔽MIPS 程序的 sw 指令回读数据时读优先和写优先的结果可能不同测试向量如果没覆盖到很难发现。存储器的位宽和深度参数要根据数据通路需求设定。MIPS 指令和数据统一编址时一般用 32 位宽、字寻址如果实验要求字节寻址则要在地址输入端加字节偏移处理。资源里的存储器设计实验建议按 32 位字宽、1K 深度起步先把单口逻辑跑对再扩展到双口 RAM为取指和数据访问并行做准备。4.2 MIPS 汇编器把 add $t0, $t1, $t2 变成 32 位机器码汇编器的作用是把人类可读的汇编文本转换成二进制指令。自己实现一个汇编器最直接的好处是能双向验证指令编码你写的 R 型指令机器码是否正确可以通过汇编-反汇编对照检查。MIPS 的指令格式分为三类R 型、I 型和 J 型其中 R 型指令的编码最关键。R 型指令的 32 位划分opcode 占 6 位rs 占 5 位rt 占 5 位rd 占 5 位shamt 占 5 位funct 占 6 位。add 指令的 opcode 是全 0funct 是 0x20sub 的 funct 是 0x22and 是 0x24or 是 0x25。寄存器编号是一个容易出错的地方$t0 对应 8$t1 对应 9$s0 对应 16很多同学第一次写汇编器时把 $t0 当成 0这正是典型翻车点。REG_MAP { $zero: 0, $at: 1, $v0: 2, $v1: 3, $a0: 4, $a1: 5, $a2: 6, $a3: 7, $t0: 8, $t1: 9, $t2: 10, $t3: 11, $t4: 12, $t5: 13, $t6: 14, $t7: 15, $s0: 16, $s1: 17, $s2: 18, $s3: 19, $s4: 20, $s5: 21, $s6: 22, $s7: 23, $t8: 24, $t9: 25, $sp: 29, $ra: 31, } def encode_r_type(rs, rt, rd, shamt, funct): return (0 26) | (rs 21) | (rt 16) | (rd 11) | (shamt 6) | funct def assemble_line(line): parts line.replace(,, ).split() op parts[0] if op add: rd, rs, rt REG_MAP[parts[1]], REG_MAP[parts[2]], REG_MAP[parts[3]] return encode_r_type(rs, rt, rd, 0, 0x20)逐行解析的思路是这样的先把逗号替换成空格避免$t0,$t1粘连然后按空白切分得到指令名和操作数操作数出现顺序是 rd, rs, rt寄存器编号之间没有可加性必须严格查表。encode_r_type 里的移位串起来看rs 左移 21 位是因为它紧接在 6 位 opcode 之后rt 左移 16 位刚好落在 rs 后面的 5 位。这里最常见的错误是把移位位数写错比如把 rs 左移 21 写错成左移 26结果多个字段相互覆盖机器码乱了而在模拟器里还不一定立刻报错。4.3 MIPS 模拟器取指-译码-执行循环的 Python 骨架模拟器实验和汇编器实验搭配着做汇编器生成机器码模拟器执行机器码两者共用一套指令编码表天然形成闭环。模拟器的核心是一个主循环从 PC 指向的地址取指令递增 PC译码执行回写。def run(program): pc 0 regs [0] * 32 mem [0] * (1 16) while True: inst mem[pc] pc 4 opcode (inst 26) 0x3F if opcode 0x00: # R 型指令 rs (inst 21) 0x1F rt (inst 16) 0x1F rd (inst 11) 0x1F funct inst 0x3F if funct 0x20: # add regs[rd] regs[rs] regs[rt] elif funct 0x22: # sub regs[rd] regs[rs] - regs[rt] elif funct 0x24: # and regs[rd] regs[rs] regs[rt] elif opcode 0x23: # lw base (inst 21) 0x1F rt (inst 16) 0x1F imm inst 0xFFFF if imm 0x8000: imm - 0x10000 regs[rt] mem[(regs[base] imm) // 4]程序启动前要把汇编器生成的机器码按字加载进 memPC 从 0 开始逐条执行。取指后先读指令再更新 PC这是冯诺依曼结构的基本约定。注意 mem 的下标用的是字地址而 lw 指令里的偏移是字节地址所以访问 mem 时要除以 4很多模拟器实现卡在这里。R 型指令的 opcode 全部是 0所以译码时先判 opcode再判 funct才能确定具体运算。模拟器的测试策略建议分两层第一层先跑一条 add 指令检查 regs[rd] 的值第二层跑一段包含 add、sub、and、lw、sw 的小程序验证指令间的数据依赖和存储访问。资源和代码都拿到手之后我一般会先删掉模拟器里自己写的指令把它当黑匣子用汇编器喂数据这样可以同时暴露汇编器和模拟器哪一端出了问题。5. 取指译码与 R 型指令执行常见问题与四个排查记录5.1 取指令与指令译码32 位指令如何被拆解取指令阶段的工作是把 PC 送到指令存储器的地址端在时钟沿把指令读到指令寄存器 IR 里同时 PC 加 4 指向下一条指令。译码阶段则从 IR 中按位段提取字段R 型指令的字段划分如下IR[31:26] 是 opcodeIR[25:21] 是 rsIR[20:16] 是 rtIR[15:11] 是 rdIR[10:6] 是 shamtIR[5:0] 是 funct。I 型指令则没有 rd 和 shamtIR[15:0] 是立即数字段需要在译码阶段做符号扩展。在多周期 CPU 设计里取指和译码分属两个时钟周期资源里的实验如果按单周期方式做则取指和译码组合在同一周期内完成但信号含义不变。实现指令译码时比对重点应该是opcode 为全 0 时进入 R 型链路其余 opcode 进入 I 型链路。R 型指令的与或非等逻辑运算由 funct 字段决定这时 ALU 的 alu_op 不能直接用 funct而要通过 ALU 控制单元映射这是实验报告里最容易被忽略的中间层。5.2 R 型指令的控制信号与数据通路R 型指令的数据通路路径是从寄存器堆读出 rs 和 rt 指向的两个源操作数送入 ALUALU 根据 funct 映射出的运算类型执行计算结果写回 rd 指向的寄存器。这条路径上的关键控制信号如下信号取值作用RegDst1写寄存器地址选择 rd 而非 rtALUSrc0ALU 第二输入选择寄存器堆 rt而非立即数MemtoReg0写回数据选择 ALU 结果而非存储器读出数据RegWrite1允许寄存器写入MemRead0不读数据存储器MemWrite0不写数据存储器Branch0非分支指令PC 不跳转ALUOp10ALU 控制单元按 funct 解析具体操作用 Logisim 搭数据通路时R 型指令走的是最典型的连线寄存器堆的 rd 端口接到写地址 mux 的 RegDst 分支ALU 的两个输入分别来自读数据 1 和读数据 2写回 mux 保持 ALU 结果直通。调试时如果发现某个 R 型指令结果不对先查这 8 个控制信号特别是 RegDst 和 ALUSrc这两个信号反了会直接导致写错寄存器或把立即数当寄存器值参与运算现象非常隐蔽因为程序还能往下跑只是算出来的数值完全不对。5.3 避坑记录仿真 X 态、偏移量算错与控制信号反接现象 1仿真波形里寄存器堆全是 X 态R 型指令算出个未知数。原因寄存器堆没有初始化也没有异步复位仿真器把所有 reg 初值置为 X第一条指令读到的源操作数就是 X结果自然无法确定。解决毫秒量级的修复办法是在寄存器堆模块里加 initial 循环把所有寄存器清零代码参考本文 3.2 节如果要综合上板改成同步复位逻辑在复位有效时把寄存器全部清 0。现象 2beq 指令总是跳转到错误的位置但单步检查汇编器编码看不出问题。原因MIPS 分支偏移量的计算方式是目标地址等于 PC4 加符号扩展后的偏移左移 2 位很多汇编器实现直接拿当前 PC 加偏移差了一条指令且负偏移的符号扩展在 Python 里要用 if imm 0x8000: imm - 0x10000 手动处理。解决先把汇编器改成两遍扫描第一遍收集标签地址第二遍统一计算偏移计算偏移时明确写清target (pc 4) (sign_extended_imm 2)。现象 3R 型指令的 add 结果正确但 sub 结果错误。原因sub 的无符号减法在硬件上等价于 a 加 b 的补码即 a (~b 1)模拟器里如果直接写 regs[rs] - regs[rt] 没问题但硬件实现时如果 ALU 没有把 b 取反加 1而是直接做了减法器的另一套逻辑控制信号的映射就对不上。解决检查 ALU 控制单元里 sub 对应的 alu_op 是否与 ALU 模块默认分支一致逐条比对 funct 到 alu_op 的映射表先跑一个全 1 减 1 的边界测试这个组合能同时暴露借位和符号扩展两个问题。现象 4仿真中 ALU 结果出现毛刺波形上能看到短暂错误值。原因comb 逻辑存在不同路径的延迟差比如 a、b 同时变化时进位链的不同位到达时间不一致导致输出在稳态之前短暂出现错误组合。解决这属于组合逻辑的静态竞争不影响最终功能用 block 赋值加固定延迟的教科书做法来检查或者把波形比较的采样点放到时钟沿之后避开毛刺区间。上板时问题不大但如果用仿真波形的光标手工取点就会被这个毛刺骗到。6. 验证与进阶用 testbench 和模拟器把八个实验串起来硬件模块做完不代表指令能跑通我验证这套资源时习惯建立一张检查清单每个模块先单独仿真再联合起来跑一条实际指令最后用汇编器生成的机器码喂给模拟器做交叉验证。这个顺序本身就是一个从部件到系统的递进过程。比如验证 ALU用下面的 testbench 模板遍历所有操作码module alu_tb; reg [31:0] a, b; reg [3:0] op; wire [31:0] result; wire zero; alu dut(.a(a), .b(b), .alu_op(op), .result(result), .zero(zero)); initial begin a 32h7FFFFFFF; b 32h00000001; op 4b0001; #100 $display(sub: %h, zero%b, result, zero); $finish; end endmodule而验证完整链路时我通常会做这样一件事在汇编器里写一条 add $t0, $t1, $t2得到机器码再把它放进模拟器的 mem 数组里跑一遍同时把同样这条指令放进 Verilog 的寄存器堆和 ALU 组合模块里两者输出做硬比对。两边输出一致说明指令编码、寄存器编号、ALU 运算三个环节全对。这个交叉验证的流程只需要一个简单的 Python 脚本把汇编器输出转成 Verilog 的 $readmemh 可加载格式每次改动硬件后都重新跑一遍。从那以后我每次拿到这类实验资源都会强制自己先做一次最小闭环全加器仿真通过然后 CLA 通过然后 ALU 通过最后汇编器和模拟器对拍一条指令。整套流程走完只需要十几分钟但能省下后面排查数据通路的大把时间。这套杭电的计算机组成原理实验包建议你也照这个顺序完整跑一遍希望帮到你。本文还有配套的精品资源点击获取
返回列表