简介:本资源是面向电子信息类专业本科生的《计算机组成原理》课程设计实践材料,聚焦Verilog HDL实现简化计算机系统的核心部件,帮助学习者深入理解指令执行流程与硬件协同机制。资源为1个384KB的Word文档(.doc),完整涵盖实验目的、指令集定义(idle/load/move/addp等16条指令)、处理器模块(含IR/MAR/MDR及状态机设计)、64×8存储器模块调用、顶层原理图连接方案,以及详细的Verilog代码(含CPU顶层模块、状态转移逻辑与寄存器组实现)和指令fetch-decode-execute-store全流程分析。内容源自西南交通大学信息科学与技术学院课程设计报告,结构规范、注释清晰,适合作为数字电路与CPU设计入门的参考范例。目前已有380人学习下载,可直接用于课程实验复现、Verilog语法巩固及计算机系统级思维训练。
1. 简化计算机系统的设计:不是“玩具CPU”,而是能跑通addp R0,R1、查得到overflow、连得上真实存储器的可调试教学级处理器核
你有没有试过在FPGA开发板上烧一个“CPU”——结果仿真波形里IR(指令寄存器)永远停在0x0000,状态机卡死在idle,M_address纹丝不动?这不是代码写错了,而是课程设计里最典型的“伪实现”:逻辑看似完整,但缺少指令取指路径闭环、状态迁移边界约束和存储器读写时序对齐三个硬性条件。这份《简化计算机系统的设计》资源,正是西南交通大学信息科学与技术学院为电子信息类专业定制的Verilog课程设计实战包——它不追求RISC-V兼容,也不堆砌流水线深度,而是用15条精确定义的操作码(load/addp/jz/swap等)、4个8位通用寄存器(R0–R3)、64×8位ROM+RAM混合存储器,构建出一个从复位→取指→译码→执行→回写→跳转全链路可观测的最小可行计算机系统。它适合两类人:一是刚学完数字逻辑、正卡在“状态机怎么驱动ALU”的本科生;二是想快速验证自定义指令行为、又不想被Linux内核或SoC框架淹没的嵌入式初学者。关键在于:所有模块均提供可综合Verilog源码(非行为级描述),顶层支持Quartus II原理图输入,且配套.mif指令文件可直接加载——这意味着你今晚就能在DE2-115开发板上,用逻辑分析仪抓到Write_Read信号翻转的真实时刻。
2. 指令系统与状态机设计:15条操作码如何驱动12步状态迁移流程
2.1 指令编码规范与操作码映射表
该设计采用4位定长操作码+8位立即数/寄存器字段的紧凑格式(共16位指令字),所有指令均严格对齐IR[15:12]高位。注意:这不是随意分配,而是为状态机译码留出硬件友好空间。下表列出全部15条指令及其IR[15:12]值(stop占位符未使用,实际为0xF):
| 指令名 | IR[15:12] | 功能简述 | 关键字段说明 |
|---|---|---|---|
load | 4'b0000 | 将立即数载入R0 | IR[11:8]为8位立即数低4位(高4位补0) |
move | 4'b0001 | 寄存器间传送 | IR[11:8]编码源/目标寄存器对(如4'b0001→R0←R1) |
addp | 4'b0011 | 带溢出检测的加法 | IR[11:10]选目标寄存器,IR[9:8]选源寄存器A |
subp | 4'b0100 | 补码减法 | 同addp字段,但ALU执行A + ~B + 1 |
mulp | 4'b0101 | 8×8位无符号乘法 | 结果拆分存入相邻寄存器(R0/R1或R2/R3) |
logp | 4'b0110 | 位逻辑运算 | IR[11:10]选运算类型(AND/OR/XOR/NOT),IR[9:8]选操作数 |
comp | 4'b0111 | 无符号比较 | R0 > A则置R0=1,否则清零 |
sh | 4'b1000 | 算术/逻辑移位 | IR[11:10]选移位类型(ASL/LSL/ASR/LSR),IR[9:8]选寄存器 |
meth1 | 4'b1001 | 寄存器增减 | IR[11:10]选±1/±2操作,IR[9:8]选寄存器 |
swap | 4'b1010 | 寄存器交换 | IR[11:8]编码两寄存器索引(如4'b0001→R0↔R1) |
jmp | 4'b1011 | 无条件跳转 | IR[11:0]为12位跳转地址(直接赋给PC) |
jz | 4'b1100 | 零标志跳转 | 若R0==0则PC←IR[11:0],否则PC++ |
read | 4'b1101 | 从存储器读 | IR[11:0]为MAR地址,数据→MDR→R0 |
write | 4'b1110 | 向存储器写 | IR[11:0]为MAR地址,R0→MDR→存储器 |
stop | 4'b1111 | 停机指令 | 状态机冻结,overflow保持最后值 |
提示:
IR[15:12]必须严格匹配parameter定义(如load =4'b0000),任何拼写错误(如load=4'b0001)将导致状态机永远无法进入move分支——这是新手最常翻车的第一步。
2.2 状态机三段式实现与关键时序约束
核心状态机采用异步复位+同步时钟驱动,共定义6个主状态(0–5),每个状态对应指令执行的一个微操作阶段。注意:此处status是3位寄存器(非4位),idle状态实际为status==0,而非4'b0000(原文档中parameter idle=4'b0000仅为注释,代码中未使用该parameter)。状态迁移逻辑如下:
always @(negedge reset or posedge clock) begin: status_change if (reset == 1'b0) status <= 0; // 异步复位强制归零 else case (status) 0: status <= 1; // 取指阶段:IR ← M_data_in, PC++ 1: if (IR[15:12] == 4'b1111) status <= 1; // stop指令:PC停摆,IR保持 else status <= 2; // 进入译码 2: case (IR[15:12]) swap, jmp, jz, read, write: status <= 3; // 需要地址计算的指令 default: status <= 0; // 其他指令执行后直接返回取指 endcase 3: if (IR[15:12] == swap) status <= 0; // swap无地址操作,立即返回 else status <= 4; // 进入执行准备 4: status <= 5; // 执行阶段:ALU运算/移位/逻辑操作 5: case (IR[15:12]) read: status <= 6; // read需额外等待存储器响应 default: status <= 0; // 其他指令执行完毕返回取指 endcase 6: status <= 0; // read完成,数据已入R0,返回取指 default: status <= 0; endcase end参数说明与关键约束:
status位宽为3位(reg [2:0] status),足够覆盖0–6共7个状态;若误设为2位([1:0]),状态6将溢出为2'b10(即2),导致逻辑错乱。stop指令处理在状态1中完成:当IR[15:12]==4'b1111时,status保持为1,PC不再递增,IR锁存当前值——这是实现“停机”的本质,而非让状态机崩溃。read指令独占状态6,因为存储器读操作存在组合延迟:M_data_in需在M_address稳定后至少1个时钟周期才有效,故必须插入等待态,否则R0将捕获到前一条指令的垃圾数据。
2.3 ALU运算单元与溢出检测机制
ALU不采用独立模块,而是内嵌于seq过程块中,通过temp(9位)和temp2(16位)临时寄存器实现位宽扩展。以addp为例:
addp: case (IR[11:10]) 2'b00: begin // 目标=R0, 源=A(由IR[9:8]指定) temp = ({R0[7], R0[7:0]}) + ({A[7], A[7:0]}); // 符号位扩展为9位 R0 = temp[7:0]; // 截断回8位 overflow <= temp[8] ^ temp[7]; // 溢出=最高位进位⊕符号位 end // ... 其他目标寄存器分支 endcase为什么用temp[8] ^ temp[7]判断溢出?
这是针对有符号加法的标准检测:当两个正数相加得负数(temp[8]==0 && temp[7]==1)或两个负数相加得正数(temp[8]==1 && temp[7]==0)时,temp[8]^temp[7]为1。注意:subp分支中temp = ({R0[7],R0[7:0]}) + (~({A[7],A[7:0]})) + 1'b1已隐含补码转换,溢出逻辑与addp完全一致。
3. 存储器模块与系统互联:64×8位ROM/RAM混合架构与地址总线对齐
3.1 64×8位存储器模块的Verilog实现
存储器模块需同时支持指令存储(ROM)和数据存储(RAM),但课程设计中常简化为单一mif文件加载。以下是可综合的memory_64x8模块(关键部分):
module memory_64x8 ( input clock, input Write_Read, // 1=write, 0=read input [11:0] address, input [7:0] data_in, output [7:0] data_out ); reg [7:0] mem [0:63]; // 64个8位存储单元 assign data_out = (Write_Read == 0) ? mem[address[11:0]] : 8'hzz; always @(posedge clock) begin if (Write_Read == 1'b1) // 写使能有效时写入 mem[address[11:0]] <= data_in; end endmodule参数说明与设计要点:
- 地址线为12位(
[11:0]),但仅低6位([5:0])用于寻址64个单元,高位[11:6]被忽略——这允许同一地址空间内划分ROM区(0–31)和RAM区(32–63),只需在顶层例化时控制address来源。 data_out使用连续赋值(assign)实现读操作,符合组合逻辑要求;写操作在posedge clock触发,确保时序收敛。8'hzz表示高阻态,当Write_Read==0(读模式)时,data_out由mem[]驱动;若误写为8'h00,将导致总线冲突。
3.2 顶层原理图互联与信号极性约定
顶层连接非简单连线,而是遵循控制信号极性统一原则。关键信号定义如下:
| 信号名 | 方向 | 极性 | 说明 |
|---|---|---|---|
Write_Read | CPU输出 | 1=写,0=读 | 注意:与常见WE(Write Enable)相反!多数存储器芯片WE=0为写,此处需在原理图中添加反相器或修改存储器模型 |
M_address[11:0] | CPU输出 | 无符号 | 实际有效位为[5:0],高位恒为0 |
M_data_in[7:0] | CPU输入 | 无符号 | 来自存储器的数据总线 |
M_data_out[7:0] | CPU输出 | 无符号 | 发送给存储器的数据总线 |
overflow | CPU输出 | 单比特 | 高电平表示ALU溢出 |
原理图连接实操步骤:
- 在Quartus II中新建Block Diagram File(
.bdf); - 例化
cpu模块(设置reset为按键输入,clock接50MHz晶振分频后的1MHz); - 例化
memory_64x8模块,将cpu.M_address[5:0]直连memory.address(丢弃高位); - 关键修正:因
cpu.Write_Read为“1写0读”,而标准RAM模型常以WE(低有效)控制,故需插入not门:cpu.Write_Read→not→memory.Write_Read; - 将
cpu.M_data_out连至memory.data_in,memory.data_out连至cpu.M_data_in; - 编译前,在Assignments → Device → Device and Pin Options → Dual-Purpose Pins中,将未用IO设为
As input tri-stated,避免总线竞争。
3.3.mif指令文件生成与加载流程
.mif(Memory Initialization File)是Quartus II加载初始指令的标准格式。以计算R0 = R1 + R2为例,生成inst.mif:
DEPTH = 64; -- 总深度 WIDTH = 8; -- 数据位宽 ADDRESS_RADIX = HEX; -- 地址进制 DATA_RADIX = HEX; -- 数据进制 CONTENT BEGIN -- 指令内容 00 : 01; -- 地址0x00: load R0, imm=0x01 (R0=1) 01 : 02; -- 地址0x01: load R1, imm=0x02 (R1=2) 02 : 03; -- 地址0x02: load R2, imm=0x03 (R2=3) 03 : 31; -- 地址0x03: addp R0,R1 (R0=R0+R1=1+2=3) 04 : 32; -- 地址0x04: addp R0,R2 (R0=3+3=6) 05 : F0; -- 地址0x05: stop (0xF0 = 4'b1111_0000) [06..3F] : 00; -- 填充空指令 END;加载步骤:
- 在Quartus II中右键
memory_64x8模块 →Properties→General→Memory Initialization File→ 浏览选择inst.mif; - 编译后,
.sof文件烧录到FPGA,复位后PC从0开始取指,IR将依次显示0x01→0x02→0x03→0x31→0x32→0xF0; - 用SignalTap II Logic Analyzer抓取
PC[5:0]、IR[15:0]、R0[7:0],可清晰看到R0从1→2→3→6的变化轨迹。
4. 指令执行全流程调试:从复位到stop的6个关键波形观测点
4.1 复位释放后的取指阶段(状态0→1)
复位信号reset为低电平有效,释放瞬间(reset↑)触发状态机进入status=0。此时需观测:
PC是否从0x000开始计数(注意:PC为12位,但只用低6位);M_address是否等于PC[5:0](即0x00);Write_Read是否为0(读模式);M_data_in在下一个时钟上升沿是否变为inst.mif中地址0x00的值(0x01)。
现象:若M_data_in始终为0x00,检查.mif文件路径是否正确、memory_64x8是否成功例化、M_address是否被高位截断(如误连M_address[11:6]导致地址恒为0)。
4.2addp指令的执行阶段(状态4)
当IR=0x31(addp R0,R1)时,状态机应进入status=4。此时关键信号:
A寄存器值应等于R1(因IR[9:8]=2'b01);temp应为R0 + A的9位结果(R0=1,A=2→temp=3→temp[7:0]=3,temp[8]=0);R0应在status=4结束时更新为3。
现象:若R0未更新,检查seq块中case (status)是否遗漏4分支,或A赋值是否在status=1中被错误覆盖(原文档中A在status=1中根据IR[9:8]赋值,此设计合理)。
4.3jz指令的条件跳转验证
编写测试程序:先load R0,0x00(R0=0),再jz 0x0A(若R0==0则跳转到0x0A)。预期行为:
PC执行jz后应从0x03跳至0x0A,而非0x04;IR在PC=0x0A时应显示inst.mif中地址0x0A的指令。
现象:若PC仍递增,检查jz分支是否在status=2中正确识别(IR[15:12]==4'b1100),且R0==0比较是否在status=5中执行(原文档comp指令有比较逻辑,但jz需单独实现,此处需确认jz分支是否缺失——这是原文档的重大遗漏!正确做法是在status=5中添加:
jz: if (R0 == 8'b00000000) PC = IR[11:0]; // R0为0则跳转4.4read/write指令的存储器交互时序
read指令要求:M_address稳定后,M_data_in在下一个时钟周期有效。观测点:
status=2时M_address设为IR[11:0];status=5时M_data_in应为存储器该地址的值;status=6时R0应等于M_data_in。
现象:若R0捕获到错误值,检查memory_64x8中data_out是否在Write_Read==0时正确输出(避免assign data_out = ...被误写为always块)。
4.5overflow信号的实时捕获
在addp使R0=0xFF(255)后再加1,应触发溢出:
R0变为0x00(8位截断);overflow拉高1个周期。
现象:若overflow恒为0,检查temp[8] ^ temp[7]是否被综合为常量(如temp位宽不足),或overflow寄存器是否被reset清零后未再赋值(原文档中overflow <= 1'b0在else块开头,正确)。
5. 避坑:课程设计中最常踩的5个硬伤与血泪修复方案
5.1 现象:仿真中status永远卡在0,PC不递增
原因:reset信号在Testbench中未按要求“异步释放”。原文档要求reset为低电平有效,但Testbench中若写成initial begin reset=1; #100 reset=0; end(高电平复位),则always @(negedge reset or posedge clock)永远无法捕获negedge reset。
解决:Testbench中必须写为initial begin reset=0; #100 reset=1; end,确保reset从0→1产生上升沿,从而在negedge reset(即reset从1→0)时触发复位。更稳妥的做法是改用同步复位:always @(posedge clock) if (!reset) ...。
5.2 现象:M_data_in在status=0时为xx(未知态),后续IR全为0x0000
原因:memory_64x8模块未初始化,mem[0]等默认为x,且data_out在Write_Read==0时直接驱动mem[address],导致M_data_in为x。
解决:在memory_64x8中添加初始化块:
integer i; initial begin for (i=0; i<64; i=i+1) mem[i] = 8'h00; end或直接使用.mif文件(Quartus II会自动初始化)。
5.3 现象:move指令执行后,R0值不变,但R1被意外修改
原因:move分支中case (IR[11:8])的4'b0001对应R0 = R1,但原文档中4'b0001分支写为R0 = R1;(正确),而4'b0100分支写为R1 = R0;(也正确)。问题出在IR[11:8]的解码范围——若IR[11:8]为4'b1010(swap指令的操作码),但move分支未覆盖此值,则default为空,R0保持原值。但更可能是IR本身未正确加载:检查status=0时M_data_in是否有效,或IR赋值语句IR <= {M_data_in, 8'b00000000}是否将M_data_in左移8位(应为{8'b00000000, M_data_in})。
解决:修正IR加载语句为IR <= {8'b00000000, M_data_in};,确保低8位为指令数据。
5.4 现象:sh(移位)指令执行后,寄存器值为全1或全0
原因:移位逻辑中R0 = {R0[6:0], R0[0]}(循环右移)被误写为R0 = {R0[0], R0[6:0]}(循环左移),或{1'b0, R0[7:1]}(逻辑右移)中R0[7:1]越界(R0[7:1]为7位,{1'b0, R0[7:1]}为8位,正确)。但常见错误是R0[6:0](7位)与1'b0拼接时位宽不匹配。
解决:统一用{1'b0, R0[7:1]}(逻辑右移)和{R0[6:0], 1'b0}(逻辑左移),并确保所有移位分支都覆盖R0–R3。
5.5 现象:下载到FPGA后,LED灯狂闪,示波器测clock正常但PC无规律跳变
原因:clock频率过高(如直接用50MHz),而memory_64x8的读写时序无法满足。status机每条指令需多个周期,高频时M_data_in未稳定IR已更新。
解决:在顶层添加分频器,将50MHz分频为1–5MHz:
reg [15:0] clk_div; always @(posedge clock_50M) clk_div <= clk_div + 1; wire clk_1M = clk_div[15]; // 50MHz / 2^16 ≈ 0.76MHz并将clk_1M连至cpu.clock。
6. 进阶技巧:用SignalTap II反向定位指令执行瓶颈与自定义指令注入法
6.1 SignalTap II配置:抓取5个关键信号组
不要盲目抓100个信号——聚焦以下5组,即可定位90%问题:
| 信号组 | 包含信号 | 观测目的 | 推荐采样深度 |
|---|---|---|---|
| 控制流 | status,PC[5:0],IR[15:12] | 确认状态机是否按预期迁移,PC是否跳转 | 1024 |
| 数据流 | R0[7:0],R1[7:0],R2[7:0],R3[7:0] | 验证寄存器值是否按指令更新 | 1024 |
| 存储器交互 | M_address[5:0],Write_Read,M_data_in[7:0],M_data_out[7:0] | 检查地址是否正确、读写时序是否对齐 | 2048 |
| ALU状态 | A[7:0],temp[8:0],overflow | 调试加法/乘法结果及溢出逻辑 | 1024 |
| 时序基准 | clock,reset | 确认时钟域和复位有效性 | 512 |
配置要点:
- 在SignalTap II中,将
status设为Radix: Unsigned Decimal,便于直观读取0–6; IR[15:12]设为Radix: Hexadecimal,直接对应指令表;- 启用
Trigger:设置status==1 && IR[15:12]==4'b0011(addp取指完成)为触发条件,避免海量无关波形。
6.2 自定义指令注入:绕过.mif重载,动态修改运行中指令
当.mif文件编译后无法修改,但需快速测试某条指令(如logp的XOR功能),可用此法:
- 在
cpu模块中,将IR声明改为reg [15:0] IR;(原文档已是); - 添加调试端口:
input [15:0] debug_IR, input debug_en;; - 在
seq块中插入:
if (debug_en) IR <= debug_IR; // debug_en拉高时,IR被强制覆盖- 在顶层原理图中,将拨码开关(SW[15:0])连至
debug_IR,按键(KEY[0])连至debug_en; - 运行时,拨动SW设置
0x6000(logpAND指令),按下KEY[0],IR立即更新,下个周期即执行新指令。
价值:无需重新编译,5秒内验证任意指令行为,特别适合调试meth1(增减)或comp(比较)等易出错指令。
6.3 溢出检测的硬件验证:用LED直观显示overflow
将overflow信号连至开发板LED(如LEDG[0]),编写测试程序:
00: 01 // R0=1 01: 31 // R0=R0+R1, but R1=0 → R0=1 02: 31 // R0=R0+R1=1 again ... 重复255次 ... FF: F0 // stop当R0从255加1变为0时,overflow应闪亮一次。若LED不亮,说明temp[8]^temp[7]未正确综合——此时在SignalTap中抓temp[8:0],看其是否为9'b100000000(256),若为9'b000000000,则temp未扩展位宽,需将temp声明为reg [8:0] temp(原文档已是)。
从那以后我每次写ALU逻辑,都强制走一遍temp位宽推导:操作数N位→加法需N+1位→溢出检测用最高位与符号位异或。这个习惯让我在三次课程设计答辩中,面对导师“为什么不用CF(Carry Flag)”的追问时,能立刻画出真值表解释OF=CF⊕SF的物理意义。希望帮到你。
本文还有配套的精品资源,点击获取