🎯 圆心 · 目标(每环都从这里出发)
造一台 Salt OS 手机:基于 DeepSeek 的 AI 操作系统,iPhone 4S 外形(含底部圆形 Home 键),开机只有一个 AI(无桌面、无图标),软硬协同设计(OS + 芯片 + 主板一起做),为别人搭台、让别人在平台上赚钱。

🧭 完整框架(同心圆)

①文字闭环 → ②技术深入 → ③挂SRAM → ④权重INT4 → ⑤激活INT4 → ⑥NPU.STORE → ⑦KV Cache → ⑧注意力QK^T → ⑨Softmax → ⑩加权V → ⑪前馈FFN → ⑫残差·LayerNorm → ⑬多层堆叠 → ⑭QKV投影 → ⑮K/V分离 → ⑯自回归解码 → ⑰连续生成 → ⑱采样 → ⑲KV环形缓冲 → ⑳滑窗注意力 → ㉑多头注意力 → ㉒输出投影 W_o → ㉓多头块入残差流 → ㉔多头块进解码循环 → ㉕整层一块 → ㉖整网一块 → ㉗ISA契约 → ㉘…

本环:把「启动整张网」做成一条指令

① 目标是什么

还是同一个圆心:一台开机只有一个 AI 的手机。上一环「整张网」已经收进一个引擎了,可软硬之间那道契约还很啰嗦:核要说「跑整网」这件事,得先 sw NET_X 把残差流放进寄存器、sw NET_LAYERS 声明层数、sw NET_GO 发令,然后 lw NET_BUSY 一圈圈轮询等它跑完,最后逐 lane lw NET_Y0..3 再 slli/or 拼回一个 32bit——一轮解码里核围着寄存器转了十几条访存指令。
这一环把这道契约写进指令集:整网的「启动 / 等待 / 读回」各变成一条 NPU 指令。芯片软硬协同的真正分界线就在这里——硬件长出引擎,ISA 长出接口。

② 怎么实现(不脱圆心)

继续「端上紧凑、全定点、每步硬件自洽、只扩不推翻」的主线:
· 核升 v7,加三条 custom-0 指令(RISC-V 留给自定义加速器的 opcode 0001011):npu.net rs1, rs2(rs1=输入残差流、rs2=层数 → 一条指令启动整网)、npu.wait(整网没跑完就冻结整条流水线)、npu.rd rd, sel(rd 收结果,sel 选源:0=整网输出 4 lane packed、1=层号、2=末层两头落点、3=每层 K 条数);
· 操作数直接进硬件——npu.net 在 EX 拍把两个操作数当场当「启动脉冲 + 输入」送进整网引擎(不经过任何寄存器),这就是指令驱动外设;
· 读回并进 load 通路——npu.rd 的结果与 lw 走同一条回写通路(含 MEM→EX 转发),所以指令读回和访存读回天然同源;
· 只扩不推翻——前 26 圈的寄存器路径一个字没删(0x280~0x2BC 照旧可用、老 OS 照跑),新指令只是多了一条更短的入口;数据面(整网引擎)一个门都没动。

③ 本环做了什么(真代码)

rtl/riscv_core.sv 升 v7(三条自定义指令 + 解码 + 阻塞 + 读回通路),rtl/min_bus.sv 把整网引擎的结果线直接接进核,rtl/layer_net.sv 在 start 那一拍锁存操作数,OS 主路改成三条指令:

// 核 v7:custom-0 三条指令
localparam OP_CUSTOM0 = 7'b0001011;
NPU_NET  = 3'b000   // npu.net  rs1=输入残差流, rs2=层数
NPU_WAIT = 3'b001   // npu.wait 阻塞到整网跑完
NPU_RD   = 3'b010   // npu.rd   rd=结果, funct7=选源

assign npu_start  = ex_npu_net;      // EX 拍单脉冲
assign npu_x      = ex_opA;          // 操作数直接当输入
assign npu_layers = ex_opB;

wire ex_npu_stall = ex_npu_wait && npu_busy;   // npu.wait:整条流水线冻结
wire ex_slow_read = ex_mem_read || ex_npu_read; // npu.rd 与 load 同类:紧跟的消费者等一拍

# OS 主路:一轮解码 = 三条 NPU 指令(前 26 圈这里是十几条访存)
.word 0x016A000B   # npu.net  s4, s6    ← 一条指令启动整网
.word 0x0000100B   # npu.wait           ← 一条指令等它跑完
.word 0x0000240B   # npu.rd   s0, sel=0 ← 一条指令读回整网输出(4 lane packed)

读回源:npu.rd sel=0 → 整网输出(4×INT8 packed,正好是一个寄存器)、sel=1 → 片内层号、sel=2 → 末层两头落点、sel=3 → 每层 K 条数;另加 0x2C0 NET_PROBE 只读探针,锁住「启动那一拍引擎到底收到了什么」(x 低 16bit / layers / 是否来过)。

④ 怎么算成(闭环验证)
ISA 契约 · RESULT=PASS
一轮解码 = npu.net + npu.wait + npu.rd 三条指令 · 屏幕 0xF905FD04(与第 25/26 环逐位一致)· SCREEN2 = 0x02040404 · SCREEN3 = 0x04000204 · 单元仿真 + 协同仿真 + 全量回归全部通过

这一环要证明「软硬之间那道契约真的变成指令了」,现场五条硬证据:
· 证据一:一条指令启动——一轮解码从「sw NET_X → sw NET_LAYERS → sw NET_GO → lw NET_BUSY 轮询 → 逐 lane lw NET_Y0..3 再拼」的十几条访存,缩成三条指令;
· 证据二:操作数真进了硬件——npu.net 的 rs1/rs2 就是引擎的输入与层数:layers=1 时片内层号只停在 1、layers=2 时跑到 2;只换 npu.net 的第二个操作数,整网的行为就跟着变;
· 证据三:npu.wait 真的阻塞——整网没跑完,核就冻结在 EX 拍(不阻塞的话第一条 npu.rd 会读到上一轮结果,SCREEN 立刻就不对了);
· 证据四:指令路径 == 寄存器路径——npu.rd 读回的整网输出与逐 lane lw NET_Y0..3 拼回的逐位相同,层号 / 每层 K 条数 / 两头落点也都与寄存器路径一致;
· 证据五:计算一个字没变——最终残差流 0xF905FD04 与第 25、26 环完全相同,SCREEN2 0x02040404 同值;新的 SCREEN3 0x04000204 = 层 0 环条数 4 | 末层两头落点 0 | 片内层号 2 | 输出 lane0 4,四项全部由 npu.rd 读回算出;
· 单元仿真新增 tb_isa_npu:六项逐条对表(层号=2 / 每层环条数 / 指令读回==逐 lane 拼回 / 指令层号==寄存器层号 / 指令落点==寄存器落点 / layers=1 时层号停在 1)。

⑤ 顺手记下的两课(这两颗钉子都不是「玄学」)

第一课:先用后声的隐式线。新接的 32bit 操作数总线,代码里在声明之前就被用了;Verilog 见到「先用后声」会悄悄造一根 1bit 的隐式线——编译不报错、仿真不报错,只是那根 32bit 的总线上什么都没接上。表现是「指令启动了整网,但引擎收到的层数是 0」。教训:新加的总线,声明必须在任何使用之前,哪怕编译器不吭声。
第二课:紧跟在读指令后面的消费者。npu.rd 的结果要到 MEM 拍才拿得到,可它长得像一条 R 型指令,紧跟在后面的 mv 会被 EX→EX 转发塞进ALU 算出来的垃圾值(addi 的 0+0),于是残差流被静默清零、采样序列全乱。load 早就有「紧跟消费者等一拍」的保护,npu.rd 是新造的「读」,必须与 load 同类对待(消费者等一拍、分支紧跟也冻结重取)。
还有第三条小坑:引擎必须在 start 那一拍锁存操作数——指令的操作数只在总线上存在一拍,引擎却要到十几拍之后才用它(li_nxt >= layers)。不锁存的话,那一拍过后总线换回别的值,整网就按旧的层数跑完了。

⑥ 回到目标

本环走完一整圈:目标 → 一台只有一个对话窗口的手机,它的 OS 得像用 CPU 一样用 NPU → 硬件长出引擎之后,ISA 长出接口:核里多三条自定义指令,整网的启动 / 等待 / 读回各一条 → 数据面一个字没动,最终残差流与上一环逐位一致 → 单元仿真 + 协同仿真 + 回归全绿 → 回到目标。「用芯片」这件事从「敲寄存器」升级成「执行指令」——这才是一台手机里 OS 调 AI 该有的样子。下一环把重量级的东西交给芯片自己搬:层权重从内存流式取。