← 返回博客CPU 体系结构

在仿真器中从零构建 CPU

一份连贯且可验证的指南,用于构建具有寄存器、ALU、共享总线、字节宽 RAM、16 位指令、标志位和微序列控制单元的 8 位教学 CPU。

DennyDenny
阅读约需 14 分钟
Chalkboard feature illustration for “在仿真器中从零构建 CPU”

简述: 本指南会在连线之前先定义一套完整的教学架构:8 位数据通路和地址空间、字节宽统一 RAM、以两个字节取回的 16 位指令、四个通用寄存器、ALU 与标志位、一条共享总线,以及一个微序列控制器。逐一构建并验证各子系统,确保任一时刻只有一个总线驱动器,随后运行一个五指令程序,计算 5 + 3 并将 8 存入内存。

“构建 CPU”既可以指连接现成模块,也可以指进行晶体管级布局。本教程介于两者之间:使用寄存器、多路复用器、译码器、ALU、存储器和控制存储器等数字构件搭建处理器,同时将各构件内部的逻辑门视为可独立验证的设计单元。

目标不是模仿现代商用处理器核心,而是揭示每条指令实现都必须满足的约定:

  1. 取回正确的指令字节;
  2. 在译码期间保留这些字节;
  3. 精确传送预定的操作数;
  4. 执行定义好的操作;
  5. 只更新允许更新的体系结构状态;
  6. 选择下一条指令的正确地址。

1. 先固定架构

CPU 教学项目中的常见错误,是在构建过程中改变存储器宽度、指令宽度或 PC 递增方式。本文后续内容统一采用下列规格:

属性教学 CPU 定义
数据宽度8 位
地址宽度8 位
存储器256 个按字节寻址的位置,指令和数据统一存放
指令宽度16 位,存为两个连续字节
指令字节顺序高字节先存于指令起始地址
顺序 PC 前进取指期间每次前进一个字节,共递增两次
通用寄存器四个 8 位寄存器:R0–R3
操作数锁存器A 和 B,各 8 位
指令寄存器16 位
标志位Z(零)、N(负)、C(进位)、V(有符号溢出)
互连一条 8 位教学总线
控制微步骤计数器加控制 ROM

8 位地址选择 256 个位置;此架构明确规定每个位置为一个字节,因此存储器为 256 字节。由于每条指令占用两个字节,其起始地址通常为偶数。

方框图

                         ┌──────────────────────┐
                         │控制 ROM +         │
                         │微步骤序列发生器     │
                         └──────────┬───────────┘
                                    │控制信号
                                    ▼
 ┌────┐   ┌─────┐   ┌──────────────8 位共享总线 ───────────────┐
 │PC │──▶│MAR ││││││││
 └────┘   └──┬──┘  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼──┐  ┌▼───┐
             ││MDR││R0││R1││R2││R3││A ││B │
             ▼└─┬─┘  └───┘  └───┘  └───┘  └───┘  └┬──┘  └┬───┘
        ┌────────┐   │└──┬───┘
        │256 × 8 ││┌────▼────┐
        │RAM   │││8 位    │
        └────────┘   ││ALU    │
                     │└────┬────┘
                     └──────▶ 16 位 IR ◀────────────────────┘
                                      │
                                      ▼
                                操作码/ 字段

该图是概念性的。IR 从总线装入两个彼此独立的字节,ALU 的结果经由其自身被选中的输出路径抵达总线。

可打开顺序指令执行器作为参考,但本练习应以本文架构为基准规范。若模板采用不同的内部约定,应记录差异,不能在未作说明的情况下混用两套约定。

2. 定义指令格式

这套自定义 ISA 使用两种 16 位格式。

寄存器格式

15            12 11             8 7              4 3              0
┌───────────────┬────────────────┬────────────────┬────────────────┐
│操作码[3:0]  │RA [3:0]       │RB [3:0]       │RD [3:0]       │
└───────────────┴────────────────┴────────────────┴────────────────┘

立即数/地址格式

15            12 11             8 7                               0
┌───────────────┬────────────────┬─────────────────────────────────┐
│操作码[3:0]  │RD [3:0]       │立即数 / 地址[7:0]             │
└───────────────┴────────────────┴─────────────────────────────────┘

在这台四寄存器机器中,只有寄存器编码 0–3 合法。译码器必须拒绝或安全地陷阱处理 4–15,不能意外选中某根控制线。

操作码助记符格式定义的效果
0NOP—除 PC 在取指阶段的正常递增外,不改变体系结构状态
1LDI立即数RD ← imm8
2LD立即数RD ← RAM[addr8]
3ST立即数RAM[addr8] ← RD
4ADD寄存器RD ← RA + RB;更新标志位
5SUB寄存器RD ← RA − RB;更新标志位
6JMP立即数PC ← addr8
7JZ立即数若 Z = 1,则 PC ← addr8
8HLT—进入停机的控制器状态

其余操作码均为非法。调试期间应设置显式的 illegal_instruction 路径,或将其映射到安全的陷阱/停机状态。

3. 构建寄存器时不要门控时钟

一个 8 位寄存器由八个沿触发 D 触发器组成,它们共享一个时钟和一个逻辑装载使能。

对于每一位:

Dff={Dbus,若 load=1Q,若 load=0D_{\text{ff}} = \begin{cases} D_{\text{bus}}, & \text{若 load}=1 \\ Q, & \text{若 load}=0 \end{cases}

可在 D 输入前用一个 2:1 多路复用器实现该方程,或使用器件专用的时钟使能功能。让每个状态元件都使用同一个全局时钟。

不要通过将全局时钟与 load 做 AND 来产生本地时钟。未经控制的时钟门控在硬件中可能引入偏斜和毛刺。集成时钟门控单元与 FPGA 时钟控制资源是独立且具备时序意识的机制。

每个连接到总线的寄存器都需要:

  • 一个来自总线的 8 位输入;
  • 一个存储的 8 位输出;
  • 一个 load_x 控制信号;以及
  • 一条允许驱动教学总线的 oe_x 路径。

PC 还需要递增路径和显式装载路径。IR 需要独立的高字节和低字节装载。MAR 保存存储器地址;MDR 捕获或提供一个存储器数据字节。

复制寄存器之前先验证一个

测试以下情形:

  1. load = 0:多个时钟周期后 Q 保持不变。
  2. load = 1:Q 在有效边沿捕获总线值。
  3. 数据在边沿之外变化:Q 只在下一个启用的边沿变化。
  4. 若提供复位:Q 到达文档规定的状态。
  5. 输出使能关闭:寄存器不驱动总线。

数字逻辑仿真器能够验证该模型的逻辑时序,但不能复现模拟域亚稳态,也不能证明物理建立时间和保持时间满足要求。

4. 构建 ALU 并定义每个标志位

使用 A 和 B 作为稳定的操作数锁存器。ALU 是组合逻辑:它不得包含隐藏的体系结构状态。

alu_op操作结果
000ADDA + B
001SUBA − B
010ANDA & B
011OR`A
100XORA ^ B
101NOT~A
110SHLA << 1,零填充
111SHR逻辑 A >> 1,零填充

ALU 指南详细介绍了各运算通路及输出多路复用器。核心算术设计包括:

  • 一个八级全加器链;
  • 以 A + (~B) + 1 实现减法;
  • 并行按位逻辑门;
  • 固定的一位移位器连线;以及
  • 由 alu_op 选择的输出多路复用器。

标志位定义

把标志位作为 ISA 的一部分来定义,包括逻辑和移位指令如何处理 C 与 V。

  • Z: 结果的每一位均为零时为 1。
  • N: 结果的第 7 位;仅对有符号运算将其解释为符号位。
  • ADD 时的 C: 第 7 位的进位输出。
  • SUB 时的 C: 发生无符号借位时为 1(A<BA<B)。这与 DigiSim 内置 4 位和 8 位 ALU 的约定一致;加法器/减法器实现通过反相最终进位输出得到该值。
  • ADD 时的 V: 操作数符号相同而结果符号相反。
  • SUB 时的 V: 操作数符号不同且结果符号与 A 不同。

当减法通路使用反相 B 操作数和进位输入实现时,进位输入/进位输出的 XOR 等价公式适用于二进制补码加法与减法。参见CPU 标志位中的演算示例。

这套教学 ISA 在执行 ADD 和 SUB 时更新 Z、N、C 与 V。对于逻辑和移位操作,必须在编写微代码前明确并记录策略:保留 C/V,或为其定义新的含义。未明确定义的线路瞬时值不能作为体系结构行为。

ALU 验证表

至少测试:

情形预期关注点
0x00 + 0x00Z
0xFF + 0x01无符号进位和零
0x7F + 0x01有符号溢出和 N
0x80 + 0x80进位加有符号溢出
0x00 − 0x01借位约定和 N
0x80 − 0x01有符号溢出
AND/OR/XOR 恒等式按位逻辑通路
0x81 的移位方向、填充位、移出位策略

5. 构建共享总线

教学总线由八根导线组成,可有多个可能的源。一个核心不变量是:

∑已启用的总线驱动器≤1\sum \text{已启用的总线驱动器} \le 1

可能的驱动源包括 PC、MDR、R0–R3、A、B、一个 IR 字节以及 ALU 结果。驱动源清单不能仅由“寄存器数量”推断,必须在控制规范中逐项列明。

在 DigiSim 中,可使用三态缓冲器对共享总线建模:

  • 在一次传送期间恰好启用一个源;
  • 为下一个有效边沿置位一个或多个目的端装载信号;以及
  • 总线没有源驱动时绝不捕获总线。

争用是设计错误。物理输出冲突可能导致错误电压和过大电流;后果取决于工艺和持续时间,因此“芯片总会烧毁”并不是有用的规则。

现代 FPGA 通常以多路复用器而非真正的内部三态布线实现共享总线,ASIC 的实现方式也各不相同。三态总线仍是说明总线所有权与数据传送的清晰教学模型。

总线仲裁测试

在 R0 和 R1 中放入已知值:

  • R0 = 0xAA
  • R1 = 0x55

分别验证仅 R0、仅 R1、无驱动器和两个驱动器。后两种都应被视为无效的控制器状态。然后添加断言或可见指示器,以便立即发现后续微代码错误。

6. 连接字节宽存储器

将仿真器中的 RAM 元件用作体系结构存储器模型。不能据此断言物理 RAM 就是 D 锁存器阵列:SRAM、DRAM、块 RAM 和综合得到的寄存器阵列采用不同的存储单元与时序机制。

该 CPU 的接口为:

  • ADDR[7:0],由 MAR 驱动;
  • DATA[7:0],经由 MDR 或受控总线接口传送;
  • mem_read;
  • mem_write;以及
  • 若模型支持可变延迟,则使用可选的 ready 。

本演练假定读请求可在一个专用微步骤内发出并捕获。若所选存储器为同步存储器或具有可见延迟,则应插入等待状态,并且仅在数据有效时捕获数据。

指令和数据共用相同的 256 字节。采用独立存储器的哈佛设计同样有效,但那会是不同的架构和取指时序。

有关可写存储与非易失/常量存储抽象之间的区别,参见RAM 与 ROM。

7. 构建微序列控制器

控制器将操作码、微步骤、标志位和机器状态转换为:

  • 总线输出使能;
  • 寄存器装载使能;
  • ALU 操作;
  • 存储器读/写控制;
  • PC 递增/装载控制;
  • 标志位装载;
  • IR 高/低字节装载;以及
  • 下一微步骤或结束指令控制。

一种实现采用控制 ROM:

控制地址= {操作码,微步骤}
控制字   = {oe 信号,load 信号,alu_op, 存储器控制,下一控制}

标志位可以限定所选操作——例如,对于 JZ,load_pc = microcode_load_pc AND Z——也可以参与控制存储器地址。显式的下一状态逻辑通常比假定微步骤计数器总会递增更清晰。

这是一个用于教学的微代码控制器。真实处理器会采用组合译码、可编程控制存储器、状态机和序列发生器的多种组合。不要把 RISC/CISC 标签当作实现上的捷径。

安全默认值

每个未使用的控制 ROM 条目都应:

  • 禁用所有总线驱动器;
  • 禁用存储器写;
  • 禁用体系结构寄存器写;以及
  • 进入已定义的非法指令或停机路径。

未初始化的控制字绝不能变成“将任意数据写入任意存储器”。

8. 一致地取回两个指令字节

由于 RAM 与数据通路均为八位宽,而每条指令为 16 位,取指需要两次字节读取。按照定义,先存储高字节。

假定所选 RAM 的一次读取只需一个微步骤:

步骤寄存器传送重要控制
F0MAR ← PCoe_pc, load_mar
F1MDR ← RAM[MAR]; PC ← PC + 1mem_read, load_mdr, inc_pc
F2IR[15:8] ← MDRoe_mdr, load_ir_hi
F3MAR ← PCoe_pc, load_mar
F4MDR ← RAM[MAR]; PC ← PC + 1mem_read, load_mdr, inc_pc
F5IR[7:0] ← MDRoe_mdr, load_ir_lo

F5 之后,IR 含有一条完整指令,PC 已指向下一条指令。译码会选择执行序列。

若 RAM 从地址有效到数据有效之间存在等待时间,则应将 F1 和 F4 分别拆分为请求、等待和捕获步骤。不能把一次 16 位取指隐含为一次字节宽传送。

9. 定义执行微操作

ADD RD, RA, RB

步骤传送
E0A ← R[RA]
E1B ← R[RB]
E2R[RD] ← A + B; FLAGS ← ALU flags
E3结束指令;返回 F0

RA、RB 和 RD 通过译码使能选择 R0–R3 中的一个。无效寄存器编码必须进入非法指令路径。

LDI RD, imm8

步骤传送
E0R[RD] ← IR[7:0]
E1结束指令

LD RD, addr8

步骤传送
E0MAR ← IR[7:0]
E1MDR ← RAM[MAR]
E2R[RD] ← MDR
E3结束指令

ST RD, addr8

步骤传送
E0MAR ← IR[7:0]
E1MDR ← R[RD]
E2RAM[MAR] ← MDR
E3结束指令

JMP 和 JZ

  • JMP:PC ← IR[7:0],随后结束。
  • JZ:仅当 Z 为 1 时装载 PC,但无论如何都结束。

HLT

进入一个不发出写操作的稳定控制器状态。手动停止仿真器时钟较为方便,但即使时钟边沿继续到达,体系结构状态也必须保持明确定义。

10. 手工汇编并装载测试程序

该程序装入 5 和 3,将它们相加到 R2,把结果存到地址 0x10,然后停机。

起始地址指令16 位机器字内存中的字节
0x00LDI R0, #50x10050x10 0x05
0x02LDI R1, #30x11030x11 0x03
0x04ADD R2, R0, R10x40120x40 0x12
0x06ST R2, 0x100x32100x32 0x10
0x08HLT0x80000x80 0x00

将这十个字节装入地址 0x00 至 0x09,然后复位:

  • PC = 0x00
  • 控制器微步骤 = F0
  • halted = 0
  • 除非控制字选中某个驱动器,否则所有总线驱动器均禁用

单步执行第一次取指,并确认:

  1. F2 之后,IR 高字节为 0x10;
  2. F5 之后,IR 为 0x1005;
  3. PC 为 0x02;
  4. LDI 将 0x05 放入 R0;以及
  5. 没有其他寄存器或内存位置发生变化。

停机时:

  • R0 = 0x05
  • R1 = 0x03
  • R2 = 0x08
  • RAM[0x10] = 0x08
  • PC = 0x0A

这些最终值是必要条件,但并不充分。损坏的 CPU 也可能破坏另一个位置,却仍然得到这些值。

11. 验证不变量,而不只是最终答案

添加以下检查:

  • 任一时刻总线输出使能不超过一个;
  • 仅在 ST 写步骤期间置位存储器写;
  • 仅在 F2 和 F5 期间装载 IR 字节;
  • 每次正常取指中 PC 恰好递增两次;
  • 仅由定义为更新标志位的指令写入标志位;
  • 无效操作码或寄存器编码不会导致意外写入;
  • 停机状态不产生体系结构副作用;以及
  • 复位进入已知的控制器状态。

对每条指令,记录如下轨迹:

时钟,微步骤,PC, IR, 总线所有者,总线值,
寄存器装载,存储器读/写,ALU 操作,标志位

测试失败时,应定位第一个偏离预期轨迹的周期。若从最终错误值反向调试,往往要经过多个周期才能定位根因。

示波器调试指南说明如何检查建模波形,同时不把它们与物理时序签核混为一谈。

12. 常见故障模式

症状可能原因首先检查
IR 含有互换的字节装载器与取指使用不同的指令字节顺序地址 PC 和 PC+1
PC 前进四个字节每个字节递增两次,或混用了宽度假设F0–F5 轨迹
总线发生争用两个输出使能共用一个控制位译码后的控制字
捕获时总线无驱动缺少源使能总线所有者断言
ADD 写入错误寄存器RD 字段译码或寄存器编码顺序有误IR[3:0] 与装载线
存储操作破坏了指令存储器地址/立即数或 mem_write 的时序MAR 和 ST E2
分支使用过期的 Z标志位未写入,或 JZ 采样了错误状态ADD/SUB E2 和 JZ
控制器从不返回取指缺少结束/下一状态条目微步骤转换
非法操作码执行了操作未安全初始化未使用的控制 ROM默认控制字

13. 教学范围与模型边界

此设计教授寄存器传送、指令编码、总线所有权、ALU 标志位、存储器时序和控制器状态。

它不模拟:

  • 晶体管级 RAM 或逻辑;
  • 缓存层次结构或虚拟内存;
  • 推测执行或乱序执行;
  • 时钟的模拟电气特性或亚稳态;
  • 物理传播、布线、功耗或热限制;或
  • 生产级安全或异常架构。

它的单条共享总线有意保持易于观察,并非声称现代 CPU 的内部拓扑如此。

功能扩展的建议顺序

  1. 添加自动化指令测试。 在添加功能前验证每个操作码和非法编码。
  2. 添加 AND、OR、XOR 和移位。 先定义标志位行为,再添加控制序列。
  3. 添加使用直接比较的条件分支。 这会揭示基于标志位与比较后分支 ISA 的区别。
  4. 添加栈指针和调用/返回。 规定栈增长方向、字节顺序和溢出行为。
  5. 添加中断状态机。 装载向量前保存返回地址和标志位;定义优先级和屏蔽。
  6. 分离指令与数据存储器。 将新的取指时序与统一存储器设计进行比较。
  7. 仅在多周期参考实现通过后再流水线化。 然后定义流水级有效性、停顿、前递、冲刷和异常。

在尝试流水线扩展前,可先阅读取指–译码–执行深入解析和指令寄存器/译码指南。

综合验证

可使用顺序指令执行器理解整体流程,使用8 位 ALU 系统独立验证算术通路,并使用 CPU 标志位寄存器测试状态行为。原理图应配套保存书面的体系结构规范和微操作表。

验证目标不只是观察到一次结果 8,而是能够说明每次状态变化的控制依据,在轨迹中复现该变化,并证明其他状态未被意外修改。

将想法变成真正可运行的电路。