ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Verilog实现简化CPU:取指-执行全流程与存储器设计

Verilog实现简化CPU:取指-执行全流程与存储器设计 简介这份课程设计文档面向电子信息类专业学生及计算机组成原理学习者围绕「简化计算机系统设计」这一经典实验课题提供从指令系统到处理器与存储器联调的完整实现思路。内容涵盖简单指令集idle、load、move、addp、subp、mulp、logp、comp、sh、swap、jmp、jz、read、write、stop 等的编码与执行流程用 Verilog HDL 编写处理器模块调用 64×8 存储器模块并在顶层以原理图方式连接成简化计算机核心部件最后将指令序列存入存储器分析取指、译码、执行与存储过程。资源包共 1 个 doc 文件约 384KB包含实验目的、实验内容、预习要求与实验报告结构并附有顶层 BLOCK 图说明及 CPU 模块的 Verilog 代码节选可直接对照复现状态机与寄存器设计。已有 382 人学习适合作为课程设计参考与 Verilog 实践排错思路的补充材料。1. 简化计算机系统设计一份能跑通取指-执行全流程的 Verilog 课设资源如果你正在做计算机组成原理的课程设计大概率会遇到一个尴尬局面课本上讲清楚了冯·诺依曼结构实验指导书也给了指令格式但真到动手写处理器模块时取指、译码、执行这几个状态怎么串起来、存储器什么时候读、PC 什么时候加一全得自己摸索。这份资源就是冲着这个痛点来的——它用 Verilog HDL 实现了一个简化处理器模块配套 64×8 的存储器模块顶层把两者连成完整的计算机核心部件系统指令序列写进存储器后能完整跑出取指、译码、执行、写回的流程。适合电子信息类专业做《计算机组成实验》课程设计的学生也适合想从零理解 CPU 数据通路的手工派。下面按“指令系统怎么定、处理器怎么写、存储器怎么配、顶层怎么连、坑在哪”的顺序拆开讲。2. 指令系统与状态机16 条指令怎么编码、状态怎么跳2.1 指令格式与操作码分配这份设计的指令字长 16 位高 4 位IR[15:12]是操作码低 12 位按指令类型拆成寄存器编号、移位模式或地址偏移。操作码用parameter定义一共 16 个状态值其中idle到stop覆盖了数据传送、算术运算、逻辑运算、移位、跳转和访存几大类。先把这张表吃透后面写状态机才不会乱操作码助记符功能关键字段0000idle空操作/复位态无0001load立即数装入 R0IR[11:8] 为立即数0010move寄存器间传送IR[11:8] 指定源/目的0011addp加法IR[11:10] 目的IR[9:8] 源0100subp减法同上0101mulp乘法结果跨两个寄存器同上0110logp与/或/异或/取反IR[11:10] 运算类型0111comp比较IR[11:10] 比较模式1000sh移位循环/算术/逻辑IR[11:10] 类型IR[9:8] 寄存器1001meth1自增/自减/加二/减二IR[11:10] 模式1010swap交换走访存路径1011jmp无条件跳转地址在低 12 位1100jz零跳转依赖状态标志1101read读存储器走访存路径1110write写存储器走访存路径1111stop停机状态机锁死这里有个容易忽略的点load的立即数只有 4 位所以能装的常数范围是 0 到 15。如果指令序列里需要更大的数得靠meth1反复自增或者用addp拼出来。我见过有人直接往load里塞 8 位数仿真出来高 4 位被截掉排查半天以为是寄存器位宽写错了。2.2 状态机的两段式写法处理器内部用status寄存器驱动整个执行流程status_change块在时钟上升沿推进状态seq块在时钟下降沿做数据操作。这种“上升沿改状态、下降沿动数据”的写法在课设里很常见好处是状态切换和数据更新错开半个周期避免同一时刻读写冲突。状态跳转的核心逻辑是这样的always (negedge reset or posedge clock) begin: status_change if (reset 1b0) status 0; else case (status) 0: status 1; // 取指 1: if (IR[15:12] stop) // 译码遇 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) // swap 单独处理 status 0; else status 4; 4: status 5; 5: case (IR[15:12]) read: status 6; // read 多一个等待周期 default: status 0; endcase default: status 0; endcase end这段代码的逻辑说明状态 0 负责把存储器数据打进IR并让 PC 加一状态 1 做译码同时把PC送到MAR准备取操作数状态 2 到 5 是访存类指令的公共路径read因为要等存储器数据回来多占一个状态 6。参数上要注意reset是低电平有效仿真时如果复位信号只给一个时钟周期的低电平status能回到 0但seq块里的寄存器不一定全清干净——因为seq块用的是negedge clock复位释放和时钟沿的先后关系会影响初始化结果。稳妥做法是复位信号至少保持两个完整时钟周期。2.3 取指与 PC 更新的时序配合取指阶段在status 0时执行IR {M_data_in, 8b00000000}把存储器输出的 8 位数据放到指令寄存器高 8 位低 8 位补零同时PC PC 1b1。这里PC用的是阻塞赋值而IR用的是非阻塞赋值混用是有意为之PC要在当前状态内立刻更新供后续MAR PC使用IR则等到时钟沿统一生效。如果两个都用非阻塞状态 1 里读到的PC还是旧值取操作数地址就会错位。这个细节在课本上不会专门讲但仿真波形一拉就能看出来——MAR比预期晚一个周期变化基本就是这里出的问题。3. 处理器模块实现寄存器组、ALU 与访存控制怎么落地3.1 寄存器组与数据通路的声明方式seq块内部声明了PC、R0到R3、累加器A、以及两个临时变量temp9 位和temp216 位。temp多一位是为了捕获加法进位temp2用于乘法结果的高低字节拆分。这些寄存器全部在always块内部用reg声明属于块内局部变量综合工具会把它映射成触发器。写法上有个讲究R0到R3用阻塞赋值而IR、MAR、MDR用非阻塞。前者是组合逻辑风格的即时更新后者是时序逻辑的沿触发更新两套语义混在一个块里初看别扭但在这个状态机框架下能跑通——因为状态推进和数据操作分属不同时钟沿不会产生竞争。数据通路的宽度要盯紧M_address是 12 位对应 64×8 存储器的地址空间实际只用了低 6 位64 个单元高 6 位在顶层连接时要么接地要么悬空建议显式接 0否则综合工具可能报位宽不匹配的警告。M_data_in和M_data_out都是 8 位和寄存器组宽度一致算术运算的溢出判断靠temp[8] ^ temp[7]实现——最高进位和次高进位异或结果为 1 表示有符号溢出。这个判断式在加法和减法里都用了减法走的是补码加法路径(~A) 1逻辑是对的但要注意A取反后位宽仍是 8 位加 1 产生的进位由temp的第 9 位接住。3.2 算术与逻辑指令的执行分支以加法addp为例执行阶段根据IR[11:10]选择目的寄存器把寄存器值和累加器A做带符号扩展的加法addp: case (IR[11:10]) 2b00: begin temp ({R0[7], R0[7:0]}) ({A[7], A[7:0]}); R0 temp[7:0]; overflow temp[8] ^ temp[7]; end 2b01: begin temp ({R1[7], R1[7:0]}) ({A[7], A[7:0]}); R1 temp[7:0]; overflow temp[8] ^ temp[7]; end // R2、R3 分支结构相同略 endcase{R0[7], R0[7:0]}是符号扩展把 8 位有符号数扩成 9 位再相加这样temp[8]就是真正的符号位进位。如果直接写R0 AVerilog 会按 8 位运算进位丢失溢出标志永远为 0。乘法mulp的结果是 16 位低 8 位存目的寄存器高 8 位存相邻寄存器比如R0 * A的低字节进R0、高字节进R1这个约定要在指令序列设计时提前规划好不然乘完发现高字节把别的数据覆盖了。逻辑指令logp里有个小陷阱IR[9:8]为2b11的分支写的是R1 R1 R1实际效果是保持R1不变看起来像占位符如果指令序列里误用了这个编码结果不会报错但逻辑不对调试时容易漏掉。3.3 访存指令的握手时序read和write走的是状态 3 到 6 的路径。write在状态 3 把M_data_out赋给MDR状态 4 拉高Write_Read信号状态 5 撤销read则在状态 5 之后多等一个状态 6让存储器数据稳定后再打进MDR。Write_Read是输出端口高电平写、低电平读顶层连到存储器的读写控制端。这里的关键参数是存储器模型的读延迟——如果用的 IP 核或行为模型有一个时钟周期的读延迟状态 6 就是必须的如果存储器是组合逻辑输出地址一变数据立刻有效状态 6 可以省掉但省掉后read和write的状态路径不对称状态机得改。我一般建议保留状态 6多一个周期换时序确定性课设场景下不值得为省一个周期去冒险。4. 存储器模块与顶层连接64×8 存储器和原理图顶层怎么配4.1 64×8 存储器的初始化文件格式存储器模块用 64 个 8 位单元地址线 6 位顶层接M_address[5:0]数据线 8 位。初始化靠.mif文件格式是地址和数据的键值对每行一个单元DEPTH 64; WIDTH 8; ADDRESS_RADIX HEX; DATA_RADIX HEX; CONTENT BEGIN 00 : 10; 01 : 1F; 02 : 20; 03 : 30; 04 : 40; 05 : F0; END;DEPTH和WIDTH必须和存储器实例化参数一致ADDRESS_RADIX用 HEX 时地址写两位十六进制。指令序列按 16 位一条拆成两个 8 位单元存放高字节在前、低字节在后取指时IR {M_data_in, 8b00000000}先拿高字节下一个周期再拿低字节拼成完整指令——等等这里要核对一下原设计的取指只在一个状态里读了一次 8 位数据说明指令是 8 位对齐存放的每条指令占一个存储单元的高 8 位低 8 位留空或存操作数。如果指令序列里连续放多条指令地址要按 2 递增00、02、04…奇数地址空着。这个约定在写.mif时最容易翻车地址连续写 00、01、02 会导致第二条指令的高字节被当成低字节读进来仿真波形上IR的值和预期对不上但状态机照跑不误属于典型的“能跑但结果错”。4.2 顶层原理图连接的端口映射顶层用原理图方式连接处理器模块和存储器模块的端口对应关系如下处理器端口方向存储器端口说明M_address[11:0]输出address[5:0]高 6 位接 0低 6 位接地址M_data_out[7:0]输出data_in[7:0]写数据M_data_in[7:0]输入data_out[7:0]读数据Write_Read输出wren高电平写、低电平读clock输入clock全局时钟reset输入—只接处理器原理图里要额外加一个输入引脚接clock一个接resetoverflow接输出引脚方便观察。地址线高位接地的做法在原理图上就是拉一个接地符号连到M_address[11:6]别悬空——悬空在高阻态仿真下可能被解析成 XMAR的值带 X 后整个地址译码失效波形上一片红。如果原理图工具不支持总线分线就在处理器模块内部把M_address声明成 12 位、实际只驱动低 6 位高 6 位赋 0顶层直接整条总线连过去省去分线的麻烦。4.3 指令序列设计与执行流程验证指令序列要覆盖取指、译码、执行、写回四个阶段建议按“装入立即数 → 算术运算 → 存结果 → 跳转 → 停机”的顺序编排。一个能验证加法的最小序列00 : 10; // load R0, 立即数 0 02 : 1F; // load R0, 立即数 15覆盖前值 04 : 30; // addpR0 R0 A 06 : E0; // write把结果写回存储器 08 : F0; // stop仿真时重点看三个信号status是否按 0→1→2→0 循环推进PC是否在每条指令取指后加一M_address在访存指令时是否指向预期单元。如果status卡在 1 不动检查IR[15:12]是不是stop的编码1111被误读如果PC跳变不规则看jmp/jz分支里PC的赋值是不是被后面的取指覆盖了。验证通过的标准是给定指令序列R0到R3的终值和手工推算一致overflow在溢出场景下拉高stop执行后status锁死不再变化。5. 避坑与排查五个让仿真结果对不上的典型问题5.1 现象IR 值始终为 0状态机空转原因通常是.mif文件没加载成功或者存储器实例化时初始化文件路径写错。综合工具对路径大小写敏感Windows 下写的mem.mif到 Linux 环境可能找不到。解决方法是仿真前先单独例化存储器模块给一个固定地址读一次看data_out是否有值没有就检查文件路径和$readmemh语句如果用的是行为模型。另外.mif里CONTENT段的关键字拼写错误也会导致静默失败工具不报错但存储器全零。5.2 现象加法结果正确但 overflow 永远为 0前面提过根因是加法没做符号扩展temp只声明了 8 位或者赋值时截断了第 9 位。检查temp的位宽声明是不是reg [8:0]以及加法表达式里两个操作数是否都做了{sign, value}扩展。还有一种隐蔽情况overflow在seq块开头被清零overflow 1b0如果加法分支里的赋值被后面的分支覆盖比如case没写default导致落空标志就丢了。给每个算术分支都显式赋值别依赖默认值。5.3 现象read 指令读回的数据比写入晚一个周期这是存储器读延迟和状态机不匹配的典型表现。如果存储器模型是同步读时钟沿触发输出read的状态 6 必须保留如果误删了状态 6MDR在数据有效前就被采样读回的是上一个地址的内容。排查方法是在read分支里加一个临时输出把status和M_data_in同时打到波形上看数据有效的时刻和MDR更新时刻差几个周期。差一个周期就补一个状态差半个周期说明存储器是组合输出反而要去掉状态 6。5.4 现象顶层原理图编译报位宽不匹配M_address是 12 位存储器地址是 6 位直接连线工具会警告。解决方式有两种在处理器内部把M_address改成 6 位但原设计的端口声明是 12 位改端口要同步改所有引用或者在顶层加一个总线分线器取低 6 位、高位接地。我一般选后者不动底层模块顶层多一个分线符号而已。注意分线后M_address[11:6]接地的那个网络要命名不然原理图网表里出现匿名网络后续查信号对不上号。5.5 现象jz 跳转条件永远不成立jz依赖状态标志判断但原设计里status是 3 位、只用于状态机推进没有独立的零标志寄存器。如果指令序列里用jz做循环控制得先确认比较指令comp是否把结果写进了某个寄存器比如R0 1表示相等然后jz判断的是这个寄存器而不是隐式标志。常见错误是照搬课本上“零标志位”的概念以为jz会自动读 ALU 的输出实际这份设计里没有这条通路得靠comp显式产生可判断的值。改法是在comp分支里把比较结果写进固定寄存器jz分支读同一个寄存器做条件。6. 进阶验证用波形对照表把每条指令跑穿课设验收时最怕的是“仿真能跑但说不清为什么对”。我的习惯是给每条指令建一张波形对照表把预期值和实测值并排放跑完一轮指令序列后逐条打勾。具体做法是在 testbench 里用$monitor打印status、PC、IR、R0到R3、overflow采样时刻选在时钟下降沿之后避开状态切换的竞争窗口输出重定向到日志文件。然后手工推算每条指令执行后的寄存器值和日志逐行比对。这张表长这样指令执行前 R0预期 R0实测 R0overflow 预期实测load 150151500addp R0A15303000subp R0-A30151500mulp R0*A15低字节低字节——乘法那行故意留空因为结果跨两个寄存器得同时看R0和R1单列放不下。对照表跑完如果某条指令的实测值和预期差一个固定偏移大概率是取指时地址步进错了该加 2 加了 1如果差得毫无规律回去查.mif里指令字节的高低位顺序。这套流程走一遍验收时被问到“这条指令为什么这么跳”能直接翻日志指给对方看比现场口算靠谱得多。从那以后我每次做状态机类的课设都强制先把指令序列的预期波形手画一遍再写代码画不出来的地方就是设计没想清楚的地方。希望这份拆解帮到你少在仿真波形里泡两个晚上。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表