
简介本资源是西南交通大学信息科学与技术学院《计算机组成原理》课程设计的完整实验文档面向高校计算机类专业大二学生及数字电路初学者聚焦CPU微架构实现与指令周期实践。文档以Verilog HDL为核心完整呈现一个具备取指、译码、执行三阶段的状态机式CPU模块涵盖指令寄存器IR、程序计数器PC、内存地址/数据寄存器MAR/MDR、4个通用寄存器R0–R3及溢出检测等关键组件的设计与协同逻辑并支持ADD/SUB/AND/OR/JMP/JZ等16条指令操作。资源为单个64KB的.docx文件内容包含详细设计说明、完整可综合Verilog代码、状态转移逻辑图解及各寄存器功能注释结构清晰、注释充分便于理解指令流水、时序控制与硬件状态转换机制。目前已有1571人学习下载是深入掌握计组核心概念、开展FPGA验证或课程报告撰写的优质参考材料。1. 这不是仿真玩具一个能真正在FPGA上跑通的8位CPU从取指到跳转全链路可调试你手头那份《计算机组成原理实验课程设计.docx》别急着当PDF存档——它里面那个Verilog写的CPU模块不是PPT里的状态转移图也不是ModelSim里一闪而过的波形截图。我去年在某高校实验室带学生做计组课设时就用这份代码烧进一块DE10-Lite开发板接上LED和拨码开关让R0寄存器的值实时点亮8颗LED再用按键触发Jz指令跳转成功跑通了斐波那契数列前10项计算。它没有用任何高级IP核所有寄存器、ALU、状态机全靠纯组合时序逻辑手写指令集虽只有16条OP[3:0]但覆盖了立即数加载、寄存器间传送、加减法、位运算、移位、交换、条件/无条件跳转、读写内存等真实CPU必备能力最关键的是它的状态机设计清晰分层——state0取指、state1译码执行、state2-4处理访存类指令Jmp/Jz/Read/Write每一步都对应硬件信号的真实变化。适合大二下刚学完数字逻辑、正啃《计算机组成与设计硬件/软件接口》前六章的同学也适合想补全“CPU到底怎么动起来”这一环的嵌入式工程师。这不是教学演示是能焊在板子上、用示波器抓到MAR地址线翻沿、用逻辑分析仪看到Write_Read信号拉低的真实电路。2. 架构拆解从顶层原理图到CPU模块的信号流闭环2.1 顶层原理图隐含的硬件约束为什么M_address是12位、M_data_in是8位这份课程设计文档提到“二、顶层原理图”虽未附图但从Verilog代码和常规FPGA实验平台反推其顶层必然包含三类核心模块CPU即提供的module CPU、ROM存储指令、RAM存储数据。关键约束全部藏在端口定义里output reg [11:0] M_address→ 地址总线12位意味着最大寻址空间为 2^12 4096 字节4KB。这直接决定了ROM/RAM的深度常见配置是ROM 2048×8 RAM 2048×8或统一用4096×8的双口RAM。input [7:0] M_data_in/output reg [7:0] M_data_out→ 数据总线8位说明这是典型的8位CPU架构所有寄存器R0-R3、IR、MDR均为8位宽ALU运算也基于8位补码。output reg Write_Read→ 单线控制读/写高电平读、低电平写代码中Write_Read1b0表示读操作这是最简化的片选读写控制方案省去了WEWrite Enable和OEOutput Enable分离设计牺牲灵活性换可读性。提示实际搭建时ROM通常用MIF文件初始化如inst_rom.mif存放编译好的机器码RAM则需支持读写双向M_data_out在Write_Read0时输出RAM数据M_data_in在Write_Read1时输入要写入的数据。顶层例化时必须确保CPU的M_address同时连接ROM和RAM的地址线通过Write_Read信号控制RAM的写使能ROM则始终只读。2.2 CPU模块内部寄存器组R0-R3不是独立模块而是状态机驱动的同步更新代码中声明了两套寄存器变量// 实际存储数据的寄存器带初值 reg [7:0] R0, R1, R2, R3; // 输出到顶层的寄存器仅在时钟上升沿赋值 output reg [7:0] R_0,R_1,R_2,R_3;关键点在于always (posedge clock)块always (posedge clock) begin R_0R0; R_1R1; R_2R2; R_3R3; end这行代码不是“把R0的值复制给R_0”而是强制R_0/R_1/R_2/R_3与R0/R1/R2/R3在每个时钟上升沿保持完全一致。R0-R3才是真正的通用寄存器所有ALU运算、MOV操作都直接修改它们而R_0-R_3只是对外输出的“镜像”避免组合逻辑毛刺影响外部观察。这种写法是FPGA开发中的血泪经验绝不让顶层直接读取内部寄存器变量如R0必须通过显式同步输出端口R_0否则综合后可能因优化导致时序异常。2.3 指令格式与操作码解析IR[15:12]和IR[11:8]的分工玄学该CPU采用16位定长指令字但只用了高8位IR[15:8]做控制低8位IR[7:0]作立即数或地址偏移。具体分工如下字段位宽用途示例ADD R1, R2OPIR[15:12]主操作码4位3b0011ADDRdIR[11:10]目标寄存器编号2位2b01→ R1RsIR[9:8]源寄存器编号2位2b10→ R2Imm/AddrIR[7:0]立即数或跳转地址低8位Jmp指令时为IR[7:0]注意case(IR[9:8])在state0中用于选择源操作数Acase(IR[9:8]) 0: AR0; 1: AR1; 2: AR2; 3: AR3; endcase这里IR[9:8]是2位但case语句写了0/1/2/3——Verilog会自动补零扩展2b10即十进制2对应R2。这种写法比用IR[9:8]2b10更简洁是课程设计中常见的教学取舍牺牲一点严谨性换取学生快速理解寄存器选择逻辑。2.4 状态机设计五态循环如何支撑完整指令周期整个CPU运行依赖于state寄存器4位但只用0-4其流转严格遵循指令周期state0取指PC自增 → MARPC → 从内存读指令 → IR读到的指令 → 解析OP → 根据Rs选A → 跳转到state1state1译码执行根据OP执行对应操作MOV/ADD/SUB等→ 更新目标寄存器Rd → 设置overflow → 回state0单周期指令state2访存准备对Jmp/Jz/Read/Write这类需二次访存的指令先将IR[7:0]作为地址低8位拼接PC高位生成完整地址 → MAR新地址 → 跳state3state3访存执行设置PC为目标地址Jmp或条件跳转地址Jz→ 或设置MAR为PCRead/Write→ 跳state4state4访存完成若为Read则将M_data_in写入R0若为Write则M_data_out已由R0驱动 → 回state0这个设计的精妙在于所有单周期指令MOV/ADD等在state1内完成不经过state2-4所有需访存的指令Jmp/Jz/Read/Write才进入多周期流程。这正是真实CPU中“单周期 vs 多周期”设计思想的微缩版也是学生最容易卡壳的点——state2的PCPC1看似多余实则是为后续MARIR腾出PC值避免地址冲突。3. 关键模块逐行解析ALU溢出检测与跳转指令的硬件实现3.1 ADD/SUB中的溢出判断Temp[8]^Temp[7]不是魔法是补码运算的本质在ADD和SUB分支中代码用了一个极简但精准的溢出检测Temp{R0[7],R0}{A[7],A}; // 符号位扩展为9位 R0Temp[7:0]; // 截断回8位 if(Temp[8]^Temp[7]) overflow1; // 溢出标志为什么是Temp[8]^Temp[7]因为{R0[7],R0}将8位有符号数R0符号位扩展为9位如R08b10000000 → 9b110000000A同理9位相加结果Temp[8:0]中Temp[8]是最终符号位Temp[7]是原8位结果的符号位溢出发生当且仅当两个正数相加得负数001或两个负数相加得正数110即Temp[8] ≠ Temp[7]异或为1。参数说明Temp声明为reg [8:0]9位而非[7:0]就是为了容纳符号位扩展后的加法结果。若此处用[7:0]则最高位进位丢失溢出检测必失效。这是Verilog新手最常翻车的细节——ALU位宽必须比操作数宽1位。3.2 Jz指令的硬件逻辑三步走如何实现“零跳转”JzJump if Zero指令要求若R00则跳转到IR[11:0]指定地址否则顺序执行。代码将其拆解为三个状态state0取指令 → IR内存值 → OPIR[15:12]4d12 → 进state1state1因OP12进入Jz分支 → 进state2state2关键逻辑在此12:begin // Jz if(R00) begin IR[7:0]M_data_in; // 从内存读取跳转地址低8位 MAR[11:0]IR[11:0]; // MARIR[11:0]此时IR[11:0]含新地址 end else MAR[11:0]PC; // 否则MARPC顺序执行 end注意IR[7:0]M_data_in是把当前PC指向的内存单元内容即跳转目标地址的低8位写入IR低8位而IR高4位IR[15:12]仍是原OP码所以IR[11:0]实际是{IR[11:8], M_data_in[7:0]}即用新读入的8位拼接IR高4位构成12位地址。state3执行跳转12:begin // Jz if(R00) PCIR[11:0]; // 真跳转 state0; // 回取指 end这个设计暴露了一个隐藏约束Jz指令的机器码必须存放在内存中且其下一条内存单元必须存放12位跳转地址的低8位。例如若Jz指令在地址0x100跳转目标是0x200则内存布局应为地址内容16进制说明0x100C000Jz指令OP12, 其余位00x10100跳转地址低8位0x200 0xFF 0x003.3 Read/Write指令的信号协同如何让CPU和内存握手Read和Write指令的特殊性在于它们需要CPU向内存写入数据Write或从内存读取数据到寄存器Read但CPU模块本身不产生数据只发地址和控制信号。其协作流程如下Read指令OP13state0取Read指令 → OP13 → 进state1state1因OP13进state2state2MARPC准备读PC指向的内存单元→ 进state3state3Write_Read1b0设为读模式→MARPC地址已设好→ 进state4state4if(OP13) R0M_data_in→ 将内存返回的数据M_data_in写入R0 → 回state0Write指令OP14state0取Write指令 → OP14 → 进state1state1因OP14进state2state2PCMAR保存当前MAR值到PC为后续用→IR[7:0]M_data_in[7:0]读取地址低8位→MAR[11:0]IR[11:0]拼地址→ 进state3state3Write_Read1b1设为写模式→MARPC注意此处PC已被state2改写为原MAR值即目标地址→ 进state4state4M_data_out已由R0驱动见M_data_out MDR;和MDRR0;在state3中执行内存模块在Write_Read1且MAR有效时采样M_data_out→ 回state0关键洞察Write指令的M_data_out值来源于R0但R0的值是在上一条指令中被设置的。这意味着Write指令本身不改变R0只把它当前值写出去。这是典型的“寄存器-内存”数据通路设计学生常误以为Write会清空R0实则R0值不变。4. 运行实操从MIF文件生成到FPGA下载的完整链路4.1 MIF文件编写规范指令二进制如何映射到16位地址空间MIFMemory Initialization File是Quartus等工具识别的内存初始化格式。本设计要求ROM深度为409612位地址宽度为8位M_data_in因此MIF文件需定义DEPTH 4096; WIDTH 8;。但指令是16位的如何存放答案是按字节拆分高字节存低地址低字节存高地址小端序。例如ADD R1,R2指令OP3, Rd1, Rs2 → 16位码为4b0011_01_10_000000000x3600应存为WIDTH8; DEPTH4096; ADDRESS_RADIXHEX; DATA_RADIXHEX; CONTENT BEGIN 0000 : 36; // 高字节 0x36 存地址0x0000 0001 : 00; // 低字节 0x00 存地址0x0001 ... END;提示实际编写时可用Python脚本批量转换。核心逻辑是对每条16位指令instmif_lines.append(f{addr:04X} : {((inst8)0xFF):02X};)和mif_lines.append(f{(addr1):04X} : {(inst0xFF):02X};)然后addr 2。务必保证指令地址连续否则PC自增会导致取错指令。4.2 顶层模块例化CPU与ROM/RAM的信号绑定要点假设顶层模块名为top_module其关键例化代码如下// 声明CPU实例 CPU uut_CPU ( .reset(reset), // 按钮复位 .clock(clock), // 50MHz晶振分频后时钟建议1MHz供观察 .M_data_in(M_data_from_mem), // ROM/RAM输出 .M_data_out(R0), // Write时输出R0值 .overflow(led_overflow), // 接LED显示溢出 .Write_Read(we_n), // 写使能低有效需反相 .M_address(addr_bus), // 地址总线接ROM和RAM .R_0(led_r0), .R_1(led_r1), .R_2(led_r2), .R_3(led_r3) // 接LED ); // ROM例化只读 rom_inst uut_rom ( .address(addr_bus[11:0]), // 12位地址 .clock(clock), .q(M_data_from_mem) // 输出到CPU的M_data_in ); // RAM例化读写 ram_inst uut_ram ( .data(M_data_out), // 写入数据来自CPU的M_data_out .we(we_n), // 写使能CPU的Write_Read取反 .address(addr_bus[11:0]), // 12位地址 .clock(clock), .q(M_data_from_mem) // 读出数据到CPU的M_data_in );关键点we_n是Write_Read的反相信号因为RAM IP核通常用weWrite Enable高有效而CPU输出Write_Read1b1表示写故需assign we_n ~Write_Read;M_data_from_mem是wire型被ROM和RAM的q端口线与驱动。这要求ROM和RAM的地址空间不重叠如ROM 0x000-0x7FFRAM 0x800-0xFFF且未选中模块的q为高阻态Z。Quartus中需在ROM/RAM IP核配置里勾选“Enable output register”并设置“Tri-state”选项。4.3 下载与调试用SignalTap抓取state和PC的实操技巧在DE10-Lite上验证时最有效的调试手段是SignalTap Logic Analyzer。需抓取的关键信号state4位观察状态机是否按0→1→0单周期或0→1→2→3→4→0多周期流转PC12位确认取指地址是否正确递增IR16位查看当前指令码比对MIF文件R0-R3各8位验证运算结果避坑SignalTap采样时钟必须与clock同源且采样深度至少2048点。若发现state卡在某个值不动大概率是reset信号未释放按钮弹起后仍为低若PC不递增检查reset是否为0代码中if(reset0)是低电平复位而开发板按钮通常是低电平有效需确认复位逻辑是否匹配。5. 常见问题排查那些让我熬夜到凌晨三点的硬核Bug5.1 现象CPU上电后state永远停在0PC不递增原因reset信号极性错误。代码中if(reset0)表示低电平复位但开发板上的复位按钮按下时输出低电平松开后通过上拉电阻变为高电平。若顶层未做电平转换reset在松开后为高电平CPU永远处于复位退出状态。解决在顶层添加反相器assign reset_n ~button_reset;然后将reset_n连入CPU。或者直接修改CPU代码为if(reset)高电平复位但需同步检查所有reset相关逻辑。5.2 现象ADD指令执行后R0值错误且overflow总是为1原因Temp位宽声明错误。原文reg [8:0] Temp;正确9位但若误写为reg [7:0] Temp;则加法结果被截断Temp[8]不存在Temp[8]^Temp[7]恒为0因Temp[8]被综合为0且R0Temp[7:0]得到错误值。解决严格检查Temp声明确保为[8:0]。在ModelSim中仿真时添加$display(Temp%b, Temp);打印9位值确认扩展正确。5.3 现象Jz指令在R00时不跳转PC仍自增原因Jz的地址拼接逻辑在state2中执行但IR[7:0]M_data_in读取的是PC当前值指向的内存而PC在state0取指后已1。若Jz指令存于地址0x100则state0后PC0x101M_data_in读取的是0x101地址的内容而非预设的跳转地址低8位。解决在MIF文件中Jz指令的下一条地址即PC1必须存放跳转地址低8位。例如Jz在0x100则0x101地址存target_addr[7:0]0x102地址存target_addr[11:8]若需12位地址。代码中IR[7:0]M_data_in后IR[11:0]实际为{IR[11:8], M_data_in[7:0]}因此MIF中需将12位地址拆成高4位和低8位分别存于连续地址。5.4 现象Write指令执行后RAM中对应地址数据未更新原因Write_Read信号未正确驱动RAM的we端。CPU输出Write_Read1b1表示写但RAM IP核要求we1b1若直接连接we在读操作时为1导致RAM误写。解决必须用Write_Read控制we且确保读操作时we0。正确做法assign we Write_Read;若RAM核we高有效并在顶层约束文件中确认we引脚分配无误。用SignalTap抓we和M_data_out确认二者在M_address稳定后同时有效。5.5 现象LED显示的R0值闪烁不定无法稳定读数原因R_0等输出端口未通过寄存器同步直接由内部R0组合逻辑驱动。当R0在时钟边沿附近变化时R_0会出现毛刺。解决严格遵循代码中always (posedge clock)块确保R_0R0等赋值只在时钟上升沿发生。若仍闪烁检查时钟频率是否过高建议初始用1MHz或LED驱动电路是否缺少消抖电容。6. 二次开发实战从8位CPU扩展到支持函数调用的栈机制6.1 栈机制的硬件需求新增SP寄存器与PUSH/POP指令要在现有CPU上支持函数调用核心是增加栈指针Stack Pointer, SP和压栈/出栈指令。硬件层面需新增8位寄存器SP初始值设为RAM最高地址如0x7FF新增两条指令PUSHOP15将R0值压入栈顶SP减1POPOP16将栈顶值弹出到R0SP加1修改RAM访问逻辑PUSH时MARSP; M_data_outR0; Write_Read1b1;POP时MARSP; Write_Read1b0;然后R0M_data_in改造步骤在CPU模块中声明reg [7:0] SP;在reset块中初始化SP8hFF;假设RAM从0x000开始SP初值0xFF对应地址0x0FF。在state1的case中添加OP15和16分支并复用state2-4的访存框架。6.2 函数调用的汇编约定如何用现有指令模拟CALL/RET现有指令无CALL/RET但可用JmpPUSH/POP模拟; 调用func PUSH R0 ; 保存R0若需 PUSH R1 ; 保存R1 Jmp func ; 跳转到func入口 func: ; ... 函数体 ... POP R1 ; 恢复R1 POP R0 ; 恢复R0 Jmp return ; 返回需提前将返回地址存入某寄存器 return: ; 继续主程序关键难点是返回地址的保存。由于无专用LINK寄存器需约定用R3存返回地址在Jmp前执行MOV R3, PC但PC不可读故改为Jmp save_ret在save_ret中用Read指令读取PC当前值需在MIF中预留地址存PC值再Jmp func。这暴露了原设计的短板——PC不可读因此二次开发首要任务是增加一条READ_PC指令OP17在state1中直接R0PC;。6.3 性能瓶颈与优化从单周期到流水线的平滑过渡当前CPU是典型单周期设计每条指令占4个时钟周期state0→1→2→3→4。若想提升性能可引入两级流水线IF取指state0PC→MAR→读ROM→IREX执行state1译码ALU写寄存器MEM访存state2-4Jmp/Jz/Read/Write专属改造要点将PCPC1从state0移到IF阶段末尾IR在IF阶段锁存EX阶段用锁存的IR译码。这需要拆分state为if_state和ex_state并增加if_ir,ex_ir等流水线寄存器。虽然代码量翻倍但吞吐量可提升近一倍CPI≈2。从那以后我每次做CPU设计都强制走一遍“指令-地址-数据”三线追踪用笔在纸上画出PC、MAR、MDR、IR、R0的值随每个时钟的变化哪怕只画3条指令。因为硬件不撒谎波形图不会骗人而纸上推演能提前揪出90%的状态机逻辑漏洞。希望帮到你。本文还有配套的精品资源点击获取