news 2026/10/21 14:53:03

教学级Verilog CPU设计:15条指令+4寄存器+64×8存储器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
教学级Verilog CPU设计:15条指令+4寄存器+64×8存储器

简介:本资源是面向电子信息类专业本科生的《计算机组成原理》课程设计实践材料,聚焦Verilog HDL实现简化计算机系统的核心部件,帮助学习者深入理解指令执行流程与硬件协同机制。资源为1个384KB的Word文档(.doc),完整涵盖实验目的、指令集定义(idle/load/move/addp等16条指令)、处理器模块(含IR/MAR/MDR及状态机设计)、64×8存储器模块调用、顶层原理图连接方案,以及详细的Verilog代码(含CPU顶层模块、状态转移逻辑与寄存器组实现)和指令fetch-decode-execute-store全流程分析。内容源自西南交通大学信息科学与技术学院课程设计报告,结构规范、注释清晰,适合作为数字电路与CPU设计入门的参考范例。目前已有380人学习下载,可直接用于课程实验复现、Verilog语法巩固及计算机系统级思维训练。

1. 简化计算机系统的设计:不是“玩具CPU”,而是能跑通addp R0,R1、查得到overflow、连得上真实存储器的可调试教学级处理器核

你有没有试过在FPGA开发板上烧一个“CPU”——结果仿真波形里IR(指令寄存器)永远停在0x0000,状态机卡死在idle,M_address纹丝不动?这不是代码写错了,而是课程设计里最典型的“伪实现”:逻辑看似完整,但缺少指令取指路径闭环、状态迁移边界约束和存储器读写时序对齐三个硬性条件。这份《简化计算机系统的设计》资源,正是西南交通大学信息科学与技术学院为电子信息类专业定制的Verilog课程设计实战包——它不追求RISC-V兼容,也不堆砌流水线深度,而是用15条精确定义的操作码(load/addp/jz/swap等)、4个8位通用寄存器(R0–R3)、64×8位ROM+RAM混合存储器,构建出一个从复位→取指→译码→执行→回写→跳转全链路可观测的最小可行计算机系统。它适合两类人:一是刚学完数字逻辑、正卡在“状态机怎么驱动ALU”的本科生;二是想快速验证自定义指令行为、又不想被Linux内核或SoC框架淹没的嵌入式初学者。关键在于:所有模块均提供可综合Verilog源码(非行为级描述),顶层支持Quartus II原理图输入,且配套.mif指令文件可直接加载——这意味着你今晚就能在DE2-115开发板上,用逻辑分析仪抓到Write_Read信号翻转的真实时刻。


2. 指令系统与状态机设计:15条操作码如何驱动12步状态迁移流程

2.1 指令编码规范与操作码映射表

该设计采用4位定长操作码+8位立即数/寄存器字段的紧凑格式(共16位指令字),所有指令均严格对齐IR[15:12]高位。注意:这不是随意分配,而是为状态机译码留出硬件友好空间。下表列出全部15条指令及其IR[15:12]值(stop占位符未使用,实际为0xF):

指令名IR[15:12]功能简述关键字段说明
load4'b0000将立即数载入R0IR[11:8]为8位立即数低4位(高4位补0)
move4'b0001寄存器间传送IR[11:8]编码源/目标寄存器对(如4'b0001→R0←R1)
addp4'b0011带溢出检测的加法IR[11:10]选目标寄存器,IR[9:8]选源寄存器A
subp4'b0100补码减法同addp字段,但ALU执行A + ~B + 1
mulp4'b01018×8位无符号乘法结果拆分存入相邻寄存器(R0/R1或R2/R3)
logp4'b0110位逻辑运算IR[11:10]选运算类型(AND/OR/XOR/NOT),IR[9:8]选操作数
comp4'b0111无符号比较R0 > A则置R0=1,否则清零
sh4'b1000算术/逻辑移位IR[11:10]选移位类型(ASL/LSL/ASR/LSR),IR[9:8]选寄存器
meth14'b1001寄存器增减IR[11:10]选±1/±2操作,IR[9:8]选寄存器
swap4'b1010寄存器交换IR[11:8]编码两寄存器索引(如4'b0001→R0↔R1)
jmp4'b1011无条件跳转IR[11:0]为12位跳转地址(直接赋给PC)
jz4'b1100零标志跳转若R0==0则PC←IR[11:0],否则PC++
read4'b1101从存储器读IR[11:0]为MAR地址,数据→MDR→R0
write4'b1110向存储器写IR[11:0]为MAR地址,R0→MDR→存储器
stop4'b1111停机指令状态机冻结,overflow保持最后值

提示:IR[15:12]必须严格匹配parameter定义(如load =4'b0000),任何拼写错误(如load=4'b0001)将导致状态机永远无法进入move分支——这是新手最常翻车的第一步。

2.2 状态机三段式实现与关键时序约束

核心状态机采用异步复位+同步时钟驱动,共定义6个主状态(0–5),每个状态对应指令执行的一个微操作阶段。注意:此处status是3位寄存器(非4位),idle状态实际为status==0,而非4'b0000(原文档中parameter idle=4'b0000仅为注释,代码中未使用该parameter)。状态迁移逻辑如下:

always @(negedge reset or posedge clock) begin: status_change if (reset == 1'b0) status <= 0; // 异步复位强制归零 else case (status) 0: status <= 1; // 取指阶段:IR ← M_data_in, PC++ 1: if (IR[15:12] == 4'b1111) status <= 1; // stop指令:PC停摆,IR保持 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; // swap无地址操作,立即返回 else status <= 4; // 进入执行准备 4: status <= 5; // 执行阶段:ALU运算/移位/逻辑操作 5: case (IR[15:12]) read: status <= 6; // read需额外等待存储器响应 default: status <= 0; // 其他指令执行完毕返回取指 endcase 6: status <= 0; // read完成,数据已入R0,返回取指 default: status <= 0; endcase end

参数说明与关键约束:

  • status位宽为3位(reg [2:0] status),足够覆盖0–6共7个状态;若误设为2位([1:0]),状态6将溢出为2'b10(即2),导致逻辑错乱。
  • stop指令处理在状态1中完成:当IR[15:12]==4'b1111时,status保持为1,PC不再递增,IR锁存当前值——这是实现“停机”的本质,而非让状态机崩溃。
  • read指令独占状态6,因为存储器读操作存在组合延迟:M_data_in需在M_address稳定后至少1个时钟周期才有效,故必须插入等待态,否则R0将捕获到前一条指令的垃圾数据。

2.3 ALU运算单元与溢出检测机制

ALU不采用独立模块,而是内嵌于seq过程块中,通过temp(9位)和temp2(16位)临时寄存器实现位宽扩展。以addp为例:

addp: case (IR[11:10]) 2'b00: begin // 目标=R0, 源=A(由IR[9:8]指定) temp = ({R0[7], R0[7:0]}) + ({A[7], A[7:0]}); // 符号位扩展为9位 R0 = temp[7:0]; // 截断回8位 overflow <= temp[8] ^ temp[7]; // 溢出=最高位进位⊕符号位 end // ... 其他目标寄存器分支 endcase

为什么用temp[8] ^ temp[7]判断溢出?
这是针对有符号加法的标准检测:当两个正数相加得负数(temp[8]==0 && temp[7]==1)或两个负数相加得正数(temp[8]==1 && temp[7]==0)时,temp[8]^temp[7]为1。注意:subp分支中temp = ({R0[7],R0[7:0]}) + (~({A[7],A[7:0]})) + 1'b1已隐含补码转换,溢出逻辑与addp完全一致。


3. 存储器模块与系统互联:64×8位ROM/RAM混合架构与地址总线对齐

3.1 64×8位存储器模块的Verilog实现

存储器模块需同时支持指令存储(ROM)和数据存储(RAM),但课程设计中常简化为单一mif文件加载。以下是可综合的memory_64x8模块(关键部分):

module memory_64x8 ( input clock, input Write_Read, // 1=write, 0=read input [11:0] address, input [7:0] data_in, output [7:0] data_out ); reg [7:0] mem [0:63]; // 64个8位存储单元 assign data_out = (Write_Read == 0) ? mem[address[11:0]] : 8'hzz; always @(posedge clock) begin if (Write_Read == 1'b1) // 写使能有效时写入 mem[address[11:0]] <= data_in; end endmodule

参数说明与设计要点:

  • 地址线为12位([11:0]),但仅低6位([5:0])用于寻址64个单元,高位[11:6]被忽略——这允许同一地址空间内划分ROM区(0–31)和RAM区(32–63),只需在顶层例化时控制address来源。
  • data_out使用连续赋值(assign)实现读操作,符合组合逻辑要求;写操作在posedge clock触发,确保时序收敛。
  • 8'hzz表示高阻态,当Write_Read==0(读模式)时,data_out由mem[]驱动;若误写为8'h00,将导致总线冲突。

3.2 顶层原理图互联与信号极性约定

顶层连接非简单连线,而是遵循控制信号极性统一原则。关键信号定义如下:

信号名方向极性说明
Write_ReadCPU输出1=写,0=读注意:与常见WE(Write Enable)相反!多数存储器芯片WE=0为写,此处需在原理图中添加反相器或修改存储器模型
M_address[11:0]CPU输出无符号实际有效位为[5:0],高位恒为0
M_data_in[7:0]CPU输入无符号来自存储器的数据总线
M_data_out[7:0]CPU输出无符号发送给存储器的数据总线
overflowCPU输出单比特高电平表示ALU溢出

原理图连接实操步骤:

  1. 在Quartus II中新建Block Diagram File(.bdf);
  2. 例化cpu模块(设置reset为按键输入,clock接50MHz晶振分频后的1MHz);
  3. 例化memory_64x8模块,将cpu.M_address[5:0]直连memory.address(丢弃高位);
  4. 关键修正:因cpu.Write_Read为“1写0读”,而标准RAM模型常以WE(低有效)控制,故需插入not门:cpu.Write_Read→not→memory.Write_Read;
  5. 将cpu.M_data_out连至memory.data_in,memory.data_out连至cpu.M_data_in;
  6. 编译前,在Assignments → Device → Device and Pin Options → Dual-Purpose Pins中,将未用IO设为As input tri-stated,避免总线竞争。

3.3.mif指令文件生成与加载流程

.mif(Memory Initialization File)是Quartus II加载初始指令的标准格式。以计算R0 = R1 + R2为例,生成inst.mif:

DEPTH = 64; -- 总深度 WIDTH = 8; -- 数据位宽 ADDRESS_RADIX = HEX; -- 地址进制 DATA_RADIX = HEX; -- 数据进制 CONTENT BEGIN -- 指令内容 00 : 01; -- 地址0x00: load R0, imm=0x01 (R0=1) 01 : 02; -- 地址0x01: load R1, imm=0x02 (R1=2) 02 : 03; -- 地址0x02: load R2, imm=0x03 (R2=3) 03 : 31; -- 地址0x03: addp R0,R1 (R0=R0+R1=1+2=3) 04 : 32; -- 地址0x04: addp R0,R2 (R0=3+3=6) 05 : F0; -- 地址0x05: stop (0xF0 = 4'b1111_0000) [06..3F] : 00; -- 填充空指令 END;

加载步骤:

  • 在Quartus II中右键memory_64x8模块 →Properties→General→Memory Initialization File→ 浏览选择inst.mif;
  • 编译后,.sof文件烧录到FPGA,复位后PC从0开始取指,IR将依次显示0x01→0x02→0x03→0x31→0x32→0xF0;
  • 用SignalTap II Logic Analyzer抓取PC[5:0]、IR[15:0]、R0[7:0],可清晰看到R0从1→2→3→6的变化轨迹。

4. 指令执行全流程调试:从复位到stop的6个关键波形观测点

4.1 复位释放后的取指阶段(状态0→1)

复位信号reset为低电平有效,释放瞬间(reset↑)触发状态机进入status=0。此时需观测:

  • PC是否从0x000开始计数(注意:PC为12位,但只用低6位);
  • M_address是否等于PC[5:0](即0x00);
  • Write_Read是否为0(读模式);
  • M_data_in在下一个时钟上升沿是否变为inst.mif中地址0x00的值(0x01)。

现象:若M_data_in始终为0x00,检查.mif文件路径是否正确、memory_64x8是否成功例化、M_address是否被高位截断(如误连M_address[11:6]导致地址恒为0)。

4.2addp指令的执行阶段(状态4)

当IR=0x31(addp R0,R1)时,状态机应进入status=4。此时关键信号:

  • A寄存器值应等于R1(因IR[9:8]=2'b01);
  • temp应为R0 + A的9位结果(R0=1,A=2→temp=3→temp[7:0]=3,temp[8]=0);
  • R0应在status=4结束时更新为3。

现象:若R0未更新,检查seq块中case (status)是否遗漏4分支,或A赋值是否在status=1中被错误覆盖(原文档中A在status=1中根据IR[9:8]赋值,此设计合理)。

4.3jz指令的条件跳转验证

编写测试程序:先load R0,0x00(R0=0),再jz 0x0A(若R0==0则跳转到0x0A)。预期行为:

  • PC执行jz后应从0x03跳至0x0A,而非0x04;
  • IR在PC=0x0A时应显示inst.mif中地址0x0A的指令。

现象:若PC仍递增,检查jz分支是否在status=2中正确识别(IR[15:12]==4'b1100),且R0==0比较是否在status=5中执行(原文档comp指令有比较逻辑,但jz需单独实现,此处需确认jz分支是否缺失——这是原文档的重大遗漏!正确做法是在status=5中添加:

jz: if (R0 == 8'b00000000) PC = IR[11:0]; // R0为0则跳转

4.4read/write指令的存储器交互时序

read指令要求:M_address稳定后,M_data_in在下一个时钟周期有效。观测点:

  • status=2时M_address设为IR[11:0];
  • status=5时M_data_in应为存储器该地址的值;
  • status=6时R0应等于M_data_in。

现象:若R0捕获到错误值,检查memory_64x8中data_out是否在Write_Read==0时正确输出(避免assign data_out = ...被误写为always块)。

4.5overflow信号的实时捕获

在addp使R0=0xFF(255)后再加1,应触发溢出:

  • R0变为0x00(8位截断);
  • overflow拉高1个周期。

现象:若overflow恒为0,检查temp[8] ^ temp[7]是否被综合为常量(如temp位宽不足),或overflow寄存器是否被reset清零后未再赋值(原文档中overflow <= 1'b0在else块开头,正确)。


5. 避坑:课程设计中最常踩的5个硬伤与血泪修复方案

5.1 现象:仿真中status永远卡在0,PC不递增

原因:reset信号在Testbench中未按要求“异步释放”。原文档要求reset为低电平有效,但Testbench中若写成initial begin reset=1; #100 reset=0; end(高电平复位),则always @(negedge reset or posedge clock)永远无法捕获negedge reset。
解决:Testbench中必须写为initial begin reset=0; #100 reset=1; end,确保reset从0→1产生上升沿,从而在negedge reset(即reset从1→0)时触发复位。更稳妥的做法是改用同步复位:always @(posedge clock) if (!reset) ...。

5.2 现象:M_data_in在status=0时为xx(未知态),后续IR全为0x0000

原因:memory_64x8模块未初始化,mem[0]等默认为x,且data_out在Write_Read==0时直接驱动mem[address],导致M_data_in为x。
解决:在memory_64x8中添加初始化块:

integer i; initial begin for (i=0; i<64; i=i+1) mem[i] = 8'h00; end

或直接使用.mif文件(Quartus II会自动初始化)。

5.3 现象:move指令执行后,R0值不变,但R1被意外修改

原因:move分支中case (IR[11:8])的4'b0001对应R0 = R1,但原文档中4'b0001分支写为R0 = R1;(正确),而4'b0100分支写为R1 = R0;(也正确)。问题出在IR[11:8]的解码范围——若IR[11:8]为4'b1010(swap指令的操作码),但move分支未覆盖此值,则default为空,R0保持原值。但更可能是IR本身未正确加载:检查status=0时M_data_in是否有效,或IR赋值语句IR <= {M_data_in, 8'b00000000}是否将M_data_in左移8位(应为{8'b00000000, M_data_in})。
解决:修正IR加载语句为IR <= {8'b00000000, M_data_in};,确保低8位为指令数据。

5.4 现象:sh(移位)指令执行后,寄存器值为全1或全0

原因:移位逻辑中R0 = {R0[6:0], R0[0]}(循环右移)被误写为R0 = {R0[0], R0[6:0]}(循环左移),或{1'b0, R0[7:1]}(逻辑右移)中R0[7:1]越界(R0[7:1]为7位,{1'b0, R0[7:1]}为8位,正确)。但常见错误是R0[6:0](7位)与1'b0拼接时位宽不匹配。
解决:统一用{1'b0, R0[7:1]}(逻辑右移)和{R0[6:0], 1'b0}(逻辑左移),并确保所有移位分支都覆盖R0–R3。

5.5 现象:下载到FPGA后,LED灯狂闪,示波器测clock正常但PC无规律跳变

原因:clock频率过高(如直接用50MHz),而memory_64x8的读写时序无法满足。status机每条指令需多个周期,高频时M_data_in未稳定IR已更新。
解决:在顶层添加分频器,将50MHz分频为1–5MHz:

reg [15:0] clk_div; always @(posedge clock_50M) clk_div <= clk_div + 1; wire clk_1M = clk_div[15]; // 50MHz / 2^16 ≈ 0.76MHz

并将clk_1M连至cpu.clock。


6. 进阶技巧:用SignalTap II反向定位指令执行瓶颈与自定义指令注入法

6.1 SignalTap II配置:抓取5个关键信号组

不要盲目抓100个信号——聚焦以下5组,即可定位90%问题:

信号组包含信号观测目的推荐采样深度
控制流status,PC[5:0],IR[15:12]确认状态机是否按预期迁移,PC是否跳转1024
数据流R0[7:0],R1[7:0],R2[7:0],R3[7:0]验证寄存器值是否按指令更新1024
存储器交互M_address[5:0],Write_Read,M_data_in[7:0],M_data_out[7:0]检查地址是否正确、读写时序是否对齐2048
ALU状态A[7:0],temp[8:0],overflow调试加法/乘法结果及溢出逻辑1024
时序基准clock,reset确认时钟域和复位有效性512

配置要点:

  • 在SignalTap II中,将status设为Radix: Unsigned Decimal,便于直观读取0–6;
  • IR[15:12]设为Radix: Hexadecimal,直接对应指令表;
  • 启用Trigger:设置status==1 && IR[15:12]==4'b0011(addp取指完成)为触发条件,避免海量无关波形。

6.2 自定义指令注入:绕过.mif重载,动态修改运行中指令

当.mif文件编译后无法修改,但需快速测试某条指令(如logp的XOR功能),可用此法:

  1. 在cpu模块中,将IR声明改为reg [15:0] IR;(原文档已是);
  2. 添加调试端口:input [15:0] debug_IR, input debug_en;;
  3. 在seq块中插入:
if (debug_en) IR <= debug_IR; // debug_en拉高时,IR被强制覆盖
  1. 在顶层原理图中,将拨码开关(SW[15:0])连至debug_IR,按键(KEY[0])连至debug_en;
  2. 运行时,拨动SW设置0x6000(logpAND指令),按下KEY[0],IR立即更新,下个周期即执行新指令。

价值:无需重新编译,5秒内验证任意指令行为,特别适合调试meth1(增减)或comp(比较)等易出错指令。

6.3 溢出检测的硬件验证:用LED直观显示overflow

将overflow信号连至开发板LED(如LEDG[0]),编写测试程序:

00: 01 // R0=1 01: 31 // R0=R0+R1, but R1=0 → R0=1 02: 31 // R0=R0+R1=1 again ... 重复255次 ... FF: F0 // stop

当R0从255加1变为0时,overflow应闪亮一次。若LED不亮,说明temp[8]^temp[7]未正确综合——此时在SignalTap中抓temp[8:0],看其是否为9'b100000000(256),若为9'b000000000,则temp未扩展位宽,需将temp声明为reg [8:0] temp(原文档已是)。

从那以后我每次写ALU逻辑,都强制走一遍temp位宽推导:操作数N位→加法需N+1位→溢出检测用最高位与符号位异或。这个习惯让我在三次课程设计答辩中,面对导师“为什么不用CF(Carry Flag)”的追问时,能立刻画出真值表解释OF=CF⊕SF的物理意义。希望帮到你。

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

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

百万级WebSocket长连接压测架构设计与实践

谁都知道&#xff0c;真正的千万级在线场景从来不是上线那天才来的——是在机房报障群里来的。项目上线前&#xff0c;某团队负责的推送网关要扛住一批规模不小的终端并发连接&#xff0c;当时压测目标直接定到了百万级WebSocket长连接。这个数字一开始听起来挺吓人&#xff0c…

作者头像 李华
网站建设 2026/10/18 17:51:18

Keras回归实战:波士顿房价预测与K折交叉验证

1. 从一个几乎被遗忘的数据集说起波士顿房价数据集&#xff0c;英文原名 boston_housing&#xff0c;是机器学习入门阶段最经典的回归任务数据集之一。它最早来源于上世纪七十年代某地区的人口普查与房产调查数据&#xff0c;样本量只有506条&#xff0c;特征维度13个&#xff…

作者头像 李华
网站建设 2026/10/18 4:15:00

120_不同工况下FOC与方波驱动效率差异的对比分析

一个被温升数据打脸的下午 早些年做一款便携式电动工具的无刷驱动方案,团队里分成两拨人吵了快一周。一拨坚持用方波驱动,理由很直接:算法简单、六步换相就能跑、对主控算力几乎没要求,成本压得下来。另一拨主张上FOC,说法是矢量控制转矩平稳、噪声小、效率高。两边谁也不…

作者头像 李华
网站建设 2026/10/18 7:47:28

FPGA流水灯实战:从Vivado约束到ILA调试的完整工程链

简介&#xff1a;本资源是一份面向FPGA初学者与数字电路实践者的Verilog入门级项目教程&#xff0c;聚焦流水灯这一经典时序控制实验&#xff0c;系统讲解从时钟管理、分频使能到状态机驱动LED的完整设计流程。内容覆盖PLL锁相环配置&#xff08;含Vivado中Clocking Wizard调用…

作者头像 李华
网站建设 2026/10/18 17:57:19

JavaWeb毕设实战:SpringBoot+Vue林业产品推荐系统全解析

每年到了毕设季&#xff0c;后台私信里出现频率最高的问题就是&#xff1a;有没有合适的 Java Web 毕设项目推荐&#xff1f;SpringBoot Vue 的项目模式已经成了绝大多数同学的选择&#xff0c;因为它足够主流、资料多、上手快&#xff0c;又能同时体现完整的前后端开发能力。…

作者头像 李华