#!/usr/bin/env python3 """把中断测试程序适配到这块 CPU 上板运行(只改 ROM 内容,不改 CPU 语义)。 背景 ---- programs/risc-v-intrupt-ccab.hex 是课程的中断/异常测试程序(270 字)。 直接放进板上 ROM 会有两个问题: 1) ISR 里的 ecall 会停机 三个中断服务程序里各有一条 "addi a7,zero,10" + "ecall"。这颗 CPU 的约定是 a7==34 的 ecall 是历史"显示"调用(无操作),其余 a7 值一律让流水线停机并等待 go_i。于是 ISR 跑到这里就卡住:后面的中断服务显示循环永远不执行,主程序的 跑马灯也停在原地。 -> 把 "addi a7,zero,10" + "ecall" 换成两条 NOP。 2) 显示刷新太快,数码管上看不出跑马灯 原来的显示是 "lui a7,1" + "sw a0,0(a7)"(写 MMIO 的 LED 寄存器)。主循环里 两次显示只隔几拍(50 MHz 下约 0.1 us),而数码管是按位动态扫描的,根本来不及 分辨,只会看到一片亮。 -> 把每条显示换成 "jal ra,DISP_DLY" + "nop",由追加在 ROM 末尾的 DISP_DLY 完成"写 LED 寄存器 + 约 10 ms 延时"。 (注意:早先把旧的 "addi a7,zero,34 + ecall" 换成 MMIO 存储时,rs1 域算错, 把 sw 写成了 sb —— 只更新 LED 寄存器的低字节,高位一直是旧值,显示自然是 错的。DISP_DLY 内部统一用正确的 sw 写整个字,这个脚本会顺带纠正。) 3) **修正 ISR2 / ISR3 的现场恢复顺序** 原始 hex 是手工改过的,两个 ISR 的出栈顺序被写乱(ISR1 是对的): * ISR3:把 s1 从 s3 的槽位弹出,于是 s1 被写成 s3 保存的值 0xFFFFF111; ISR3 返回后,正在跑 BLT 测试的 ISR2 的循环计数 s1 就变成了 -3823, 循环要跑 3800 多次(约 170 万周期)才退出 —— 看起来就是“二级中断返回 之后卡住”; * ISR2:s0/s2/a0/a7/s1/t3/t1 全部错位,返回后调用者的这些寄存器被破坏。 -> 按 asm 清单里正确的 LIFO 顺序,只改 12 条 `lw` 的目标寄存器(字数不变、 地址不动),ISR3 改 5 条、ISR2 改 7 条。 5) **修正 ISR3 的 SB 测试用到的未保存寄存器** ISR3 的现场只保存了 s1/a7/a0/s0/s3/s4/s5/s6/uepc,但它的 SB 测试却用 t1(地址)、t3(计数)、s2(增量)—— 这三个都没保存。后果: * 若 IRQ3 抢占正在跑 DIVU 测试的 ISR1(ISR1 用 t3 做递减计数),ISR1 的 计数会被 ISR3 改成 0;ISR1 恢复后 `addi t3,t3,-1` 得到 -1,`bne t3,zero` 永远成立 —— ISR1 的 DIVU 循环再也出不来,表现为“三级中断之后卡住”; * 主程序的跑马灯步长 t1 也会被改掉。 -> 改用 ISR3 自己保存的 s4/s5/s6:SB 测试结束后紧接着的 IntLoop3 (0x3B4 起)会重新给它们赋值,所以这样换是安全的;同样只改寄存器域, 字数与地址不变。 6) **ISR 不能破坏被打断代码的 ra** 三个 ISR 的现场里都保存了 s1/a7/a0/s0/s3/s4/s5/s6(外加各自的测试寄存器), 但**都没有保存 ra**,而它们要调用显示例程,`jal ra,DISP_DLY` 会把 ra 写成 ISR 内部的返回地址。后果:如果中断恰好落在被打断代码依赖 ra 的位置 —— 典型就是主程序正停在 DISP_DLY 末尾的 `jalr zero,0(ra)`(第 0x454 字) —— 中断返回后 `jalr` 会跳到 ra 里的地址,也就是 ISR 内部的某条指令上, 于是一路执行到 ISR 结尾的 `csrrci zero,0x4,1`,把 MIE 永久清零: “跑一阵子之后拨开关再也不响应,只能按复位”。窗口只有 1~2 拍,所以平时 很难遇到(仿真里把延时改到极小才会碰上)。 -> 给 ISR 另加一份显示例程 DISP_DLY_ISR(0x458),链接寄存器改用 **a0**, 显示值改走 **a7**:这两个寄存器三个 ISR 都保存/恢复,所以 * ISR 完全不碰 ra,被打断的代码停在 DISP_DLY 哪里都安全; * ISR 自己被内层中断打断时,内层 ISR 返回也会把 a0/a7 恢复成被打断时 的值,所以 DISP_DLY_ISR 自己的 `jalr zero,0(a0)` 同样安全。 主程序的 3 个显示点仍用原来的 DISP_DLY(链接在 ra,行为不变)。 每个 ISR 显示点:`jal ra,DISP_DLY` + `nop` 换成 `add a7,a0,zero` + `jal a0,DISP_DLY_ISR` —— 仍是 2 字换 2 字。 以上替换都是 **2 字换 2 字**,两个例程又追加在最后一条指令(uret)之后,所以 **所有指令地址都保持不变**,三个中断入口仍然是 0x0AC / 0x1E4 / 0x308。 DISP_DLY(8 字,追加在程序末尾) -------------------------------- sw ra, 256(sp) # 保存返回地址 lui a7, 1 # a7 = 0x1000 = LED_ADDR sw a0, 0(a7) # 把 a0 写进 LED 寄存器(数码管显示它的 8 位十六进制) lui t2, DELAY # 计数初值为负,向上数到 0(约 10 ms @50 MHz) 1: addi t2, t2, 1 blt t2, zero, 1b lw ra, 256(sp) jalr zero, ra, 0 为什么返回地址存在 sp+256 而不是程序栈里: 本程序的 ISR 是“先把 sp 加上去,再往里存”的向上增长栈,一层最多占 48 字节, 最多嵌套 4 层(IRQ_STACK_DEPTH=4)共 192 字节,所以 [sp, sp+192) 是中断随时 可能写到的区域。把返回地址放在 sp+256(越过了这个区域): * 从 ISR 里调用时,不会覆盖 ISR 存好的 s6 / uepc(早期用 sp-8 就是这么坏掉的: uepc 被写坏后 uret 会返回到 ISR 内部地址,最后执行到 csrrci 把 MIE 永久清零); * 中断(包括多层嵌套)随时打断延时也不会破坏它,因为嵌套 ISR 只会在 [sp, sp+192) 里压栈; * 每次调用各用自己 sp+256 的位置,所以嵌套调用也互不影响。 延时计数不用“递减到 0”而用“负数向上数到 0”:万一被嵌套调用改掉了 t2,循环也 只会在 t2 >= 0 时立即退出,不会像递减写法那样绕成 40 亿次而死等。 用法 ---- python tools/adapt_intrupt_ccab.py <输入.hex> <输出.vh> [输出_fast.hex] <输入.hex> programs/risc-v-intrupt-ccab.hex(未适配的原始镜像) <输出.vh> srcs/board/risc-v-intrupt-ccab.vh(上板综合用) [输出_fast.hex] 可选:延时改成约 0.6 us 的仿真专用版本。上板一次中断服务要跑 几十毫秒(几十万个时钟),仿真跑完整流程会很慢,因此仿真用这个 快版来验证控制流(入口 / 中断返回 / 不停机),延时量本身不影响 控制流。 """ import sys NOP = 0x00000013 LED_ADDR_HI = 1 # lui a7, 1 -> a7 = 0x1000(LED_ADDR) # 延时计数:计数初值为负,向上数到 0。0xFFFC0000 -> 0x40000 = 262144 次 # (每轮约 2 个时钟)= 52.4 万个时钟 ≈ 10 ms @50 MHz。 DELAY_NEG_HI = 0xFFFC0 DELAY_FAST_NEG = -16 # 仿真用:addi t2, zero, -16 ≈ 0.6 us # DISP_DLY 保存返回地址的偏移:必须大于 ISR 最大嵌套栈用量(4 层 x 48B = 192B) LINK_SLOT = 256 # 寄存器号 ZERO, RA, SP, GP, T2, A0, A7 = 0, 1, 2, 3, 7, 10, 17 def enc_i(imm, rs1, f3, rd, op=0x13): return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op def enc_s(imm, rs2, rs1, f3): imm &= 0xFFF return (((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | ((imm & 0x1F) << 7) | 0x23) def enc_b(imm, rs2, rs1, f3): imm &= 0x1FFF return ((((imm >> 12) & 1) << 31) | (((imm >> 5) & 0x3F) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (((imm >> 1) & 0xF) << 8) | (((imm >> 11) & 1) << 7) | 0x63) def enc_u(imm, rd): return ((imm & 0xFFFFF) << 12) | (rd << 7) | 0x37 def enc_j(imm, rd): imm &= 0x1FFFFF return ((((imm >> 20) & 1) << 31) | (((imm >> 1) & 0x3FF) << 21) | (((imm >> 11) & 1) << 20) | (((imm >> 12) & 0xFF) << 12) | (rd << 7) | 0x6F) HALT_LOAD = enc_i(10, ZERO, 0, A7) # addi a7, zero, 10 ECALL = 0x00000073 DISP_LUI = enc_u(LED_ADDR_HI, A7) # lui a7, 1 DISP_SW = enc_s(0, A0, A7, 2) # sw a0, 0(a7) <- 正确:字写 DISP_SB_BAD = enc_s(0, A0, A7, 0) # sb a0, 0(a7) <- 误写成字节写,一并修正 # 说明:这两种都当成"显示点"。DISP_DLY 内部一律用正确的 sw 写整个字,因此即使 # 输入镜像里是 sb(只写低字节,显示的高位会残留旧值),适配后也会恢复正常。 def disp_dly(delay_word): """DISP_DLY 的 8 条指令(delay_word 决定延时长短)。 返回地址存在 sp+LINK_SLOT,位于 ISR 可能压栈的区域之上,所以中断(含嵌套) 打断延时也不会破坏它 —— 见文件头的说明。 """ return [ enc_s(LINK_SLOT, RA, SP, 2), # sw ra, 256(sp) DISP_LUI, # lui a7, 1 DISP_SW, # sw a0, 0(a7) delay_word, # lui t2, 0xFFFC0 / addi t2, zero, -16 enc_i(1, T2, 0, T2), # addi t2, t2, 1 enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4 enc_i(LINK_SLOT, SP, 2, RA, 0x03), # lw ra, 256(sp) enc_i(0, RA, 0, ZERO, 0x67), # jalr zero, ra, 0 ] def disp_dly_isr(delay_word): """ISR 专用的显示/延时例程(8 条)—— 链接寄存器用 a0,不用 ra。 三个 ISR 的现场都保存了 a7 与 a0(s1/a7/a0/s0/s3/s4/s5/s6),所以把链接放 a0、把显示值放 a7 不会额外破坏任何东西;而 ra 完全不被触碰,被打断的代码 即使停在主 DISP_DLY 的 `jalr zero,0(ra)` 上也不会被破坏。 """ return [ enc_s(LINK_SLOT, A0, SP, 2), # sw a0, 256(sp) 保存链接寄存器 enc_u(LED_ADDR_HI, A0), # lui a0, 1 a0 = 0x1000 enc_s(0, A7, A0, 2), # sw a7, 0(a0) 显示 a7 delay_word, # lui t2, 0xFFFC0 / addi t2,zero,-16 enc_i(1, T2, 0, T2), # addi t2, t2, 1 enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4 enc_i(LINK_SLOT, SP, 2, A0, 0x03), # lw a0, 256(sp) enc_i(0, A0, 0, ZERO, 0x67), # jalr zero, a0, 0 ] # 三个中断入口的原指令(核对地址没有跑偏) ISR_ENTRY = {43: 0x00512023, 121: 0x00612023, 194: 0x00912023} # 三个 ISR 的代码从 ISR1 入口开始一直排到程序末尾(主程序在它之前) ISR1_WORD = min(ISR_ENTRY) # 需要修正目标寄存器的 `lw X,0(sp)`:addr -> (当前错误的 rd, 正确的 rd) # ISR3(入口 0x308)恢复顺序应为:uepc,s6,s5,s4,s3,s0,a0,a7,s1 # ISR2(入口 0x1E4)恢复顺序应为:uepc,s6,s5,s4,s3,s0,a0,a7,s2,s1,t3,t1 FIX_POPS = { 0x410: (9, 19), 0x418: (19, 8), 0x420: (8, 10), 0x428: (10, 17), 0x430: (17, 9), # ISR3 0x2D0: (18, 8), 0x2D8: (9, 10), 0x2E0: (28, 17), 0x2E8: (6, 18), 0x2F0: (8, 9), 0x2F8: (10, 28), 0x300: (17, 6), # ISR2 } # ISR3 的 SB 测试用了未保存的 t1/t3/s2 -> 改成 ISR3 自己保存的 s4/s5/s6 # addr -> (要改的域, 原寄存器号, 新寄存器号) T1, T3, S2, S4, S5, S6 = 6, 28, 18, 20, 21, 22 FIX_ISR3_SB = { 0x358: ('rd', T1, S4), 0x35C: ('rd', T3, S5), 0x364: ('rd', S2, S6), 0x368: ('rs1', T1, S4), 0x378: ('rs2', S2, S6), 0x37C: ('rd_rs1', T1, S4), 0x380: ('rd_rs1', T3, S5), 0x384: ('rs1', T3, S5), 0x388: ('rd', T3, S5), 0x38C: ('rd', T1, S4), 0x390: ('rs1', T1, S4), 0x3A0: ('rd_rs1', T1, S4), 0x3A4: ('rd_rs1', T3, S5), 0x3A8: ('rs1', T3, S5), } def read_hex(path): words = [] with open(path, encoding="ascii") as f: for line in f: line = line.split("//")[0].strip() if line: words.append(int(line, 16)) return words def patch(words): """返回 (适配后的字表, 适配后的快版字表, 统计信息)。""" halt_sites = [i for i in range(len(words) - 1) if words[i] == HALT_LOAD and words[i + 1] == ECALL] if len(halt_sites) != 3: raise SystemExit("预期 3 处 'addi a7,zero,10 + ecall'(ISR 停机点)," "实际找到 %d 处;输入文件不是预期的原始镜像?" % len(halt_sites)) disp_sites = [i for i in range(len(words) - 1) if words[i] == DISP_LUI and words[i + 1] in (DISP_SW, DISP_SB_BAD)] if len(disp_sites) != 11: raise SystemExit("预期 11 处 'lui a7,1 + sw/sb a0,0(a7)'(显示点)," "实际找到 %d 处;输入文件不是预期的原始镜像?" % len(disp_sites)) bad_store = [i for i in disp_sites if words[i + 1] == DISP_SB_BAD] for i in halt_sites: words[i] = NOP words[i + 1] = NOP # plain 变体:只去掉停机 ecall,显示保持原来的 "lui + sw" 两字写法 # (必须在把显示点改成 jal 之前取副本,否则会生成“跳到没有函数体的地址”的镜像) plain = list(words) # DISP_DLY 紧跟在原程序的最后一条指令之后(原程序长度不变) disp_pc = 4 * len(words) isr_pc = disp_pc + 32 # ISR 专用的 DISP_DLY_ISR 再占 8 字 isr_sites = [] for i in disp_sites: if i >= ISR1_WORD: # 三个 ISR 里的显示点:改用 a0 做链接寄存器、显示值走 a7 # 注意 jal 落在显示点的后一条(i+1)上,偏移要相对它自己算 isr_sites.append(i) words[i] = enc_i(0, A0, 0, A7) # add a7, a0, zero words[i + 1] = enc_j(isr_pc - 4 * (i + 1), A0) # jal a0, DISP_DLY_ISR else: # 主程序里的显示点:照旧用 ra 链接 words[i] = enc_j(disp_pc - 4 * i, RA) words[i + 1] = NOP if not isr_sites: raise SystemExit("没有找到 ISR 里的显示点;中断入口地址对不上?") # 自检:把刚写进去的 jal 偏移反解出来,确认真的跳到两个例程的入口 def jal_target(w, pc): imm = (((w >> 31) & 1) << 20) | (((w >> 21) & 0x3FF) << 1) \ | (((w >> 20) & 1) << 11) | (((w >> 12) & 0xFF) << 12) if imm & 0x100000: imm -= 0x200000 return pc + imm for i in disp_sites: if i >= ISR1_WORD: want, at = isr_pc, 4 * (i + 1) # ISR:jal 在显示点后一条 else: want, at = disp_pc, 4 * i # 主程序:jal 就在显示点上 got = jal_target(words[at // 4], at) if got != want: raise SystemExit("显示点 0x%03X 的跳转目标算错了:0x%03X != 0x%03X" % (4 * i, got, want)) for idx, want in ISR_ENTRY.items(): if words[idx] != want: raise SystemExit("中断入口 0x%03X 处的指令被改动了(%08X != %08X)" % (4 * idx, words[idx], want)) # --- 4) 修正 ISR2 / ISR3 的现场恢复顺序(只改 lw 的 rd 域) --- fixed_pops = 0 for pc, (bad_rd, rd) in FIX_POPS.items(): w = words[pc // 4] op, f3 = w & 0x7F, (w >> 12) & 7 rs1, imm = (w >> 15) & 0x1F, (w >> 20) & 0xFFF old_rd = (w >> 7) & 0x1F if not (op == 0x03 and f3 == 2 and rs1 == SP and imm == 0): raise SystemExit("0x%03X 不是预期的 `lw X,0(sp)`(%08X)" % (pc, w)) if old_rd == rd: continue # 已经是对的(重复运行时跳过) if old_rd != bad_rd: raise SystemExit("0x%03X 的目标寄存器不是预期的 %d(实际 %d)" % (pc, bad_rd, old_rd)) words[pc // 4] = (w & ~(0x1F << 7)) | (rd << 7) fixed_pops += 1 # --- 5) 把 ISR3 SB 测试里的 t1/t3/s2 换成 s4/s5/s6 --- def set_field(w, name, val): if name == 'rd': return (w & ~(0x1F << 7)) | (val << 7) if name == 'rs1': return (w & ~(0x1F << 15)) | (val << 15) if name == 'rs2': return (w & ~(0x1F << 20)) | (val << 20) if name == 'rd_rs1': return ((w & ~(0x1F << 7)) | (val << 7)) & ~(0x1F << 15) | (val << 15) raise SystemExit('未知域 ' + name) fixed_sb = 0 for pc, (field, old, new) in FIX_ISR3_SB.items(): w = words[pc // 4] idx = {'rd': (w >> 7) & 0x1F, 'rs1': (w >> 15) & 0x1F, 'rs2': (w >> 20) & 0x1F} if field == 'rd_rs1': cur = idx['rd'] if idx['rs1'] != cur: raise SystemExit('0x%03X 的 rd/rs1 不一致' % pc) else: cur = idx[field] if cur == new: continue # 已经改过了 if cur != old: raise SystemExit('0x%03X 的 %s 不是预期的 %d(实际 %d)' % (pc, field, old, cur)) words[pc // 4] = set_field(w, field, new) fixed_sb += 1 slow = disp_dly(enc_u(DELAY_NEG_HI, T2)) + disp_dly_isr(enc_u(DELAY_NEG_HI, T2)) fast = disp_dly(enc_i(DELAY_FAST_NEG, ZERO, 0, T2)) \ + disp_dly_isr(enc_i(DELAY_FAST_NEG, ZERO, 0, T2)) return (words + slow, words + fast, plain, (halt_sites, disp_sites, disp_pc, [i for i in disp_sites if i < ISR1_WORD], isr_sites, bad_store, fixed_pops, fixed_sb)) def write_hex(path, words): with open(path, "w", encoding="ascii", newline="\n") as f: for w in words: f.write("%08X\n" % w) def write_vh(path, words, src_name, total, note): with open(path, "w", encoding="utf-8", newline="\n") as f: f.write(" // 由 tools/adapt_intrupt_ccab.py 生成 —— 不要手改\n") f.write(" // source: %s(%d 字,其余地址填 NOP)\n" % (src_name, len(words))) f.write(" // %s\n" % note) for addr in range(total): w = words[addr] if addr < len(words) else NOP f.write(" rom[%d] = 32'h%08X;\n" % (addr, w)) def main(): if len(sys.argv) < 3: raise SystemExit(__doc__.strip().split("用法")[-1]) src, dst_vh = sys.argv[1], sys.argv[2] dst_fast = sys.argv[3] if len(sys.argv) > 3 else None dst_plain = sys.argv[4] if len(sys.argv) > 4 else None words = read_hex(src) slow, fast, plain, (halt_sites, disp_sites, disp_pc, main_sites, isr_sites, bad_store, fixed_pops, fixed_sb) = patch(words) total = 1024 write_vh(dst_vh, slow, src, total, "已适配:去掉 ISR 停机 ecall、显示改为 MMIO+DISP_DLY(约 10 ms)") if dst_fast: write_hex(dst_fast, fast) if dst_plain: write_hex(dst_plain, plain) print("适配完成:%s(原程序 %d 字 + DISP_DLY %d 字 = %d 字)" % (dst_vh, len(words), len(slow) - len(words), len(slow))) print(" 去掉的停机 ecall 位置:%s" % ", ".join("0x%03X" % (4 * i) for i in halt_sites)) print(" 换成显示调用的点:主程序 %s|ISR %s" % (", ".join("0x%03X" % (4 * i) for i in main_sites), ", ".join("0x%03X" % (4 * i) for i in isr_sites))) print(" DISP_DLY 本体:0x%03X - 0x%03X(主程序用,链接在 ra)" % (disp_pc, disp_pc + 28)) print(" DISP_DLY_ISR 本体:0x%03X - 0x%03X(ISR 用,链接在 a0,不碰 ra)" % (disp_pc + 32, disp_pc + 60)) print(" 修正的现场恢复顺序:%d 条" % fixed_pops) print(" ISR3 SB 测试改用的已保存寄存器:%d 条" % fixed_sb) if bad_store: print(" 顺带修正的字节写显示:%s" % ", ".join("0x%03X" % (4 * i) for i in bad_store)) print(" 中断入口(不变):0x0AC / 0x1E4 / 0x308") if dst_fast: print(" 仿真快版:%s(延时约 0.6 us)" % dst_fast) main()