资讯动态

从门电路到CPU:一文讲清P0-P6处理器设计全流程

发布时间:2026/10/13 17:45:25 来源:尧图企业网站定制
简介一套覆盖北航计算机组成课程P0-P6全部实验的资源包面向正在学习计算机组成与体系结构、需要完成Logisim电路设计、Verilog CPU实现及MIPS汇编编程的本科生。内容包含斐波那契数列电路、Moore与Mealy型有限状态机、单周期CPU设计以及基于Verilog的单周期与多周期流水线CPU工程另有Mars汇编实现的哈密顿回路、循环递归、字符串替换、矩阵转化等题目代码覆盖数字逻辑到流水线处理器的完整训练链条。压缩包共441个文件以173个Verilog源文件、52个Logisim工程、43个汇编代码和若干C/C工程及说明文档为主工程与笔记配套整体10.17MB目录分类清晰。已有3484人学习下载适合需要参考完整实现思路、对照调试或快速复习备考的计算机专业学生能显著节省自行摸索环境与排错的时间。1. P0-P6 不是六门小实验是从门电路到跑通程序的一条 CPU 主线P0-P6 是计算机组成课程里的一条完整项目链要求你用硬件描述语言从零实现一个能执行指令的处理器。很多人刚看到题目单时以为只是六个独立小作业今天搭个多路选择器明天写个译码器最后再拼一个 CPU。但真正动手做起来才会发现这六个阶段是一条层层收束的主线——P0-P2 把组合逻辑和时序逻辑的写法规整干净P3 让单周期数据通路跑通第一条指令P4 开始把数据通路切成五级流水线P5 把中断异常塞进处理器P6 再做访存和整机联调。做完整套你收获的不是“会写 Verilog”而是拿到任何指令集手册都敢动手搭处理器的底气看仿真波形时也不再觉得流水线、写回、异常是黑匣子。这篇文章就按这条项目链往下拆把每个阶段最可靠的落地路径、参数选择和必踩的坑一次讲清。2. P0-P2 地基怎么打组合逻辑、时序逻辑与仿真平台的三个习惯P0-P2 对应的是平台入门、组合逻辑设计和时序逻辑设计。P0 通常是最小工程目标是跑通“编写代码 → 编译 → 仿真 → 看波形”的完整流程P1 会落到加法器或 ALUP2 就开始涉及寄存器堆、计数器、有限状态机。这个阶段最重要的不是题目本身而是养成三个习惯模块化、写测试平台、分清组合逻辑与时序逻辑的写法边界。很多人在 P3 之后被数据通路绕晕根子就是 P0-P2 没把这三件事练熟导致看见一段几十行的 always 块就分不清哪里是组合哪里是时序综合时报出 latch 也只能干瞪眼。2.1 第一个能跑的最小模块4 位加法器与仿真平台我一般建议从 4 位加法器起步它足够小但能同时让你练到端口声明、位拼接和测试平台三个基本功。先看主体模块module adder4( input [3:0] a, input [3:0] b, input cin, output [3:0] s, output cout ); assign {cout, s} a b cin; endmodule这里只用了一行assign通过位拼接把进位和结果一起输出。[3:0]表示 4 位宽加法会按无符号数处理如果你后面做 32 位 CPU需要把位宽改成[31:0]逻辑完全不变。这一行代码综合出来就是一位全加器级联的结构不需要你手动画门电路。配套的测试平台也很关键很多人只写模块不写 testbench到了 P3 才后悔。随便举一个能直接用的测试平台module tb_adder4(); reg [3:0] a; reg [3:0] b; reg cin; wire [3:0] s; wire cout; adder4 uut( .a(a), .b(b), .cin(cin), .s(s), .cout(cout) ); initial begin a 4h0; b 4h0; cin 1b0; #10 a 4h3; b 4h5; cin 1b0; #10 a 4hA; b 4h5; cin 1b1; #10 a 4hF; b 4h1; cin 1b0; #10 $finish; end initial begin $monitor(time%0t a%h b%h cin%b s%h cout%b, $time, a, b, cin, s, cout); end endmodule#10表示延迟 10 个时间单位后换下一组输入$monitor在任一变量变化时打印一行适合小模块快速验证。你可以打开仿真器的波形界面把 a、b、s、cout 四根信号拖进去观察每个时间点上的变化这一步能让你直观理解组合逻辑的“传输延迟”和“立即输出”特性。P0-P2 阶段每个模块都顺手写一个这样的小测试平台后面调试 CPU 时你才有对照基线。2.2 组合逻辑与时序逻辑的分界线译码器与寄存器堆的写法差异组合逻辑用assign或者always (*)输出只取决于当前输入时序逻辑用always (posedge clk)输出在时钟边沿才更新。这个区分在 P2 阶段必须刻进脑子里因为 P3 之后的 CPU 里指令译码是组合逻辑寄存器堆和 PC 是时序逻辑两者一旦写混波形里全是竞态。寄存器堆是 P2 阶段最值得手写的模块也是后面 CPU 的底座reg [31:0] rf [0:31]; always (posedge clk) begin if (we waddr ! 5b0) rf[waddr] wdata; end这段代码生成 32 个 32 位寄存器we是写使能waddr是写地址wdata是写数据。这里用了非阻塞赋值确保所有寄存器在同一个时钟边沿同时采样不会出现“右边先更新又被左边读到”的错乱。P2 阶段就把这个写法固定下来P4 流水线里大量寄存器更新才不会翻车。组合逻辑则要特别注意“不完整分支”问题。下面这种写法非常典型always (*) begin if (en) y a; end如果en为低y 没有新的赋值电路为了保持原值会自动推断出一个锁存器。仿真阶段看不出来综合时就会报告 Found latch上板后行为完全不可控。正确做法是分支写全或者一开始就给 y 赋默认值always (*) begin y 1b0; if (en) y a; endP0-P2 阶段遇到“仿真对但综合错”的玄学问题十有八九是这类组合逻辑分支不完整。把默认赋值养成习惯后面 P5 处理异常优先级时能省掉大量排查时间。提示P0-P2 阶段别急着追求代码量把每个模块的端口、位宽、赋值方式固定成一套自己的模板P3 开始你会体会到这套模板的价值。3. P3 单周期 CPU 怎么立住从指令译码到最小可运行数据通路P3 是整个项目链的第一个真正的分水岭。你要把取指、译码、执行、访存、写回五段操作在同一个时钟周期里完成所以叫“单周期”。这个阶段不需要追求性能核心目标是让每一条指令都严格按数据通路走对。常见的选择是课程指定的 MIPS 风格 32 位 RISC 指令集指令字 32 位寄存器堆 32 个内存按字节寻址。你先别想着加指令能把 addi、lw、sw、beq、j 这五类指令跑通P3 就稳了。3.1 一条指令的控制信号怎么来从指令格式到 case 语句单周期 CPU 的主干由 PC、指令存储器、寄存器堆、ALU、数据存储器组成而把这些部件“指挥”起来的是控制信号。每个控制信号的值由操作码决定比如 R 型指令需要写寄存器堆所以reg_write为 1lw 需要把内存数据写回寄存器所以mem_to_reg为 1。先把五类指令的控制信号列成表后面写代码时直接对照指令类型reg_dstalu_srcmem_to_regreg_writemem_writebranchjumpalu_opR 型100100010addi010100000lw011100000swx1x010000beqx0x001001jxxx0001xx对应到 Verilog 就是一段译码 casealways (*) begin // 先给全部控制信号赋默认值避免锁存器 reg_dst 1b0; alu_src 1b0; mem_to_reg 1b0; reg_write 1b0; mem_write 1b0; branch 1b0; jump 1b0; alu_op 2b00; case (opcode) 6b000000: begin // R 型 reg_dst 1b1; reg_write 1b1; alu_op 2b10; end 6b001000: begin // addi alu_src 1b1; reg_write 1b1; end 6b100011: begin // lw alu_src 1b1; mem_to_reg 1b1; reg_write 1b1; end 6b101011: begin // sw alu_src 1b1; mem_write 1b1; end 6b000100: begin // beq branch 1b1; alu_op 2b01; // ALU 做减法比较 end 6b000010: begin // j jump 1b1; end default: begin // 未知 opcode所有控制信号保持默认值 end endcase end注意最前面给每个信号先赋一次默认值这是上一章强调的“防锁存器”习惯。alu_op是两级译码主译码只给出“要做什么操作类别”ALU 内部再根据funct字段细分always (*) begin case (alu_op) 2b00: alu_ctrl 3b010; // 加法 2b01: alu_ctrl 3b110; // 减法 2b10: case (funct) 6b100000: alu_ctrl 3b010; // add 6b100010: alu_ctrl 3b110; // sub 6b100100: alu_ctrl 3b000; // and 6b100101: alu_ctrl 3b001; // or 6b101010: alu_ctrl 3b111; // slt default: alu_ctrl 3b010; endcase default: alu_ctrl 3b010; endcase endalu_ctrl通常用 3 位编码010是加、110是减、000是与、001是或、111是 slt。这样 ALU 本身不关心你是什么指令只看 ALU 控制信号模块间耦合很干净。P3 阶段最忌讳把指令译码和 ALU 操作写进同一个 always后面 P4 切流水线时你会被自己拆哭。3.2 联调顺序先 addi再访存最后让 beq 回头拿到一个空 CPU不要一次性把所有指令都跑起来那只会让波形里全是噪声。我一般会按三条路径逐步建立信心。第一步只跑addi $t0, $zero, 10这类算术指令。把 PC 初始化成 0观察四个关键信号PC 是否每个周期加 4、指令存储器输出是否稳定、寄存器堆是否在写回周期写入、ALU 结果是否等于 0 10。这一步通了说明取指、译码、寄存器堆、ALU、写回的主干链路是通的。第二步加 lw/sw。用一段极小的汇编程序先addi $t0, $zero, 0x1000把基址写到寄存器再sw $t1, 0($t0)存一个数然后lw $t2, 0($t0)读回来。这时要盯住数据存储器的读写使能、mem_to_reg选择器的方向和写回数据是否来自内存。最容易错的是地址对齐MIPS 风格的访存要求地址按 4 字节对齐你写 0x1001 这种地址仿真可能不报错但上板会出问题。第三步再引入 beq 和 j。先让 beq 条件为真观察 PC 是否变成PC4 (imm 2)再让条件为假观察 PC 是否走顺序路径。这里要特别留意立即数的符号扩展beq 的立即数是 16 位有符号数先扩展到 32 位再左移两位最后加上 PC4 作为跳转基准。很多人在这里把基准写成 PC结果每次跳转都偏一条指令。wire [31:0] imm_ext {{16{instr[15]}}, instr[15:0]}; wire [31:0] branch_target pc_plus_4 (imm_ext 2);j 指令则是另一个算法把 PC4 的高 4 位、26 位立即数、两个 0 拼起来assign jump_target {pc_plus_4[31:28], instr[25:0], 2b00};P3 的最后一步我会把这几条指令组成一个不依赖外设的循环程序比如用addi累加计数、用beq和j回跳把仿真时间拉长到几百个周期看 PC 是否稳定循环。这一步能通过说明单周期数据通路基本可用了。注意单周期 CPU 的时钟频率上不去因为它一个周期要串行走完整条链路组合逻辑延迟很长。这不是 bug是架构决定的P4 切流水线就是为了解决这个问题。4. P4-P5 流水线改造吞吐率翻倍背后的冒险与转发设计P4 开始做五级流水线把单周期里一大坨组合逻辑切分成取指、译码、执行、访存、写回五个阶段每个阶段之间插一组流水线寄存器。这个改造的核心收益是吞吐率单周期 CPU 一个周期只能完成一条指令流水线理想状态下每个周期都能完成一条。但代价是控制逻辑复杂度陡增数据冒险、分支冒险、结构冒险三座大山同时压过来。P5 通常再叠加中断和异常处理优先级、保存现场、恢复现场又给流水线塞了更多状态。这两级是整个项目链里投入产出比最低、最容易劝退人的部分但也是收获最大的部分。4.1 从单周期到五级流水线切分寄存器与硬件布局的第一次重构单周期 CPU 里所有信号在一个时钟周期内“流”完整条链路而流水线的核心思想是让信号分段流每段在时钟边沿被流水线寄存器捕捉。典型的五段划分是 IF、ID、EX、MEM、WB对应的四组寄存器是 IF/ID、ID/EX、EX/MEM、MEM/WB。先看 ID/EX 这一组最简单的写法always (posedge clk) begin if (rst) begin id_ex_reg_write 1b0; id_ex_mem_write 1b0; id_ex_alu_op 2b00; id_ex_rs 5b0; id_ex_rt 5b0; id_ex_rd 5b0; end else begin id_ex_reg_write id_ctrl_reg_write; id_ex_mem_write id_ctrl_mem_write; id_ex_alu_op id_ctrl_alu_op; id_ex_rs id_rs; id_ex_rt id_rt; id_ex_rd id_rd; end end看到规律没有每一组流水线寄存器就是把上一阶段的控制信号和数据信号在时钟边沿打一拍。id_ex_前缀表示这些信号属于 ID/EX 寄存器组后面 EX 阶段只认id_ex_开头的信号不再直接碰 ID 阶段的原始信号。这个命名约定非常重要P4 后期调试时你光是看信号前缀就知道它当前在流水线哪一级。P4 改造最常见的翻车点不是新增信号而是“漏打一拍”。有些控制信号你忘了插进流水线寄存器导致它提前或延后一个周期到达指令的行为就全乱了。我习惯的做法是开工前先画一张五级流水线的信号流表把每条控制信号在哪个阶段产生、在哪个阶段用、需要经过哪几组寄存器全部列出来再回到代码里逐个核对。这个表也是后面处理冒险的基础。4.2 数据冒险、分支冒险转发和停顿哪个先做数据冒险的本质是后面指令要用前面指令还没写回的结果。比如lw $t0, 0($t1)后面紧跟add $t2, $t0, $t3add 在 EX 阶段需要读 $t0但 lw 的$t0 要等到 MEM 阶段结束才有值。解决数据冒险有两个武器转发和停顿。转发是把还在流水线里的结果直接拉到 ALU 输入端不等写回停顿是让后面的指令在 ID 阶段空等一个周期。转发条件的 Verilog 写法很固定关键在于“源寄存器非零”这个条件wire [1:0] forward_a (ex_mem_reg_write ex_mem_rd ! 5b0 ex_mem_rd id_ex_rs) ? 2b10 : (mem_wb_reg_write mem_wb_rd ! 5b0 mem_wb_rd id_ex_rs) ? 2b01 : 2b00;2b10表示从 EX/MEM 寄存器转发2b01表示从 MEM/WB 寄存器转发2b00表示不转发。ex_mem_rd ! 5b0这个条件必须写因为 $zero 是恒零寄存器往它转发会把数值覆盖掉。转发不能解决所有问题lw 的“载入使用冒险”就是例外。当 lw 的结果直接给下一条指令做 ALU 输入时结果还在 MEM 阶段ALI 输入等不到。这时候只能停顿一周期wire lw_stall id_ex_mem_read (id_ex_rt if_id_rs || id_ex_rt if_id_rt);id_ex_mem_read由 lw 的mem_read控制信号打一拍得到。停顿的执行方式是冻结 PC 寄存器和 IF/ID 寄存器让取指停住同时把 ID/EX 的控制信号清零插入一个空操作气泡。P4 阶段把这两段逻辑写对数据冒险基本就解了。分支冒险比数据冒险更隐蔽。分支指令在 ID 阶段译码、在 EX 阶段用 ALU 比较如果跳转成立后面已经进入流水线的两条指令都要作废。常见做法是采用延迟槽分支后面紧跟的那条指令无论跳转与否都执行如果你实现的指令集没有延迟槽就要在跳转周期冲刷 IF/ID 和 ID/EX 的寄存器。这里一定要想清楚分支目标的计算基准是 PC4冲刷时机和转发时机要联合起来否则会出现“分支跳对了但后面指令执行了半条”的诡异波形。P5 加入异常后情况更复杂异常可能在 EX 阶段产生也可能在访存阶段产生处理器要先把已经在流水线里的指令全部处理完再把异常原因写入状态寄存器最后把 PC 切换到异常入口地址。常见的可靠做法是把所有异常都集中到 WB 阶段统一处理虽然浪费几个周期但保证不丢指令、不重复写寄存器。5. P3-P6 避坑实录五类高频症状的波形定位与修复顺序到了这个阶段大家开始各自踩坑。我把项目链里遇到的高频问题按“现象 → 原因 → 解决”整理成五条每条都是血泪经验你在波形里看到对应症状时可以直接按顺序排查。5.1 寄存器堆永远是 X复位与写使能没有真正落进时序逻辑现象仿真波形里 PC 正常递增指令存储器有输出但寄存器堆读端口一直是 X哪怕你给寄存器写了值读出来还是 X。原因你声明了reg [31:0] rf [0:31];但没有给寄存器堆加复位逻辑。仿真开始时所有寄存器都是 X只有等到写使能且写地址有效的周期才会被赋成具体值。如果你的测试程序第一条指令就在读一个还没写入的寄存器读出来的自然是 X。解决给寄存器堆加同步复位复位时把所有寄存器清零integer i; always (posedge clk) begin if (rst) begin for (i 0; i 32; i i 1) rf[i] 32b0; end else if (we waddr ! 5b0) begin rf[waddr] wdata; end end这里waddr ! 5b0仍然要保留防止向 $zero 写入。5.2 case 缺 default仿真正常综合却多出 32 个锁存器现象功能仿真一路绿灯但综合时报告里出现大量 “Found latch” 警告上板后处理器行为完全随机。原因你在译码 case 里只写了已定义的 opcode漏了 default。组合逻辑分支不完整时综合器为了保持输出会推断锁存器。锁存器是电平敏感的和寄存器时钟行为完全不一致直接导致数据通路上出现“隐形存储”。解决译码 always 块开头把所有输出赋默认值case 末尾补default: begin end。这两个动作加起来能从根上避免锁存器也是 P0-P2 就该养成的习惯。你可以在综合报告里搜索 “latch” 关键词确认是否清理干净。5.3 提前一个周期翻转非阻塞赋值被混用现象某个寄存器值总比你预期快一个周期比如连续执行两条 addi 指令第二条的结果提前出现在寄存器读端口。原因时序逻辑的 always 块里混用了阻塞赋值导致信号在时钟边沿立即更新和同一周期内其他信号产生相互覆盖。非阻塞赋值才是为寄存器建模的标准写法它在时钟边沿采样右侧的旧值然后统一更新到左侧。解决把时序块里的全部改成。组合逻辑块保留两者界限要严格。有一个简单自检法看到always (posedge clk)第一反应是检查块内是否全是只要出现一个这模块大概率要翻车。5.4 beq 永远不跳立即数符号扩展和左移对齐错位现象beq 条件明明为真PC 却总是走顺序路径或者跳了但跳到错误地址程序跑飞。原因分支目标算错。常见错误有两个一是立即数没有先符号扩展成 32 位再左移两位导致高 16 位全是 0负偏移直接变成正偏移二是基准地址错用 PC 而不是 PC4导致目标地址偏一条指令。MIPS 风格的分支偏移是相对于 PC4 的并且指令地址是字节地址但偏移字段以指令为单位所以要左移两位。解决把分支目标计算固定成一行标准写法每个项目都复用wire [31:0] branch_offset {{16{instr[15]}}, instr[15:0]} 2; wire [31:0] branch_target pc_plus_4 branch_offset;注意pc_plus_4必须是你真正传给下一条指令的那个 PC4不是重新组合出来的值。很多人的 PC4 在暂停或冲刷时并没有保持跳转就会算歪。5.5 lw 后面紧跟运算指令转发条件少了 rd ! 0现象lw $t0, 0($t1)后面紧跟add $t2, $t0, $t3仿真中 add 的结果等于旧 $t0 值而不是内存读出的新值。原因转发逻辑的条件写得过于宽泛漏掉了目标寄存器非零的判断。当 EX/MEM 或 MEM/WB 阶段的写目标寄存器是 $zero 时数据被转发到 ALU 输入端把恒零值冲掉了运算结果自然不对。解决检查转发条件确保每组都带! 5b0wire [1:0] forward_b (ex_mem_reg_write ex_mem_rd ! 5b0 ex_mem_rd id_ex_rt) ? 2b10 : (mem_wb_reg_write mem_wb_rd ! 5b0 mem_wb_rd id_ex_rt) ? 2b01 : 2b00;顺带确认 stall 条件里也用id_ex_rt和if_id_rs、if_id_rt做比较别写成了组合逻辑阶段的 rs/rt 原始值。流水线里所有信号都要加上阶段前缀这是排查这类问题最快的方式。6. P6 最后阶段的验证习惯给 CPU 装一台自动回归测试机P6 整机集成完成后你不再是一行行去看波形而是要建立一套自动对比的回归测试。具体做法是在 testbench 里用“任务 断言”的方式把每段汇编程序的预期结果写进去仿真跑完自动比对寄存器值。下面是一个可以直接套用的模板task expect_reg; input [4:0] idx; input [31:0] val; begin if (rf[idx] ! val) begin $display(FAIL: reg[%0d] %h, expect %h at time %0t, idx, rf[idx], val, $time); $stop; end end endtask initial begin // 让程序跑一段足够长的周期 repeat(200) (posedge clk); expect_reg(8, 32h0000_1000); // $t0 expect_reg(9, 32h0000_0034); // $t1 expect_reg(10, 32h0000_1000); // $t2 $display(PASS: all assertions passed); $finish; endexpect_reg是一个任务参数是寄存器号索引和期望值。每次执行到if判断rf 是寄存器堆变量仿真器会对这个模块直接访问。注意!是不定值敏感比较能把 8hXX 的情况也抓出来。跑一遍回归如果所有断言通过说明这 200 个周期里关键路径都走对了。我自己的习惯是每加一个小功能就往测试程序里补一段对应指令再补一条断言。比如加了 slt 指令就写slt $t3, $t0, $t1然后断言$t3等于 0 或 1。这种方法比盯着波形目测可靠得多也能在你改动转发逻辑后迅速发现回归问题。我曾因为在转发条件里删掉了一个零判断导致三条指令计算错误波形看一眼根本发现不了回归测试一跑立刻定位到了。后 P6 阶段别急着上板先把这套自动回归测试跑绿上板时遇到问题才有一个可回退的基线。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑