Categories: 硬件设计.

前言

本文是对一个自行设计的 LoongArch32 (LA32) 指令集 CPU 的架构总结。该 CPU 用于龙芯杯(NSCSCC)竞赛,在 Verilator 仿真环境下主频达到 75MHz,已通过全部 58 个功能测试点。与上一篇文章聚焦于 I-Cache 一致性和 RAW 冒险的调试不同,本文从架构设计的角度,用 STAR 框架系统性地梳理整个 CPU 的设计决策、模块划分和技术选型。


Situation(背景)

项目目标

设计一个完整支持 LoongArch32 精简指令集的流水线 CPU,要求:

  • 支持 LA32R 全部指令(算术逻辑、访存、分支、特权、CSR、TLB、原子等)
  • 支持虚拟内存(页表映射、TLB 管理)
  • 支持 Cache(I-Cache / D-Cache,含 cacop 维护指令)
  • 支持异常和中断处理(11 种异常 + 外部中断)
  • 通过 Chiplab 仿真环境的 NEMU difftest 验证
  • 在 Verilator 仿真下达到 75MHz 主频

LoongArch32 指令集特点

LoongArch32 是龙芯自主设计的 RISC 指令集,主要特点:

  • 32 个通用寄存器($r0 恒为 0)
  • 定长 32-bit 指令编码
  • 延迟槽分支(B/BL 后有一条延迟槽指令)
  • 特权等级:PLV0(内核态)/ PLV3(用户态)
  • 软件管理 TLB(TLBSRCH / TLBRD / TLBWR / TLBFILL / INVTLB)
  • 直接映射窗口(DMW0/DMW1)支持无页表直访
  • CSR 寄存器通过 csrrd / csrwr / csrxchg 访问

Task(任务)

将上述需求映射为一个可综合的 Verilog 设计,核心设计任务包括:

  1. 流水线划分:确定流水线级数和各级职责
  2. 取指子系统:I-Cache 设计、PC 管理、分支预测
  3. 译码与执行:指令译码、ALU 设计、乘除法实现
  4. 访存子系统:D-Cache 设计、Load/Store 对齐处理
  5. 特权架构:CSR 寄存器、TLB、异常/中断处理
  6. 总线接口:AXI4 桥接器设计
  7. 冒险处理:数据转发与流水线停顿

Action(行动)

一、流水线架构

1.1 六级流水线划分

本设计采用 6 级流水线,与经典的 5 级(IF→ID→EX→MEM→WB)相比,将执行阶段拆分为 EXE1 和 EXE2 两级:

1
2
3
4
5
6
7
8
┌──────┐    ┌──────┐    ┌───────┐    ┌───────┐    ┌──────┐    ┌──────┐
│ IF │───▶│ ID │───▶│ EXE1 │───▶│ EXE2 │───▶│ MEM │───▶│ WB │
│取指 │ │译码 │ │ALU计算│ │访存地址│ │数据访存│ │写回 │
│I-Cache│ │分支判定│ │乘除启动│ │D-Cache│ │结果选择│ │RegFile│
│TLB查 │ │冒险检测│ │CSR读 │ │TLB查 │ │异常聚合│ │CSR写 │
└──────┘ └──────┘ └───────┘ └───────┘ └──────┘ └──────┘
↑ ↓
└────────────────── PC 重定向(异常/分支/ertn)─────────────┘

设计决策:为什么拆分 EXE1/EXE2?

在 5 级流水线中,EXE 阶段需要在一个周期内完成 ALU 计算 + D-Cache 地址翻译 + D-Cache 访问。这条组合逻辑路径较长:

1
ALU计算 → 物理地址生成(TLB查找) → D-Cache Tag比较 → 数据选择

在 75MHz 的目标频率下,这条路径的时序裕量不足。将 ALU 计算放在 EXE1,将访存地址发射和 TLB 查找放在 EXE2,有效缩短了关键路径。代价是增加了一级流水线延迟,加大了数据冒险窗口和分支惩罚。

1.2 流水线控制信号

每一级通过 ready_goallow_in 两个信号控制推进:

1
2
3
// 每级的推进条件
assign {stage}_allow_in = {stage}_ready_go && {next_stage}_allow_in;
assign {stage}_ready_go = /* 无冒险、无停顿条件 */;

这种”握手”式控制使得每一级可以独立地暂停,而不影响后级的正常推进。全局停顿信号 pipline_is_not_stalled 作为取指请求的门控条件,确保在冒险停顿时不会发起新的取指。

二、取指子系统

2.1 PC 管理

PC 寄存器(pc_reg.v)维护当前取指地址和下一地址,处理以下重定向源:

重定向源 优先级 说明
Reset 最高 初始化 PC 到 0x1bfffffc
wb_ex 异常处理,跳转到 CSR.EENTRY
wb_is_ertn 异常返回,跳转到 CSR.ERA
pc_br_taken ID 阶段分支判定跳转
btb_hit BTB 预测跳转
顺序执行 默认 nextpc = pc + 4

BTB 命中时,PC 在顺序推进的同时预取预测目标地址的指令,实现零延迟分支预测。

2.2 分支预测:BTB

采用 16 项直接映射 BTB(btb.v),结构简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
// BTB 表项结构
// {valid(1), tag(28), target(32)} = 60 bits
// 索引: pc[5:2] 标签: pc[31:6]

// 查找:组合逻辑,零延迟
assign hit = entry_valid && (entry_tag == lookup_pc[31:6]);
assign target = entry_target;

// 更新:WB 阶段写入实际跳转目标
always @(posedge clk) begin
if (update)
entries[update_idx] <= {1'b1, update_pc[31:6], update_target};
end

设计特点

  • 无预测器:BTB 只记录目标地址,不预测是否跳转(taken/not-taken)。分支是否跳转由 ID 阶段的 ALU 组合逻辑判定,延迟为 1 个周期。
  • 直接映射:16 项,索引 4-bit,冲突率较高但面积小。
  • 延迟槽处理:LoongArch 的分支指令后有一条延迟槽,BTB 预测的是跳转指令本身的目标,延迟槽指令正常执行。

2.3 I-Cache 设计

I-Cache 采用 2 路组相联设计(icache_two_way.v):

参数
相联度 2 路
组数 256
缓存行大小 128 bit(4 个字)
总容量 16 KB
替换策略 伪 LRU(1-bit used)
地址分解 Tag[31:12] | Index[11:4] | Word[3:2] | Byte[1:0]

I-Cache 采用阻塞式(Blocking)状态机:

1
2
3
4
S_IDLE ──hit──▶ S_HIT_RESP ──▶ S_IDLE

└──miss──▶ S_REF_REQ ──▶ S_REF_WAIT ──┐
└─▶ S_REF_REQ (重复4次) ──▶ S_REF_RESP ──▶ S_IDLE
  • 命中路径:2 个周期(IDLE → HIT_RESP → IDLE)
  • 缺失惩罚:10+ 周期(4 拍 Refill + AXI 延迟)
  • Cacop 支持:S_CACOP_RESP 状态处理 cacop 指令的 Index Invalidate 和 Hit Invalidate 操作

三、译码与执行

3.1 指令译码

译码器(ID_stage.v)采用两级译码结构:

  1. 一级译码:使用 decoder_6_64 将 opcode[31:26] 译码为 64 个 one-hot 信号
  2. 二级译码:根据子操作码字段([25:22]、[21:20]、[19:15] 等)进一步区分具体指令

译码器输出以下控制信号:

信号类别 示例 说明
寄存器读 id_rj, id_rk, id_rd 源/目标寄存器编号
ALU控制 id_alu_op[4:0] ALU 操作码(20 种操作)
立即数 id_imm12, id_imm5, id_imm16, id_imm20, id_imm26 各类立即数
访存控制 id_dram_we, id_dram_re, id_rdram_num, id_wdram_num Load/Store 控制
分支控制 id_br_taken, id_br_target 分支判定结果
CSR控制 id_csr_num, id_csr_we, id_csr_mask_all_one CSR 读写
TLB控制 id_invtlb_valid, id_inst_tlbrd, id_tlb_we TLB 维护指令
异常检测 id_ex_adef, id_ex_ale_h, id_ex_ale_w, id_ex_ine, id_ex_brk 取指/执行阶段异常

3.2 ALU 设计

ALU(alu.v)支持 20 种操作,全部在 EXE1 阶段组合逻辑完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case (alu_op)
5'd0: result = src1 + src2; // ADD.W
5'd1: result = src1 - src2; // SUB.W
5'd2: result = ($signed(src1) < $signed(src2)) ? 1 : 0; // SLT
5'd3: result = (src1 < src2) ? 1 : 0; // SLTU
5'd4: result = src1 & src2; // AND
5'd5: result = src1 | src2; // OR
5'd6: result = ~(src1 | src2); // NOR
5'd7: result = src1 ^ src2; // XOR
5'd8: result = src1 << src2[4:0]; // SLL.W
5'd9: result = src1 >> src2[4:0]; // SRL.W
5'd10: result = $signed(src1) >>> src2[4:0]; // SRA.W
5'd11: /* BEQ: 条件跳转 */
5'd12: /* BNE: 条件跳转 */
5'd13: /* B: 无条件跳转,结果 = PC+4 */
5'd14: /* JIRL: 寄存器跳转,结果 = PC+4 */
5'd15: result = {src2[19:0], 12'b0}; // LU12I.W
5'd16: result = {src2[19:0], 12'b0} + exe_pc; // PCADDI
5'd17: result = mul_signed[31:0]; // MUL.W (低32位)
5'd18: result = mul_signed[63:32]; // MULH.W.S (高32位有符号)
5'd19: result = mul_unsigned[63:32]; // MULH.W.U (高32位无符号)
endcase

乘法器:使用 Verilog $signed * $signed,综合时映射到 DSP 硬块,单周期完成。

地址对齐异常检测:ALU 同时检测访存地址的对齐错误:

1
2
3
4
5
6
// 半字访存地址最低位不为0 → ALE异常
if (exe_ex_ale_h && (exe_alu_result[0] != 1'b0))
exe_ex_ale = 1'b1;
// 字访存地址低2位不为0 → ALE异常
else if (exe_ex_ale_w && (exe_alu_result[1:0] != 2'b00))
exe_ex_ale = 1'b1;

3.3 除法器设计

除法器(div.v)采用迭代恢复余数法,32 周期完成一次除法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 核心思想:将被除数逐位移入余数寄存器,试减除数
always @(posedge div_clk) begin
if (~(count[7])) begin // count 从 32 递减到 0
if (tmp_d[32]) begin // 试减结果为负,商位=0
UnsignS <= {UnsignS[31:0], 1'b0};
tmp_r <= result_r; // 恢复余数
end else begin // 试减结果非负,商位=1
UnsignS <= {UnsignS[31:0], 1'b1};
tmp_r <= tmp_d; // 保留余数
end
count <= count - 8'd1;
end
end

特点

  • 支持 div.w(有符号除)、div.wu(无符号除)、mod.w(取余)、mod.wu(无符号取余)
  • 通过取绝对值实现有符号除法,最后根据符号位恢复结果
  • 除数为 0 时不产生异常(LoongArch 硬件不检测除零,由软件处理)
  • 除法期间流水线停顿,EXE1 阶段等待 complete 信号

四、访存子系统

4.1 D-Cache 设计

D-Cache(dcache_two_way.v)与 I-Cache 结构对称,但增加了写回(Write-Back)支持:

参数
相联度 2 路
组数 256
缓存行大小 128 bit
总容量 16 KB
写策略 Write-Back + Write-Allocate
脏位 每路每行 1-bit dirty

写回流程:当 Store 指令命中 D-Cache 且目标行已脏时,需要先将脏行写回内存,再写入新数据。这通过状态机中的 S_WB_REQ / S_WB_WAIT 状态完成。

4.2 Load/Store 对齐处理

LoongArch 要求访存地址自然对齐(半字地址 2 对齐,字地址 4 对齐)。非对齐访问触发 ALE 异常。对齐处理在 MEM 阶段完成:

  • Load:根据 id_rdram_num(ld.b/ld.h/ld.w)和地址低 2 位,从 32-bit D-Cache 数据中选择对应字节/半字,并进行零扩展或符号扩展
  • Store:根据 id_wdram_num 和地址低 2 位,将数据放置到正确的字节通道(通过 wstrb 字节使能掩码控制)

五、特权架构

5.1 CSR 寄存器

CSR 模块(csr.v)实现了完整的 LA32 CSR 集合:

CSR 地址 说明
CRMD 0x0 当前模式信息(PLV, IE, DA, PG, DATF, DATM)
PRMD 0x1 异常前模式信息(PPLV, PIE)
ECFG 0x4 异常配置(中断使能掩码)
ESTAT 0x5 异常状态(中断挂起位, ECode, ESubCode)
ERA 0x6 异常返回地址
BADV 0x7 错误虚地址
EENTRY 0xC 异常入口地址
TLBIDX 0x10 TLB 索引
TLBEHI 0x11 TLB 高位
TLBELO0/1 0x12/0x13 TLB 低位(奇偶页)
ASID 0x18 地址空间标识
TLBRENTRY 0x88 TLB 重填异常入口
DMW0/1 0x180/0x181 直接映射窗口
SAVE0-3 0x30-0x33 通用保存寄存器
TID 0x40 定时器编号
TCFG 0x41 定时器配置

CSR 写机制:采用掩码写入方式,支持 csrxchg 指令的原子读-改-写:

1
2
3
// 通用 CSR 写模式:mask + value
csr_field <= csr_wmask[field] & csr_wvalue[field] |
~csr_wmask[field] & csr_field;

异常时的 CSR 自动更新

  • wb_ex 触发时:PRMD.PPLV ← CRMD.PLV,PRMD.PIE ← CRMD.IE,CRMD.PLV ← 0,CRMD.IE ← 0,ERA ← WB.PC,ESTAT.ECode ← 异常码
  • wb_ertn 触发时:CRMD.PLV ← PRMD.PPLV,CRMD.IE ← PRMD.PIE

5.2 TLB 设计

TLB(tlb.v)实现 16 项全相联查找,支持 4KB 和 4MB 两种页大小:

1
2
3
4
5
6
7
8
9
10
// 三端口并行查找
// Port 0: 取指地址翻译(IF阶段)
// Port 1: 数据地址翻译(EXE2/MEM阶段)
// Port 2: TLBSRCH 指令查找

// 匹配条件:VPPN匹配 + (ASID匹配 || 全局标志G) + 有效标志E
assign match0[i] = (s0_vppn[18:9] == tlb_vppn[i][18:9]) &&
(tlb_ps4MB[i] || s0_vppn[8:0] == tlb_vppn[i][8:0]) &&
((s0_asid == tlb_asid[i]) || tlb_g[i]) &&
tlb_e[i];

TLB 维护指令支持

指令 功能
TLBSRCH 查找 TLB 中是否存在指定虚页映射,结果写入 TLBIDX.NE
TLBRD 读取 TLBIDX.Index 指向的表项到 TLBEHI/TLBELO0/1
TLBWR 将 CSR 中的 TLB 表项数据写入 TLBIDX.Index 指向的表项
TLBFILL 查找空闲项或随机替换一项,写入 CSR 中的表项数据
INVTLB 按 op 码无效化 TLB 表项(6 种模式:全清、按ASID、按VPPN等)

5.3 异常与中断处理

异常处理采用 WB 阶段统一检测 策略(trap_unit.v + wb_stage.v):

1
2
3
4
5
6
7
8
9
异常检测点         传递路径              最终判定
─────────────── ───────────────── ──────────
IF: ADEF(取指地址错误) ──→ 流水线传递 ──→ WB阶段
ID: INE(非法指令) ──→ 流水线传递 ──→ WB阶段
ID: BRK(断点) ──→ 流水线传递 ──→ WB阶段
ID: SYS(系统调用) ──→ 流水线传递 ──→ WB阶段
EXE: ALE(地址对齐) ──→ 流水线传递 ──→ WB阶段
MEM: TLB异常 ──→ 流水线传递 ──→ WB阶段
外部: 中断 ──→ 直接注入 ──→ WB阶段

异常优先级(从高到低):

1
2
3
中断(INT) > TLB重填(TLBR) > 页修改(PME) > TLB无效(PIL/PIS/PIF)
> 权限(PPI) > 取指地址错误(ADEF) > 访存对齐(ALE) > 系统调用(SYS)
> 断点(BRK) > 非法指令(INE)

中断处理interrupt.v):

1
2
3
// 中断使能:CRMD.IE && (ESTAT.IS & ECFG.LIE) != 0
wire [12:0] int_masked = csr_estat_is & csr_ecfg_lie;
wire has_int = csr_crmd_ie && (|int_masked || |ext_intrpt);

支持 13 个中断源:2 个软件中断(IS[1:0])、8 个硬件中断(IS[9:2])、定时器中断(IS[11])、核间中断(IS[12])。

定时器:64 位自由运行计数器 csr_timer_64,配合 32 位倒计时器 timer_cnt 和 TCFG 配置寄存器,支持单次和周期定时中断。

六、总线接口

6.1 AXI4 桥接器

AXI 桥接器(sram_axi_bridge.v)将 CPU 的 SRAM-like 接口转换为 AXI4 总线协议,取指和数据共享同一组 AXI 通道:

1
2
3
4
5
6
7
8
9
CPU侧                    AXI Bridge                    AXI总线
──────── ──────────── ────────
inst_sram_req ──┐ ┌── AR channel (ID=0)
data_sram_req ──┤── 读写仲裁 ──▶ AXI状态机 ──┤── R channel
│ ├── AW channel (ID=1)
│ ├── W channel
│ └── B channel
inst_sram_rdata ◀── 响应分发 ◀──┘
data_sram_rdata ◀── 响应分发 ◀──┘

读通道仲裁:取指和数据读请求通过 read_req_is_data 信号仲裁,数据请求优先。请求被接受后锁定(read_hold_* 寄存器),直到响应返回。

读响应缓冲:AXI 读响应通过一级寄存器缓冲(r_buf_rvalid/r_buf_rid/r_buf_rdata),过滤潜在的毛刺,并按 ID 分发到正确的端口:

1
2
assign inst_sram_data_ok = r_buf_rvalid && r_buf_rid == 4'b0;  // ID=0: 取指
assign data_sram_data_ok = r_buf_rvalid && r_buf_rid == 4'b1; // ID=1: 数据

写通道:AW 和 W 通道独立握手,支持地址和数据先后到达的灵活时序。写响应(B 通道)用于确认写完成。

七、冒险处理

7.1 数据冒险

采用 ID 阶段集中转发 策略,从 EXE1、EXE2、MEM、WB 四个后级阶段转发:

1
2
3
4
5
6
// 优先级:EXE1 > EXE2 > MEM > WB(越近越新)
assign id_src1_fwd = (exe1_ref_we && exe1_rd == rf_raddr1 && rf_raddr1 != 0) ? exe1_alu_result :
(exe2_ref_we && exe2_rd == rf_raddr1 && rf_raddr1 != 0) ? exe2_alu_result :
(mem_ref_we && mem_rd == rf_raddr1 && rf_raddr1 != 0) ? mem_alu_result :
(wb_rf_we && wb_rd == rf_raddr1 && rf_raddr1 != 0) ? rf_wdata :
rf_rdata1;

Load-Use 停顿:当 MEM 阶段有 Load 指令且 ID 阶段的消费者依赖其结果时,停顿 IF 和 ID:

1
2
3
4
assign if_ready_go = /* ... */ :
(mem_ref_we && mem_rd != 0 &&
((id_src1_from_ref && rf_raddr1 == mem_rd) ||
(id_src2_from_ref && rf_raddr2 == mem_rd))) ? 1'b0 : /* ... */;

7.2 控制冒险

  • BTB 预测:取指阶段根据 BTB 预测跳转目标,减少分支惩罚
  • ID 阶段判定:分支指令在 ID 阶段通过 ALU 组合逻辑判定是否跳转,延迟 1 周期
  • 预测失败恢复:ID 阶段发现实际跳转方向与 BTB 预测不一致时,冲刷 IF 阶段的错误预取指令
  • 异常冲刷:异常和 ertn 指令在 WB 阶段触发全流水线冲刷

7.3 结构冒险

  • AXI 总线竞争:取指和数据访存共享 AXI 通道,通过 ID 区分(0=取指, 1=数据),数据请求优先
  • RegFile 读写竞争:RegFile 采用同步写、异步读,WB 阶段写入的数据在下一周期可见。同一周期内 WB 写入和 ID 读取同一寄存器时,通过 WB→ID 转发解决

八、Difftest 支持

设计中集成了 NEMU difftest 接口,通过 DIFFTEST_EN 宏条件编译:

  • 寄存器对比:RegFile 导出全部 32 个寄存器值(rf_regs_diff
  • CSR 对比:CSR 模块导出 26 个 CSR 的完整状态(csr_all_diff,832-bit 总线)
  • 在线比对:每条指令在 WB 阶段提交时,将 PC、寄存器、CSR 状态发送给 NEMU 比对

Result(结果)

设计成果

指标 结果
指令集 LoongArch32 (LA32R) 全指令支持
主频 75 MHz(Verilator 仿真)
流水线 6 级(IF → ID → EXE1 → EXE2 → MEM → WB)
I-Cache 16 KB,2 路组相联
D-Cache 16 KB,2 路组相联,Write-Back
TLB 16 项全相联,3 端口并行查找
分支预测 16 项直接映射 BTB
功能测试 58 个测试点全部通过(82 个用例)
验证方法 Verilator + NEMU difftest

模块规模统计

模块 文件 功能
core_top core_top.v 顶层模块,流水线互连和冒险控制
PC_Reg pc_reg.v PC 寄存器,重定向管理
ID_stage ID_stage.v 指令译码器
ALU alu.v 算术逻辑单元
Div div.v 迭代除法器
regfile regfile.v 通用寄存器文件
icache_two_way icache_two_way.v 指令 Cache
dcache_two_way dcache_two_way.v 数据 Cache
btb btb.v 分支目标缓冲器
tlb tlb.v 地址翻译旁路缓冲器
CSRREG csr.v CSR 寄存器文件
trap_unit trap_unit.v 异常优先级判定
Wb_stage wb_stage.v 异常编码
interrupt interrupt.v 中断检测
axi_bridge sram_axi_bridge.v AXI4 总线桥接器
tools tools.v 译码器宏模块
*_readygo / *_allow_in / *_reg 多个文件 流水线级寄存器和控制