news 2026/10/28 5:11:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 == 1'b0) 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, 8'b00000000}把存储器输出的 8 位数据放到指令寄存器高 8 位,低 8 位补零,同时PC = PC + 1'b1。这里PC用的是阻塞赋值,而IR用的是非阻塞赋值,混用是有意为之:PC要在当前状态内立刻更新,供后续MAR <= PC使用;IR则等到时钟沿统一生效。如果两个都用非阻塞,状态 1 里读到的PC还是旧值,取操作数地址就会错位。这个细节在课本上不会专门讲,但仿真波形一拉就能看出来——MAR比预期晚一个周期变化,基本就是这里出的问题。

3. 处理器模块实现:寄存器组、ALU 与访存控制怎么落地

3.1 寄存器组与数据通路的声明方式

seq块内部声明了PC、R0到R3、累加器A、以及两个临时变量temp(9 位)和temp2(16 位)。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]) 2'b00: begin temp = ({R0[7], R0[7:0]}) + ({A[7], A[7:0]}); R0 = temp[7:0]; overflow <= temp[8] ^ temp[7]; end 2'b01: 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 + A,Verilog 会按 8 位运算,进位丢失,溢出标志永远为 0。乘法mulp的结果是 16 位,低 8 位存目的寄存器,高 8 位存相邻寄存器(比如R0 * A的低字节进R0、高字节进R1),这个约定要在指令序列设计时提前规划好,不然乘完发现高字节把别的数据覆盖了。逻辑指令logp里有个小陷阱:IR[9:8]为2'b11的分支写的是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, 8'b00000000}先拿高字节,下一个周期再拿低字节拼成完整指令——等等,这里要核对一下:原设计的取指只在一个状态里读了一次 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,一个接reset,overflow接输出引脚方便观察。地址线高位接地的做法在原理图上就是拉一个接地符号连到M_address[11:6],别悬空——悬空在高阻态仿真下可能被解析成 X,MAR的值带 X 后整个地址译码失效,波形上一片红。如果原理图工具不支持总线分线,就在处理器模块内部把M_address声明成 12 位、实际只驱动低 6 位,高 6 位赋 0,顶层直接整条总线连过去,省去分线的麻烦。

4.3 指令序列设计与执行流程验证

指令序列要覆盖取指、译码、执行、写回四个阶段,建议按“装入立即数 → 算术运算 → 存结果 → 跳转 → 停机”的顺序编排。一个能验证加法的最小序列:

00 : 10; // load R0, 立即数 0 02 : 1F; // load R0, 立即数 15(覆盖前值) 04 : 30; // addp,R0 = 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 <= 1'b0,如果加法分支里的赋值被后面的分支覆盖(比如case没写default导致落空),标志就丢了。给每个算术分支都显式赋值,别依赖默认值。

5.3 现象:read 指令读回的数据比写入晚一个周期

这是存储器读延迟和状态机不匹配的典型表现。如果存储器模型是同步读(时钟沿触发输出),read的状态 6 必须保留;如果误删了状态 6,MDR在数据有效前就被采样,读回的是上一个地址的内容。排查方法是在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 150151500
addp R0+A15303000
subp R0-A30151500
mulp R0*A15低字节低字节——

乘法那行故意留空,因为结果跨两个寄存器,得同时看R0和R1,单列放不下。对照表跑完,如果某条指令的实测值和预期差一个固定偏移,大概率是取指时地址步进错了(该加 2 加了 1);如果差得毫无规律,回去查.mif里指令字节的高低位顺序。这套流程走一遍,验收时被问到“这条指令为什么这么跳”能直接翻日志指给对方看,比现场口算靠谱得多。

从那以后我每次做状态机类的课设,都强制先把指令序列的预期波形手画一遍再写代码,画不出来的地方就是设计没想清楚的地方。希望这份拆解帮到你,少在仿真波形里泡两个晚上。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 5:11:29

MCP模块化计算平台技术解析与应用实践

1. MCP技术现象观察&#xff1a;从默默无闻到全网刷屏去年三季度开始&#xff0c;我的技术社群突然被"MCP"这个缩写词刷屏。最初以为是某个新开源框架&#xff0c;直到连续三天看到不同领域的KOL都在讨论它——前端圈在说"MCP渲染性能提升300%"&#xff0c…

作者头像 李华
网站建设 2026/10/28 5:11:29

百炼API参数分组:大模型调优实战指南

1. 百炼模型调优API参数分组实战指南在AI模型部署和调优的实际工作中&#xff0c;参数管理往往是让人头疼的问题。最近我在使用百炼平台进行大模型调优时&#xff0c;发现其API的参数分组功能能显著提升工作效率。这个功能特别适合需要同时调整多个超参数、处理复杂模型配置的场…

作者头像 李华
网站建设 2026/10/28 5:11:11

AI代码生成的质量陷阱与工程化解决方案

1. AI代码生成的双刃剑&#xff1a;效率与风险的博弈当我在IDE中第一次使用GitHub Copilot时&#xff0c;那种"代码自动补全"的震撼感至今难忘——就像有个资深程序员在实时指导我编码。但三周后&#xff0c;我在生产环境debug一段AI生成的SQL查询时&#xff0c;发现…

作者头像 李华
网站建设 2026/10/28 5:10:06

YOLO26多路视频实时分析实战:智能安防与工业质检

1. 项目概述去年在做一个安防监控项目时&#xff0c;客户要求同时对16路摄像头进行实时分析&#xff0c;检测特定目标并触发告警。当时尝试了多种方案&#xff0c;最终基于YOLO26和Python构建的解决方案不仅实现了98%的准确率&#xff0c;还能在普通服务器上稳定运行。今天就来…

作者头像 李华
网站建设 2026/10/28 5:09:22

腾讯智能协作技术沙龙:API开发与社交创新实践

1. 项目背景与活动定位腾讯智能协作团队近期推出的"码力全开"系列分享会正在北京、上海、深圳三地巡回举办。这场以"NPC小龙虾"为特色的技术沙龙&#xff0c;本质上是一场融合了专业分享与轻松社交的开发者活动。作为参与过首场活动的亲历者&#xff0c;我…

作者头像 李华
网站建设 2026/10/28 5:09:15

当自演化代码智能体遇上数字孪生:从写代码到懂物理世界

1. 从代码智能体到物理世界智能&#xff0c;差的不是算法&#xff0c;是“体感”这几年“写代码比较好的智能体”已经不算新闻了&#xff0c;从补全、聊天式生成到多智能体协作写代码&#xff0c;产品演进的节奏快得让人眼花。但如果你真拿这些智能体去接物理设备&#xff0c;大…

作者头像 李华