第 50 环 · 会话槽位 ISA 化(一条 npu.sess 按会话号开关场)
第 49 环给会话记忆区加了槽位目录,会话号终于能各自住一槽。可「换一场会话」在软件里仍是一串手工流程:open(查/分槽)→ 读基址 → 写 ARC_BASE → 发恢复 → 轮询 → 跑完再发归档 → 读摘要/字数 → commit → 清会话态。一次「切人」要核写六七个寄存器、跑两轮轮询,中间还得自己记住「我现在在哪个会话里」。
手机上的对话窗口切换是最频繁的动作。这一环把整场「开关场」收进一条指令 npu.sess:
引擎自己记住「现在在哪一场」(cur_sid/cur_slot/cur_base/cur_valid);enter 另一场时自动先把当前这场收好——软件不用再提醒它「先存再取」。
新增 rtl/sess_switch.sv(会话开关场引擎),把两个已经验证过的子引擎串起来——槽位目录(第 49 环)与归档引擎(第 48 环):
一条 npu.sess 的片内时序:
leave : 摆 cmd/base/sid → 发归档 save → 等 busy 落
→ 取 arc_digest/words → 摆 commit → 发目录 start → 等 busy 落
enter : 发目录 open → 等 busy 落 → 取 slot/base
→ 摆 ARC_BASE/sid → 发归档 load → 等 busy 落 → 进这场
switch : 先 leave(当前),再 enter(目标)
ISA 契约:核升 v15,custom-1 加 funct3=100:npu.sess rs1, rs2——rs1 = 会话号,rs2[1:0] = enter/leave/switch。EX 拍两个操作数直接送进开关场引擎,核一个配置寄存器都不写。
min_bus 新页 0xD00(bit11+bit10+bit8):
0xD00 SX_CMD 0xD04 SX_SID 0xD08 SX_GO 0xD0C SX_RST
0xD10 SCREEN26
读回:0xD20 BUSY 0xD24 DONE 0xD28 OK 0xD2C SLOT
0xD30 BASE 0xD34 CUR 0xD38 WORDS 0xD3C DIGEST
0xD40 MISMATCH 0xD44 ENTER 0xD48 LEAVE 0xD4C CORE
0xD50 MEMOK 0xD54 STATUS 0xD58 ERR 0xD5C ST
npu.rd 新选源 21~29:
21=槽号 22=基址 23=状态{mem_ok,ok,err,cur_valid}
24=当前会话号 25=本次字数 26=本次摘要 27=逐条比对不一致数
28=累计 leave 29=累计 enter
软硬协同的三个接口约定:① 目录只给基址,归档只认基址——两个引擎之间不传内容;② commit 的摘要/字数直接取归档引擎刚算出来的 digest/words,核不搬运;③ 没有旧上下文的会话(空槽/首次开机)不是错误:enter 照常成功,mem_ok=0 表示「这场还没存过东西」,上层当新会话继续。
协同仿真 tb_min_soc50 在既有全部观察点之后跑完整的「开关场」流程:
| 步骤 | 做什么 | 结果 |
|---|---|---|
| M-B | MMIO 三条 sw 进 B 场(第 49 环存过) | cur=0x52 / 槽1 / 基址0x40 / SX_CORE=3 |
| RST | 清计数 | core=0 / enter=0 / leave=0,会话状态保留 |
| E0 | 一条 npu.sess 进新会话 C(0x61) | 自动 leave 了 B(leave=1);新槽 0 / 基址 0 / mem_ok=0 |
| C | 跑一场对话(prompt=0)→ 一条 leave C | 归档 12 字 → commit → digestC |
| E2 | 一条 enter 新会话 D(0x62) | 槽 2 / 基址 0x80 / mem_ok=0 |
| D | 另一场对话(prompt=2)→ 一条 leave D | digestD ≠ digestC |
| E4 | 一条 enter C | mem_ok=1 / mismatch=0 / 摘要 == digestC |
| E5 | 一条 enter D → 跑整网 | 摘要 == digestD → Y_D |
| E6 | 一条 enter C → 跑整网 | Y_C ≠ Y_D(恢复出的上下文真的影响输出) |
| 收尾 | 核写入口次数 | SX_CORE=0 / enter=5 / leave=5 |
单元仿真 tb_sess_switch 用真的 sess_slots + kv_archive + 一个两层 K/V 环模型对表:一条 enter 取回 12 字且环内容逐条等于参考;换场自动 leave 前一场;leave 后目录不作废;再 enter 上下文原样回来;重复 enter 同一场是 no-op;新会话空槽也能进;switch 一条命令换人;不在场里时 leave 明确 err=1。
主通路未动:SCREEN=0xF905FD04、SCREEN2=0x02040404、SCREEN23~25 与第 25~49 环逐位一致。
其一(4bit 装不下 17 个状态)。开关场有 18 个状态,状态寄存器写成 [3:0],4'd16 被截成 0、4'd17 截成 1,S_LOADD/S_FIN 直接和 S_IDLE/S_ARC 撞在一起(Verilator 报 CASEOVERLAP)。扩到 5bit。教训:状态数超过 15 就必须先看位宽——这类截断不会报错,只会让 FSM 走错门。
其二(测试台连线名差了三个字母)。DUT 端口接的是 sxs_ss_start,测试台 mux 里写的是 sx_ss_start。Verilog 默认允许隐式网线,于是悄悄生成一根 1bit 的空线,start 恒 0——引擎收到一条「没有 start 的指令」,测试台却在等 busy。教训:开 default_nettype none,或至少把「没驱动的线」当错误查。
其三(不拉 busy 的成功也是一次成功)。重复 enter 同一场是 no-op:done 置起、busy 从没拉高。测试台按「先等 busy 起、再等 busy 落」轮询,直接挂死。软件侧同理——不能假设每个命令都会忙,要看 done/状态位。
其四(状态 nibble 的位序算错)。状态是 {mem_ok, ok, err, cur_valid}:新会话是 0b0101=5、恢复会话是 0b1101=0xD。第一版按 0b0011/0x0B 断言,程序在自检里自曝 0xDEAD0084/0xDEAD0088。教训:打包位序要写在纸上再算十六进制。
其五(leave 成功却忘了置 ok)。leave 分支只置了 err,成功路径没人把 ok 拉高——状态读回 ok=0,上层会以为退出失败。补上成功分支。
其六(空会话的 load 失败不是失败)。新槽的表头是全 0,归档 load 会返回 ok=0/err=1。如果把「恢复到空记忆」当错误,第一次开机就崩。enter 只看目录是否给出槽位;mem_ok 单独报告「这场有没有旧上下文」。
到这一环,Salt OS 的「同一个对话窗口」不仅能分区记忆,还能一句话换人:一条 npu.sess 把「问目录、取基址、存取上下文、记住现在在哪一场」全收进硬件;换场时自动把上一场收好,核一个入口寄存器都不用写(SX_CORE=0)。
对一台手机的意义:用户点开另一个会话,不该是一串系统调用和轮询——它就该是一个动作。会话切换变成一条指令之后,OS 的主循环可以短到「读一句话 → npu.chat → 显示」,中间不再有会话管理的胶水代码。再往上,是开机自动接着上次那场(npu.boot)、跨设备同步、断电持久化。
现在「进哪一场」已经是一条指令,可开机时进哪一场还要 OS 自己记。下一环让目录记住最后一场,一条 npu.boot 在开机时把上次的会话接回来——手机开机只有一个 AI,而它记得你们上次说到哪儿。