对应原书:第 6 章
6.3 编程~6.4 机器语言
摘要
上一篇笔记认识了寄存器、立即数和内存操作数。本篇只保留第七章 RTL 设计直接需要的程序流程、分支/跳转、word 访存和机器码字段;高级语言控制结构、ABI 和字符串等软件内容不再展开。
我不需要把重点放在写复杂软件上,而要能从一条指令判断:它读哪些寄存器、立即数怎样生成、会不会改写 PC、最终允许哪些体系结构状态发生变化
预备概念:控制流、PC、标签和位字段
程序计数器 PC
程序计数器(Program Counter,PC)是保存当前指令字节地址的 32 位体系结构状态。RV32I 基础指令长 32 bit,即 4 byte,所以正常顺序执行时:
PCnext=PC+4 PC_{next}=PC+4PCnext=PC+4
PC+4是组合逻辑算出的候选值;PC 寄存器只在时钟沿更新。遇到分支或跳转时,控制逻辑会选择另一个候选地址写入 PC。
控制流
控制流(control flow)是指令地址随程序执行而变化的顺序:
- 顺序执行:下一条指令地址为
PC+4; - 条件分支:比较条件成立时转到目标地址,否则仍为
PC+4; - 无条件跳转:直接转到目标地址;
jal:跳转的同时把PC+4写入目的寄存器。
标签、目标地址和偏移量
汇编中的标签(label)是某条指令地址的可读名称。汇编器会将标签转换为数值地址或相对当前 PC 的有符号偏移量。
loop: addi s0, s0, 1 bne s0, s1, loop标签不是硬件状态;机器码只保存编码后的分支偏移量。
位字段和掩码
位字段(bit field)是一个数据字中具有特定含义的一段连续 bit。掩码(mask)通过0和1选择要清除、保留或置位的 bit。例如与0xFF做 AND,只保留最低 8 bit。
6.3 编程
6.3.1 程序流程
指令和数据一样存放在内存中。原书示例中,三条连续指令的地址为:
0x538 addi s1, s2, s3 0x53C lw t2, 8(s1) 0x540 sw s3, 3(t6)每条指令占 4 byte,所以地址依次增加 4:
0x538+4=0x53C,0x53C+4=0x540 0x538+4=0x53C, \qquad 0x53C+4=0x5400x538+4=0x53C,0x53C+4=0x540
从硬件看,取指阶段至少需要:
PC_q ─────────────→ 指令存储器地址 │ └→ 32 位加法器计算 PC_q + 4 │ 分支/跳转目标 ───────────┤ ↓ PC 下一值 MUX → PC_d下面预设一个单周期控制流情景:指令存储器组合读;当前 PC 稳定时,指令字段、寄存器读数据、立即数、比较结果和下一 PC 都在本周期内组合产生;PC 与寄存器堆只在上升沿更新。该模型只用于理解顺序执行、beq和jal的状态变化,不等同于完整处理器实现。
**实际时序情景:**复位释放后的某一周期,当前 PC 为0x0000_1000,指令 RAM 输出0x0100_00EF,即jal x1,+16。
| 时刻 | 组合/时序结果 |
|---|---|
| 上升沿后至下一上升沿前 | 取指后识别为jal;J-type 偏移量重排为0x10;跳转目标为0x1010;同时形成返回地址0x1004和对x1的写使能。 |
| 下一上升沿 | PC 更新为0x1010,同时x1更新为0x1004。 |
因此jal不是“先写返回地址、再跳转”的两步操作,而是在同一个上升沿向两个不同体系结构状态提交结果。这个示例只适用于组合读指令存储器;若 FPGA Block RAM 配置为同步读,取指会多出一个寄存器周期,应改用多周期或流水线结构。
需要区分:
PC_q是时序逻辑保存的当前状态;PC+4、分支目标和跳转目标是组合逻辑产生的候选值;- PC 选择信号错误会改变后续全部指令流,不能当作普通数据错误处理。
6.3.2 逻辑与移位指令
1. 逻辑指令
RISC-V 的and、or、xor对两个 32 位源寄存器逐位运算,并把结果写入目的寄存器。andi、ori、xori则用一个寄存器和一个经符号扩展的 12 位立即数作为输入。
| 指令 | 典型用途 | 某一位的结果 |
|---|---|---|
and/andi | 清零、提取字段 | 两个输入都为 1 才为 1 |
or/ori | 合并字段、置位 | 任一输入为 1 就为 1 |
xor/xori | 翻转、比较差异 | 两个输入不同时为 1 |
例如提取s7=32'h1234_ABCD的s7[15:8]:
srli s6, s7, 8 # s6 = 0x0012_34AB andi s6, s6, 0xFF # s6 = 0x0000_00AB位级计算是:
0x1234_ABCD >> 8 = 0x0012_34AB 0x0012_34AB & 0x0000_00FF = 0x0000_00ABxori rd, rs1, -1可以实现按位取反,因为 12 位的-1编码为12'hFFF,符号扩展后为32'hFFFF_FFFF。
slt和sltu都把比较结果写成 32 bit 的0或1,但前者按有符号数解释,后者按无符号数解释。它们和后面的blt/bltu复用了“比较”的概念,却有不同的体系结构副作用:slt写rd,分支只可能改写 PC。
在完整单周期数据通路中,比较器输出也是本周期的组合写回候选值;只有该指令被译码为slt/sltu时,控制器才会在下一个上升沿使能对应rd的写入。不能把比较表达式本身误认为一次独立的“软件赋值”。
2. 移位指令
| 指令 | 操作 | 空位补什么 |
|---|---|---|
sll/slli | 逻辑左移 | 低位补 0 |
srl/srli | 逻辑右移 | 高位补 0 |
sra/srai | 算术右移 | 高位复制原符号位 |
RV32 的立即数移位量只需 5 bit,因为它只能选择0~31:
25=32 2^5=3225=32
左移nnn位在不发生有效位截断时相当于乘以2n2^n2n。算术右移适合保持补码符号,逻辑右移则把输入当作无符号位串。移出 32 位边界的 bit 会被直接丢弃,因此不能把移位与数学乘除完全等同。
硬件中,移位类指令通常由桶形移位器完成。若把它并入 ALU,组合路径大致为:
寄存器数据 + 5 位 shamt → 移位器 → 写回 MUX → 寄存器堆6.3.3 分支
分支通过修改 PC 改变程序流程。条件成立称为分支成功(taken),条件不成立称为分支失败(not taken)。
1. 条件分支
| 指令 | 条件 | 数值解释 |
|---|---|---|
beq rs1, rs2, label | rs1 == rs2 | 位模式相等 |
bne rs1, rs2, label | rs1 != rs2 | 位模式不等 |
blt rs1, rs2, label | rs1 < rs2 | 有符号 |
bge rs1, rs2, label | rs1 >= rs2 | 有符号 |
bltu rs1, rs2, label | rs1 < rs2 | 无符号 |
bgeu rs1, rs2, label | rs1 >= rs2 | 无符号 |
例如:
beq s0, s1, target对应的 PC 选择关系是:
PCnext={PC+sext(branch_imm),s0=s1PC+4,s0≠s1 PC_{next}= \begin{cases} PC+sext(branch\_imm), & s0=s1 \\ PC+4, & s0\ne s1 \end{cases}PCnext={PC+sext(branch_imm),PC+4,s0=s1s0=s1
blt与bltu的输入 bit 可以完全相同,但比较结果可能不同。因此译码器必须把有符号/无符号比较模式传给比较器或 ALU。
2. 跳转
j label无条件跳转到标签处。它是伪指令,实际可展开为:
jal x0, label因为结果写向x0会被丢弃,所以只改变 PC,不保存返回地址。jal和jalr还会在函数调用中保存返回地址,稍后在 6.3.7 讨论。
分支的硬件理解
条件分支至少包含三部分组合逻辑:
rs1、rs2 → 比较器 ─────────────┐ PC、B-type 立即数 → 目标加法器 ├→ PC MUX PC → PC+4 加法器 ──────────────┘条件分支不应写寄存器,也不应写数据存储器。验证时需要同时检查“PC 是否选对”和“无关写使能是否保持为 0”。
按6.3.1的时序模型,beq在组合阶段比较两个源寄存器:条件成立时选择PC+B_imm,否则选择PC+4。该指令没有普通寄存器写使能,因此下一上升沿只更新 PC,不会写寄存器。
6.3.6 数组
数组元素连续存放在内存中。若数组基址为base,索引为i,每个元素大小为element_sizebyte,则:
element_address=base+i×element_size element\_address=base+i\times element\_sizeelement_address=base+i×element_size
1. 32 位整数数组
RV32I 中一个int通常按 32 位 word 处理,占 4 byte:
address(scores[i])=base+(i≪2) address(scores[i])=base+(i\ll2)address(scores[i])=base+(i≪2)
原书“每个分数加 10”示例的核心地址计算为:
slli t0, s1, 2 # t0 = i * 4 add t0, t0, s0 # t0 = &scores[i] lw t1, 0(t0) addi t1, t1, 10 sw t1, 0(t0)这里左移 2 位只是把 word 索引转换为字节偏移;本篇只关注 32 bit word 访问。
这五条汇编指令对应五个连续的指令周期,而不是一个组合数据流:先由slli在上升沿写回缩放后的索引,再由add写回元素地址;随后lw读出元素并在上升沿写入t1,addi再写回t1+10,最后sw在存储器写入沿提交数据。它们复用同一套寄存器堆、ALU 和存储器接口。若数据 RAM 为同步读,lw的读数据会额外跨越一个时钟边界,不能继续套用这里的单周期假设。
6.3.7 函数调用
第七章直接用到的是jal的硬件语义:
rd ← PC + 4 PC ← PC + J_imm这要求写回 MUX 能选择PC+4,PC MUX 能选择跳转目标。
按6.3.1的时序模型,jal在组合阶段同时生成PC+J_imm与PC+4;下一个上升沿把前者提交到 PC、把后者提交到 J-type 的rd。上面的jal x1,+16情景中,这两个提交结果分别是0x1010与0x1004。
6.3.8 伪指令
伪指令在进入处理器前已被汇编器展开;译码器只识别真实 opcode,不为j、nop或ret增加专用硬件。
6.4 机器语言
汇编器把助记符、寄存器别名和标签转换成 32 位机器指令。RV32I 实际有 R、I、S、B、U、J 六种格式;原书根据字段相似性把它们归为 R-type、I-type、S/B-type 和 U/J-type 四组。
所有格式都满足两个重要规律:
opcode固定放在instr[6:0];rd、rs1、rs2和立即数字段尽量占据相同位置。
这种折中让汇编编码看起来不够直观,但能减少译码与立即数选择硬件。
这正对应原书的设计原则 4:好的设计需要好的折中。RISC-V 没有强迫所有指令只用一种格式,也没有为每条指令设计完全不同的格式。
各类指令共享的字段在取指后由组合连线一次性切出:opcode做主译码,rd/rs1/rs2连接寄存器堆端口,funct3细分具体操作。这些都是组合结果;只有进入流水线设计时,才需要将字段和控制信号锁存到流水级寄存器。
6.4.1 R-type 指令
R-type 用两个源寄存器和一个目的寄存器:
31 25 24 20 19 15 14 12 11 7 6 0 +--------------+--------+--------+------+--------+---------+ | funct7 | rs2 | rs1 |funct3| rd | opcode | +--------------+--------+--------+------+--------+---------+ 7 bit 5 bit 5 bit 3 bit 5 bit 7 bitrs1、rs2:寄存器堆两个读地址;rd:写地址;opcode、funct3、funct7:共同决定 ALU 操作和控制信号。
6.4.2 I-type 指令
I-type 用一个源寄存器、一个目的寄存器和一个 12 位立即数:
31 20 19 15 14 12 11 7 6 0 +----------------------+--------+------+--------+---------+ | imm[11:0] | rs1 |funct3| rd | opcode | +----------------------+--------+------+--------+---------+ 12 bit 5 bit 3 bit 5 bit 7 bit它用于addi、andi、ori、xori、Load 和jalr等。除立即数移位指令外,imm[11:0]按 12 位补码解释并符号扩展到 32 位。
对slli、srli和srai,imm[4:0]是 5 位无符号移位量;高 7 位不再是普通数值,其中srai通过instr[30]=1与srli区分。译码时不能把整个 12 位字段都送入移位量端口。
对lw rd,-36(rs1),I-type 的imm[11:0]=12'hFDC会符号扩展为32'hFFFF_FFDC,再由地址 ALU 计算:
effective_address=rs1+(−36) effective\_address=rs1+(-36)effective_address=rs1+(−36)
这里需要掌握的是“12 bit 字段 → 32 bit 有符号操作数”的路径,而非手算整条机器码。
6.4.3 S/B-type 指令
S-type 和 B-type 都没有rd,因为 Store 写内存、Branch 写 PC,不需要普通寄存器写回。二者共同保留rs1、rs2、funct3和opcode,但立即数拼接方式不同。
1. S-type
31 25 24 20 19 15 14 12 11 7 6 0 +--------------+--------+--------+------+-----------+---------+ | imm[11:5] | rs2 | rs1 |funct3| imm[4:0] | opcode | +--------------+--------+--------+------+-----------+---------+对sw rs2, imm(rs1):
rs1提供基址;rs2提供写数据;imm经符号扩展后与基址相加;- 立即数被拆成高 7 位与低 5 位放入指令。
硬件必须把两段重新拼回:
S_imm = sext({instr[31:25], instr[11:7]})Store 的控制不变量是RegWrite=0。如果译码器误把instr[11:7]当作rd,就可能产生错误寄存器副作用。
2. B-type
B-type 的 13 位有符号偏移量最低位固定为 0,指令只保存其余 12 位:
B_imm = sext({instr[31], instr[7], instr[30:25], instr[11:8], 1'b0}) branch_target = PC + B_imm最低位不编码的原因是分支目标至少按 2 byte 对齐,所以偏移量必为偶数。这里编码的是字节偏移量,不是“跨过几条指令”。
6.4.4 U/J-type 指令
1. U-type
31 12 11 7 6 0 +------------------------------------------+--------+---------+ | imm[31:12] | rd | opcode | +------------------------------------------+--------+---------+ 20 bit 5 bit 7 bitlui rd, imm20把指令中的 20 位立即数放到结果的高 20 位,低 12 位补 0:
U_imm = {instr[31:12], 12'b0}这与上一篇构造大常数时的lui行为相同。
2. J-type
jal使用 21 位有符号 PC 相对偏移量,最低位同样固定为 0:
J_imm = sext({instr[31], instr[19:12], instr[20], instr[30:21], 1'b0}) jump_target = PC + J_imm rd = PC + 4RTL 只需保证 J-type 立即数按该位序重排,并同时让 PC 选择jump_target、写回 MUX 选择PC+4;无需手算其整条机器码。
6.4.5 立即数编码
六种格式的立即数生成关系可以集中写成:
I: sext(instr[31:20]) S: sext({instr[31:25], instr[11:7]}) B: sext({instr[31], instr[7], instr[30:25], instr[11:8], 1'b0}) U: {instr[31:12], 12'b0} J: sext({instr[31], instr[19:12], instr[20], instr[30:21], 1'b0})R-type 不需要立即数。
为什么字段看起来被打乱
RISC-V 尽量让相同含义的立即数 bit 落在相同指令位置,并让instr[31]作为有符号立即数的符号来源。这样可以减少立即数生成器中的 MUX 输入和布线交叉。
硬件可以把它理解为:
instr[31:7] ↓ 按 ImmSrc 选择并重排 ↓ 符号扩展 / 低位补 0 ↓ 32 位 ImmExt立即数生成器是组合逻辑,不应保存状态。验证时至少覆盖:
- I/S-type 的最大正数
+2047与最小负数-2048; - B/J-type 的正向与负向偏移;
- B/J-type 生成结果 bit 0 恒为 0;
- U-type 低 12 位恒为 0;
- 不同格式下
instr[31]的符号扩展。
真正实现时应把这些拼接式放在同一组合译码区;B/J 型的目标地址计算已在6.3.1的时序情景中说明。I/S/U 型遵循同样原则:字段重排和符号扩展在当前周期完成,产生的值只在驱动 PC 或写回使能的下一个上升沿改变体系结构状态。
6.4.6 寻址方式
寻址方式说明指令从哪里取得操作数或怎样产生新 PC。
| 寻址方式 | 例子 | 地址/操作数来源 | 硬件路径 |
|---|---|---|---|
| 寄存器寻址 | add s0,s1,s2 | rs1、rs2 | 寄存器堆 → ALU |
| 立即数寻址 | addi s0,s1,4 | rs1、立即数 | 寄存器堆/立即数 MUX → ALU |
| 基址寻址 | lw t0,8(s1) | rs1+sext(imm) | ALU 算有效地址 → 数据存储器 |
| PC 相对寻址 | beq、jal | PC+sext(offset) | PC/立即数 → 目标加法器 |
同一个加法运算在不同寻址方式下具有不同输入来源,因此数据通路需要 MUX;不能只看见“都是加法”就忽略控制信号。
6.4.7 解释机器语言代码
译码器的稳定顺序是:
- 用
instr[6:0]的opcode选择主控制信号和指令格式; - 并行切出
rd、rs1、rs2、funct3、instr[30]; - 由功能字段选择 ALU/比较操作,并由格式选择立即数重排;
- 只将明确需要的字段送入寄存器堆、ALU、存储器接口或 PC 选择逻辑。
重点是“字段→控制/数据通路”的连接,反汇编或 ABI 名称转换不是学习的重点。
6.4.8 存储程序
存储程序的核心是:机器指令和普通数据一样,以二进制形式存储在内存中。更换程序只需改变内存中的指令内容,不需要重新连接硬件。
原书示例从地址0x0000_0830开始保存 4 条指令:
0x0000_0830 0x0149_8933 0x0000_0834 0x4073_02B3 0x0000_0838 0xFF23_0913 0x0000_083C 0xFFA9_A383PC 初始化为0x0000_0830后,处理器重复执行:
按 PC 取指 → 译码 → 读取操作数 → 执行 → 更新体系结构状态 → 选择下一 PC取指数据流已经在6.3.1首次出现 PC 时给出。这里只需记住:PC 的体系结构含义始终是字节地址;pc_q[31:2]仅在连接 32 bit 指令 RAM 时转换为 word 索引,并不改变 PC 本身的单位。
在本章范围内,RV32I 的主要体系结构状态包括:
- PC;
- 32 个整数寄存器的可见内容;
- 内存中的可见数据。
组合逻辑的中间节点、流水线寄存器和分支预测状态不属于这里的软件可见体系结构状态。它们属于具体微体系结构,下一章才会展开。
本篇硬件视角总结
从指令语义到硬件资源
| 书中概念 | 主要硬件资源或行为 |
|---|---|
| 顺序执行 | PC 寄存器、PC+4加法器 |
| 条件分支 | 两端口寄存器读取、比较器、目标加法器、PC MUX |
| 逻辑/移位 | ALU 逻辑单元、桶形移位器 |
| 数组访问 | 索引缩放、地址加法、Load/Store 接口 |
jal | PC+4写回、跳转目标写 PC |
| 指令格式 | opcode 译码器、寄存器地址字段、立即数生成器 |
| 存储程序 | 指令存储器、PC 驱动的取指路径 |
常见指令的副作用
| 指令类别 | 写rd | 读内存 | 写内存 | 改变 PC 的方式 |
|---|---|---|---|---|
| R/I-type ALU | 是 | 否 | 否 | PC+4 |
| Load | 是 | 是 | 否 | PC+4 |
| Store | 否 | 否 | 是 | PC+4 |
| Branch | 否 | 否 | 否 | PC+4或分支目标 |
jal | 是,写入PC+4 | 否 | 否 | 跳转目标 |
这张表适合直接转化为后续控制器的检查清单。尤其要防止 Store、Branch 对寄存器产生非预期写回,也要防止分支失败时仍选择目标 PC。
后续小节内容均偏向于软件,不再进行过于深度的学习,后续直接开始第七章 微体系结构 的学习。