ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Verilog HDL实现简化计算机系统:指令集、状态机与存储器协同设计

Verilog HDL实现简化计算机系统:指令集、状态机与存储器协同设计

简介:这份资源是面向电子信息类专业学生的《计算机组成实验》课程设计文档,聚焦用Verilog HDL实现简化计算机核心部件,帮助读者理解指令系统、处理器与存储器模块的协同工作原理。压缩包内仅含1个doc文件,约384KB,完整呈现实验目的、内容、预习要求与实验报告框架,并附有顶层BLOCK图说明及cpu模块的Verilog代码节选,涵盖idle、load、move、addp、subp、mulp、logp、comp、sh、meth1、swap、jmp、jz、read、write、stop等指令的状态机实现。文档还给出64×8存储器模块的调用方式与顶层原理图连接思路,便于对照分析指令fetch、decode、execute、store的执行流程。已有382人学习,适合作为课程设计参考与Verilog入门实践材料,帮助读者快速把握简化计算机系统的设计脉络与代码组织方式。

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

很多同学做《计算机组成实验》课设时,第一反应是先把 CPU 的 Verilog 敲出来,结果仿真波形一跑,PC 不动、寄存器全是 X,连一条最简单的 load 都执行不下去。问题往往不在代码写得多复杂,而在于一开始就没想清楚:这套简化计算机系统到底由哪几块组成、指令怎么编码、状态机怎么走、存储器和处理器之间的握手信号怎么对齐。这份方案要解决的就是这件事——用 Verilog HDL 实现一个简单处理器模块,配一块 64×8 的存储器,顶层把两者连成可运行的简化计算机核心部件,再把指令序列写进存储器,逐条分析取指、译码、执行、写回的完整流程。它适合电子信息类专业正在做计算机组成课程设计的学生,也适合需要快速搭一个可演示 CPU 原型的从业者参考。下面按需求、架构、模块实现、避坑、验收的顺序,把这份课设拆成能直接照着做的方案。

2. 需求与指令系统设计:先定指令集,再谈处理器

2.1 从实验目的反推功能边界

这份课设的实验目的写得很明确:学习简单指令系统及其各指令的操作流程,用 Verilog HDL 实现简单处理器模块,调用存储器模块,把两者连接成简化的计算机核心部件系统。换句话说,交付物不是一颗通用 CPU,而是一个“能证明你理解了指令执行流程”的最小系统。功能边界通常这样划:处理器要能按 PC 取指、按操作码译码、执行算术逻辑与访存类指令、在 stop 指令处停机;存储器要是 64×8 的字节编址空间,能存指令也能存数据;顶层要能把地址、数据、读写控制三组信号正确对接。我一般会先把这条边界写进设计说明的第一页,后面所有模块划分都围绕它展开,避免做到一半发现“要不要加中断”“要不要加流水线”这类超出课设范围的问题。

2.2 指令编码:16 位指令字的字段划分

从给出的处理器模块端口和内部寄存器可以看出,指令寄存器 IR 是 16 位,高 4 位 IR[15:12] 是操作码,剩余 12 位按指令类型再细分。这套编码是整个设计的骨架,必须先定死。下面这张表是按项目正文整理出的指令集与字段含义,实际写报告时可以直接作为“指令系统设计”一节的核心表格。

操作码指令高 4 位编码低 12 位用途
idle空操作0000保留
load立即数装入0001IR[11:8] 为目标寄存器号,低 8 位为立即数
move寄存器传送0010IR[11:8] 指定源/目的寄存器对
addp加法0011IR[11:10] 目的寄存器,IR[9:8] 源寄存器
subp减法0100同上
mulp乘法0101同上,结果高低字节分存两个寄存器
logp逻辑运算0110IR[11:10] 运算类型,IR[9:8] 寄存器
comp比较0111IR[11:10] 比较对象
sh移位1000IR[11:10] 移位类型,IR[9:8] 寄存器
meth1自增自减1001IR[11:10] 操作类型,IR[9:8] 寄存器
swap交换1010与累加器 A 交换
jmp跳转1011目标地址
jz条件跳转1100条件满足时跳转
read读存储器1101地址来源
write写存储器1110地址与数据来源
stop停机1111保留

这张表的价值在于:它同时约束了译码逻辑(case 语句的分支)、寄存器堆的读写端口选择、以及测试指令序列的编写方式。很多同学后期调试崩溃,根源就是指令字段定义前后不一致——写测试程序时按一种理解填机器码,译码逻辑里又按另一种理解取位,波形上看起来“指令执行了”,结果却完全不对。

2.3 寄存器与状态资源盘点

处理器内部需要哪些寄存器,直接决定了数据通路怎么画。按项目正文,核心资源包括:12 位 PC(程序计数器)、16 位 IR(指令寄存器)、12 位 MAR(存储器地址寄存器)、8 位 MDR(存储器数据寄存器)、8 位累加器 A、四个 8 位通用寄存器 R0~R3,以及 3 位 status 状态寄存器和 overflow 溢出标志。这里有个容易被忽略的点:PC、R0~R3、A 在原始代码里是定义在 always 块内部的 reg 变量,而 IR、MAR、MDR、status、overflow 是模块级 reg。这种写法在仿真里能跑,但综合时会带来隐患,后面避坑章节会专门讲。做方案时我建议在文档里明确列出“架构寄存器”和“临时变量”两类,前者是体系结构可见的,后者只服务于单条指令的执行,这样写代码时心里有数,答辩时也讲得清。

3. 总体架构设计:状态机驱动的处理器与存储器协同

3.1 顶层结构与信号连接关系

顶层采用原理图方式连接处理器模块和存储器模块,这是课设的硬性要求。从端口定义看,处理器对外暴露的信号可以分成四组:时钟与复位(clock、reset)、存储器读写控制(Write_Read)、地址总线(M_address,12 位)、数据总线(M_data_in 输入、M_data_out 输出),外加 overflow 溢出输出。存储器模块则对应提供地址输入、数据输入输出和读写使能。顶层要做的就是把同名信号一一对接,并保证复位信号同时送到两个模块。这里的关键设计点是数据总线的方向:M_data_in 是处理器从存储器读回的数据,M_data_out 是处理器写给存储器的数据,两者分开命名而不是共用一条双向总线,好处是仿真时波形清晰、不用处理三态,代价是端口略多。对于课设规模,这种单向分离的做法更稳妥,我一般会优先选它。

3.2 状态机划分:从取指到写回的六个阶段

处理器内部用 status 状态寄存器驱动整个执行流程,这是这套设计的核心控制逻辑。按原始代码,状态迁移大致是:状态 0 取指,把存储器读出的数据装入 IR 高 8 位并 PC 加一;状态 1 译码并准备操作数,同时把 PC 送入 MAR 准备取下一条指令的操作数;状态 2 根据操作码分派到算术、逻辑、移位等执行分支;状态 3 处理 swap、jmp、jz、read、write 这类需要额外访存或跳转的指令;状态 4、5 继续推进访存类指令;状态 6 完成 read 指令的数据回写。用文字描述这条链是:取指 → 译码/取操作数 → 执行 → 访存/跳转 → 写回 → 回到取指。状态机的时钟沿选择也值得说一句:status 在 posedge clock 上迁移,而数据通路的 seq 块在 negedge clock 上动作,这种“正沿改状态、负沿做数据”的半周期分工,能让一条指令在多个时钟周期内分步完成,是这类简化 CPU 常见的节奏安排。

3.3 存储器模块的地址空间规划

存储器是 64×8,即 64 个字节单元,地址线需要 6 位,但处理器侧 MAR 是 12 位,这里存在位宽不匹配。通常的做法是:存储器模块内部只取地址的低 6 位作为实际寻址,高位忽略或用于片选判断;顶层连接时把 12 位地址整体送过去,由存储器模块自己截取。指令序列从地址 0 开始存放,数据区放在指令区之后,比如前 32 字节放程序、后 32 字节放数据,这样在写 mif 初始化文件时边界清晰,调试时也容易对照“第几条指令对应哪个地址”。如果课设要求里没有强制规定数据区起点,我建议在报告里显式声明这个划分,答辩时被问到“数据会不会覆盖指令”就能直接回答。

4. 处理器模块实现:译码、执行与访存的关键代码

4.1 取指与译码阶段的实现

取指阶段的核心动作是把存储器输出的数据锁进 IR,并推进 PC。原始代码里状态 0 写的是IR <= {M_data_in, 8'b00000000},也就是把读回的一个字节放到 IR 高 8 位,低 8 位清零。这个写法对应的是“指令分两次读入”的假设——第一次读高字节,第二次读低字节。实际实现时,如果存储器一次只能给 8 位,就需要两个取指周期拼成 16 位指令;如果存储器按 16 位组织,则可以一次读全。课设里 64×8 是字节宽,所以分两次读是合理的。译码阶段则用case (IR[15:12])对操作码分支,把不同指令导向不同的执行路径。下面这段是取指与译码骨架的示意,保留了原始设计的状态推进思路,并补上了必要的注释。

// 状态机:正沿迁移,负沿执行数据动作 always @(negedge reset or posedge clock) begin if (!reset) status <= 0; else case (status) 0: status <= 1; // 取指完成,进入译码 1: if (IR[15:12] == 4'b1111) // stop 指令停在状态 1 status <= 1; else status <= 2; // 普通指令进入执行 2: case (IR[15:12]) 4'b1010, 4'b1011, 4'b1100, 4'b1101, 4'b1110: status <= 3; // swap/jmp/jz/read/write 需额外周期 default: status <= 0; // 其余指令执行完回取指 endcase 3: if (IR[15:12] == 4'b1010) status <= 0; // swap 完成 else status <= 4; 4: status <= 5; 5: if (IR[15:12] == 4'b1101) status <= 6; // read 进入数据回写 else status <= 0; default: status <= 0; endcase end

这段代码的设计说明有三点:第一,stop 指令让状态停在 1 而不是回到 0,是为了让 PC 不再前进,形成停机效果;第二,swap、jmp、jz、read、write 被单独拉到状态 3 及以后,是因为它们要么需要再访存一次,要么需要改 PC,不能在一个周期内草草结束;第三,default 分支统一回状态 0,保证任何未定义操作码都不会把状态机卡死。参数上,状态编码用 3 位 status 表示 0~6 足够,原始代码里用 parameter 定义了 idle 到 stop 共 16 个操作码常量,这个做法值得保留,译码时写IR[15:12] == stop比写4'b1111可读性高得多。

4.2 算术与逻辑指令的执行路径

执行阶段是数据通路最密集的地方。以加法 addp 为例,原始代码用 9 位 temp 承接两个 8 位数相加,temp = ({R0[7], R0[7:0]}) + ({A[7], A[7:0]}),这里把符号位复制到第 8 位做符号扩展,再用temp[8] ^ temp[7]判断溢出。这个溢出判断的逻辑是:最高位进位与次高位进位不同则溢出,对有符号数加法成立。减法 subp 则用补码实现,temp = ({R0[7], R0[7:0]}) + (~({A[7], A[7:0]})) + 1'b1,即加上减数的按位取反再加一。乘法 mulp 用 16 位 temp2 承接 8×8 结果,低字节写回目的寄存器、高字节写回相邻寄存器,这个“结果分存两个寄存器”的约定必须在指令系统文档里写清楚,否则测试程序没法预期结果。逻辑运算 logp 按 IR[11:10] 区分与、或、异或、取反,移位 sh 按 IR[11:10] 区分循环右移、循环左移、逻辑右移、逻辑左移。这些分支的共同点是:源操作数先经 IR[9:8] 选进累加器 A,再与目的寄存器运算,结果写回目的寄存器。把“选操作数”和“做运算”分成两步,代码结构会清楚很多。

4.3 访存指令与跳转指令的处理

read 和 write 是这套设计里最考验时序的两条指令。write 要把寄存器内容写到存储器,需要先把地址送 MAR、数据送 MDR,再拉低或拉高 Write_Read 表示写,等存储器完成后再撤销。read 则相反,先送地址,发读使能,等数据从 M_data_in 回来,再在状态 6 把数据写回寄存器。原始代码里 read 在状态 5 判断后进入状态 6,正是为了留出存储器访问的延迟窗口。jmp 和 jz 的处理相对简单:jmp 直接把目标地址装入 PC;jz 则要先判断条件,条件成立才改 PC,否则顺序执行。swap 指令把累加器 A 与某个寄存器交换,原始代码在状态 3 判断 swap 后回状态 0,说明交换动作在状态 2 或 3 内完成。这里有个实现细节值得提醒:改 PC 的指令(jmp、jz)在取指阶段已经让 PC 加过一,所以跳转时要写入的是目标地址而不是“当前 PC 加偏移”,否则会多跳一条。这个坑我在第一次做类似设计时就踩过,波形上表现为跳转后总是跳过一条指令,查了半天才发现是 PC 自增和跳转赋值顺序的问题。

5. 避坑与常见问题排查:那些仿真跑不通的真实原因

5.1 现象:波形里寄存器全是 X,PC 不前进

原因通常有两个:一是复位信号极性或时序不对,原始代码用negedge reset触发复位,如果测试平台给的是高电平有效复位,状态机永远不会初始化;二是 always 块内部定义的 reg 变量(PC、R0~R3、A)在复位分支里用阻塞赋值初始化,而在非复位分支里又参与非阻塞赋值,仿真器对这类混合赋值的处理容易出现竞争。解决办法是统一复位极性,在测试平台里显式拉低 reset 至少一个时钟周期;同时把 PC、R0~R3、A 提升为模块级 reg,复位时统一清零,避免块内变量作用域带来的不确定性。

5.2 现象:指令能取到,但执行结果总差一位

原因多半出在指令字段的位选上。比如 load 指令约定 IR[11:8] 是目标寄存器号、低 8 位是立即数,但写测试机器码时把寄存器号放到了 IR[7:4],译码时却按 IR[11:8] 取,结果寄存器选错、立即数也错。还有一种情况是取指时 IR 只装了高 8 位,低 8 位还没读进来就开始译码,导致低半段字段全是 0。解决办法是先把指令编码表贴在显示器旁边,写机器码和写译码逻辑时逐位对照;取指阶段严格按“高字节 → 低字节 → 拼装完成 → 才允许译码”的顺序推进状态。

5.3 现象:read 指令读回的数据写不进寄存器

原因是访存延迟没有留够。存储器模型如果是同步读,数据要在时钟沿之后才出现在 M_data_in 上,而处理器如果在同一个状态里既发地址又采样数据,采到的就是旧值。原始设计用状态 5 发读、状态 6 回写,正是为了隔开一个周期。解决办法是检查状态机是否为 read 单独保留了回写状态,以及 M_data_in 的采样是否发生在数据有效之后;如果存储器是组合读,则要确认地址稳定后再采样,必要时在测试平台里加监视点打印 MAR 和 M_data_in 的对应关系。

5.4 现象:综合报错“不能对同一变量多次赋值”

原因是同一个 reg 在多个 always 块里被赋值,或者在一个 always 块里既用阻塞又用非阻塞赋值。原始代码里 status 在 status_change 块赋值,IR、MAR、MDR 在 seq 块赋值,分工是清楚的;但如果把 PC 也放到两个块里改,就会触发这类报错。解决办法是给每个寄存器指定唯一的“主人”always 块,数据通路归 seq 块、状态归 status_change 块,跨块只读不写;临时变量(temp、temp2)留在块内,不提升为模块级。

5.5 现象:顶层原理图连好后,存储器读出的全是 0

原因是 mif 初始化文件没有正确绑定,或者存储器模块的地址位宽截取和顶层连接不一致。64×8 存储器只需要 6 位地址,如果顶层把 12 位 MAR 直接接到存储器地址端口而存储器模块内部没做截取,高位悬空或全 0,实际寻址就会错位。解决办法是在存储器模块内部显式取address[5:0],并在报告里写明“高位忽略”;同时确认 mif 文件路径、数据格式(每行一个字节的十六进制)和仿真工具的初始化设置一致,仿真开始后先看存储器输出端口在地址 0 处是否等于 mif 第一行。

6. 验收与答辩:用一条指令序列证明系统真的跑通了

验收这件事,我一般不看代码写得多漂亮,只看一条完整的指令序列能不能从头跑到 stop,并且中间每一步的寄存器值都能对上预期。具体做法是:在 mif 里写一段 8 到 12 条指令的小程序,覆盖 load、addp、subp、write、read、jz、stop 这几类,程序逻辑设计成“先算一个已知结果、写进存储器、再读回来比较、相等则跳转到停机、不等则继续”。这样一段程序跑通,等于把取指、译码、算术、访存、跳转、停机全验证了一遍。下面是一个测试序列的示意,机器码按第 2 章的字段表手工拼装,实际写 mif 时每行一个字节。

地址 00: 0001_0000 ; load R0, 立即数 0x00(高字节) 地址 01: 0000_0101 ; 低字节 0x05,R0 = 5 地址 02: 0001_0001 ; load R1, 立即数 0x00 地址 03: 0000_0011 ; 低字节 0x03,R1 = 3 地址 04: 0011_0001 ; addp R0, R1,R0 = 8 地址 05: 1110_0000 ; write,把 R0 写到数据区 地址 06: 1101_0000 ; read,从数据区读回 地址 07: 0111_0000 ; comp,比较读回值与 R0 地址 08: 1100_0000 ; jz,相等则跳转 地址 09: 1111_0000 ; stop

答辩时老师最爱问的三个问题,提前准备好答案能省很多事:一是“你的状态机为什么这么分”,回答时把取指、译码、执行、访存四段和每条指令占用的周期数说清楚;二是“溢出标志怎么来的”,把符号扩展和temp[8] ^ temp[7]的判据讲明白;三是“存储器和处理器怎么握手”,把地址、数据、读写使能三组信号的时序关系画在纸上讲。验收表格可以按“指令类别 / 测试用例 / 预期结果 / 实测结果 / 是否通过”五列来填,每类指令至少一条用例,stop 单独一行验证停机。我自己的习惯是,每次交板子或交仿真前,都强制把这条“算—写—读—比—停”的序列重跑一遍,波形存成截图附在报告后面。有一次就是靠这条序列发现 read 回写晚了一个周期,否则答辩现场被问到数据通路细节就露馅了。希望这套结构能帮到你,照着把指令表、状态机、测试序列三样对齐,这份课设基本就稳了。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进