简介:本资源是西南交通大学信息科学与技术学院《计算机组成原理》课程设计的完整实验文档,面向高校计算机类专业大二学生及数字电路初学者,聚焦CPU微架构实践,解决从理论指令周期到Verilog可综合代码实现的关键转化问题。文档为单个64KB的Word文件(.docx),系统梳理了基于有限状态机的8位简化CPU设计,涵盖取指、译码、执行全流程,包含完整的Verilog HDL源码、寄存器功能定义(IR/PC/MAR/MDR/R0–R3)、16种指令操作(MOV/ADD/SUB/AND/OR/JMP/JZ等)及溢出检测逻辑,并对状态跳转机制与时序控制(posedge clock)作了清晰注释。已有1571人学习下载,读者可直接复现该CPU模块,在FPGA开发平台验证指令执行行为,深入理解状态机建模、寄存器堆组织、ALU运算控制及内存读写时序等核心概念,是课程设计报告撰写与硬件描述语言入门的优质参考范例。
1. 这不是“跑通就行”的课设:一个能上板验证、能讲清状态跳转、能答辩时挡住三连问的计组CPU设计
很多同学交完计组实验就删代码,答辩前临时翻 Verilog,结果被问“为什么 state=2 要等两个周期才跳转”“Jz 指令里 R0==0 判断是在哪个时钟沿生效”,当场卡壳——不是不会,是设计时没把状态机时序边界和寄存器采样时机真正落到信号流里。这份来自某高校信息类专业课程设计的真实 CPU 模块,不堆砌花哨功能,只聚焦 14 条指令(含 JMP/JZ/READ/WRITE/STOP),用纯同步时序+明确定义的 4 个主状态(取指/译码/访存/写回)搭出可复现、可调试、可逐周期追踪的微架构。它适合两类人:一是大二下正在啃《计算机组成原理》实验课的同学,需要一份结构清晰、注释到位、能直接烧进 FPGA 开发板(如 DE1-SoC 或类似 Cyclone IV 平台)跑通流水灯式测试程序的参考;二是带课设的助教或青年教师,需要一套覆盖“寄存器堆建模→指令格式定义→状态机编码→溢出检测实现→顶层例化”全链路的教学级范本。它不承诺支持 Linux 内核,但保证你能看着波形图,指着 SignalTap 里的 PC、IR、MAR 变化,向导师说清楚“第 5 个 clock 上升沿,state 从 1 跳到 2,是因为 ADD 指令执行完毕,下一步要写回 R1,所以 MAR 被赋值为当前 PC 值准备取下条指令”。
2. 总体架构设计:四状态机驱动的同步数据通路与寄存器堆划分
这个 CPU 不是“黑匣子”,它的行为完全由state寄存器控制,每个posedge clock只推进一个状态,所有寄存器更新严格对齐时钟边沿。整个数据通路围绕五个核心寄存器组展开:PC(程序计数器)、IR(指令寄存器)、MAR(内存地址寄存器)、MDR(内存数据寄存器)和 R0–R3(通用寄存器)。它们不是孤立存在,而是通过明确的读写使能和数据流向构成闭环。
2.1 指令格式与操作码映射:8 位指令如何承载 14 种功能
该设计采用固定长度 16 位指令字,但实际有效指令仅使用高 4 位(IR[15:12])作为操作码(OP),低 12 位按需分配。这种设计刻意避开复杂寻址模式,把教学重点落在“指令译码→状态触发→数据搬运”主干上:
| OP (4-bit) | 指令名 | 功能简述 | 关键字段用途 |
|---|---|---|---|
| 4'b0000 | NOP | 空操作(隐含在 PC+1 中) | — |
| 4'b0001 | LDI | 立即数加载:Rd ← imm | IR[11:8]为立即数,IR[9:8]指定目标寄存器 Rd |
| 4'b0010 | MOV | 寄存器传送:Rd ← Rs | IR[9:8]源寄存器 Rs,IR[11:10]目标寄存器 Rd |
| 4'b0011 | ADD | 加法:Rd ← Rd + Rs | 同上,Rs 由IR[9:8]指定,Rd 由IR[11:10]指定 |
| 4'b0100 | SUB | 减法:Rd ← Rd - Rs | 同上 |
| 4'b0101 | AND | 逻辑与:Rd ← Rd & Rs | 同上 |
| 4'b0110 | OR | 逻辑或:Rd ← Rd | Rs | 同上 |
| 4'b0111 | XOR | 异或:Rd ← Rd ^ Rs | 同上 |
| 4'b1000 | SHR | 逻辑右移:Rd ← Rd >> 1 | IR[11:10]指定 Rd |
| 4'b1001 | SHL | 逻辑左移:Rd ← Rd << 1 | IR[11:10]指定 Rd |
| 4'b1010 | SWAP | 寄存器交换:Rd ↔ Rs | IR[11:8]为 Rs 编号,IR[11:10]为 Rd 编号 |
| 4'b1011 | JMP | 无条件跳转:PC ← addr | IR[7:0]为跳转地址低 8 位,高 4 位补零 |
| 4'b1100 | JZ | 零标志跳转:if R0==0 then PC ← addr | 同 JMP,但增加 R0 判零条件 |
| 4'b1101 | READ | 从内存读:R0 ← M[addr] | IR[7:0]为读地址 |
| 4'b1110 | WRITE | 向内存写:M[addr] ← R0 | IR[7:0]为写地址 |
| 4'b1111 | STOP | 停机指令 | 状态机停在 state=1,不再推进 |
提示:
IR是 16 位宽,但M_data_in只有 8 位。这意味着取指阶段必须分两次读入:第一次读高 8 位(IR[15:8]),第二次读低 8 位(IR[7:0])。原文中IR={M_data_in,8'b00000000}是简化写法,实际工程中需用IR <= {IR[15:8], M_data_in}在连续两个周期完成拼接。我们将在“避坑章节”详述此问题。
2.2 四状态机详解:每个状态只做一件事,且严格同步
状态机不是为了炫技,而是为了把“取指-译码-执行-访存”流程显式暴露出来。state是 2 位寄存器(0–3),其转移逻辑完全由当前状态和 OP 共同决定,无组合逻辑冒险:
// 状态定义(在 module 内部声明) localparam S_FETCH = 2'b00, // 取指:PC→MAR,读内存→IR S_DECODE = 2'b01, // 译码:分析 OP,准备源/目的操作数 S_MEM = 2'b10, // 访存:JMP/JZ/READ/WRITE 的地址计算与内存交互 S_WRITEBACK = 2'b11; // 写回:将 ALU 结果或内存数据写入寄存器- S_FETCH(state=0):这是唯一主动推进 PC 的状态。
PC <= PC + 1发生在此刻,紧接着MAR <= PC,然后发出读请求(Write_Read = 1'b0)。注意:IR的高 8 位在此周期载入,低 8 位需下一周期补全。 - S_DECODE(state=1):核心译码阶段。
OP <= IR[15:12]已就绪,根据 OP 值进入case(OP)分支。此时A寄存器(操作数暂存)根据IR[9:8]选择R0–R3的值,为后续 ALU 运算准备输入。所有算术/逻辑指令在此状态完成计算,并判断溢出(Temp[8]^Temp[7]是带符号加减溢出的经典判据)。 - S_MEM(state=2):专用于涉及内存地址计算的指令。JMP/JZ 需将
IR[7:0]与当前 PC 组合成 12 位地址;READ/WRITE 需将IR[7:0]直接作为MAR。此状态不修改寄存器堆,只更新MAR和PC(JMP/JZ 会预置新 PC)。 - S_WRITEBACK(state=3):将
MDR(READ 指令读回的数据)或 ALU 临时结果(如 ADD 后的Temp[7:0])写入目标寄存器Rd。R_0–R_3的输出赋值在此状态完成。
这种“一状态一职责”的设计,让波形调试变得极其直观:你只需定位state信号的跳变沿,就能立刻知道 CPU 正在执行指令生命周期的哪一环。
2.3 寄存器堆与数据通路:R0–R3 如何被安全读写
通用寄存器堆R0–R3是纯同步写入的 8 位寄存器,其写使能由state和OP共同控制。关键点在于:读操作是组合逻辑,写操作是时序逻辑。
// 寄存器堆声明(内部变量) reg [7:0] R0, R1, R2, R3; // 输出端口赋值(组合逻辑,随时反映当前值) assign R_0 = R0; assign R_1 = R1; assign R_2 = R2; assign R_3 = R3; // 写入逻辑(仅在 posedge clock 且满足条件时发生) always @(posedge clock) begin if (reset == 0) begin R0 <= 8'h00; R1 <= 8'h00; R2 <= 8'h00; R3 <= 8'h00; end else begin case(state) S_WRITEBACK: begin case(IR[11:10]) // 目标寄存器编号 2'b00: R0 <= MDR; // READ 指令:R0 ← MDR 2'b01: R1 <= Temp[7:0]; // ADD/SUB 等:R1 ← ALU结果 2'b10: R2 <= Temp[7:0]; 2'b11: R3 <= Temp[7:0]; endcase end // 其他状态不写寄存器堆 endcase end end设计说明:
R_0–R_3是output reg类型,但它们的值始终等于内部R0–R3的当前值(通过assign实现)。真正的写入动作发生在always @(posedge clock)块内,且只在S_WRITEBACK状态下,由IR[11:10]解码出的目标寄存器编号触发。这种分离确保了读写互不干扰——你在S_DECODE读R0的值去算Temp,同时S_WRITEBACK正在把上一条指令的结果写入R1,二者在时钟边沿严格隔离。
3. 关键模块实现:从溢出检测到指令跳转的 Verilog 落地细节
这一章不讲概念,只抠代码。每一个always块、每一处case分支、每一个信号赋值,都对应着硬件电路中真实存在的连线与触发器。我们聚焦三个最易出错、也最体现设计功底的模块:带符号溢出检测、JZ 条件跳转的时序处理、以及 READ/WRITE 指令的内存交互协议。
3.1 带符号加减溢出检测:为什么用Temp[8] ^ Temp[7]?
在ADD和SUB指令分支中,计算使用了 9 位中间结果Temp:
Temp = {Rd[7], Rd} + {A[7], A}; // ADD // 或 Temp = {Rd[7], Rd} - {A[7], A}; // SUB这里Rd和A是 8 位寄存器,{Rd[7], Rd}将其符号位Rd[7]扩展为 9 位(即符号扩展),确保加减运算在带符号数域内进行。Temp的最高位Temp[8]是结果的符号位,Temp[7]是原 8 位结果的符号位。溢出发生的充要条件是:运算结果的符号位与两个操作数符号位的逻辑关系不一致。对于加法,当两正数相加得负数,或两负数相加得正数时溢出;对于减法,当正减负得负,或负减正得正时溢出。Temp[8] ^ Temp[7]正是这一逻辑的简洁实现:
- 若
Temp[8] == Temp[7]:结果符号位与原 8 位符号位一致,无溢出(^结果为 0)。 - 若
Temp[8] != Temp[7]:结果符号位翻转,发生溢出(^结果为 1)。
// 在 ADD/SUB 分支内 if(Temp[8] ^ Temp[7]) overflow <= 1'b1; else overflow <= 1'b0;参数说明:
overflow是output reg,其值在S_DECODE状态结束时更新。它不参与任何运算,仅作为状态标志输出。在实际 FPGA 实验中,你可以将overflow接到开发板的一个 LED 上,当加0x7F + 0x01时,LED 亮起,直观验证溢出逻辑。
3.2 JZ 指令的时序陷阱:为什么需要 state=2 作为中转?
JZ(Jump if Zero)指令要求:若R0 == 0,则跳转到IR[7:0]指定的地址;否则顺序执行。看似简单,但时序上极易出错。错误做法是:在S_DECODE(state=1)直接判断R0==0并立即PC <= IR[11:0]。这会导致组合逻辑毛刺——R0的值可能尚未稳定(因上一周期刚写入),PC就被错误更新。
正确解法是引入S_MEM(state=2)作为缓冲:
// 在 state=1 (S_DECODE) 的 OP==4'b1100 分支中 12: begin // JZ state <= S_MEM; // 明确跳转到访存状态,不在此刻改PC end // 在 state=2 (S_MEM) 中 2: begin Write_Read <= 1'b0; MDR <= R0; // 将R0值暂存MDR,确保采样稳定 PC <= PC + 1; // 为下条指令准备 case(OP) 4'b1100: begin // JZ if(MDR == 8'h00) begin // 使用已稳定的MDR值判断 IR[7:0] <= M_data_in; // 读入跳转地址低8位 MAR[11:0] <= {4'b0000, IR[7:0]}; // 构造12位地址 end else begin MAR[11:0] <= PC; // 不跳转,MAR指向下一指令 end end endcase state <= S_WRITEBACK; // 进入写回状态 end设计说明:
S_MEM状态的核心作用是提供一个时钟周期的采样窗口。R0的值在S_DECODE结束时已稳定,S_MEM的第一个posedge clock将其锁入MDR,第二个posedge clock(在S_WRITEBACK)再用MDR的值做最终判断。这避免了在同一个时钟沿内既读又写的竞争风险,是数字电路设计的黄金法则。
3.3 READ/WRITE 指令的内存协议:如何与外部存储器握手?
READ和WRITE指令不改变 PC,而是与外部存储器交互。它们的实现依赖于M_address、M_data_out和Write_Read三个信号的协同:
// READ 指令流程(OP==4'b1101) // S_DECODE (state=1): 设置 MAR = IR[7:0], Write_Read = 0 (读) // S_MEM (state=2): MAR 已更新,等待下一个时钟沿 // S_WRITEBACK (state=3): M_data_in 有效,将其赋给 R0 // WRITE 指令流程(OP==4'b1110) // S_DECODE (state=1): 设置 MAR = IR[7:0], Write_Read = 1 (写), M_data_out = R0 // S_MEM (state=2): MAR 和 M_data_out 已就绪,等待下一个时钟沿 // S_WRITEBACK (state=3): 无操作,等待内存写入完成关键配置在顶层模块中:
// 顶层例化时,将 CPU 的输出连接到内存控制器 wire [11:0] cpu_M_address; wire [7:0] cpu_M_data_out; wire cpu_Write_Read; CPU uut ( .reset(reset), .clock(clock), .M_data_in(memory_data_out), // 内存读出的数据 .M_data_out(cpu_M_data_out), // CPU 写入内存的数据 .overflow(overflow), .Write_Read(cpu_Write_Read), // 写=1,读=0 .M_address(cpu_M_address), // CPU 请求的地址 .R_0(R0_out), .R_1(R1_out), .R_2(R2_out), .R_3(R3_out) ); // 内存控制器(简化版) always @(posedge clock) begin if (cpu_Write_Read == 1'b1) begin // WRITE memory_array[cpu_M_address] <= cpu_M_data_out; end end assign memory_data_out = (cpu_Write_Read == 1'b0) ? memory_array[cpu_M_address] : 8'hxx;参数说明:
memory_array是一个 12 位地址线(4096 个单元)、8 位数据线的 RAM。cpu_Write_Read是核心控制信号:为1时,cpu_M_data_out的值被写入memory_array[cpu_M_address];为0时,memory_array[cpu_M_address]的值被驱动到memory_data_out,供 CPU 下一周期采样。这个协议完全符合标准 SRAM 读写时序,可直接对接 Quartus 的 IP Catalog 中的 On-Chip Memory。
4. 常见问题排查:那些让你在实验室熬通宵的“玄学”Bug
写 Verilog 最怕的不是语法错,而是功能看似正常,波形却对不上预期。以下是我在带多个计组课设小组时,高频出现的 4 类“血泪经验”型问题,每一条都附带现象、根因和可立即执行的修复方案。
4.1 现象:CPU 启动后 PC 一直停在 0x000,IR 始终为 0,无法取到第一条指令
原因:reset信号未正确同步释放,或reset是异步高电平有效,但代码中写成了if(reset==0)。更隐蔽的是,reset信号在 FPGA 上电时持续时间过短,未满足寄存器的复位建立时间。
解决:
- 确认
reset是低电平有效(reset_n),并在always @(posedge clock or negedge reset_n)中使用。 - 在顶层加入同步复位释放电路:
reg [1:0] rst_sync; always @(posedge clock) begin rst_sync <= {rst_sync[0], reset_n}; end wire rst_clean = ~rst_sync[1]; // 经过两级寄存器同步后的 clean reset // 将 rst_clean 传入 CPU 模块- 在仿真中,
reset信号至少保持 10 个clock周期。
4.2 现象:ADD 指令执行后overflow信号在不该亮的时候亮了,比如0x01 + 0x02
原因:Temp的位宽定义错误。原文中reg [8:0] Temp是 9 位(索引 8 到 0),但Temp[8] ^ Temp[7]判据要求Temp[8]是符号扩展位,Temp[7:0]是结果。如果Temp被误写为reg [7:0] Temp,则Temp[8]不存在,综合工具会报错或产生不可预测行为。
解决:
- 严格检查
Temp声明:reg [8:0] Temp;(9 位)。 - 在
ADD/SUB计算中,务必使用{Rd[7], Rd}和{A[7], A}进行 9 位运算,而非直接Rd + A(那是 8 位无符号加法)。 - 在 ModelSim 中添加
Temp波形,观察其 9 位值,确认Temp[8]是否随符号正确扩展。
4.3 现象:JMP 指令跳转后,下一条指令总是取错,PC 值比预期小 1
原因:S_FETCH状态中PC <= PC + 1的执行时机与MAR <= PC的顺序冲突。PC在S_FETCH开始时加 1,MAR却被赋了加 1 后的PC值,导致取的是下下条指令。
解决:
- 将
PC的自增操作移到S_FETCH的末尾,或更稳妥地,在S_WRITEBACK状态统一处理所有PC更新:
// 在 S_WRITEBACK 分支中 S_WRITEBACK: begin case(OP) 4'b1011, 4'b1100: PC <= MAR; // JMP/JZ 的最终跳转 default: PC <= PC + 1; // 其他指令顺序执行 endcase endS_FETCH状态只负责MAR <= PC和Write_Read = 0,不碰PC。
4.4 现象:READ 指令后R0的值是乱码,或 WRITE 指令后内存内容没变
原因:M_data_in信号未在S_WRITEBACK状态的posedge clock采样。M_data_in是内存输出,其稳定需要时间,若在S_MEM状态就试图用M_data_in赋值R0,会采到无效数据。
解决:
- 严格遵循“读-采样-写回”三阶段:
S_MEM:发出读请求(MAR设好,Write_Read=0),等待内存响应。S_WRITEBACK:在下一个posedge clock,M_data_in已稳定,此时R0 <= M_data_in。
- 在仿真中,给
M_data_in添加 10ns 的延迟(模拟内存访问时间),并观察R0是否在S_WRITEBACK的上升沿后才变化。
5. 课程设计验收与答辩技巧:如何用一份报告讲清你的 CPU 设计
验收不是看代码能不能跑,而是看你是否真正理解了自己写的每一行。某高校计组实验的验收标准通常包含三个硬性环节:功能演示、波形分析、原理图讲解。我带过的 A 同学曾因在答辩时被问“state=2 为什么不能合并到 state=1”,当场愣住,后来他总结出一套“三问自检法”,现在每次交作业前必走一遍。这套方法不教你背答案,而是帮你构建一个能自圆其说的设计叙事。
5.1 功能演示:用最小测试集覆盖全部指令类型
别一上来就跑 Fibonacci,验收老师没时间看。准备一个 16 行的 MIF 文件(内存初始化文件),覆盖五类指令:
| 地址 | 指令(16进制) | 功能 | 验证点 |
|---|---|---|---|
| 0x000 | 0x1001 | LDI R0, #0x01 | R0 应为 0x01 |
| 0x001 | 0x2002 | MOV R1, R0 | R1 应为 0x01 |
| 0x002 | 0x3102 | ADD R1, R0 | R1 应为 0x02 |
| 0x003 | 0x4102 | SUB R1, R0 | R1 应为 0x01 |
| 0x004 | 0x5102 | AND R1, R0 | R1 应为 0x01 |
| 0x005 | 0x6102 | OR R1, R0 | R1 应为 0x01 |
| 0x006 | 0x8100 | SHR R1 | R1 应为 0x00 |
| 0x007 | 0x9100 | SHL R1 | R1 应为 0x00 |
| 0x008 | 0xA101 | SWAP R1, R0 | R0/R1 应交换 |
| 0x009 | 0xB005 | JMP 0x005 | PC 应跳至 0x005 |
| 0x00A | 0xC001 | JZ 0x001 (R0!=0) | PC 应+1,不跳 |
| 0x00B | 0xD008 | READ 0x008 | R0 应读入 0x008 地址的值 |
| 0x00C | 0xE009 | WRITE 0x009 | 0x009 地址应写入 R0 当前值 |
| 0x00D | 0xF000 | STOP | CPU 停在 state=1 |
技巧:在 Quartus 中,用 SignalTap II Logic Analyzer 抓取
state,PC,IR,R_0,R_1,overflow六个信号,运行 20 个clock周期,导出 CSV。验收时,直接打开 CSV,指着第 12 行说:“看,PC=0x00B,IR=0xC001,state=1,说明正在译码 JZ;第 13 行state=2,MDR=0x01(R0 值),第 14 行state=3,PC=0x00C,证明未跳转,逻辑闭环。”
5.2 波形分析:用三个关键波形讲清状态机本质
老师最想听的不是“我的 CPU 跑起来了”,而是“我怎么知道它跑对了”。拿出三张波形截图,每张只标 3 个信号,讲清一个故事:
图1:取指周期(Fetch Cycle)
信号:clock,state,PC,MAR
故事:“state从 0 跳到 1 的瞬间,PC值是 0x000,MAR立即变为 0x000,证明取指地址正确;下一个clock上升沿,state变 1,PC变为 0x001,说明 PC 已自增,为下条指令准备。”图2:ALU 运算周期(Execute Cycle)
信号:clock,state,A,R_0,Temp,overflow
故事:“state=1时,A选中 R0(值 0x7F),R_0是 0x01,Temp计算为 0x0080(9 位),Temp[8]=1,Temp[7]=0,overflow=1,LED 亮,证明带符号溢出检测生效。”图3:内存读写周期(Memory Cycle)
信号:clock,state,Write_Read,M_address,M_data_out
故事:“state=2时,Write_Read=0,M_address=0x008,发出读请求;state=3时,M_data_out变为 0xAA(预设值),R_0在下一个周期变为 0xAA,证明 READ 指令完整闭环。”
注意:波形图必须标注时间轴(ns)、信号值(十六进制)、关键事件(如
state跳变沿)。不要用自动缩放,手动调整时间窗口,让一个完整指令周期(4 个clock)清晰可见。
5.3 原理图讲解:从顶层到模块,讲清数据流向
验收时,老师会指着顶层原理图问:“这条线(箭头)是从哪来,到哪去,代表什么?” 你必须能脱稿回答。以M_data_out为例:
- “它从 CPU 模块的
output reg [7:0] M_data_out引出(指向 CPU 符号)。” - “在顶层,它连接到内存控制器的
data_in输入端口(指向内存符号)。” - “当
Write_Read=1时,M_data_out的值被写入memory_array[M_address];当Write_Read=0时,M_data_out是高阻态,不影响内存输出。”
终极技巧:画一张手绘数据流图。用 A4 纸,画五个方框(PC、IR、MAR、MDR、R0-R3),用带箭头的线连接,线上标注信号名(如PC → MAR)和触发条件(如state==0)。答辩时,把它放在桌角,老师问到任何寄存器,你就手指图说:“看,它从这里来,到这里去,由这个状态控制。” 这比翻 PPT 有效十倍。
从那以后我每次带课设,都强制学生在提交前,用这三张波形图+一张手绘流图+一份 16 行 MIF 测试集,自己过一遍“三问自检”:
- 我的
state跳变是否严格对齐clock? - 我的
overflow是否只在S_DECODE结束时更新? - 我的
M_data_in是否只在S_WRITEBACK的posedge clock被采样?
只要这三个问题的答案都是“是”,答辩时你就有了底气。希望帮到你。
本文还有配套的精品资源,点击获取