概要: 程序计数器(PC)在一些架构中也称为指令指针,用于标识下一条待取指令。顺序执行时,它按照解码得到的指令长度向前推进;分支、跳转、异常、中断和复位则会选择不同的下一 PC 值。
程序计数器是位于取指中心的一项专用架构状态。在教学 CPU 中,它可能就是一个直接驱动指令存储器地址的实体寄存器。在现代处理器中,预测、队列、缓存、地址转换和推测取指会让实现更加分散,但架构仍然会定义下一条指令的地址。
本文会说明 PC 存储什么、怎样递增、分支和跳转如何重定向它,以及怎样用寄存器、加法器和多路复用器把它连接起来。每个元件都会链接到顺序指令执行器模板中可以实际操作的电路。
什么是程序计数器?
程序计数器是只负责一项任务的寄存器:指向内存中的下一条指令。它不是让用户代码随意读写的通用寄存器,而是由 CPU 控制逻辑驱动的专用寄存器。
在最简单的机器中,它的位宽与内存的可寻址单位共同限定代码地址空间:
- 8 位 PC可以选择 个可寻址位置。
- 16 位 PC可以选择 个位置。
- 32 位 PC有 种可能的位模式。
- 64 位 PC有 种可能的位模式。
只有在按字节寻址的架构中,这些位置才一定是字节。真实处理器实际实现的地址位数可能少于寄存器的名义位宽,也可能把虚拟指令地址转换为物理内存地址,并施加对齐或规范地址规则。
DigiSim 的 PROGRAM_COUNTER_8BIT 元件实现了 8 位情况。它足以在仿真器中运行真实程序,同时又足够小,可以逐位观察。
PC 位于取指阶段的最前端
取指—译码—执行周期的取指阶段按顺序处理四个信号:
- PC 把当前值送上地址总线。
- 内存返回该地址上的指令字。
- 指令字被锁存到指令寄存器中。
- PC 更新,使下一次取指读取下一条指令。
更完整的取指内容会讨论第 1 至第 3 步。本文关注的是第 4 步。
为什么 PC 必须自动递增?
如果 PC 保持不变,CPU 就会永远获取同一条指令。因此,硬件会在 PC 中连接自动递增路径,使每个取指周期在默认情况下都向前推进一步。
递增值由专用加法器计算,通常是一个小型行波进位加法器,类似掌握二进制加法中详述的电路。它的两个输入是当前 PC 和一个常量偏移量。加法器输出反馈到 PC 的数据输入,PC 在下一个时钟边沿将其锁存。
PC + 1 与 PC + 4:为什么指令宽度很重要
加到 PC 上的常量是以可寻址单位计量的指令宽度,不一定就是字面值 1。
| 架构 | 指令宽度 | 增量 | 原因 |
|---|---|---|---|
| DigiSim 8 位玩具 CPU | 1 字节(1 个字) | PC + 1 | 每个地址保存一条完整指令 |
| 不使用压缩指令的 MIPS、RISC-V | 4 字节 | PC + 4 | 内存按字节寻址;这些编码宽 4 字节 |
| ARM(A32) | 4 字节 | PC + 4 | 原因相同 |
| 带 C 扩展的 RISC-V;Arm Thumb/Thumb-2 | 2 或 4 字节 | PC + 解码长度 | 两者都包含 16 位编码,也可能出现 32 位编码 |
| x86 | 1–15 字节 | PC +(解码长度) | 指令长度可变;解码器给出长度 |
架构原则是相同的:顺序下一地址紧随当前指令的编码之后。定长指令集可以使用固定增量;变长指令集必须确定当前指令的长度,即使高性能前端会在解码结束前预测并取回该边界之后的内容。
布尔描述
对于固定指令宽度为 的架构,默认的下一 PC 值为:
在 DigiSim 的 8 位情形中,,所以递增器就是一个 +1 加法器。
分支和跳转如何覆盖 PC?
顺序执行是最普通的情况。真实程序需要循环、条件、函数调用和返回,而这些操作都要求 PC 跳到顺序下一地址以外的位置。
两类常见的控制转移说明了这种变化:
- 跳转(无条件): 无论条件如何,都用目标地址替换 PC。
- 分支(有条件): 只有在指令集定义的条件为真时才选择目标,否则选择顺序地址。有些指令集检查零、进位、符号或溢出等状态标志;包括基础 RISC-V 在内的另一些指令集会直接比较寄存器,并且没有算术条件码寄存器。CPU 标志指南解释了基于标志的设计。
两种操作归根结底都是把一个非顺序值写入 PC。
目标地址来自哪里?
目标通常采用以下三种编码方式之一:
- 立即数(PC 相对): 指令字包含偏移量,下一 PC 为
PC + offset。RISC-V 的BEQ、ARM 的B和 x86 短跳转都采用这种形式,优势是支持位置无关代码。 - 直接或区域相对: 指令贡献部分目标位,这些位可能与固定基址或当前 PC 的高位组合。经典 MIPS 的
J是常见示例。一个完整绝对地址通常无法装进普通定长指令。 - 寄存器间接: 目标位于通用寄存器或 ALU 输出中。RISC-V 的
JALR、ARM 的BX和 x86 的JMP rax都是例子。这种形式支持函数指针、虚分派和计算型goto。
在小型单周期数据通路中,这些候选值可以送入一个多路复用器。更先进的实现可能在多个单元中计算、预测并排队保存目标,最后才提交被选择的架构路径。
实现:寄存器 + 递增器 + 多路复用器
PC 的硬件结构是 CPU 中最清晰的数据通路之一:
┌──────────────┐
PC current ─────▶│ Adder (+W) │──── PC + W ──┐
└──────────────┘ │
▼
┌───────────────┐
│ MUX (2:1) │
target_addr ─────────────────▶│ sel = take? │──── next_PC
└───────────────┘
│
▼
┌───────────────┐
clock ─────────────────────▶│ PC register │──── current_PC
└───────────────┘
它由三部分组成:
- 用于保存当前地址的寄存器。在教学数据通路中,这是一个8 位寄存器,由边沿触发的存储元件扩展到与地址总线等宽。
- 一个加法器,其中一个输入连接 PC,另一个输入连接固定宽度 。
- 一个 2:1 多路复用器,一路输入来自加法器(默认顺序路径),另一路来自分支目标(分支被采用时的路径)。选择线由控制单元驱动;当前指令是跳转,或者是条件成立的分支时,选择线置位。数据交通控制器详细介绍了多路复用器。
每个时钟边沿,PC 都会锁存多路复用器当前选择的值。正是这一次锁存区分了“前进”和“分支”。
多路复用器的可选输入
真实设计会为同一个多路复用器增加更多来源:
| 来源 | 何时选择 |
|---|---|
| PC + W | 默认——顺序执行 |
| PC + offset(PC 相对) | 条件分支、短跳转 |
| 直接目标/向量 | 直接跳转、复位或架构定义的控制转移 |
| 寄存器/ALU 输出 | 间接跳转、函数返回、switch 分派 |
| 复位/启动向量 | 上电或外部复位后 |
| 异常/中断向量 | 陷阱发生后 |
选择线从 1 位扩展到 3 位,但结构思路没有改变。
复位行为:PC 从哪里开始?
CPU 上电或复位时,PC 必须保存一个已知地址,否则程序会从随机位置开始执行。
常见约定有两种:
- PC = 0: 从地址
0x0000…0开始执行。DigiSim 教学 CPU 使用这种简单约定,第一条指令位于零地址。 - 启动向量: 使用固定的非零地址。x86 冷复位到物理地址
0xFFFFFFF0(4 GB 空间顶部、靠近 BIOS ROM 的位置)。ARM Cortex-M 内核从地址0x00000004保存的复位向量中读取初始 PC。一些 6502 系列芯片从0xFFFC/0xFFFD读取复位向量。
在硬件中,“复位”是一根连接到 PC 异步清零输入(或同步加载输入)的导线。复位置位时,无论多路复用器选择什么,PC 都会被强制设为启动值。
完整示例:五条指令的循环
考虑以下运行在 8 位 CPU 上、 的伪程序:
0x00: LDI R0, 5 ; load 5 into R0
0x01: LDI R1, 0 ; load 0 into R1
0x02: ADD R1, R0 ; R1 = R1 + R0
0x03: DEC R0 ; R0 = R0 - 1, sets Z flag if R0 hits 0
0x04: BNZ 0x02 ; if Z == 0, branch back to 0x02
0x05: HLT
逐步观察 PC 值:
| 周期 | 取指时的 PC | 指令 | 取指后 | 是否采用分支? | 下一 PC |
|---|---|---|---|---|---|
| 1 | 0x00 | LDI R0, 5 | PC + 1 = 0x01 | 不适用 | 0x01 |
| 2 | 0x01 | LDI R1, 0 | PC + 1 = 0x02 | 不适用 | 0x02 |
| 3 | 0x02 | ADD R1, R0 | PC + 1 = 0x03 | 不适用 | 0x03 |
| 4 | 0x03 | DEC R0 (R0=4, Z=0) | PC + 1 = 0x04 | 不适用 | 0x04 |
| 5 | 0x04 | BNZ 0x02 | PC + 1 = 0x05; target = 0x02 | 是(Z=0) | 0x02 |
| 6 | 0x02 | ADD R1, R0 | … | … | … |
在第 5 个周期中,由于 Z 标志为清零状态,教学数据通路选择分支目标(0x02),而不是顺序下一地址(0x05)。真实控制流机构要复杂得多,但架构结果仍然是在顺序落空路径和控制转移目标之间作出选择。
常见陷阱
- PC + W 的差一错误。 常见仿真器错误是在指令宽 4 字节的架构中使用
PC + 1。CPU 会把每条指令的第二个字节当作下一条指令的起点,于是所有程序都像随机数据一样运行。 - 在取指读取内存之前更新 PC。 内存读取期间,PC 必须保持当前地址。应在时钟边沿更新,而不是以组合方式更新。
- 忘记复位。 没有复位线的 PC 会在上电后停留在触发器随机形成的值上。务必连接复位。
- 分支到无效对齐地址。 所需的指令对齐取决于指令集及其扩展。只支持 32 位对齐指令的机器会拒绝
0x1003这样的目标;带 16 位编码的指令集可能允许更多目标。应执行具体架构的规则,而不是硬编码一种通用假设。
在 DigiSim 中构建
打开顺序指令执行器模板,可观察到:
- 位于取指路径顶部的
PROGRAM_COUNTER_8BIT模块。 - 一个硬接线为每个时钟周期加
1的加法器。 - 一个在
PC + 1与控制单元给出的目标地址之间选择的多路复用器。 - 一个把 PC 强制设为
0x00的复位开关。
手动逐步触发时钟,观察 PC 向前推进。然后加载包含分支指令的程序,观察多路复用器从“顺序”切换到“目标”的瞬间——这一位信号就是最纯粹的控制流。
PC 在完整系统中的位置
程序计数器是更大 CPU 数据流中的一个节点。它所标识的指令会被捕获,以进行指令寄存器与译码处理。算术结果可能更新基于标志的分支设计会检查的 CPU 标志。
阅读取指—译码—执行案例研究了解完整循环,然后加载顺序指令执行器模板,逐步执行一条分支指令。第一次看到 PC 非顺序跳转,就是抽象不再只是图示、而真正成为电路的时刻。



