☰
Verilog HDL实现简化计算机:16条指令与64×8存储器设计
2026/10/28 14:01:07 网站建设 项目流程

简介:这份资源是面向电子信息类专业学生的《计算机组成实验》课程设计文档,聚焦用Verilog HDL实现简化计算机核心部件,帮助读者理解处理器与存储器协同工作的底层原理。设计内容涵盖简单指令系统(idle、load、move、addp、subp、mulp、logp、comp、sh、swap、jmp、jz、read、write、stop等)的编码与执行流程、处理器模块的寄存器与指令寄存器设计、64×8存储器模块调用,以及顶层原理图连接与指令fetch、decode、execute、store流程分析。资源包共1个doc文件,约384KB,为完整实验报告文档,含顶层BLOCK图、Verilog源码与状态机实现细节,可直接对照复现实验。已有382人学习,适合课程设计参考与Verilog入门练习。

1. 从一条“跑不通”的指令说起:这套简化计算机系统到底要交什么

课程设计里最让人心里没底的,往往不是写不出代码,而是把处理器和存储器接起来之后,指令跑出来的结果和手算对不上。这套《计算机组成实验》课设要交付的东西其实很明确:用 Verilog HDL 写一个简单处理器模块,配一个 64×8 的存储器模块,顶层把两者连成简化计算机核心部件,再把一段指令序列灌进存储器,逐条分析取指、译码、执行、写回的流程。它面向电子信息类专业的学生,也适合需要一份可复现组成原理方案的从业者参考。指令集里 idle、load、move、addp、subp、mulp、logp、comp、sh、meth1、swap、jmp、jz、read、write、stop 这些状态怎么流转,才是这份方案真正要讲清楚的东西。

2. 需求与指令系统设计:16 条指令如何撑起一个最小可运行模型

2.1 先定指令格式,再谈处理器

这套方案最容易翻车的地方,是一上来就写 always 块,写到一半发现指令字段不够用。我一般会先把 16 位指令字的格式钉死:高 4 位IR[15:12]是操作码,中间若干位是寄存器编号或操作类型,低位留给立即数或地址偏移。原文里load指令取IR[11:8]作为立即数装入 R0,move用IR[11:8]选源和目的寄存器,sh用IR[11:10]选移位类型、IR[9:8]选目标寄存器,addp/subp/mulp/logp/comp用IR[11:10]选参与运算的寄存器,meth1则同时用IR[11:10]和IR[9:8]区分加一、减一、加二、减二。把这些字段位置整理成一张表,后面写 case 才不会乱。

指令操作码 IR[15:12]关键字段用途结果去向
load0001IR[11:8] 立即数R0
move0010IR[11:8] 源/目的组合寄存器间搬移
addp0011IR[11:10] 目标寄存器目标寄存器 + A
subp0100IR[11:10] 目标寄存器目标寄存器 - A
mulp0101IR[11:10] 目标寄存器低字节与高字节分存
logp0110IR[11:10] 运算类型、IR[9:8] 目标与/或/异或/取反
comp0111IR[11:10] 比较对象结果写回标志值
sh1000IR[11:10] 移位类型、IR[9:8] 目标移位结果
meth11001IR[11:10] 增减类型、IR[9:8] 目标自增自减
swap/jmp/jz/read/write1010~1110地址或跳转条件控制流与访存
stop1111无停机

这张表不是摆设。写case (IR[15:12])的时候,每个分支里再嵌套case (IR[11:10])或case (IR[9:8]),层级和字段一一对应,调试时看波形就能定位到具体哪一位译码错了。很多同学把sh的移位类型和目标寄存器位宽记混,结果右移变成了逻辑右移和算术右移不分,这就是字段没先定清楚的代价。

2.2 状态机是这套处理器的主心骨

原文的status_change块用status寄存器把一条指令拆成多个节拍,这是简化处理器最朴素也最有效的做法。复位时status <= 0,之后每个时钟上升沿推进:状态 0 取指,把M_data_in拼到IR高 8 位并让 PC 加一;状态 1 根据操作码做寄存器准备和 MAR 赋值;状态 2 处理swap/jmp/jz/read/write这类需要额外节拍的指令;状态 3 到 5 继续推进访存和写回。stop指令在状态 1 里判断到就停在状态 1,不再往下走,这就是停机逻辑。

always @(negedge reset or posedge clock) begin: status_change if (reset == 1'b0) status <= 0; else case (status) 0: status <= 1; // 取指节拍 1: if (IR[15:12] == stop) status <= 1; // 停机,锁死在状态1 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; else status <= 4; 4: status <= 5; 5: case (IR[15:12]) read: status <= 6; default: status <= 0; endcase default: status <= 0; endcase end

这段状态机的设计说明要讲透:状态 0 和状态 1 是所有指令共用的取指与译码准备;状态 2 只对需要访存或跳转的指令开放,普通算术逻辑指令在状态 1 里就把活干完直接回 0,这样节拍数最少;状态 3 到 5 是read这类多节拍访存的专用通道。参数上,status用 3 位就够表示 0 到 6,原文声明成reg [2:0]是合理的。改的时候注意,如果把某条指令挪到状态 2 处理,一定要同步改状态 2 的 case 分支,否则它会掉进default回 0,表现为“这条指令好像没执行”。

2.3 寄存器组与运算单元的职责边界

seq块里声明了 PC、R0 到 R3、A、temp、temp2 这些变量,其中 PC 和 R0~R3、A 用的是阻塞赋值=,IR、MAR、MDR 用的是非阻塞<=。这个混用在教学代码里很常见,但必须理解它的意图:PC 和通用寄存器在同一个节拍内要立刻参与后续组合判断,所以用阻塞;IR、MAR、MDR 是跨节拍保持的时序元件,用非阻塞避免竞争。A 寄存器扮演的是运算暂存角色,addp/subp/mulp/logp/comp/swap在状态 1 先把目标寄存器内容读进 A,状态 2 再拿 A 和另一个寄存器运算,这样运算单元只需要一个操作数端口。

溢出判断用temp[8] ^ temp[7],这是 8 位有符号加法溢出的标准写法:第 8 位是进位扩展,第 7 位是符号位,两者异或为 1 表示溢出。subp用~A + 1做补码减法,同样套这个溢出公式。mulp的结果是 16 位,低字节写回目标寄存器、高字节写到相邻寄存器,原文里 R0 乘 A 时低字节给 R0、高字节给 R1,这个约定要在报告里写清楚,否则验收时别人不知道高字节去哪了。

3. 存储器与顶层连接:64×8 存储器和原理图顶层怎么落地

3.1 64×8 存储器的端口与初始化

存储器模块要提供 12 位地址、8 位数据输入、8 位数据输出、读写控制,容量是 64 个 8 位字。地址线虽然给到 12 位,但实际只用低 6 位寻址,高 6 位在顶层可以接地或忽略,这一点在报告里要说明,不然评审会问“12 位地址为什么只接 64 个字”。初始化用 mif 文件最省事,把指令序列按地址逐行写进去,每条指令 16 位要拆成两个 8 位字节存放,取指时状态 0 先读高字节、下一个节拍读低字节拼成 IR,或者按原文做法一次读入高 8 位再左移拼接。两种方式都行,关键是地址递增节奏要和 PC 对齐。

module mem64x8( input wire clk, input wire we, // 1 写 0 读 input wire [11:0] addr, input wire [7:0] din, output reg [7:0] dout ); reg [7:0] ram [0:63]; initial begin $readmemh("inst.mif", ram); // 指令序列预装 end always @(posedge clk) begin if (we) ram[addr[5:0]] <= din; // 只取低6位寻址 else dout <= ram[addr[5:0]]; end endmodule

这段存储器代码的关键点在addr[5:0]:64 个字只需要 6 位地址,高位截掉是有意为之。we为 1 时写入、为 0 时读出,读操作打一拍输出,所以处理器在状态 0 发出地址后,下一个节拍才能拿到数据,这也是状态机要分节拍的原因之一。mif 文件格式按地址递增写十六进制字节,每条 16 位指令拆成高字节在前、低字节在后,写完后用仿真波形核对前几条取指数据是否和 mif 一致,这是最便宜的验证手段。

3.2 顶层原理图连接的信号对应关系

顶层用原理图方式连接,本质是把处理器的端口和存储器的端口一一对接。处理器的M_address接存储器addr,M_data_out接din,M_data_in接dout,Write_Read接we,clock和reset共用。这里有个容易忽略的点:Write_Read的极性要和存储器we的极性对齐,原文端口名叫Write_Read,如果约定 1 表示写,那存储器we就直接接;如果约定反了,读操作会变成写,表现为存储器内容被莫名改写。顶层连线完成后,先不跑复杂程序,用一条load加一条stop验证取指和停机,再逐步加指令。

提示:顶层原理图里所有悬空输入都要接固定电平,尤其是存储器的地址高位和未用控制脚,悬空在仿真里可能表现为不定态,波形上全是 X,排查起来很费时间。

3.3 指令序列装载与执行流程分析

把指令序列存进存储器后,分析流程要按节拍写清楚。以load R0, #imm为例:状态 0 从存储器读出指令高字节拼进 IR,PC 加一;状态 1 判断操作码是 load,把IR[11:8]立即数写进 R0,同时因为不是 stop 也不属于需要额外节拍的指令,状态回 0。再以read为例:状态 0 取指,状态 1 把 PC 或指定地址送 MAR,状态 2 进入访存分支,状态 3、4 等待存储器数据,状态 5 判断是 read 则进状态 6 完成写回。报告里的流程图不要画成笼统的“取指-译码-执行”,要按status的实际取值画,每个状态旁边标注该节拍做的事和涉及的寄存器,这样答辩时被问到“第三条指令第二个节拍在干嘛”能直接指出来。

4. 避坑与排查:这套课设里最容易翻车的五个点

4.1 现象:仿真波形里 IR 一直是 X,指令像没取进来

原因通常是存储器读数据打了一拍,而状态机在同一个节拍就用了M_data_in,时序对不上;或者 mif 文件路径不对,$readmemh静默失败,存储器全是初值 X。解决方法是先单独仿真存储器模块,确认给地址后下一拍dout有确定值;再把处理器状态 0 改成“发地址”而不是“同时取数”,取数放到状态 1 或让 IR 赋值延后一拍。mif 路径用相对路径时,注意仿真工具的工作目录,最稳的是把 mif 放在工程根目录并在报告里注明。

4.2 现象:加法结果对,但 overflow 标志偶尔乱跳

原因是overflow在seq块里每个时钟先清 0,再在具体运算分支里赋值,如果同一条指令跨了多个节拍,后面节拍又把 overflow 清掉了。原文在else分支开头写overflow <= 1'b0,然后状态 2 里再赋溢出值,这要求溢出判断和清零在同一个节拍完成。如果改了状态划分,把运算拆到两个节拍,就会出现“算的时候置 1、下一拍又被清 0”。解决办法是把 overflow 的清零挪到取指节拍,或者用单独的标志寄存器只在运算节拍更新。

4.3 现象:jz 跳转后 PC 跑飞,程序进入死循环

原因是 jz 判断的条件标志没有在比较指令里正确更新,或者跳转时 PC 赋值和取指 PC 加一冲突。comp指令把比较结果写成 1 或 0 存到寄存器,jz 再判断这个寄存器是否为零,如果 comp 的比较分支写错(比如把大于和小于写反),jz 就永远跳或不跳。排查时在 jz 节拍打印 PC 和目标地址,确认跳转目标是不是指令边界;另外注意 PC 是 12 位而存储器只有 64 字,跳转地址超出 63 会回绕,写测试程序时把跳转目标控制在有效范围内。

4.4 现象:mulp 高字节丢失,乘法结果只有低 8 位对

原因是原文约定低字节写目标寄存器、高字节写相邻寄存器,但相邻寄存器的选择在不同分支里不一致,R0 乘 A 时高字节给 R1,R2 乘 A 时高字节给 R3,写测试用例时如果只检查目标寄存器就会以为高字节丢了。解决办法是在报告里把每条 mulp 分支的高低字节去向列成表,测试时两个寄存器都看;如果希望结果更直观,可以改成高字节固定写一个专用寄存器,但这会偏离原文设计,改之前想清楚验收要求。

4.5 现象:顶层连接后综合报多驱动,或者仿真和综合结果不一致

原因是原理图顶层里同一个网络被两个输出端口驱动,比如处理器的M_data_out和存储器的dout误接到同一根线;或者status在多个 always 块里被赋值。排查时先看综合报告的 multi-driven net 警告,逐个网络核对驱动源;仿真和综合不一致多半是阻塞/非阻塞混用导致的,把时序元件统一成非阻塞、组合逻辑统一成阻塞,再跑一遍回归。这类问题在答辩现场被问到会很被动,提前用综合工具过一遍比事后解释省事。

5. 验收与答辩:怎么用一份波形和一张表证明系统真的跑通了

验收时最有效的做法不是口头讲架构,而是准备一份“指令-节拍-结果”对照表加一段关键波形。具体操作是:在测试平台里给复位、时钟,装载一段覆盖 load、move、addp、subp、mulp、logp、comp、sh、meth1、swap、jmp、jz、read、write、stop 的指令序列,仿真足够长时间后,把每条指令执行完时相关寄存器的值和手算值逐条比对,填进下面这种表。

指令序号指令执行后关键寄存器手算期望值波形节拍范围是否一致
1load R0,#05R0=05050~1是
2addp R0R0=R0+A按A初值算0~2是
3mulp R0R0低字节、R1高字节乘积拆分0~2是
4jz R2PC跳转或不跳依R20~3是

这张表的价值在于,答辩时评委随便指一条指令,你能立刻说出它在哪几个节拍执行、结果写到了哪个寄存器、波形上哪一段对应。波形截图要标出status、IR、PC、目标寄存器和M_address/M_data_in,让取指和写回一眼可见。如果某条指令结果不对,先看status有没有按预期推进,再看IR译码字段对不对,最后看运算分支,这个顺序能排掉八成问题。

还有一个小技巧:把stop指令放在程序末尾,仿真跑到status锁在 1 且 PC 不再变化就说明程序正常结束,这比设一个固定仿真时长可靠。答辩前我一般会强制自己把复位后的前三个节拍波形逐信号讲一遍,讲不顺的地方就是自己没吃透的地方。从那以后我每次交这类课设,都强制走一遍“对照表 + 关键波形 + 逐节拍口述”的验收流程,翻车次数明显少了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询