ARTICLE DETAIL

资讯详情

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

Verilog实战:16位指令集处理器与64×8存储器系统设计

Verilog实战:16位指令集处理器与64×8存储器系统设计

简介:这份资源是面向电子信息类专业学生的《计算机组成实验》课程设计文档,聚焦用Verilog HDL实现简化计算机核心部件,帮助读者理解处理器与存储器协同工作的底层原理。压缩包内共1个doc文件,约384KB,内容涵盖实验目的、实验内容、预习要求与实验报告结构,并附有顶层BLOCK图说明及处理器模块的Verilog源码节选,涉及idle、load、move、addp、subp、mulp、logp、comp、sh、swap、jmp、jz、read、write、stop等指令的状态机实现。读者可从中获得完整的课程设计框架、64×8存储器模块的调用思路、处理器模块中IR、MDR、MAR、PC及通用寄存器的设计参考,以及指令fetch、decode、execute、store各阶段的执行流程分析,便于对照完成自己的Verilog实现与实验报告撰写。目前已有382人学习,适合正在做计算机组成原理课程设计、需要参考指令系统与模块连接方案的学生使用。

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

课程设计里最让人头疼的,往往不是写不出代码,而是写完一堆 Verilog 模块之后,发现它们根本连不起来。处理器在仿真里空转,存储器里的指令读不进来,波形图上信号全是红线——这种“玄学”现场,几乎每个做计算机组成课程设计的人都遇到过。这套简化计算机系统的设计,要解决的就是这个问题:用 Verilog HDL 实现一个能真正跑通指令序列的处理器模块,再配一块 64×8 的存储器,顶层把两者接成一个最小可运行的计算机核心部件系统。它适合电子信息类专业正在做《计算机组成实验》课程设计的学生,也适合需要一份可交付、可验收的处理器设计方案的从业者参考。整套方案的核心不是堆功能,而是把“指令系统—处理器—存储器—顶层连接—执行流程分析”这条链路完整走通,让每一条指令都能在波形上被解释清楚。

2. 指令系统与总体架构:16 条指令如何撑起一个最小处理器

2.1 指令集设计:从 idle 到 stop 的编码逻辑

这套处理器的指令系统采用 16 位定长指令字,高 4 位IR[15:12]是操作码,低 12 位根据指令类型拆分为寄存器编号、寻址字段或立即数。操作码一共定义了 16 种状态,正好对应 4 位编码空间:

操作码指令功能定位
0000idle空操作/状态复位
0001load立即数装入寄存器
0010move寄存器间数据搬移
0011addp加法运算
0100subp减法运算
0101mulp乘法运算
0110logp逻辑运算(与/或/异或/取反)
0111comp比较运算
1000sh移位运算
1001meth1自增/自减/加二/减二
1010swap交换类操作
1011jmp无条件跳转
1100jz条件跳转
1101read读存储器
1110write写存储器
1111stop停机

这里的设计取舍值得说清楚:指令字长固定为 16 位,意味着取指阶段一次读入两个 8 位存储字,IR <= {M_data_in, 8'b00000000}这行代码就是取指动作的落点——先读高字节,再拼上低字节。低 12 位的字段划分不是随意的,IR[11:10]通常用作目标寄存器或运算模式选择,IR[9:8]用作源寄存器或子操作选择,IR[11:8]在 move 指令里直接编码源/目的寄存器对。这种“操作码 + 模式位 + 寄存器位”的三段式划分,是教学型处理器里最常见也最好调试的做法,因为每一位在波形上都能对应到一个明确的控制信号。

提示:指令编码表一定要在动手写代码之前定死,后面写测试激励、写 mif 文件、分析执行流程都依赖这张表。中途改编码,等于把已经调通的波形全部推翻。

2.2 总体架构:处理器、存储器与顶层连接的职责划分

整个系统分三层。底层是处理器模块cpu,端口包括reset、clock、Write_Read、M_address[11:0]、M_data_in[7:0]、M_data_out[7:0]和overflow。中间层是 64×8 的存储器模块,地址宽度 6 位有效(64 个存储单元),数据宽度 8 位。顶层用原理图方式把两者连起来,处理器输出地址和写数据,存储器返回读数据,Write_Read作为读写控制线。

处理器内部的关键寄存器组包括:16 位指令寄存器IR、8 位存储器数据寄存器MDR、12 位存储器地址寄存器MAR、3 位状态机寄存器status,以及四个 8 位通用寄存器R0~R3、一个 8 位累加器A。PC在时序块内部声明为 12 位,实际只用到低 6 位去寻址 64 个单元。这套寄存器配置不算豪华,但足够覆盖算术、逻辑、移位、访存和跳转五类操作,是一个“刚好够用”的最小集合。

顶层连接时最容易翻车的地方是位宽不匹配:M_address是 12 位,存储器地址只认 6 位,多出来的高位在顶层要么接地要么悬空,通常的做法是只连低 6 位,高位在处理器内部保证不越界。另一个点是M_data_in和M_data_out的方向——从处理器视角看,M_data_in是“从存储器读进来的数据”,M_data_out是“要写进存储器的数据”,这两个命名和直觉相反,接线时搞反了,波形上就是读也读不对、写也写不进。

2.3 状态机划分:六个状态覆盖取指到写回

处理器的控制核心是一个 3 位status状态机,配合IR[15:12]的操作码做二级译码。状态流转的逻辑是:

  • 状态 0:取指。把存储器读出的数据装入IR高字节,PC加一。
  • 状态 1:译码与操作数准备。根据操作码把MAR指向PC(为读操作数做准备),同时完成 load、move、sh 以及算术逻辑指令的源操作数装载(把源寄存器值送入A)。
  • 状态 2:执行。算术、逻辑、比较、乘法等指令在这一拍完成运算并写回目标寄存器。
  • 状态 3:访存指令(swap、jmp、jz、read、write)的地址建立阶段。
  • 状态 4:访存指令的数据传输阶段。
  • 状态 5:read 指令的读数据回写阶段。

这个状态划分的精髓在于“不是所有指令都走满六个状态”。load、move、addp 这类寄存器操作在状态 2 就结束了,下一拍直接回到状态 0;而 read 要走到状态 5 才能把存储器数据取回来。状态机里那些case (IR[15:12])的分支,本质上是在做“哪些指令需要多走几拍”的调度。写代码时如果所有指令都强行走满状态,功能上也许能跑,但时序会变得很难解释,答辩时被问“这条指令为什么用了六拍”就答不上来。

3. 处理器模块实现:时序块与组合逻辑的分工

3.1 双时钟沿设计:status 与 seq 两个 always 块

处理器代码里有两个核心always块,这是整个设计最需要讲清楚的地方。第一个块status_change对negedge reset和posedge clock敏感,负责状态机流转;第二个块seq对negedge reset和negedge clock敏感,负责数据通路的寄存器更新。一个用上升沿、一个用下降沿,形成半拍错开的节拍:

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; 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

这段状态机的关键参数含义:status是 3 位寄存器,但case里出现了6,说明实际用了 4 位编码空间(reg [2:0]声明下写6会截断,这是原代码里一个需要留意的细节,实际综合时通常把status扩到 4 位更稳妥)。stop指令在状态 1 就锁死在状态 1,形成停机循环,这是最朴素的停机实现——不关时钟,只是不再推进状态。

第二个块seq在negedge clock触发,和状态机错开半拍,保证状态先稳定、数据通路再动作。这种“上升沿改状态、下降沿改数据”的双沿写法在教学代码里很常见,好处是波形上状态和数据的跳变沿清晰可辨,坏处是综合到实际器件时对时钟质量要求更高。如果目标平台只允许单沿设计,通常的做法是把seq也改成posedge clock,然后在状态机里多插一拍等待,用面积换时序安全。

3.2 取指与译码:IR 装载和 PC 推进

取指动作落在seq块的状态 0:

0: begin IR <= {M_data_in, 8'b00000000}; PC = PC + 1'b1; end

这里IR只装了高 8 位,低 8 位补零,说明取指是分两步完成的——状态 0 取高字节,后续状态再取低字节拼进去。PC用阻塞赋值=而不是非阻塞<=,在同一个always块里混用两种赋值方式是有风险的:PC在块内被多处读写,阻塞赋值能保证块内顺序执行时读到最新值,但和IR的非阻塞赋值混在一起,仿真时容易出现“这一拍到底用旧 PC 还是新 PC”的歧义。我一般会建议把PC也改成非阻塞,或者在块内单独用一个组合逻辑算PC_next,把时序和组合彻底分开,波形会干净很多。

译码在状态 1 完成。以 move 指令为例,IR[11:8]直接编码源/目的寄存器对,4'b0001表示R0 = R1,4'b0100表示R1 = R0,一共 12 种有效组合,其余编码走default: ;空操作。这种“用字段值直接查表”的译码方式,写起来快,但可读性差——答辩时评委问“4'b0110是什么操作”,得翻回编码表才能答。更稳妥的做法是用parameter给每种寄存器组合起名字,或者至少加注释标明方向。

3.3 算术与逻辑运算:溢出判断和移位模式

加法运算的溢出判断用的是经典的双符号位方法:

2'b00: begin temp = ({R0[7], R0[7:0]}) + ({A[7], A[7:0]}); R0 = temp[7:0]; overflow <= temp[8] ^ temp[7]; end

temp是 9 位,把操作数符号位扩展后相加,结果最高位temp[8]和次高位temp[7]异或,得到溢出标志。减法用补码实现:({R0[7], R0[7:0]}) + (~({A[7], A[7:0]})) + 1'b1,溢出判断逻辑相同。这个写法在 8 位数据通路上是标准做法,参数上唯一要注意的是temp必须声明为reg [8:0],少一位就会丢掉进位。

移位指令sh用IR[11:10]选移位类型、IR[9:8]选目标寄存器,四种模式分别是算术右移、循环右移、逻辑右移、逻辑左移。原代码里循环右移那一支写成了R1 = {R0[6:0], R1[0]},把R0的高 7 位和R1的最低位拼在一起,这明显是复制粘贴时漏改了寄存器名——正确写法应该是{R1[6:0], R1[0]}。这类“看起来能跑、结果不对”的 bug,是移位模块里最高频的翻车点,因为语法没错、仿真不报错,只有对着具体数据算一遍才能发现。

乘法mulp用 16 位中间变量temp2存积,低 8 位写目标寄存器、高 8 位写相邻寄存器。这里有个隐含约定:R0存低字节时R1存高字节,R2存低字节时R3存高字节。这个约定不在指令编码里体现,只存在于实现中,写测试激励时必须按这个约定去检查结果,否则会误判为乘法出错。

4. 存储器模块与顶层连接:64×8 存储器的落地细节

4.1 存储器模块设计:地址、数据与控制信号

存储器模块是一个 64×8 的 RAM,6 位地址线、8 位数据线、一根读写控制线。处理器侧输出的Write_Read信号通常定义为:高电平写、低电平读(或反之,取决于顶层约定,关键是全系统统一)。M_address是 12 位,存储器只取低 6 位,高位在顶层直接截断或接地。

存储器初始化用 mif 文件完成,把指令序列按地址逐条写入。这里有个容易忽略的点:指令是 16 位的,存储器是 8 位宽的,所以一条指令要占两个存储单元。取指时先读偶地址取高字节,再读奇地址取低字节,PC每次加一,实际指令地址是PC的一半。如果 mif 文件里按“一条指令一个地址”去写,取指就会错位,波形上表现为IR的高低位拼反或者指令完全对不上。我一般会在 mif 文件里按字节排布,偶地址放高字节、奇地址放低字节,并在文件头注释里写清楚这个约定。

4.2 顶层原理图连接:端口映射与位宽处理

顶层用原理图方式连接,核心是把cpu的端口和存储器端口一一对应:

处理器端口方向存储器端口说明
M_address[11:0]输出addr[5:0]取低 6 位,高位截断
M_data_out[7:0]输出data_in[7:0]写数据
M_data_in[7:0]输入data_out[7:0]读数据
Write_Read输出we读写控制
clock输入clk全局时钟
reset输入—复位只接处理器

位宽处理是顶层连接的第一道坎。M_address12 位接 6 位地址,多出的 6 位如果悬空,综合工具可能报 warning 也可能直接优化掉,仿真时表现为高地址访问行为不确定。稳妥做法是在顶层加一个截断逻辑,显式只连M_address[5:0],并在报告里说明“处理器设计保证地址不越界,高位恒为 0”。

第二个坎是读写时序。存储器是同步读还是异步读,直接决定read指令要几个状态。如果存储器是同步读(时钟沿采样地址、下一拍出数据),那read指令从状态 3 建立地址到状态 5 取回数据,正好三拍,和状态机对得上;如果是异步读,状态 4 就能拿到数据,状态 5 就多余了。顶层连接前一定要确认存储器的读时序模型,否则状态机拍数和实际数据到达时间对不上,读回来的永远是上一拍的旧数据。

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

把一段指令序列写进 mif 文件,是验证整个系统是否跑通的关键动作。一个典型的测试序列可以这样组织:先用 load 给R0、R1装立即数,用 addp 做加法并检查 overflow,用 move 搬移结果,用 sh 做移位,用 write 把结果写回存储器,最后用 stop 停机。每条指令占两个字节地址,序列写完后在仿真里跑足够长的时钟周期,逐拍对照status、IR、PC和寄存器值。

执行流程分析要落到“每一拍发生了什么”。以一条 addp 为例:状态 0 取指,IR高字节装入;状态 1 译码,把源寄存器值送入A,MAR指向操作数地址;状态 2 执行加法,结果写回目标寄存器,overflow更新;状态回到 0,取下一条指令。分析报告里通常用表格列出“时钟拍—状态—关键信号变化”,这张表就是答辩时最有说服力的材料,因为它把“指令怎么跑起来的”从抽象描述变成了可核对的时序证据。

5. 避坑与排查:这类设计里最容易翻车的五个点

5.1 取指高低字节拼反,指令全乱

现象:仿真跑起来IR的值和 mif 文件里写的指令对不上,状态机跳到莫名其妙的分支。原因:16 位指令存进 8 位存储器时,高字节和低字节的存放顺序与取指拼接顺序不一致,{M_data_in, 8'b00000000}先装的是高字节,如果 mif 里偶地址放的是低字节,拼出来就是反的。解决:在 mif 文件头写死约定“偶地址高字节、奇地址低字节”,取指代码里加注释标明拼接方向,仿真第一拍就检查IR[15:12]是否等于预期操作码。

5.2 阻塞与非阻塞赋值混用,波形对不上

现象:同一个always块里PC用=、IR用<=,仿真结果和手算不一致,改一个信号牵连一片。原因:阻塞赋值立即生效、非阻塞赋值在块末统一更新,混用时块内后续语句读到的值取决于赋值方式,形成隐式的执行顺序依赖。解决:时序块内统一用非阻塞赋值,需要组合逻辑的中间量单独开一个always @(*)块用阻塞赋值,两类逻辑物理分开。

5.3 存储器读时序没对齐,读回旧数据

现象:read指令执行完,目标寄存器里是上一次读的值,或者干脆是 X。原因:状态机按“地址建立后两拍取数据”设计,但存储器实际是同步读、需要三拍,或者顶层把读写控制极性接反了。解决:先单独仿真存储器模块,确认从地址有效到数据有效的延迟拍数,再回头调整状态机的状态数;读写控制线在顶层用显式赋值固定极性,不依赖默认值。

5.4 移位和乘法模块的复制粘贴错误

现象:移位结果只有部分寄存器正确,乘法高字节写错寄存器。原因:多分支case里复制粘贴后漏改寄存器名,比如循环右移写成{R0[6:0], R1[0]},语法合法、仿真不报错。解决:每个分支写完后用一组已知数据手算预期结果,和仿真值逐个比对;代码里给每个分支加“目标寄存器:R1”这类注释,review 时逐行核对。

5.5 状态位宽和编码越界

现象:状态机在某个状态卡死,或者status波形出现不该有的值。原因:status声明为reg [2:0],但case里写了6,3 位最大表示 7,写 6 本身不越界,但如果后续再加状态就会溢出;更常见的是parameter定义用了 4 位编码而寄存器只有 3 位,高位被截断。解决:状态寄存器位宽按最大状态编号向上取整,parameter编码和寄存器位宽保持一致,综合后看 warning 里有没有位宽截断提示。

6. 验收与答辩:怎么证明这套系统真的跑通了

验收这套设计,核心不是看代码写了多少行,而是看“指令序列能不能被完整解释”。我一般会准备三样东西:一张指令编码表、一份 mif 文件、一张逐拍时序对照表。验收时按这个顺序走:先复位,确认PC=0、status=0、所有寄存器清零;然后单步跑前三条指令,每拍记录status、IR[15:12]、PC和目标寄存器值,和手算结果比对;最后全速跑完整序列,确认stop指令让状态机锁在状态 1、PC不再推进。

答辩时最容易被问的三个问题,提前准备好答案:第一,“你的指令为什么这样编码”——答字段划分的依据和可扩展性;第二,“这条指令为什么用了这么多拍”——答状态机里哪些状态是访存专用、哪些指令走捷径;第三,“overflow 怎么算的”——答双符号位扩展加异或的判断方法,最好能当场用一组数据手算演示。如果评委追问“如果存储器改成 128×8 要改哪里”,能答出“地址位宽从 6 扩到 7、MAR和PC相应调整、mif 文件扩容、状态机拍数不变”就算过关。

下面这张验收检查表可以直接拿去用:

检查项通过标准检查方法
复位行为所有寄存器和状态清零复位后看波形首拍
取指正确性IR 高 4 位等于预期操作码逐条比对 mif
算术溢出正溢出/负溢出各测一组手算对照 overflow
访存读写写入值能原样读回write 后紧跟 read
跳转指令PC 跳到目标地址看 jmp 后 PC 值
停机状态锁在 1,PC 不变跑完序列看末态

最后说个血泪经验:这套设计里最耗时间的从来不是写模块,而是“波形对不上但不知道错在哪”。从那以后我每次做这类处理器设计,都强制先写一个最小测试序列——三条指令、跑十个周期、逐拍打印状态和寄存器——确认这条最短路径通了,再往上堆功能。最短路径不通就加功能,等于在流沙上盖楼。希望这套结构和验收方法能帮到你,少走几段弯路。

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

返回列表