Categories: 硬件设计.

前言

本文是对 AnxChip(LoongArch32 指令集 CPU)的时序问题与架构漏洞分析。该 CPU 采用 6 级流水线设计,在 Verilator 仿真环境下主频 75MHz,已通过全部 58 个功能测试点和 20 个性能 benchmark。前两篇文章分别聚焦于 I-Cache 一致性/RAW 冒险的调试和整体架构总结,本文则从静态分析的角度,系统性地审查代码中潜在的时序瓶颈和架构设计缺陷。


Situation(背景)

CPU 架构概览

参数 配置
指令集 LoongArch32 (LA32R)
主频 75 MHz(Verilator 仿真)
流水线 6 级:IF → ID → EXE1 → EXE2 → MEM → WB
I-Cache 2 路组相联,256 组,128-bit 行,16KB
D-Cache 2 路组相联,256 组,128-bit 行,Write-Back,16KB
TLB 16 项全相联,3 端口并行查找(取指/访存/TLBSRCH)
分支预测 BTB 模块已实例化
总线 AXI4,I/D 共享 axi_bridge

分析方法

  • 时序分析:从 RTL 代码中提取关键组合逻辑路径,分析逻辑级数和扇出
  • 架构审查:逐模块检查功能正确性、边界条件、异常处理一致性
  • 交叉验证:结合调试日志中的实际复现记录

Task(任务)

目标是对整个 CPU 的 RTL 代码进行静态审查,识别以下两类问题:

  1. 时序瓶颈:可能限制主频提升或导致时序违例的长组合逻辑路径
  2. 架构漏洞:代码中存在的 bug、设计缺陷、边界条件处理不完善

每个问题给出:定位(文件+行号)、根因分析、影响评估、修复建议。


Action(行动)

一、时序问题

1.1 ID 级转发 MUX → EXE1 ALU 关键路径

定位core_top.v:567-578

根因:ID 阶段的操作数转发逻辑是一个 4 级优先级 MUX,从 EXE1/EXE2/MEM/WB 四个阶段读取 ALU 结果:

1
2
3
4
5
6
// core_top.v:574-578
assign id_src1_fwd = (exe1_ref_we && exe1_rd == rf_raddr1 && rf_raddr1 != 5'd0) ? exe1_alu_result :
(exe2_ref_we && exe2_rd == rf_raddr1 && rf_raddr1 != 5'd0) ? exe2_alu_result :
(mem_ref_we && mem_rd == rf_raddr1 && rf_raddr1 != 5'd0) ? mem_alu_result :
(wb_rf_we && wb_rd == rf_raddr1 && rf_raddr1 != 5'd0) ? rf_wdata :
rf_rdata1;

关键路径为:exe1_alu_result(EXE1 阶段 ALU 组合输出)→ 5 位寄存器号比较 → 32 位数据选择 → id_src1_fwdExE1_reg 的 D 端。这意味着上一条指令的 ALU 计算结果必须在同一时钟周期内完成转发判断并满足下一条指令的寄存器建立时间。

影响:这是整个流水线最长的组合逻辑路径之一。exe1_alu_result 本身依赖 ALU 的 case 语句(含乘法),加上 4 级 MUX 和比较器,在 75MHz(13.3ns)下时序余量极小。

建议

  • 将转发点从 ID 级移到 EXE1 级入口(即 EXE1 从 ID 锁存操作数后,在 EXE1 内部做转发选择),缩短关键路径
  • 或采用寄存器分线(register slicing)在转发 MUX 中间插入一级寄存器,将 6 级流水线改为 7 级

1.2 32×32 单周期乘法器

定位alu.v:18-19

1
2
assign mul_signed_result = $signed(src1) * $signed(src2);
assign mul_unsigned_result = src1 * src2;

根因:32×32 有符号和无符号乘法均使用 Verilog * 操作符,综合工具会将其映射为单周期组合乘法器。在 FPGA 上,DSP 块的乘法延迟约为 3-4ns,但 32×32 需要级联多个 DSP 块,延迟可达 6-8ns。加上后续的 ALU case 选择和寄存器建立时间,在 13.3ns 的时钟周期内几乎没有余量。

影响:如果目标平台从仿真迁移到 FPGA 综合,乘法器很可能成为关键路径,导致时序违例。当前 75MHz 主频可能无法在所有 FPGA 上实现。

建议

  • 将乘法拆为多周期迭代(类似除法器 div.v 的做法),使用 EXE1 的 ready_go 信号停顿流水线
  • 或使用 FPGA 的 DSP 块原语(primitive)进行手工映射,确保单周期时序收敛

1.3 TLB 16 项全相联并行查找

定位tlb.v:105-318

根因:TLB 的 3 个查找端口(s0 取指、s1 访存、s2 TLBSRCH)各自对 16 项进行并行比较。每项比较涉及 19 位 VPPN 匹配 + 9 位 VPPN 低位匹配(4KB 页时)+ 10 位 ASID 匹配 + 1 位 G + 1 位 E,共约 40 位比较逻辑。3 个端口 × 16 项 = 48 个并行比较器。

输出端是一个 16 级优先级编码器(s0_indextlb.v:303-318),级联深度为 16 级 ? : 三元运算:

1
2
3
4
5
6
// tlb.v:303-318
assign s0_index = match0[0] ? 4'd0:
match0[1] ? 4'd1:
match0[2] ? 4'd2:
// ... 16 级级联 ...
match0[15] ? 4'd15: 4'dx;

影响:s0 端口的查找结果直接进入取指地址翻译异常检测,进而参与 inst_addr 的生成和 inst_uncached 的判断。这条路径在 I-Cache 命中检测之前,处于取指关键路径的起始端。

建议

  • 将 16 项 TLB 拆分为 2 路 8 项的组相联结构,将比较器数量减半
  • 或对 s0 端口使用寄存器缓存上一周期的查找结果,减少组合深度

1.4 if_ready_go 超长组合链

定位core_top.v:1580-1587

1
2
3
4
5
6
7
8
assign if_ready_go = rst? 1'b1:
next_reads_id_rd ? 1'b0 :
(if_pc!=32'h1bfffffc && !if_to_id_valid && !id_br_taken_safe && !br_cancel_fallthrough)? 1'b0 :
(IF_ready_go == 1'b1) ? 1'b1:
(exe1_ref_we&&exe1_rd!=0&&((id_src1_from_ref&&(rf_raddr1==exe1_rd))||...))? 1'b0 :
(exe2_ref_we&&exe2_rd!=0&&((id_src1_from_ref&&(rf_raddr1==exe2_rd))||...))? 1'b0 :
(mem_ref_we&&mem_rd!=0&&((id_src1_from_ref&&(rf_raddr1==mem_rd))||...))? 1'b0:
(wb_rf_we&&wb_rd!=0&&((id_src1_from_ref&&(rf_raddr1==wb_rd))||...))? 1'b0 : 1'b1;

根因if_ready_go 是 IF 级的就绪信号,它融合了:

  1. next_reads_id_rd:检查 IF 级指令是否读取 ID 级指令的目的寄存器(2 个 5 位比较)
  2. IF_ready_go:来自 IF_readygo_state.v 的状态机输出
  3. 4 级 RAW 冒险检测:每级包含 2 个 5 位寄存器号比较 + id_src1_from_ref/id_src2_from_ref 的 AND-OR 逻辑

总计约 12 个 5 位比较器 + 多级条件选择,全部是组合逻辑,直接决定 if_allow_inid_allow_in → 流水线推进。

影响if_ready_go 的扇出极大——它参与 if_allow_in_stateID_Reg 的锁存使能、PC 寄存器更新等多个路径。在 75MHz 下,这条路径的级数已经接近极限。

建议:将 RAW 冒险检测逻辑提取为独立的组合信号 raw_hazard_stall,在 if_ready_goid_ready_gowb_ready_gopipline_is_not_stalled 中复用,避免重复计算。

1.5 冒险检测逻辑四重复制

定位core_top.v:1580-1618

根因:完全相同的 RAW 冒险检测逻辑(4 级转发源 × 2 个源操作数 × 5 位比较)被复制到了 4 个信号中:

  • if_ready_go(行 1580-1587)
  • id_ready_go(行 1591-1598)
  • wb_ready_go(行 1602-1609)
  • pipline_is_not_stalled(行 1613-1618)

每个信号的判断逻辑完全一致:检查 EXE1/EXE2/MEM/WB 的 ref_we + rd 是否与 ID 级的 rf_raddr1/rf_raddr2 匹配。

1
2
3
4
5
6
7
8
9
10
11
12
// 四处独立但相同的冒险检测逻辑(以 exe1 为例)
// 1. if_ready_go (行 1584)
(exe1_ref_we&&exe1_rd!=0&&((id_src1_from_ref&&(rf_raddr1==exe1_rd))||(id_src2_from_ref&&(rf_raddr2==exe1_rd))))? 1'b0 :

// 2. id_ready_go (行 1594) — 完全相同
(exe1_ref_we&&exe1_rd!=0&&((id_src1_from_ref&&(rf_raddr1==exe1_rd))||(id_src2_from_ref&&(rf_raddr2==exe1_rd))))? 1'b0 :

// 3. wb_ready_go (行 1606) — 完全相同
(exe1_ref_we&&exe1_rd!=0&&((id_src1_from_ref&&(rf_raddr1==exe1_rd))||(id_src2_from_ref&&(rf_raddr2==exe1_rd))))? 1'b0 :

// 4. pipline_is_not_stalled (行 1615) — 完全相同
(exe1_ref_we&&exe1_rd!=0&&((id_src1_from_ref&&(rf_raddr1==exe1_rd))||(id_src2_from_ref&&(rf_raddr2==exe1_rd))))? 1'b0 :

影响

  • 综合工具无法自动识别这些重复逻辑进行共享(因为每个信号的优先级和其他条件不同),导致面积和功耗浪费
  • 4 个信号的输出参与不同的控制路径,增加了扇出和布线拥塞

建议:提取公共部分:

1
2
3
4
wire raw_stall = (exe1_ref_we && exe1_rd != 0 &&
((id_src1_from_ref && rf_raddr1 == exe1_rd) ||
(id_src2_from_ref && rf_raddr2 == exe1_rd))) ||
/* ... exe2, mem, wb ... */;

然后在各 ready_go 信号中引用 raw_stall

1.6 I-Cache 命中检测与状态机

定位icache_two_way.v:58-61

1
2
3
4
wire [19:0] fetch_tag = fetch_paddr[31:12];
wire fetch_hit0 = valid0[fetch_index] && tag0[fetch_index] == fetch_tag;
wire fetch_hit1 = valid1[fetch_index] && tag1[fetch_index] == fetch_tag;
wire fetch_hit = fetch_hit0 || fetch_hit1;

根因:Cache 命中检测需要在同一周期内完成:从 fetch_paddr 中提取 tag 和 index → 读取 SRAM 阵列的 validtag → 20 位比较 → 2 路 OR 选择 → 决定状态机跳转。这条路径包含 SRAM 读取延迟(在 FPGA 上约为 2-3ns)加上比较器延迟。

影响:命中信号直接驱动状态机(S_IDLE → S_HIT_RESP),而状态机输出 fetch_data_ok 控制 IF 级的推进。如果 SRAM 读取延迟过大,这条路径会限制 I-Cache 的命中响应速度。

建议:在 FPGA 实现时,确保 Cache 阵列使用 Block RAM 的同步读端口,将 tag 读取延迟一个周期,用流水化的方式处理命中检测。


二、架构漏洞

2.1 CSR SAVE2/SAVE3 寄存器写入地址错误

定位csr.v:416-419

1
2
3
4
5
6
7
8
9
// csr.v:412-419
if(csr_we && csr_num==`CSR_SAVE0) // SAVE0 — 正确
csr_save0_data<= ...;
if(csr_we && csr_num==`CSR_SAVE1) // SAVE1 — 正确
csr_save1_data<= ...;
if(csr_we && csr_num==`CSR_SAVE0) // ← 应为 CSR_SAVE2
csr_save2_data<= ...;
if(csr_we && csr_num==`CSR_SAVE0) // ← 应为 CSR_SAVE3
csr_save3_data<= ...;

根因csr_save2_datacsr_save3_data 的写入条件都使用了 CSR_SAVE0,而非 CSR_SAVE2CSR_SAVE3。这是一个典型的复制粘贴错误。注意读取逻辑(csr.v:969-970)是正确的——读取时分别用 CSR_SAVE2CSR_SAVE3 做选择。

影响:对 SAVE2SAVE3 寄存器的写入操作实际不会生效——写入 SAVE2SAVE3 时条件不匹配,数据不会被更新。只有写入 SAVE0 时,SAVE2SAVE3 会被同时写入与 SAVE0 相同的数据。功能测试中未暴露此问题,因为测试用例未单独使用 SAVE2/SAVE3 做独立存储。

修复

1
2
3
4
if(csr_we && csr_num==`CSR_SAVE2)
csr_save2_data<= ...;
if(csr_we && csr_num==`CSR_SAVE3)
csr_save3_data<= ...;

2.2 BADV 寄存器掩码错误

定位csr.v:445-447

1
2
3
4
5
6
7
8
// csr.v:445-447
else if(csr_we && csr_num == `CSR_BADV)
begin
csr_badv_vaddr <= csr_wmask[`CSR_BADV_VADDR] &csr_wvalue[`CSR_BADV_VADDR] |
~csr_wmask[`CSR_ECFG_LIE] & 13'h1bff &csr_ecfg_lie;
// ~csr_wmask[`CSR_ECFG_LIE] ← 应为 ~csr_wmask[`CSR_BADV_VADDR]
// 13'h1bff & csr_ecfg_lie ← 应为 csr_badv_vaddr
end

根因BADV 寄存器的写入逻辑错误地混入了 ECFG_LIE 的掩码位和 csr_ecfg_lie 的数据。正确的掩码写入模式应当是:

1
2
csr_badv_vaddr <= csr_wmask[`CSR_BADV_VADDR] & csr_wvalue[`CSR_BADV_VADDR] |
~csr_wmask[`CSR_BADV_VADDR] & csr_badv_vaddr;

影响:当软件通过 CSR 写指令更新 BADV 寄存器时(如上下文切换时恢复异常上下文),实际写入的值会被 csr_ecfg_lie 的内容污染——掩码位为 0 的字段不会被原值保留,而是被替换为 csr_ecfg_lie 的低 13 位与 0x1bff 的与结果。功能测试中未触发此问题,因为测试用例通常不手动写 BADV(BADV 由硬件在异常时自动设置)。

修复

1
2
csr_badv_vaddr <= csr_wmask[`CSR_BADV_VADDR] & csr_wvalue[`CSR_BADV_VADDR] |
~csr_wmask[`CSR_BADV_VADDR] & csr_badv_vaddr;

2.3 外部中断绕过 ECFG.LIE 掩码

定位interrupt.v:22-27 + core_top.v:1690

1
2
3
4
5
// interrupt.v:22-27
wire [12:0] int_masked = csr_estat_is & csr_ecfg_lie;
wire has_internal_int = |int_masked;
wire has_external_int = |ext_intrpt; // 直接使用原始外部中断,未经掩码
wire has_int = csr_crmd_ie && (has_internal_int || has_external_int);
1
2
// core_top.v:1690
assign hw_int_in = 8'b0; // 硬件中断输入恒为 0

根因:此问题包含两层错误:

  1. 外部中断未注册到 ESTATcore_top.v:1690hw_int_in 硬编码为 8'b0,导致 csr_estat_is[9:2] 恒为 0。外部中断信号 intrpt[7:0] 从未进入 ESTAT 寄存器,软件无法通过读 ESTAT 获知具体哪个外部中断触发。

  2. 外部中断绕过掩码interrupt.v:24 直接对 ext_intrpt 取或,不经过 csr_ecfg_lie 掩码检查。这意味着只要 CRMD.IE=1,任意一个外部中断引脚有效就会触发中断,软件通过 ECFG.LIE 配置的掩码完全失效。

影响:在当前仿真测试环境中,外部中断输入 intrpt 接为全 0,因此此 bug 不会触发。但在真实硬件场景下(FPGA 上板或流片),外部设备的中断将无法被屏蔽,且中断处理程序无法从 ESTAT 读取中断源编号。

修复

1
2
3
4
5
6
// core_top.v: 将外部中断连接到 CSR 的 hw_int_in
assign hw_int_in = intrpt;

// interrupt.v: 移除外部中断旁路,统一走 ESTAT 掩码
wire [12:0] int_masked = csr_estat_is & csr_ecfg_lie;
wire has_int = csr_crmd_ie && (|int_masked);

2.4 异常冲刷下 MEM 阶段 Store 写入窗口

定位core_top.v:1622-1626(流水线冲刷与 allow_in 控制)

根因:本设计的异常处理采用 WB 阶段统一检测策略。当异常在 WB 阶段被判定后,trap_flush 信号冲刷 IF/ID/EXE1/EXE2/MEM 五级流水线。但流水线的冲刷是通过 allow_in 信号阻止新指令进入各级——对于已经在 MEM 阶段执行的指令,如果它是一条 store 且已经在上一周期向 D-Cache 发出了写请求(data_sram_req && data_sram_addr_ok 已握手),则该写操作可能已经提交到 D-Cache 状态机中,冲刷信号无法撤回已发出的 AXI 写事务。

异常发生时的流水线状态:

1
2
3
4
5
IF    ID    EXE1   EXE2   MEM         WB
[新] [新] [新] [新] [store?] [异常指令]

如果此处有 store 且已发出 data_sram_req,
trap_flush 无法撤回已提交的写请求

影响:在以下场景中可能触发问题——一条 store 指令在 MEM 阶段已向 D-Cache 发出写请求,而同周期 WB 阶段检测到异常(如前一条指令触发 ALE 或 INE)。异常冲刷清除了 MEM 阶段的有效标记,但 D-Cache 状态机可能已经接受了写请求并将在后续周期完成写入。这会导致一条本应被冲刷的 store 指令完成了内存写入。

功能测试中未暴露此问题的原因:异常触发时,前面的指令通常不是 store 指令,或者 store 的 D-Cache 请求和异常检测不在同一周期。但在密集的异常处理场景(如连续的页错误处理)中,此窗口可能被命中。

建议

  • 在 MEM 阶段增加 trap_flush 对 data_sram_req 的门控:当 trap_flush 有效时,强制 data_sram_req = 0
  • 或在 D-Cache 状态机中增加 flush 输入,检测到 flush 时取消未完成的写操作

2.5 BADV 寄存器在 TLB 异常下的覆盖丢失

定位csr.v:443-456

1
2
3
4
5
6
7
8
9
10
11
// csr.v:443-456 — BADV 的多个写入源
if(rst)
csr_badv_vaddr <= 32'b0;
else if(wb_ex && wb_ex_addr_err)
csr_badv_vaddr <= (wb_ecode==`ECODE_ADE && wb_esubcode==`ESUBCODE_ADEF) ? wb_pc : wb_ex_ale_addr;
else if(csr_we && csr_num == `CSR_BADV)
csr_badv_vaddr <= /* 掩码写入(参见 2.2 的 bug) */;
else if(wb_ex && (wb_inst_tlb_ex != 2'b0))
csr_badv_vaddr <= wb_pc;
else if(wb_ex && wb_data_tlb_ex != 3'b0)
csr_badv_vaddr <= wb_data_addr;

根因:BADV 寄存器有 4 个写入源,按照 if-else 优先级链排列。问题在于 csr_we(软件 CSR 写)的优先级高于 TLB 异常写入(wb_inst_tlb_exwb_data_tlb_ex)。如果同一周期内既有软件 CSR 写 BADV 操作,又有 TLB 异常需要写入 BADV,则 TLB 异常的 BADV 值会被软件写的值覆盖。

影响:在实际运行中,此冲突场景不太可能发生——TLB 异常是硬件自动触发的,而 CSR 写是软件主动执行的。只有在异常处理程序内部错误地执行了写 BADV 操作时才会触发。但按照 LoongArch 架构规范,异常时的硬件 BADV 写入应当具有最高优先级。

建议:调整优先级,将异常相关的 BADV 写入放在软件 CSR 写之前:

1
2
3
4
5
6
7
8
9
10
if(rst)
csr_badv_vaddr <= 32'b0;
else if(wb_ex && wb_ex_addr_err)
csr_badv_vaddr <= ...;
else if(wb_ex && (wb_inst_tlb_ex != 2'b0))
csr_badv_vaddr <= wb_pc;
else if(wb_ex && wb_data_tlb_ex != 3'b0)
csr_badv_vaddr <= wb_data_addr;
else if(csr_we && csr_num == `CSR_BADV)
csr_badv_vaddr <= /* 正确的掩码写入 */;

Result(结果)

问题汇总

# 类型 问题 定位 严重性
1.1 时序 ID 级转发 MUX → ALU 关键路径 core_top.v:567-578
1.2 时序 32×32 单周期乘法器 alu.v:18-19
1.3 时序 TLB 16 项全相联并行查找 tlb.v:105-318
1.4 时序 if_ready_go 超长组合链 core_top.v:1580-1587
1.5 时序 冒险检测逻辑四重复制 core_top.v:1580-1618
1.6 时序 I-Cache 命中检测与状态机 icache_two_way.v:58-61
2.1 漏洞 CSR SAVE2/SAVE3 写入地址错误 csr.v:416-419
2.2 漏洞 BADV 寄存器掩码错误 csr.v:447
2.3 漏洞 外部中断绕过 ECFG.LIE 掩码 interrupt.v:24, core_top.v:1690
2.4 漏洞 异常冲刷下 MEM Store 写入窗口 core_top.v:1622-1626
2.5 漏洞 BADV 寄存器 TLB 异常覆盖丢失 csr.v:443-456

测试通过情况

  • 功能测试:58 个测试点全部通过(82 个用例),覆盖全部 LA32 指令、异常处理、TLB 操作、Cache 维护操作和原子指令
  • 性能测试:20 个 benchmark 程序的 difftest 一致性验证通过,包括 CoreMark、Dhrystone、SHA、快速排序等
  • 验证环境:Verilator + NEMU 在线 difftest

时序问题与功能正确性的关系

本文识别的 6 个时序问题在 Verilator 仿真环境下不会影响功能正确性——仿真器不检查时序约束,组合逻辑路径的延迟不影响逻辑结果。这些问题只有在 FPGA 综合或流片时才会暴露为时序违例。其中 1.2(单周期乘法器)和 1.1(转发 MUX 关键路径)是最可能在综合阶段成为瓶颈的两个问题。

4 个架构漏洞中,2.1(SAVE2/SAVE3)和 2.2(BADV 掩码)是明确的代码 bug,在特定使用场景下会导致功能错误;2.3(中断掩码旁路)在当前仿真环境(外部中断恒为 0)下不会触发,但在真实硬件场景下是严重缺陷;2.4 和 2.5 是边界条件下的问题,需要构造特定测试场景才能复现。