
简介这份资源是面向电子信息类专业学生的《计算机组成实验》课程设计文档聚焦用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 位通用寄存器 R0R3以及 3 位 status 状态寄存器和 overflow 溢出标志。这里有个容易被忽略的点PC、R0R3、A 在原始代码里是定义在 always 块内部的 reg 变量而 IR、MAR、MDR、status、overflow 是模块级 reg。这种写法在仿真里能跑但综合时会带来隐患后面避坑章节会专门讲。做方案时我建议在文档里明确列出“架构寄存器”和“临时变量”两类前者是体系结构可见的后者只服务于单条指令的执行这样写代码时心里有数答辩时也讲得清。3. 总体架构设计状态机驱动的处理器与存储器协同3.1 顶层结构与信号连接关系顶层采用原理图方式连接处理器模块和存储器模块这是课设的硬性要求。从端口定义看处理器对外暴露的信号可以分成四组时钟与复位clock、reset、存储器读写控制Write_Read、地址总线M_address12 位、数据总线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, 8b00000000}也就是把读回的一个字节放到 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] 4b1111) // stop 指令停在状态 1 status 1; else status 2; // 普通指令进入执行 2: case (IR[15:12]) 4b1010, 4b1011, 4b1100, 4b1101, 4b1110: status 3; // swap/jmp/jz/read/write 需额外周期 default: status 0; // 其余指令执行完回取指 endcase 3: if (IR[15:12] 4b1010) status 0; // swap 完成 else status 4; 4: status 5; 5: if (IR[15:12] 4b1101) 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 表示 06 足够原始代码里用 parameter 定义了 idle 到 stop 共 16 个操作码常量这个做法值得保留译码时写IR[15:12] stop比写4b1111可读性高得多。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]})) 1b1即加上减数的按位取反再加一。乘法 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 直接把目标地址装入 PCjz 则要先判断条件条件成立才改 PC否则顺序执行。swap 指令把累加器 A 与某个寄存器交换原始代码在状态 3 判断 swap 后回状态 0说明交换动作在状态 2 或 3 内完成。这里有个实现细节值得提醒改 PC 的指令jmp、jz在取指阶段已经让 PC 加过一所以跳转时要写入的是目标地址而不是“当前 PC 加偏移”否则会多跳一条。这个坑我在第一次做类似设计时就踩过波形上表现为跳转后总是跳过一条指令查了半天才发现是 PC 自增和跳转赋值顺序的问题。5. 避坑与常见问题排查那些仿真跑不通的真实原因5.1 现象波形里寄存器全是 XPC 不前进原因通常有两个一是复位信号极性或时序不对原始代码用negedge reset触发复位如果测试平台给的是高电平有效复位状态机永远不会初始化二是 always 块内部定义的 reg 变量PC、R0R3、A在复位分支里用阻塞赋值初始化而在非复位分支里又参与非阻塞赋值仿真器对这类混合赋值的处理容易出现竞争。解决办法是统一复位极性在测试平台里显式拉低 reset 至少一个时钟周期同时把 PC、R0R3、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 ; 低字节 0x05R0 5 地址 02: 0001_0001 ; load R1, 立即数 0x00 地址 03: 0000_0011 ; 低字节 0x03R1 3 地址 04: 0011_0001 ; addp R0, R1R0 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 回写晚了一个周期否则答辩现场被问到数据通路细节就露馅了。希望这套结构能帮到你照着把指令表、状态机、测试序列三样对齐这份课设基本就稳了。本文还有配套的精品资源点击获取