简介:面向计算机组成原理课程学习者与备考北航计算机学院课设的同学,这份资料围绕Logisim与Verilog-HDL两条主线,覆盖从部件、状态机到单周期、流水线CPU的完整项目链。包内共1541个文件,以733个v源码、138个sdb工程库、88个c与48个asm程序、14个circ电路,以及prj、xise、pdf、docx、txt等工程与文档文件为主,压缩包约33.25MB,便于按P0—P8阶段检索。已有288人学习,内容包含CRC校验、ALU、splitter、格雷码计数器、合法表达式识别、矩阵乘法与回文判断等课下测试,也涉及7条指令单周期CPU、10条指令流水线CPU及新增指令的课上测试,可帮助读者对照实现思路、补齐工程文件并复盘验证方法。
1. 计算机组成原理课程设计到底在做什么:从一条 add 指令跑通说起
一次中期检查上,某导师让学生现场把一段汇编里的 add 指令讲清楚——从取指到写回,每个时钟周期数据走在哪条线上、哪个控制信号是 1。学生能背出五级流水线的名字,却说不清reg_write到底在哪一拍拉高。计算机组成原理课程设计考的从来不是画图,而是你能不能把一条指令拆成硬件动作,再让它在开发板上真的跑出结果。它适合已经学过数字电路、会一点 Verilog,但没完整做过一个处理器的同学;也适合想弄清楚「CPU 里那些玄学时序到底怎么来的」的从业者。下面按指令集裁子集、搭单周期、改流水线、避坑、验证的顺序走一遍。
2. 指令集子集怎么砍:从 40 条指令收到 15 条还能跑程序
课程设计的第一道坎不是写代码,是决定「做多少」。完整的 RV32I 有 40 多条指令,全做完不现实,砍多了又跑不起来像样的测试程序。我一般按「能拼出一段带循环和数组访问的汇编」这个标准来定子集,这条线以下的都保留,以上的果断删。
2.1 保留哪些指令:一份能拼出冒泡排序的最小清单
判断依据很简单:算术、逻辑、访存、跳转四类各留几条,能凑出一个带for循环和数组读写的程序就够。下表是我常用的取舍,右列写的是删掉之后的后果,方便你权衡。
| 类型 | 指令 | 是否保留 | 删掉的后果 |
|---|---|---|---|
| R 型算术 | add、sub | 保留 | 没有减法,循环计数器写不了 |
| R 型逻辑 | and、or、slt | 保留 | slt 是循环比较的命根子 |
| I 型算术 | addi、andi | 保留 | 常量初始化全靠它 |
| 访存 | lw、sw | 保留 | 没有数组,测试程序退化成纯寄存器运算 |
| 分支 | beq、bne | 保留 | 循环和 if 都做不了 |
| 跳转 | jal | 保留 | 函数调用和跳过程序头都需要 |
| 立即数高位 | lui | 视情况 | 不访存大地址可以删 |
| 移位 | sll、srl、sra | 视情况 | 只做排序可以删,做乘除必须留 |
| 字节访存 | lb、sb、lh、sh | 可删 | 只按字访问时用不上 |
砍到 15 条左右是比较舒服的量:数据通路复杂度可控,控制单元的真值表一页纸能写完,测试程序也足够有说服力。要注意slt的有符号比较实现,很多人直接写a < b,在 Verilog 里如果两个操作数都声明成wire [31:0],默认按无符号比,负数会翻车。正确写法是先做符号扩展再比较,或者显式用$signed(a) < $signed(b)。
2.2 数据通路的五段划分与端口约定
单周期 CPU 的数据通路可以按取指、译码、执行、访存、写回五段切,虽然单周期里它们在同一拍完成,但按这个思路切模块,后面改成流水线时只需要在段之间插寄存器,改动量最小。我给每个模块定的端口约定是这样的:所有模块共用clk和低有效异步复位rst_n;数据类端口一律 32 位;控制类信号尽量做成单比特,避免后面接控制单元时出现位宽不匹配的告警。
指令存储器和数据存储器分开做(哈佛结构),这是课设里最常见也最省事的做法。指令存储器只读,用$readmemh在仿真开始时加载镜像;数据存储器可读可写,初值可选。如果坚持冯诺依曼结构,单口存储器会在流水线阶段引入结构冒险,第五章会专门讲这个坑。
PC 的更新逻辑放在独立模块里,输入是下一拍地址pc_next,输出当前pc。这样无论是单周期的pc + 4,还是流水线里被分支单元改写的地址,都只改pc_next的来源,PC 寄存器本身不动。
2.3 控制信号编码表:把真值表写在纸上再写代码
控制单元是整个设计里最容易写出锁存器的地方。always @(*)里case没写default,综合工具就会推断出锁存器,仿真可能碰巧对,上板就出怪事。我的习惯是先把真值表列全再敲代码,下表是节选。
| 指令类型 | RegWrite | MemRead | MemWrite | ALUSrc | Branch | Jump | ResultSrc |
|---|---|---|---|---|---|---|---|
| R 型 | 1 | 0 | 0 | 0 | 0 | 0 | 0(ALU) |
| addi | 1 | 0 | 0 | 1 | 0 | 0 | 0(ALU) |
| lw | 1 | 1 | 0 | 1 | 0 | 0 | 1(MEM) |
| sw | 0 | 0 | 1 | 1 | 0 | 0 | x |
| beq | 0 | 0 | 0 | 0 | 1 | 0 | x |
| jal | 1 | 0 | 0 | x | 0 | 1 | 2(PC+4) |
ResultSrc用两位编码写回来源:0 是 ALU 结果,1 是存储器读出数据,2 是pc + 4。ALUOp单独给两位,交给 ALU 控制译码器再展开成四位的 ALU 操作码,这样 R 型指令的funct3、funct7只需要在 ALU 控制里处理,主控制单元不用管。
2.4 存储器与复位:两个最容易被忽略的设计决定
存储器深度定多大,直接决定你能不能跑完整测试程序。指令存储器我一般给 1K×32 位,够放 256 条指令;数据存储器同样 1K×32 位。综合到板子上时,这点容量会占掉不少块 RAM,如果资源紧张,可以缩到 256×32,但要把测试程序裁短。
复位方式选同步还是异步,是个需要提前定死的事。异步复位写起来简单(always @(posedge clk or negedge rst_n)),但如果复位信号是按键来的,毛刺会让整个 CPU 反复复位。稳妥做法是按键先过两级同步器再当复位用;如果板子上的复位是专用复位芯片给的干净信号,异步复位没问题。这个决定一旦定下,所有模块必须统一,混用会导致某些寄存器复位了、某些没复位,PC 直接跑飞。
3. 单周期 CPU 的 Verilog 骨架:五个模块加一个顶层的最小可仿真工程
先把单周期跑通,再谈流水线,这是我认为最省时间的路径。单周期的数据通路是流水线的退化形式,把单周期验证过的 ALU、寄存器堆、存储器原样搬到流水线里,只需要在段间加寄存器。这一章给出能直接编译运行的骨架。
3.1 顶层与五个子模块的职责边界
顶层只做连线,不放任何逻辑。这是硬规矩:一旦顶层里出现了assign运算,说明模块划分没切干净,后面加流水线寄存器时会非常痛苦。五个子模块分别是 PC 寄存器、指令存储器、数据通路(含译码、寄存器堆、ALU、写回选择)、数据存储器、ALU。
// cpu_top.v —— 单周期 RV32I 子集 CPU 顶层,只负责连线 module cpu_top ( input wire clk, input wire rst_n, output wire [31:0] pc, output wire [31:0] alu_result, output wire [31:0] wr_data, output wire mem_we ); wire [31:0] instr; wire [31:0] rd_data; wire [31:0] pc_next; // 取指段:PC 自增由 datapath 里的分支逻辑决定 pc_next 的来源 pc_reg u_pc (.clk(clk), .rst_n(rst_n), .pc_next(pc_next), .pc(pc)); imem u_imem (.addr(pc), .instr(instr)); // 译码 + 执行 + 写回,内部再切子模块 datapath u_dp ( .clk(clk), .rst_n(rst_n), .instr(instr), .rd_data(rd_data), .pc(pc), .pc_next(pc_next), .alu_result(alu_result), .wr_data(wr_data), .mem_we(mem_we) ); dmem u_dmem (.clk(clk), .we(mem_we), .addr(alu_result), .wd(wr_data), .rd(rd_data)); endmodule这段顶层的逻辑说明只有一句:pc送到指令存储器取指令,datapath根据指令算出下一条 PC、ALU 结果和写回值,mem_we决定数据存储器这一拍写不写。参数上唯一需要提前确认的是pc_next的位宽必须和pc一致,都是 32 位;如果为了省资源把地址裁成 16 位,后面跳转指令的偏移量计算会溢出,测试程序跑到一半就跳错地方。
3.2 ALU 与寄存器堆:先写能自测的小模块
我的习惯是先写 ALU 和寄存器堆,用独立 testbench 测通,再拼大系统。ALU 是所有模块里最容易出问题又最容易测的,六种操作写全,default分支必须有。
// alu.v —— 组合逻辑,default 必须有,否则推断锁存器 module alu ( input wire [31:0] a, input wire [31:0] b, input wire [3:0] op, output reg [31:0] y, output wire zero ); always @(*) begin case (op) 4'b0000: y = a + b; // add / addi / 地址计算 4'b0001: y = a - b; // sub / beq 比较 4'b0010: y = a & b; // and / andi 4'b0011: y = a | b; // or / ori 4'b0100: y = ($signed(a) < $signed(b)) ? 32'd1 : 32'd0; // slt 4'b0101: y = b; // 直通,给 lui 或 PC 传值 default: y = 32'b0; // 兜底,防止锁存 endcase end assign zero = (y == 32'b0); // beq/bne 用,组合输出 endmodulezero单独用assign输出而不是在always里赋值,是为了让分支判断能和其他组合逻辑并行,减少关键路径长度。slt那行用了$signed强制转换,这是血泪经验:不写这句,比较-1 < 1会得到 0。
寄存器堆是另一个易错点。32 个 32 位寄存器,两个读口一个写口,读是组合的,写是时序的,且第 0 号寄存器恒为 0、不允许写。
// regfile.v —— 双读单写,x0 恒零 module regfile ( input wire clk, input wire we, input wire [4:0] rs1, rs2, rd, input wire [31:0] wd, output wire [31:0] rd1, rd2 ); reg [31:0] regs [0:31]; assign rd1 = (rs1 == 5'd0) ? 32'b0 : regs[rs1]; assign rd2 = (rs2 == 5'd0) ? 32'b0 : regs[rs2]; always @(posedge clk) begin if (we && rd != 5'd0) regs[rd] <= wd; // 非阻塞赋值,x0 写保护 end endmodule读口用assign做组合输出,并且显式判断rs1 == 0返回零,比靠初始化regs[0]为 0 更可靠——因为写保护一旦漏写,regs[0]被覆盖,整个程序全错。写口用非阻塞赋值,rd != 0的保护必须写在else之外的条件里。
3.3 用 testbench 跑通第一条 add
顶层和子模块都齐了,下一步是让第一条指令跑起来。testbench 里要做三件事:产生时钟、加载指令镜像、在若干拍后打印状态。
// tb_cpu.v —— 最小验证环境 `timescale 1ns/1ps module tb_cpu; reg clk = 0; reg rst_n = 0; wire [31:0] pc, alu_result, wr_data; wire mem_we; cpu_top u_cpu (.clk(clk), .rst_n(rst_n), .pc(pc), .alu_result(alu_result), .wr_data(wr_data), .mem_we(mem_we)); always #5 clk = ~clk; // 周期 10ns,对应 100MHz initial begin $readmemh("inst.hex", u_cpu.u_imem.mem); // 指令镜像,每行一个字 $readmemh("data.hex", u_cpu.u_dmem.mem); // 数据初值,可选 rst_n = 0; #20 rst_n = 1; // 复位保持两拍 repeat (50) @(posedge clk); $display("pc=%h alu=%h we=%b wd=%h", pc, alu_result, mem_we, wr_data); $finish; end endmodule#5 clk = ~clk决定主频,仿真里这个值不重要,但如果后面要上板,综合后的时序必须满足这个周期。#20 rst_n = 1表示复位保持两个时钟周期,太短可能有的寄存器没复位干净,太长浪费时间。repeat (50)是拍数,具体要跑多少拍取决于程序长度,跑少了看不到最终结果,跑多了只是多花仿真时间,我一般先给一个较大值,看日志里的 PC 是否停在预期的结束位置,再把拍数固定下来。
指令镜像inst.hex的生成方式有很多,我习惯用汇编器输出二进制后再转成十六进制文本,或者用一段 Python 脚本手工拼。要注意$readmemh默认按行读,每行一个十六进制字,地址从 0 开始递增,文件里不能有空行和注释,否则某些仿真器会报 warning 后跳过。
3.4 Verilator 与 Icarus 的 lint 和仿真命令
仿真器选 Icarus 够用,编译快、报错直白;Verilator 主要用来做静态检查,它的 lint 能抓出位宽不匹配、未驱动信号、锁存器推断这三类问题,上板前跑一遍能省掉大量调试时间。
# Icarus:编译后直接跑仿真 iverilog -g2012 -o tb_cpu tb_cpu.v cpu_top.v pc_reg.v imem.v datapath.v dmem.v alu.v regfile.v vvp tb_cpu # Verilator:只做静态检查,不生成仿真器 verilator --lint-only -Wall -Wno-DECLFILENAME -Wno-UNUSED cpu_top.v-g2012是语法标准,用了$signed之类的系统函数要带上。-Wall打开全部告警,-Wno-DECLFILENAME是忽略「文件名和模块名不一致」的告警,这种告警在课设里通常是噪音。真正要盯的是WIDTH和LATCH两类:前者说明赋值两边位宽不一致,后者说明组合逻辑推断了锁存器,两个都是上板翻车的高发原因。
4. 五级流水线改造:数据冒险、控制冒险、结构冒险的三条处理路线
单周期跑通之后,改流水线是课设的重头戏,也是拉开差距的地方。改法很简单:在五个段之间插四组流水线寄存器(IF/ID、ID/EX、EX/MEM、MEM/WB),让每一拍所有段都在干活。麻烦在于指令之间开始互相干扰,这就是冒险。
4.1 流水线寄存器插在哪,每一拍带什么信号
流水线寄存器不是随便插的,每个寄存器要带走「这一拍算出来的、下一拍还要用的」全部信息。IF/ID 带pc和instr;ID/EX 带两个读出的寄存器值、立即数、目标寄存器号、控制信号、pc;EX/MEM 带 ALU 结果、写数据、目标寄存器号、访存控制;MEM/WB 带读出的存储器数据和 ALU 结果、写回控制。
控制信号要跟着流水线一路带下去,而不是在需要用的那一拍重新译码。这是常见的误用:有人在 MEM 级重新对指令译码来判断要不要写存储器,结果指令早就流水到后面几级了,译码出来的东西对不上。正确做法是控制单元只在 ID 级出现一次,译码结果打进 ID/EX 寄存器,然后逐级传递。
| 流水线寄存器 | 携带内容 | 位宽估算 |
|---|---|---|
| IF/ID | pc、instr | 64 |
| ID/EX | rs1 值、rs2 值、imm、rd 号、pc、全部控制信号 | 约 110 |
| EX/MEM | ALU 结果、rs2 值、rd 号、MemWrite、MemRead、RegWrite | 约 75 |
| MEM/WB | 读出的数据、ALU 结果、rd 号、RegWrite、ResultSrc | 约 70 |
位宽估算不需要精确到个位,但要知道大致的量级,因为这决定了触发器资源的消耗。课设板子上的逻辑资源通常够用,如果资源吃紧,可以砍掉不用的控制信号,比如不做字节访存就不用带funct3。
4.2 数据冒险:转发优先还是阻塞优先
数据冒险的经典处理是转发(forwarding)。后一条指令要用前一条指令刚算出来、还没写回的值,与其等它写回再读,不如直接从 EX/MEM 或 MEM/WB 把结果拽过来。
// hazard_unit.v —— 转发选择 + load-use 停顿 module hazard_unit ( input wire [4:0] id_rs1, id_rs2, input wire [4:0] ex_rd, mem_rd, input wire ex_regwrite, mem_regwrite, input wire ex_memread, // EX 级是 load 指令 output reg [1:0] fwd_a, fwd_b, // 00 原值 01 MEM 10 EX output reg stall_if, stall_id, flush_ex ); always @(*) begin // 转发优先级:EX 级结果比 MEM 级更新,先看 EX fwd_a = (ex_regwrite && ex_rd != 5'd0 && ex_rd == id_rs1) ? 2'b10 : (mem_regwrite && mem_rd != 5'd0 && mem_rd == id_rs1) ? 2'b01 : 2'b00; fwd_b = (ex_regwrite && ex_rd != 5'd0 && ex_rd == id_rs2) ? 2'b10 : (mem_regwrite && mem_rd != 5'd0 && mem_rd == id_rs2) ? 2'b01 : 2'b00; // load-use:数据要等 MEM 级才出,转发也救不了,只能停一拍 if (ex_memread && (ex_rd == id_rs1 || ex_rd == id_rs2)) begin stall_if = 1'b1; stall_id = 1'b1; flush_ex = 1'b1; // 同时清掉 EX 级,插入气泡 end else begin stall_if = 1'b0; stall_id = 1'b0; flush_ex = 1'b0; end end endmodulefwd_a和fwd_b的两位编码交给 EX 级的多路选择器,00 用寄存器堆原值,01 用 MEM/WB 阶段写回的值,10 用 EX/MEM 阶段的 ALU 结果。ex_rd != 5'd0这个判断不能省,否则 x0 会被当成普通寄存器转发,程序里所有「读零」的地方都拿到垃圾值。flush_ex在 load-use 时清空 EX 级,插入一个气泡,等于让流水线停一拍。停顿条件只对 load 指令成立,因为只有 load 的数据在 MEM 级末尾才出来,其他指令在 EX 级就算完了,转发能覆盖。
阻塞优先还是转发优先,我的建议是转发能覆盖的全部转发,只对 load-use 阻塞。全阻塞实现简单,但每条相邻指令都要停,流水线加速比掉到接近 1,课设答辩时会被追问为什么做了流水线却没提速。
4.3 控制冒险:延迟槽、静态预测与清空
分支指令在 EX 级才算出来跳不跳,而这时候 IF 级已经把后面两条指令取进来了。处理方式有三种:延迟槽、静态预测、清空流水线。
延迟槽的做法是规定分支指令后面的那条指令一定执行,编译器负责填有用的指令或者nop。硬件最简单,但要求写测试程序的人配合,课设里一般不推荐,因为手写汇编很容易忘。
静态预测是默认跳或不跳,猜错了再清空。我的习惯是预测不跳(顺序执行),猜错时把 IF/ID 和 ID/EX 清空,pc改成目标地址。猜错的代价是两个周期,比延迟槽灵活,实现也直观。
// 分支在 EX 级出结果,猜错则清空 IF/ID 与 ID/EX if (branch_taken_ex != predict_not_taken) begin flush_if = 1'b1; flush_id = 1'b1; pc_next = branch_target; endbranch_taken_ex由 EX 级的zero信号和分支控制拼出来,branch_target是pc + imm。清空信号要同时作用于 IF/ID 和 ID/EX 两级,只清一级会导致后一条指令变成半截状态,这个坑在调试时非常隐蔽——现象是偶尔某条指令被跳过,但仿真日志里看不出来。
4.4 结构冒险:单口存储器带来的额外停顿
如果指令和数据共用一个存储器,IF 段取指和 MEM 段访存会撞车,这就是结构冒险。课设里最简单的解法是哈佛结构,指令存储器和数据存储器各用一块,从根上避开。如果板子资源紧张必须共用,那就得在 MEM 段访存时让 IF 段停一拍,或者给存储器做双口。
我倾向于哈佛结构,理由是课设的存储器本来就不大,两块 1K×32 位的块 RAM 大多数板子都放得下,而做双口或加停顿会让控制逻辑复杂一大截,调试时间翻倍。这个取舍在答辩时也容易讲清楚:用资源换控制复杂度,是嵌入式设计里常见的思路。
5. 避坑与排查:仿真全绿、上板全黑时的五个高频问题
流水线写完、仿真通过,只是走完一半。真正耗时的是上板调试,下面五条是我和周围人反复踩过的。
5.1 现象:仿真波形完全正确,综合烧录后板子毫无反应
原因多半出在时钟和复位。仿真里时钟是 testbench 产生的理想方波,上板后时钟来自晶振经过分频,如果分频计数器复位值不对,时钟根本起不来;复位信号如果是按键,抖动会让 CPU 反复复位。
解决顺序是先用示波器或逻辑分析仪量时钟引脚有没有波形,再看复位信号在按键按下时电平是否稳定。按键复位加两级同步器,分频计数器给一个确定的初值,这两步做完再排查逻辑。
5.2 现象:复位释放后 PC 跳到一个奇怪的地址,程序从中间开始跑
原因是复位不统一。有的模块用异步复位,有的用同步复位,还有的模块忘了写复位分支。综合工具不会报错,仿真时因为所有寄存器初值都是 x,有时候碰巧也能跑对。
解决办法是全设计统一复位风格,写一个脚本把所有时序模块的always块抓出来,逐个确认有没有rst_n分支。更稳妥的做法是在顶层给一个全局复位,所有子模块只接收这一个信号,不各自做同步。
5.3 现象:某个寄存器偶尔被写入错误的值,重跑仿真结果还不一样
这是典型的阻塞赋值和非阻塞赋值混用。时序逻辑里写=,组合逻辑里写<=,在简单设计里可能看不出问题,一旦流水线里多个 always 块互相依赖,仿真器的调度顺序就会影响结果,表现为「玄学」。
规矩只有一条:always @(posedge clk)里一律用<=,always @(*)里一律用=。检查方法是用 Verilator 的-Wall跑一遍,或者人工搜一遍所有时序块。
5.4 现象:$readmemh加载失败,存储器全是 x
原因通常是路径或格式。$readmemh的相对路径是相对于仿真器的工作目录,不是相对于源文件;hex 文件里如果有空行、注释、0x前缀,部分仿真器会直接跳过整行而不报错。
解决方法是先用绝对路径确认能读到,再把路径改回相对;hex 文件每行只写八位十六进制,不带前缀,结尾不多留空行。加载后加一句$display("mem[0]=%h", u_imem.mem[0]);确认第一个字读进来了。
5.5 现象:综合报告里时序不收敛,最高频率远低于预期
原因是关键路径太长,最常见的是 ALU 后面直接接分支判断再接 PC 选择,一整条组合链穿过好几级。单周期设计里这个问题不明显,流水线里如果没把分支判断切到 EX 级,路径会穿过 IF、ID、EX 三段。
解决办法是把分支判断放在 EX 级,pc_next的选择用寄存器打一拍,或者把 ALU 的加法器和比较器拆开。看综合报告里的时序路径,找到 slack 最差的那条,顺着它往前找组合逻辑,一般能定位到一两个多路选择器嵌套的地方。
6. 收尾阶段的两个习惯:指令级自检和波形回看
课设做到最后,容易陷入「仿真对了就交」的状态,但答辩时被问一句「你怎么证明它真的按指令集在跑」就露馅了。我后来固定了两个习惯,省下不少返工。
第一个是用参考模型做指令级自检。写一段 Python 或者直接用汇编器配套的模拟器,把同一段测试程序跑一遍,输出每条指令执行后的寄存器快照,再和 Verilog 仿真里打印的快照逐条比对。比波形快得多,也比人眼看寄存器值可靠。伪代码大致是这样:
# ref_model.py —— 用软件模型产出期望的寄存器快照 regs = [0] * 32 pc = 0 trace = [] while pc < len(prog): inst = prog[pc] # 这里按你的指令子集逐条实现语义 rd, rs1, rs2, imm, op = decode(inst) if op == "add": regs[rd] = (regs[rs1] + regs[rs2]) & 0xFFFFFFFF if op == "addi": regs[rd] = (regs[rs1] + imm) & 0xFFFFFFFF # ... 其余指令略 trace.append((pc, list(regs))) pc += 4 print(trace[-1]) # 和 Verilog 打印的最终寄存器组比对& 0xFFFFFFFF是为了模拟 32 位溢出回绕,Python 的整数是任意精度,不加这句减法会出负数,和硬件对不上。decode按你实际保留的指令子集写,不用全。这个模型写起来半小时,能省掉后面几小时的波形对照。
第二个是波形回看固定看三个点:复位释放后的第一个pc、第一条访存指令的mem_we和地址、分支指令前后的pc_next变化。这三个点覆盖了绝大多数问题:复位不对、地址算错、跳转没生效。我一般把这三组信号加到波形窗口的固定位置,每次仿真直接看,比从头翻波形快得多。
踩过最疼的一次是流水线里控制信号忘了跟寄存器走,仿真时因为组合逻辑延迟碰巧对,上板后 load 指令偶尔写回错误地址。后来养成习惯,凡是跨级的信号一律进流水线寄存器,宁可多几十个触发器,也不让组合逻辑跨段。希望帮到你。
本文还有配套的精品资源,点击获取