资讯动态

8位内存口与16位指令:教学CPU五状态机设计解析

发布时间:2026/10/29 22:56:10 来源:尧图企业网站定制
简介这份西南交通大学计算机组成原理课程设计文档面向大二下阶段正在完成计组实验与课程设计的学生重点提供一套可对照学习的简单CPU模块实现方案。压缩包内共1个docx文件约64KB正文以Verilog HDL代码与设计说明为主围绕取指、译码、执行等指令周期展开组织方式便于按状态机流程查阅。文档完整呈现了CPU模块的寄存器组成包括指令寄存器IR、程序计数器PC、内存地址寄存器MAR、内存数据寄存器MDR、通用寄存器R0R3、操作数寄存器A及状态寄存器等并给出加法、减法、传送、逻辑与或异或、移位、跳转、读写和停机等指令的处理逻辑其中加减运算还包含溢出判断。通过状态0取指令、状态1分析操作码的分段写法读者可以理解多状态控制与时钟边沿触发更新的基本方法。该资源已有1569人学习适合作为课程设计参考、实验报告整理和Verilog实现思路对照。1. 8 位内存口塞 16 位指令一颗教学 CPU 为什么被逼出五状态机把这份 CPU 模块从头读到尾会发现它缺了教科书里几乎所有“标准件”没有独立的 ALU没有指令译码器没有寄存器堆四个通用寄存器 R0~R3 直接摊在模块顶部加减与或异或全塞进同一个 always 块的 case 分支里。唯一像“控制器”的东西是一个 4 位宽的 state。反直觉的地方在于撑起整套逻辑的关键因素是位宽而不是运算本身。存储器数据口只有 8 位指令却是 16 位地址又是 12 位——一条指令要分两次搬进来跳转地址得先拼后跳写内存时数据还得提前一拍挂到输出口上。五个状态真正要做的就是切出 8 位总线装不下的那些时间片并非教学装饰。看懂这条约束后面每个 state 分支为什么长那样就不再是玄学。2. 指令格式16 位 IR 怎么切出操作码、寄存器号和 12 位地址2.1 IR[15:12] 的十六个操作码一张表看全指令集指令寄存器 IR 是 16 位模块里所有译码动作都围绕它做位段切片。最高 4 位 IR[15:12] 固定作操作码 OP取值 0~15恰好把整个指令集占满。中间 4 位 IR[11:8] 承担了双重职责低 8 位 IR[7:0] 则是立即数或地址的低半部分。OP助记关键字段语义0NOPIR[9:8]A←Rs 后直接回 state 0PC 已在取指拍自增1LDIIR[11:8]R0←{4b0000, IR[11:8]}4 位立即数2MOVIR[11:10]、ARd←A即 Rd←Rs3ADD同上Rd←RdA同时更新 overflow4SUB同上Rd←Rd−A同时更新 overflow5AND同上Rd←RdA6OR同上Rd←Rd|A7XOR同上Rd←Rd^A8SHR同上逻辑右移一位高位补 09SHL同上逻辑左移一位低位补 010SWAPIR[11:8]、IR[11:10]两张查表完成寄存器间搬运11JMPIR[11:0]PC←12 位地址12JZIR[11:0]R00 时跳转否则顺序执行13READIR[11:0]内存单元→R014WRITEIR[11:0]R0→内存单元15STOP无state 钉死在 1形成停机自锁这张表里最值得看的是 2 到 9 这八个算逻指令它们的目标寄存器不是从 IR 里现场译出来的而是早在取指那一拍就已经确定——state 0 把 IR[9:8] 指向的寄存器装进 A执行拍再比对 IR[11:10]把 A 与 Rd 运算后写回 Rd。换句话讲这套 ISA 是隐式的双操作数结构源操作数永远走 A 这条通道目标操作数永远由 IR[11:10] 指定。2.2 IR[11:10] 与 IR[9:8]两个 2 位字段怎么撑起四寄存器寻址为什么寄存器号是 2 位而不是 3 位算一笔字段预算就明白16 位指令里操作码吃掉 4 位地址或立即数至少要留 8 位剩下 4 位刚好够放两个 2 位寄存器号各自编址四个寄存器。想扩到八个寄存器要么砍地址位宽要么把指令拉长到 24 位两条路都会推翻现在的两拍取指流程。字段复用的代价在 LDI 上暴露得最明显。写R0 {4b0000, IR[11:8]}的时候整个 4 位字段被当成立即数用不再区分 Rd 和 Rs所以这条指令只能写 R0且立即数只有 0~15 这十六个值。这不是实现者偷懒是 4 位字段在“两个寄存器号”和“一个立即数”之间二选一的必然结果。常见的实现思路是给 LDI 单独划一条指令格式或者干脆用 IR[7:0] 的低 8 位做立即数但那样就得再占一个取指拍。SWAP 的 16 项查表也是同一个思路的延伸。case(IR[11:8])把 Rd、Rs 的 16 种组合全部展开写死本质是用面积换译码逻辑——不写译码器直接把 16 种情况摊平。这种写法在教学代码里很常见好处是波形上一眼能看出走了哪条分支坏处是每次改寄存器数量都要重写整张表。2.3 12 位地址的拼装为什么 JMP 的地址必须跨两拍才凑齐12 位地址装不进 8 位数据口这是整个设计最要命的一条约束。这版实现的做法是把地址劈成两半高 4 位藏在指令高字节的 IR[11:8] 里随操作码一起取进来低 8 位等到第二个字节再补。// state 2为访存/跳转类指令取地址低字节 2: begin Write_Read 1b0; MDR R0; // 顺带把 R0 挂到数据输出口WRITE 指令依赖它 PC PC 1; // 指令占连续两个字节PC 再前进一格 state 3; case (OP) 11: begin // JMP IR[7:0] M_data_in; // 只改低 8 位IR[15:8] 保持取指拍的内容 MAR IR; // 12 位地址在此刻才成形 end 12: begin // JZ if (R0 0) begin IR[7:0] M_data_in; MAR[11:0] IR[11:0]; // 命中用拼出来的地址 end else MAR[11:0] PC; // 未命中顺序取下一条 end endcase endIR[7:0] M_data_in这种写法叫部分赋值Verilog 允许只对寄存器的某几位赋值其余位保持不变。这一行是整个地址拼装的关键IR[15:8] 还是上一拍取指时留下的内容IR[7:0] 被新数据覆盖拼完正好是 12 位有效地址加 4 位操作码。参数说明上要留意两点M_data_in 是 8 位赋值时高位自动补零不会污染 IR[15:8]MAR 是 12 位而 IR 是 16 位赋值时取低 12 位高 4 位被截掉所以拼出来的地址上限是 4K 字节正好对上 PC 和 M_address 的位宽。这也解释了为什么 READ 和 WRITE 只能操作 R0MDR 在 state 2 里被固定赋成 R0写入内存的数据源只有这一个READ 在 state 4 里把 M_data_in 落回 R0接收端也只有这一个。R0 在这套指令集里因此同时扮演累加器、数据门户和条件位三个角色JZ 判断的也是它。这种“一个寄存器打天下”的收敛设计让 8 位总线的往返路径变得极其短代价是写代码的人必须时刻记着“先搬进 R0 再说”。3. 五状态机一条指令周期是怎么被切成 state 0 到 state 4 的3.1 state 0取指拍里塞进了三件事state 0 是整个状态机的入口也是信息密度最高的一拍。它要在同一个时钟沿完成三件事推进 PC、更新地址寄存器、把内存返回的字节塞进 IR 的高半部分同时顺手切出操作码和源寄存器。// state 0取指令高字节 切操作码 预装源操作数 0: begin Write_Read 1b0; // 存储器方向为读 PC PC 1; // PC 语义是已消费字节数先自增 MAR PC; // 用自增后的 PC 驱动地址寄存器 IR {M_data_in, 8b00000000}; // 本拍只有高 8 位有效低 8 位先补零占位 OP IR[15:12]; // 切出 4 位操作码 case (IR[9:8]) // 源寄存器号直接装进 A 0: A R0; 1: A R1; 2: A R2; 3: A R3; endcase state 1; // 进入译码/执行拍 end这段代码里有三个顺序依赖值得逐行看。PC PC 1写在MAR PC之前用的是阻塞赋值所以 MAR 拿到的是自增后的值而不是当前值——这一行顺序决定了取指地址比 PC 初值大 1也决定了 MIF 文件里第一个字节的摆放位置。IR {M_data_in, 8b00000000}用拼接把 8 位数据顶到高半部分低 8 位先填零等第二拍再覆盖之所以能这么写是因为后面所有译码只依赖 IR[15:12] 和 IR[9:8]全在高字节里。OP IR[15:12]必须写在 IR 赋值之后同样是阻塞赋值的顺序语义在起作用换个顺序拿到的就是上一条指令的操作码。A 的装载放在取指拍是一处很省的设计。此刻 M_data_in 上挂着指令高字节IR[9:8] 的两位恰好可以就地选通四个寄存器之一等 state 1 做运算时源操作数已经在 A 里备好了不需要额外的读寄存器拍。血泪经验是这种“顺手做掉”的优化在波形上极不明显一旦把 state 0 拆成两拍所有算逻指令的时序都会跟着变。3.2 state 1算逻指令为什么能一拍收工state 1 是译码执行拍OP 的 0 到 10 全部在这一拍里结束执行完直接把 state 写回 0构成单拍闭环。以 ADD 为例它的全部动作是按 IR[11:10] 选出目标寄存器把 Rd 和 A 符号扩展成 9 位相加取低 8 位写回再用第 9 位和第 7 位判断溢出。没有写回级的流水没有旁路网络一拍之内读写同一个寄存器靠的是阻塞赋值让写回结果立刻对后续语句可见。MOV 更能说明这套 ISA 的收敛程度case(IR[11:10])四个分支每支就是一句Rd A。因为源操作数在取指拍已经进了 A目标寄存器号在 IR[11:10] 里译码器退化成两个 2-4 译码器的组合根本不需要单独的逻辑块。反过来说如果哪天要给 MOV 加一个“支持内存寻址”的变体就必须拆成两拍因为地址得跨字节拼这就掉进 state 2 到 4 那条更长的路径里去了。OP15 的 STOP 是个特例它把 state 写回 1 而不是 0于是下一拍又进 state 1、又匹配到 15、又写回 1形成自锁死循环。这版实现里没有真正的 halt 信号输出停机是靠状态机自己卡住实现的。仿真时看到波形不再变化不用怀疑挂死那是设计意图。真要做成可中断的停机得加一个 running 标志位让 state 1 之外的地方也能感知到停机状态。3.3 state 2 到 state 4访存与跳转的三拍半流程OP 从 11 到 14 的四条指令JMP、JZ、READ、WRITE走的是同一条长路径state 2 取地址低字节并拼装state 3 决定是改 PC 还是动内存state 4 收尾。这条路径之所以要三拍全是为了给 12 位地址和 8 位数据口留出拼装时间。// state 3地址已成形开始真正动内存或改 PC 3: begin Write_Read 1b0; case (OP) 11: begin PC IR[11:0]; state 0; end // JMP地址直接落进 PC 12: begin // JZ只在命中时改 PC if (R0 0) PC IR[11:0]; state 0; end 13: begin MAR PC; state 4; end // READ把 PC 当数据地址 14: begin // WRITE Write_Read 1b1; // 写使能拉高 MAR PC; // 写地址同样用 PC state 4; end endcase end // state 4撤销写使能或者把读回的数据落进 R0 4: begin Write_Read 1b0; if (OP 13) R0 M_data_in; // 只有 READ 需要收回数据 state 0; end这里有个容易被忽略的设计READ 和 WRITE 用的地址是 PC而不是 IR 里拼出来的那个 12 位地址。也就是说这两条指令实际访问的是紧跟在指令后面的那个字节位置相当于把“指令流当前指针”当成了数据指针。这种把代码段和数据段混在一起的简化做法省掉了一整套地址生成逻辑代价是没法自由指定数据区程序里想读写哪个单元就得让 PC 正好停在那儿。WRITE 的写脉冲宽度也值得算一下state 3 里Write_Read 1b1state 4 第一行就把它拉回 0所以高电平只维持一个状态周期正好一个时钟宽度。如果外部存储器模型是在时钟上升沿采样写使能这一拍刚好被采到如果存储器是电平敏感的异步模型那就要确认地址在写使能有效期间已经稳定否则会写错单元。这是后面排查章节要展开的一个坑。4. 一个 always 块扛下所有阻塞赋值、输出镜像与溢出判断4.1 阻塞赋值的执行顺序Temp 与 overflow 的先后依赖整颗 CPU 的主状态机写在一个always (posedge clock)里内部全部使用阻塞赋值。这种写法在可综合风格里通常被劝阻但在这类单块状态机里反而成了表达顺序逻辑的利器同一个时钟沿内语句按书写顺序立即生效后一句能看到前一句的结果。// state 1 中的 ADD 分支 3: begin case (IR[11:10]) 0: begin Temp {R0[7], R0} {A[7], A}; // 9 位符号扩展加法 R0 Temp[7:0]; // 只取低 8 位写回 end 1: begin Temp {R1[7], R1} {A[7], A}; R1 Temp[7:0]; end // R2、R3 分支同理 endcase if (Temp[8] ^ Temp[7]) // 依赖上一行的 Temp顺序不能颠倒 overflow 1; else overflow 0; state 0; end{R0[7], R0}是符号扩展拼接把 8 位数的最高位复制一份放到第 9 位得到一个 9 位的补码数。加上{A[7], A}之后Temp[8] 是真实的符号位Temp[7] 是截断成 8 位后的符号位。Temp[8] ^ Temp[7]为 1 就说明两个符号位不一致结果溢出。这段代码能跑通完全依赖阻塞赋值的顺序语义Temp 必须先算完R0 才能取低 8 位overflow 才能读 Temp 的高位。如果把这三行换成非阻塞赋值同一个时钟沿里 Temp 的新值要等这一拍结束才可见Temp[8] ^ Temp[7]拿到的是上一拍的残留值溢出标志就会整体滞后一条指令。这不是理论推演是这类代码最常见的翻车点之一。4.2 三个 always 块的分工与敏感列表的隐患模块里其实有三个 always一个组合块负责把内部寄存器映射到输出端口一个时钟块负责把 R0~R3 镜像到观察端口最后一个才是主状态机。// 组合块把内部 MAR/MDR 映射到存储器接口 always (reset or state) begin M_address MAR; M_data_out MDR; end // 时钟块把内部通用寄存器镜像到输出端口供仿真观察 always (posedge clock) begin R_0 R0; R_1 R1; R_2 R2; R_3 R3; end第一个块的敏感列表是reset or state而不是MAR or MDR。它之所以能工作靠的是一个巧合MAR 和 MDR 几乎只在状态切换时被改写所以用 state 当触发条件基本能覆盖所有变化。但 state 2 的 JMP 分支里恰恰有一句MAR IR这一句发生在同一个 state 内不会触发组合块重新求值M_address 要等到 state 变成 3 之后才更新整整晚了一拍。常见的实现思路是把敏感列表补全成always (*)或者干脆改成assign M_address MAR;让工具自己推导依赖关系。第二种更彻底也不会有漏信号的隐患。教学代码里保留(reset or state)的写法大概是为了让波形上每个输出变化都对齐状态边界但这就把时序正确性押在了“没人会在状态内部改 MAR”这个约定上。第二个块把 R0~R3 赋给 R_0~R_3纯粹是给仿真看的内窥窗口。麻烦在于它和主状态机是同一时钟沿触发的两个 always 块主块改了 R1镜像块在同一拍读 R1两个块的调度顺序在 Verilog 里没有保证读到新值还是旧值取决于仿真器的内部顺序。所以波形上 R_1 常常比 R1 慢一拍这不是设计错误是并发块之间没有同步的必然结果。4.3 移位与寄存器搬运拼接运算符和查表能走多远移位指令的实现几乎是把拼接运算符用到极致的例子。右移一句R0 {1b0, R0[7:1]}把最高位补 0、低 7 位右挪一格拼成新的 8 位左移反过来R0 {R0[6:0], 1b0}。两行代码、零个控制信号逻辑移位就完成了。真正的短板在算术移位上。这对拼接写法只补 0不保留符号位所以负数右移一位会变成正数——如果要支持算术右移得写成{R0[7], R0[7:1]}用原符号位填充高位这需要在译码阶段区分逻辑移位和算术移位也就意味着要再占一个操作码。4 位操作码已经用满想在现有格式里塞新指令只能复用那些语义上可以合并的槽位或者借用 IR[11:8] 的取值做二级译码。5. 常见问题与排查仿真波形里最容易翻车的五个位置5.1 取指与跳转上的三个坑现象一复位后第一条指令像是被跳过了PC 一上来就停在 1。原因在 state 0 的语句顺序PC PC 1写在MAR PC之前阻塞赋值让 MAR 拿到的是自增后的值本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑