资讯详情

手把手搭建模型机:从指令集到数据通路的CPU设计实战记录

📅 2026/10/21 18:13:25 | 华诺云谱 👁 阅读
手把手搭建模型机:从指令集到数据通路的CPU设计实战记录
把这门课的课设当成一场小型 CPU 设计实验是我最近做完之后印象极深的一件事。数电模型机设计简单来说就是让你用数字电路基本模块门电路、触发器、寄存器、ALU、存储器拼出一台能运行有限指令集的“计算机核心”。很多同学第一反应是“这不就是计算机组成原理吗”其实它更像是数电知识的综合大作业——把组合逻辑和时序逻辑全部压在一条指令周期里跑通。这篇文章把我从拿到题目到最后验收的全过程、踩过的坑、再开始会怎么做的思路都记下来给同样在做模型机设计的同学一个可参考的路线图。如果你用的是 Logisim 这类仿真工具或者正在纠结指令集和数据通路怎么编排看这篇就够了。1. 拿到题目之后我是怎么拆解“模型机”的1.1 模型机到底是什么模型机不是真的要你做一块能跑操作系统的处理器它的目标非常明确用数字电路的方式实现一个能取指、译码、执行若干条机器指令的完整数据通路。我当时理解的三个关键词是“指令周期”“数据通路”“控制信号”。把这三件事搞明白整个项目就剩动手连线了。从一个最朴素的角度看一台模型机就是在每个时钟周期里做固定动作先按程序计数器PC指向的地址从内存里取出一条指令把指令放到指令寄存器IR里由控制器译码出“这一步要做什么”控制器再发出各种控制信号让 ALU、寄存器、存储器按规则工作最后根据指令类型更新 PC进入下一条指令。听上去很简单但真正头痛的是这些动作不能乱必须严格按照时序来。比如“取指”的时候内存地址总线必须被 PC 占用而“执行”的时候地址总线可能要放操作数地址如果时序安排不对指令和数据就混了。我在开头花了不少时间把这一层关系理清楚后面连线反而快。1.2 工具和硬件平台怎么选我们课程设计提供两个方向一是用 Logisim 做纯原理图仿真二是用 Verilog 写硬件描述再综合下载到 FPGA 开发板。我选的是 Logisim原因是它更贴近数电课上学过的门电路和触发器连线直观查错也能直接看每一根线的高低电平。如果你对 FPGA 更熟用 Verilog 也没问题只是要注意仿真和上板的条件不同。说实话Logisim 的缺点是大型电路连线满天飞稍微不注意就乱成一团。所以我建议无论如何都用“分层模块”来搭把取指单元、ALU、寄存器组、控制器各自做成子电路。主电路只保留几条总线每个子电路留好输入输出引脚调试的时候逐层打开看。很多人一开始把所有模块全部放在一张图纸上后面查一个问题要找半天线纯属给自己挖坑。1.3 整体架构从指令到数据通路的分解我在动手之前画过一张很粗糙的框图虽然画得难看但很有用。我的模型机可以拆成四个大块存储部分指令/数据共用一块 RAM通过地址选择器和读写信号区分“取指令”和“读写数据”运算部分一个 8 位 ALU支持加、减、与、或、取反、直通等操作寄存部分8 个通用寄存器 R0~R7每个 8 位接在内部总线两边控制部分由状态机产生微操作序列决定数据流的走向。这里最容易犯的错误是“先画电路再定指令”结果发现指令编码和数据通路互相打架。正确顺序应该是先确定指令集根据每条指令需要的微操作去设计数据通路。比如我一开始想要一条“寄存器间接寻址”指令但发现内存地址线不够接果断砍掉换成立即数和直接地址两种寻址方式。课设不是做商用 CPU目标是在有限时间内把基础逻辑跑通所以指令集宁可精简也不要复杂到控制信号失控。2. 指令集设计先定规则再动电路2.1 指令集怎么选够用且能交差我的模型机定了 8 条指令操作码统一用 4 位二进制这样译码电路不会太复杂。指令集包含MOV寄存器之间搬运数据或者把立即数装入寄存器ADD两个寄存器相加结果存回某寄存器SUB寄存器相减AND / OR / NOT基本的逻辑运算LOAD从内存指定地址读数据到寄存器STORE把寄存器数据写入内存指定地址JMP无条件跳转到指定地址BEQ如果两个寄存器相等则跳转否则顺序执行HLT停机。你可能注意到我写了不止 8 条其实 JMP 和 BEQ 我后面才加进去编码还留了扩展位。课设的要求里“无条件跳转”和“条件跳转”通常是加分项所以建议从一开始就把操作码留一个空余位后面加指令不用推翻整个电路。2.2 指令编码与 ROM 设计指令编码是让后续电路不发疯的关键。我的方案是每条指令固定两个字节。第一个字节高 4 位是操作码低 4 位是目标寄存器编号第二个字节可以是立即数、内存地址也可以是源寄存器编号加寻址方式。这样编码长度统一取指和状态机的节拍很好控制。以 ADD 为例我设计的格式是0001目标寄存器编号 第二个字节高 4 位表示源寄存器 1低 4 位表示源寄存器 2。 这条指令执行时ALU 的 A 端和 B 端分别选通两个源寄存器输出根据操作码片选到目标寄存器。如果要把 ADD 扩展到支持立即数第二个字节直接当成 8 位立即数控制信号里加一个“ALU B 端选择立即数总线”的选项就行。ROM 里的微操作序列可以通过真值表生成但更省事的是直接用 Logisim 的 ROM 元件把每条指令对应的控制信号按地址存进去。这个思路其实就是后面要讲的微程序控制我建议先用手写译码逻辑跑通再改成微程序这样对控制逻辑理解会更深刻。2.3 为什么需要扩展位和立即数寻址很多第一次做模型机的同学会觉得指令集里只要有 ADD、MOV 就够了但如果真想运行一段有意义的小程序比如循环累加没有跳转是玩不起来的。所以我在编码设计里专门留了两类扩展操作码扩展位4 位操作码最多 16 条指令我目前用了 10 种还剩 6 个空位后续加移位、比较指令都不用改地址宽度寻址模式位第二个字节的最高位用来区分“寄存器立即数模式”和“内存地址模式”译码电路根据这个位决定把第二个字节送给 ALU 还是送给地址总线。立即数寻址很方便但要注意它和直接寻址的区别直接寻址是“把内存某个地址的内容读出来”立即数就是“这个数本身”。我在写汇编测试程序的时候这两条指令很容易写混调试时需要在波形里确认数据来源。建议所有数据都用十六进制监视图标出来一眼就能看出控制信号是否选对了通路。3. 数据通路搭建让数据和指令各走各的路3.1 存储器的读写逻辑模型机用的是同步 RAM 还是异步 RAM直接影响状态机节拍。Logisim 默认的 RAM 元件有同步读写模式我当时用的是异步读、同步写因为取指阶段只要地址稳定数据立刻就能出现在输出端少等一拍。这样整个指令周期可以控制在 4~6 个时钟节拍内。读写控制是另一个重点。我在 RAM 的输入端加了一个三态缓冲避免指令地址和数据地址同时驱动存储器的地址线。控制器里有两个状态取指状态把 PC 的值送到地址总线执行状态如果涉及 LOAD/STORE就把指令寄存器里的低 12 位地址字段送到地址总线。两个状态各自使能对应的地址源绝不共用一个驱动。这里有一个坑如果忘记给 RAM 写使能加默认值RAM 可能会在非 STORE 指令的某个时刻被意外写入数据就全乱了。我后来把所有操作都改成“仅在 STORE 指令的特定节拍置写使能为 1”别的时候强制为 0问题才消失。3.2 ALU 与寄存器组设计ALU 用 Logisim 自带的“Arithmetic Unit”元件改就行它支持多种运算函数通过函数选择位来控制。不过自带的元件输出结果虽然是正确的但它的运算函数编码和我的操作码并不一致所以我在 ALU 外面加了一个“函数译码器”把 4 位操作码转换成 ALU 需要的函数选择信号。这个做法也方便后续换指令只要改译码真值表不用动数据通路。寄存器组我一开始想用 Logisim 里的寄存器堆元件后来发现它自带的功能不透明不方便接控制信号。于是干脆用单独 8 位寄存器拼了 8 个每个寄存器配一个写入使能由控制器根据指令里的目标寄存器编号译码出“寄存器的片选信号”。这种方案虽然连线多一点但是每一步控制都看得见课堂上讲起来也更好说明白。3.3 总线结构一根线还是多根线内部总线我选了“单总线”结构所有寄存器的输出都通过三态缓冲接到同一条 8 位总线ALU 的两个输入端一直接着总线上的“当前值”和“寄存器值”需要时用锁存器把输进去的数据暂存到内部寄存器里。单总线的优点是连线少、电路清爽缺点是同一时刻只能有一个设备往总线上放数据所以控制信号必须保证互斥。我在单总线上被折磨得最惨的就是“总线竞争”两个寄存器同时把数据驱动到总线上结果高低电平打架。后来我强制给每个三态缓冲的使能端做了一组互斥编码由控制器统一生成。举个例子如果 ADD 指令第二拍要把 R1 的内容送到总线那么 R1 的三态缓冲使能有效其他所有的三态缓冲使能全部无效。这里的“其他全部无效”通常用译码器默认输出实现如果译码器没有默认零输出就要加下拉电阻来保证总线在不被驱动时为低电平。4. 控制器的两种思路组合逻辑和微程序4.1 用状态机实现取指-执行流程我用一个状态机作为控制器核心。状态机有 6 个状态S0 取指、S1 译码、S2 执行第一阶段、S3 执行第二阶段、S4 写回、S5 更新 PC。每个状态对应一个时钟节拍状态转换由时钟沿触发也就是说我的指令周期是固定节拍数。这个方案的优势就是逻辑简单所有的控制信号都通过“当前状态 当前指令”查真值表生成。真值表是让很多人崩溃的地方指令条数多的时候每个状态都要考虑每一条指令的动作表格会变得巨大。我的经验是先把所有指令按功能分组运算类ADD、SUB、AND、OR、NOT执行流程一样只是 ALU 运算函数不同访存类LOAD、STORE需要额外的地址发送和读写信号跳转类JMP、BEQ直接在译码状态就判断条件并修改 PC。分组之后真值表只需要描述组类型的行为再叠加上各自的操作码片选逻辑就清晰多了。4.2 微程序控制实现与对比状态机方案在有人问“怎么改成微程序”时往往会卡住因为组合逻辑的真值表很难解释。我后来把控制器改成微程序方案用一块 ROM 来存放每一拍的控制信号。ROM 的地址就是“当前状态号 × 指令数 指令编号”输出就是使能各种三态缓冲、写使能、RAM 读写的信号。微程序的优点是把复杂译码变成查表操作调试时直接看 ROM 里某一项的值就知道该触发什么。缺点是 ROM 容量很大如果指令多改一条控制信号就要重新计算地址。我当时试过直接用手算地址填 ROM改到第三轮就快崩溃了最后还是写了简单的 Python 脚本生成 ROM 内容。对这种课设级别项目微程序最大的意义是让控制器结构更统一但速度和面积其实没有优势。我最终提交的是状态机方案因为老师要求能现场改指令并检查连线状态机的真值表更容易对着一张表讲清楚。4.3 控制信号的时序排布不管用哪种控制器控制信号必须卡着时钟沿来。我在设计时就拟了一张时序表每个状态填上该状态要拉高的信号。比如 S0 状态PC_ENABLE、RAM_READ 必须为高IR_LOAD 也可以同时为高因为 RAM 输出的是指令码S2 状态如果是 ADDR_BUS_ENABLE 拉高ALU_A_LATCH 在时钟上升沿前已经保存了第一个操作数ALU_B_LATCH 也在同一拍保存第二个操作数。这里有一个容易忽略的细节寄存器堆的写入使能如果在时钟上升沿和数据输入同时变化很容易出现“写进旧值”的竞争。解决办法是让写回操作发生在时钟下降沿即负载的正沿前先把数据稳定在总线上这样寄存器就能在新一个时钟周期开始时可靠装入新值。我做了一个简单版本的“双相时钟”来规避竞争主状态机上升沿动作寄存器写入下降沿动作实测下来稳定很多。5. 调试阶段踩过的坑和排查顺序5.1 总线竞争和时序冲突我最有代表性的一个 bug 出现在 LOAD 指令RAM 读取出来的数据总是没错但寄存器收到的却是上一个值。查了很久发现是“写回状态”和“更新 PC 状态”没有时序错开。LOAD 执行中要先从内存读数据然后写寄存器同时还要保证 PC 不被旧指令继续驱动地址线。我给 RAM 地址源选的优先级顺序不对结果在写回那拍地址总线既接了 PC 又接了指令里保存的地址。这个教训让我养成了一个习惯任何一条总线的驱动源必须是“每个状态只有一个使能”的互斥逻辑。以后遇到奇怪现象我建议按这个顺序排查先看时钟是不是连续稳定有没有毛刺再看状态机是不是按预期跳转把状态变量引脚接出来观察然后看 RAM 读写信号有没有在非访存指令时误触发最后看数据总线上多个使能有没有同时为高。这个顺序能过滤掉 80% 以上的问题因为多数模型机坏就坏在控制信号找错了时间。5.2 调试方法模块级验证与波形观察我一开始是整体连完再看结果结果一运行全是红色错误。后来老老实实把子电路一个一个验证先单独测试 ALU输入固定值看输出符不符合再测寄存器组一边写使能一边观察存储的值最后才组合成主电路。这样做的好处是能把问题局限在“接口层”而不是“内部逻辑”。Logisim 的仿真支持用时钟步进和触发信号观察每一拍状态。我习惯把 PC、IR、RAM 地址总线、数据总线、控制信号若干位全部引出到引脚然后设置时钟频率为 1Hz用“步进时钟”一帧一帧推演。比如执行一条 LOAD就应该看到地址是 PC 输出 - 切换为指令里的地址 - RAM 输出数据 - 寄存器写使能拉高。如果哪一帧对应不上基本就能定位到控制器真值表写错还是三态缓冲插反了。5.3 从仿真到上板的注意事项如果要做 FPGA 实物仿真通过和上板通过是两件事。Logisim 的默认延迟模型和真实 FPGA 器件有差异尤其是时钟频率。我建议上板前先把时钟频率调到很低比如 1Hz确认每个按钮和拨码开关能控制复位与时钟。另外要注意开发板上的按键会产生抖动需要加一个简单的防抖电路否则按一次可能跳两拍状态机直接乱掉。还有一个特别容易忘的点复位信号。模型机上电时所有寄存器初始值不定如果 PC 不是从 0 开始程序根本跑不起来。所以在主电路里必须设计一个全局复位开关把所有计数器和状态机清零。同理程序计数器和 RAM 内容在上板前也要用初始化文件写入避免随机值导致死循环。6. 做完之后的体会和可以扩展的方向整套模型机做完后我最大的收获是终于把“冯·诺依曼结构”这个概念从字面上变成了具体电路。以前在数电课上学到的译码器、触发器、三态缓冲在这里全都变成了有明确用途的积木块。特别是控制器那部分它让我明白计算机的“聪明”并不是某个神秘部件而是一张精确到拍级的控制信号时序表。你甚至可以把每一条指令的执行过程口述出来然后用波形图验证。如果要我给下一届做这门课设的同学建议我会说别急着打开仿真工具先用 1~2 天把指令周期图示画清楚把每条指令的每一拍需要哪些控制信号列成表格。这个表格可能只有一页纸但它比任何工具都重要。连电路时每接一根线先问自己“这根线在哪个状态起作用”答不出来就先不要接。如果用 Verilog 写同样不要一上来就写一百行代码。先画数据通路再写状态机最后补各个模块的赋值逻辑。另外可以在做完基础版本后尝试加两个扩展一是把这套模型机的 ALU 并入乘除模块二是把单总线改成双总线或三总线结构。改总线结构虽然会让连线更复杂但你会更直观地感受到数据通路的带宽变化。我当时只加了一个“取反”指令但光是这个改动就逼着我又重新调整了一版控制信号。课程答辩的时候能讲清楚自己每一步选择的理由往往比功能做得花哨更重要——这也是我最后整理这篇文章的初衷。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑