第 36 环 · 行块矩阵扫 ISA 化(一条指令扫完整块矩阵)
还是同一个圆心:一台开机只有一个 AI 的手机。第 31~35 环把「一块大矩阵怎么扫」全做进了硬件——块号可编程、块号自增、起始块号可编程、段表连扫、段表流式取,五层能力都在 row_blk 引擎里。
可核那边一点没省:扫一次矩阵,核还得一条条 sw 写 RB_X / RB_TBLM / RB_TBLS / RB_TBLN / RB_TBLP 五个寄存器,再 sw 一次 RB_GO,然后自己 lw 轮询 RB_BUSY,扫完还要逐 lane lw 读回 RB_ACC。硬件越做越强,软件驱动却越长——这正是第 27 环给整网解掉的病,在矩阵扫这条路上又长回来了。
本环把「扫完整块矩阵」也收进 ISA:核只说「激活在哪、段表在哪、扫几段」,剩下硬件自己做完;忙闲由 npu.wait 一起等,结果从 npu.rd 直接回来。
· 两条新指令(custom-0):funct3=011 npu.rbmat rs1, rs2(rs1=激活向量、rs2=段表描述符,段表走流式取)、funct3=100 npu.rbmat.s rs1, rs2(同上,但段表走阻塞读,留作对照);
· 描述符就在操作数里:rs2 = {段数[15:8], 段表基址[5:0]}——段表住哪、扫几段,一条指令说完;
· 一条指令内含三件事:清累加器(一次新的矩阵乘)+ 打开「段表连扫 + 块号自增」+ 选段表取法(流式/阻塞);
· 结果线收进 npu.rd:选源从 funct7[1:0] 放宽到 funct7[2:0],新增 4=行块累加器 lane0、5=按表扫过的段数、6=扫过的块数;
· 忙闲一起等:min_bus 把 rb_busy 并进核看到的 npu_busy——npu.wait 这条给整网写的冻结握手,原样覆盖矩阵扫(核不用再写轮询循环);
· 只扩不推翻:0x378~0x3FC 那批 MMIO 地址一个字没改,老路径(核写五寄存器 + RB_GO)与指令路径逐位一致;row_blk 引擎本身、SRAM 布局(层权重 0~31、矩阵行块 32~39、段表 40~42)全不动。
改 rtl/riscv_core.sv(升 v8:新增 npu_rbmat/npu_rb_x/npu_rb_desc/npu_rb_pf 四根输出 + 三根读回线,npu.rd 选源放宽到 3bit),改 rtl/min_bus.sv(指令脉冲直连 row_blk 的配置寄存器与 go/clr,新增 0x63C SCREEN11 与第十一观察点 screen11_out),新增 tb/tb_isa_rbmat.sv(ISA 单元仿真,程序由 circle36/gen_isa36.py 生成)、circle36/gen_boot36.py(最小 OS 收尾⑮)、tb/tb_min_soc36.sv(协同仿真)。
// rtl/riscv_core.sv(节选):矩阵扫指令——操作数直接当启动脉冲送进引擎
// funct3=011 npu.rbmat rs1=激活向量, rs2=段表描述符(流式取)
// funct3=100 npu.rbmat.s 同上,但段表走阻塞读(对照)
localparam [2:0] NPU_RBMAT = 3'b011;
localparam [2:0] NPU_RBMATS = 3'b100;
assign npu_rbmat = ex_npu_rbmat; // 单拍启动
assign npu_rb_x = ex_opA; // rs1:激活向量
assign npu_rb_desc = ex_opB; // rs2:{段数[15:8], 段表基址[5:0]}
assign npu_rb_pf = ex_npu_rbpf; // 流式取 / 阻塞读
// npu.rd 选源放宽到 funct7[2:0]:4=累加器 lane0, 5=段数, 6=块数
wire [31:0] npu_rd_data = (mem_npu_sel == 3'd0) ? npu_y :
(mem_npu_sel == 3'd1) ? npu_idx :
(mem_npu_sel == 3'd2) ? npu_slot :
(mem_npu_sel == 3'd3) ? npu_kcnt :
(mem_npu_sel == 3'd4) ? npu_rb_acc :
(mem_npu_sel == 3'd5) ? npu_rb_tblcnt : npu_rb_blkcnt;
// rtl/min_bus.sv(节选):一条指令 = 五寄存器配置 + 清累加器 + 一条 start
if (core_npu_rbmat) begin
rb_x <= core_npu_rb_x;
rb_tbls_r <= core_npu_rb_desc[5:0]; // 段表基址
rb_tbln_r <= core_npu_rb_desc[15:8]; // 段数
rb_auto_r <= 1'b1; // 段内块号硬件自增
rb_tblm_r <= 1'b1; // 段表连扫
rb_tblp_r <= core_npu_rb_pf; // 流式取 / 阻塞读
rb_clr_pulse <= 1'b1; // 清累加器
rb_go_pulse <= 1'b1; // 就这一条 start
end
测试矩阵 W = [1,2,-1,0 / 0,1,1,-2 / -1,0,2,1 / 2,-1,0,1]、x = [3,-2,5,-4],手算 y = W·x = [-6,11,3,4]。段表 3 条(起 0 长 2 / 起 2 长 1 / 起 3 长 1),先把所有 MMIO 开关写 0(引擎的配置只剩指令这一条来路),最小 OS 在芯片上真跑:
| 动作 | 结果 | 说明 |
|---|---|---|
一条 npu.rbmat(段数 3) | acc = [-6, 11, 3, 4] | = W·x,结果只可能来自指令通路(MMIO 开关全关) |
npu.wait 之后读 RB_BUSY | 0 | 核没写过一个轮询循环:忙闲被同一条指令等掉了 |
npu.rd sel=4 / MMIO RB_ACC0 | 0xFA / 0xFA | 两条读回通路逐位一致 |
npu.rd sel=5 / sel=6 | 3 / 4 | 段数、块数直接从结果线读回(核没写这两个寄存器) |
| 描述符里写段数 = 2 | lane2 = -3、lane3 = 8 | 整扫是 3 / 4 —— 第三条表没走,段数真是操作数 |
npu.rbmat.s(段表阻塞读) | lane0 = -6 | 与流式取逐位一致(取表方式由指令选) |
| 改写表第 0 条(起 1 长 1)再跑 | lane0 = -5 | 指令路径读的也是内存里的表(≠ 整扫的 -6) |
| 老路径(MMIO 五寄存器 + RB_GO) | acc = [-6, 11, 3, 4] | 与指令路径逐位一致——递增不推翻 |
单元仿真 tb_isa_rbmat 只验这道契约本身(不跑生成、不碰主路):矩阵权重与段表写进 SRAM,MMIO 开关全写 0,一条 npu.rbmat → 累加器 [-6,11,3,4];npu.rd 的三路读回与 MMIO 读回逐位比对;描述符里写段数 = 2 → 第三条表没走(lane2=-3 / lane3=8,整扫是 3 / 4);npu.rbmat.s(阻塞读表)逐位一致;全通 SCREEN = 0x0000A5A5。协同仿真 tb_min_soc36:SCREEN = 0xF905FD04、SCREEN2~10 与第 25~35 环逐位一致(本环只把「谁摆配置」从一串 sw 换成一条指令,主通路一个字没动);采样序列 [0,2,0,0,0,3]、第 6 步吐停止符 3 而停;全量回归 57 项全绿。
调试中钉住一处非显而易见的时序缝:npu.rbmat 在 EX 拍只把 rb_go_pulse 置起来,row_blk 要下一拍才看到 start、再下一拍 busy 才起来,而 npu.wait 恰好就是「下一拍」进 EX 的——不把 go_pulse 也算进忙闲,wait 会在引擎忙起来的前一拍把忙闲看成空、直接放行。改成 npu_busy = net_busy | rb_busy | rb_go_pulse 后忙闲首尾不断。
从「硬件会扫」走到「一条指令就能让硬件扫」:五个配置寄存器 + 一次启动 + 一个轮询循环 + 四次逐 lane 读回,收成 npu.rbmat + npu.wait + npu.rd 三条指令——操作数就在 rs1/rs2 里,忙闲一起等,结果线直接回来。核从「扫描的驱动者」也退成「扫描的声明者」:硬件的能力终于配上了同等简洁的软件接口。
这正是软硬协同的意思:前五环把能力往硬件里长,这一环把它们收进接口契约里——否则硬件越强、驱动越长,OS 迟早被自己的驱动拖住。真芯片上「一层权重分几十上百段搬进片上」这件事,现在在软件看来只是一条指令。
现在一条指令能扫完一块矩阵。可真网里的权重是一串矩阵(每层好几张,一层接一层)。下一环把「段表连扫」再升一层:矩阵描述符链——一条指令按描述符链走完一趟,把整网每层的权重矩阵都扫进来(每段一张矩阵、扫完即用),核连「扫几段」都不必逐层说——矩阵描述符链 · 一条指令扫完整网层权重。