Files
risc-v-cpu/pipelined-cpu/programs/risc-v-intrupt-ccab.asm
Anccend f7844b7b1c feat(board): 中断综合测试程序上板 + 可变分频/刷新率;README 大幅扩写
板级顶层(srcs/board/cpu21_board_top.v)
- ROM 增加 RISC_V_INTRUPT_CCAB_ROM 分支,中断入口 0x0AC/0x1E4/0x308;
  与 SAOLEI_ROM / BOARD_ROM_FAST / 默认跑马灯构成 ifdef 链
- 新增参数 CLK_DIV_INTRUPT:跑中断测试程序时用更慢的 CPU 时钟(默认 32
  = 3.125 MHz),让数码管上每个数值停留约 160 ms 便于观察
- 数码管扫描分频随 CPU 分频缩放(SEG_REFRESH_DIV = 100000/CPU_DIV),
  保证每位数码管仍是 125 Hz(不闪);数码管改显示 MMIO 的 led_data;
  LED[2:0] 改为锁存式中断指示(请求上升沿点亮、对应 ISR 执行 uret 后熄灭)
- 复位同步器/led_we 指示等注释同步更新;srcs/rtl/cpu21_seg7.v 分频计数
  加宽到 20 位以容纳随主频变化的 REFRESH_DIV

工具与程序
- tools/adapt_intrupt_ccab.py:把课程中断测试镜像适配成上板 ROM。六处适配
  (ISR 停机 ecall、显示改 MMIO+DISP_DLY、ISR2/ISR3 出栈顺序、ISR3 SB 测试
  用未保存寄存器、ISR 破坏 ra 的 DISP_DLY_ISR),全部 2 字换 2 字,
  不移动任何指令地址;带断言与 jal 目标自检
- programs/risc-v-intrupt-ccab.asm/.hex:课程中断综合测试程序(含六处适配说明)
- create_project.tcl:新增 use_intrupt_ccab / use_saolei 开关,自动把对应
  ROM 头文件按 Verilog Header 加入 sources_1 并定义宏
- tools/rars_f0c874c.jar:课程 RARS 汇编器移到 tools/

文档(README.md)
- 新增"各模块的实现方式":按流水线主体 / 外设 / 板级逐模块讲实现要点
- 新增"内置程序与宏的用法":ROM 宏表(含各自的中断入口)、GUI/Tcl/命令行
  三种定义方式、中断程序的适配流程与仿真验证方法
- 新增"分频与刷新率一览":CLK_DIV / CLK_DIV_INTRUPT / SEG_REFRESH_DIV /
  led_we 时长 / DISP_DLY 延时常量分别在哪里改、怎么换算
- 修正引脚表(LED[2:0] 中断指示、led_we 0.34 s、CPU_DIV)与 ROM/RAM 说明
  (ROM 8192 字 = 32 KB),补充中断程序上板现象与 ROM 头文件清单

验证:不带宏 / 带 RISC_V_INTRUPT_CCAB_ROM 两种配置 xvlog 编译均无错误;
三路嵌套与 IRQ3 抢占 ISR1 的仿真通过(level 逐级升降、uepc 正确恢复、
halted=0);tools/check_rtl_text.ps1 体检 OK。
2026-09-19 15:44:47 +08:00

499 lines
15 KiB
NASM
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 注意:上板/仿真真正使用的镜像是 programs/risc-v-intrupt-ccab.hex(270 字),
# 本文件是课程原始清单。由 tools/adapt_intrupt_ccab.py 生成 ROM 时会做六处适配:
# 1) 三个 ISR 里的 "addi a7,zero,10 + ecall" 会让这颗 CPU 停机(a7 != 34 的
# ecall 是"停机等 go_i"的约定),中断服务程序跑到那里就卡住、后面的中断显示
# 循环永远不执行 -> ROM 里换成两条空操作;
# 2) 原来的 "addi a7,zero,34 + ecall" 显示改成写 MMIO 的 LED 寄存器(0x1000),
# 并且每个显示点都改为调用 DISP_DLY 加约 10 ms 延时 —— 否则 50 MHz 下刷新
# 太快,数码管的动态扫描根本分辨不出来,只会看到一片亮;
# 3) DISP_DLY 追加在程序末尾(0x438),因此所有指令地址、特别是三个中断入口
# 0x0AC / 0x1E4 / 0x308 都保持不变;
# 4) **hex 里 ISR2 / ISR3 的现场恢复(出栈)顺序被写乱了**(本清单是正确的,
# ISR1 的 hex 也是正确的)。ISR3 把 s1 从 s3 的槽位弹出,于是 s1 被写成
# s3 保存的 0xFFFFF111;ISR3 返回后,正在跑 BLT 测试的 ISR2 的循环计数 s1
# 就变成 -3823,循环要跑约 170 万周期才退出,表现就是"二级/三级中断返回
# 之后卡住";ISR2 的 s0/s2/a0/a7/s1/t3/t1 也全部错位。适配脚本按本清单的
# 正确 LIFO 顺序改回 12 条 lw 的目标寄存器(字数不变、地址不动)。
# 5) **ISR3 的 SB 测试用了它自己没有保存的寄存器**(t1 当地址、t3 当计数、
# s2 当增量)。ISR3 的现场只保存了 s1/a7/a0/s0/s3/s4/s5/s6/uepc,所以:
# 若 IRQ3 直接抢占正在跑 DIVU 测试的 ISR1(ISR1 用 t3 递减计数),ISR1 的
# 计数会被改掉;ISR1 恢复后 `addi t3,t3,-1` 得到负数,`bne t3,zero` 永远
# 成立,DIVU 循环再也回不来 —— 表现就是“拨了 SW0 再拨 SW2 之后卡住”;
# 主程序的跑马灯步长 t1 也会被改掉。适配脚本把这 14 处寄存器域改成 ISR3
# 自己保存的 s4/s5/s6(SB 测试结束后紧接着的 IntLoop3 会重新给它们赋值,
# 所以这样换是安全的),字数与地址不变。
# 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) 那一条上),中断返回后就会跳进 ISR
# 代码里,一路执行到 ISR 结尾的 csrrci zero,0x4,1,把 MIE 永久清零:表现
# 就是“跑一阵子之后拨开关再也不响应,只能复位”。窗口只有 1~2 拍,平时很
# 难遇到(仿真里把延时改到极小才会碰上)。适配脚本给 ISR 另加一份显示例程
# DISP_DLY_ISR(0x458),链接寄存器改用 a0、显示值走 a7(这两个寄存器三个
# ISR 都保存/恢复),于是 ISR 完全不碰 ra;ISR 的显示点由 "jal ra,DISP_DLY +
# nop" 换成 "add a7,a0,zero + jal a0,DISP_DLY_ISR",仍是 2 字换 2 字,
# 字数与地址不变。
# =============================================================================
.text
addi sp, zero, 2000 #内存堆栈初始化
#############################################################
#走马灯测?,测试addi,andi,slli,srli,srai,or,ori,nor,ecall LED按走马灯方式来回显示数据
#############################################################
.text
START:
addi s0,zero,1
slli s3, s0, 31 # s3=0x80000000
srai s3, s3, 31 # s3=0xFFFFFFFF
add s0,zero,zero # s0=0
addi s2,zero,12
addi s6,zero,8 #走马灯计数
zmd_loop:
addi s0, s0, 1 #计算下一个走马灯的数
andi s0, s0, 15
#######################################
addi t0,zero,8
addi t1,zero,1
left:
slli s3, s3, 4 #走马灯左移
or s3, s3, s0
add a0,zero,s3 # display s3
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
sub t0,t0,t1
bne t0,zero,left
#######################################
addi s0, s0, 1 #计算下一个走马灯的数
addi t6,zero,15
and s0, s0, t6
slli s0, s0, 28
addi t0,zero,8
addi t1,zero,1
zmd_right:
srli s3, s3, 4 #走马灯右移
or s3, s3, s0
add a0,zero,s3 # display s3
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
sub t0,t0,t1
bne t0,zero,zmd_right
srli s0, s0, 28
#######################################
sub s6,s6,t1
beq s6,zero, exit
j zmd_loop
exit:
add t0,zero,zero
xori t0,t0,-1 #test nor ori
slli t0,t0,16
ori t0,t0,-1
add a0,zero,t0 # display t0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
j START # loop forever
#InteruptProgram1:
#############################################################################################
# exceptoin 1
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram1:
sw t0, 0(sp) #保护t0现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# DIVU测试
addi t0,zero,2 # /2
addi s1,zero, 0x11
slli s1,s1,8
addi s1,s1,0x11
slli s1,s1,16
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t3,zero,28 #ѭ������
divu_branch:
divu s1,s1,t0 #����ָ��
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t3,t3, -1
bne t3,zero,divu_branch #ѭ��
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,1 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop1:
add s0,zero,s6
IntLeftShift1:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift1
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop1
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t0, 0(sp) #恢复t0现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram2:
#############################################################################################
# exceptoin 2
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram2:
sw t1, 0(sp) #保护t1现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw s2, 0(sp) #保护s2现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# BLT测试
addi s1,zero,-15 #初始值
blt_branch:
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi s1,s1,1
blt s1,zero,blt_branch #当前指令
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,2 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop2:
add s0,zero,s6
IntLeftShift2:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift2
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop2
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp) #恢复s0现场
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s2, 0(sp) #恢复s2现场
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t1, 0(sp) #恢复t1现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram3:
#############################################################################################
# exceptoin 3
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram3:
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# SB测试
# 注:ROM 镜像里下面这段的 t1/t3/s2 已改成 ISR3 自己保存的 s4/s5/s6
# (t1 当地址 -> s4,t3 当计数 -> s5,s2 当增量 -> s6),避免抢占 ISR1
# 时破坏 ISR1 的 DIVU 循环计数,详见文件头部第 5 项。
addi t1,zero,0 #init_addr
addi t3,zero,32 #counter
#sb写入 01,02,03,04
addi s1,zero, 0x00 #
addi s2,zero, 0x01 #
sb_store:
sb s1,(t1)
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
add s1,s1,s2 #data +1
addi t1,t1,1 # addr ++
addi t3,t3,-1 #counter
bne t3,zero,sb_store
addi t3,zero,8
addi t1,zero,0 # addr
sb_branch:
lw s1,(t1) #读出数据
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t1,t1,4
addi t3,t3, -1
bne t3,zero,sb_branch
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,3 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop3:
add s0,zero,s6
IntLeftShift3:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift3
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop3
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
################ 中断返回
uret #同步开中断,uepc-->pc
# -----------------------------------------------------------------------------
# DISP_DLY —— 显示 + 延时(对应 ROM 末尾 0x438 起的 8 条指令;11 个显示点都用
# "jal ra, DISP_DLY" 调用它)
#
# sw ra, 256(sp) # 保存返回地址(故意存在 sp+256,见下面的说明)
# lui a7, 1 # a7 = 0x1000 = LED_ADDR
# sw a0, 0(a7) # 把 a0 写进 LED 寄存器(数码管显示它的 8 位十六进制)
# lui t2, 0xFFFC0 # 计数初值为负(-262144),向上数到 0 ≈ 10 ms @50 MHz
# 1:addi t2, t2, 1
# blt t2, zero, 1b
# lw ra, 256(sp)
# jalr zero, ra, 0
#
# 为什么返回地址存在 sp+256,而不是按常规压在 sp-8 的栈帧里:
# 本程序的 ISR 用的是"先把 sp 加上去、再往里存"的向上增长栈,一层最多 48 字节,
# 最多嵌套 4 层(IRQ_STACK_DEPTH=4)共 192 字节 —— 也就是说 [sp, sp+192) 是中断
# 随时可能写到的区域。而显示调用既可能被中断打断,也可能从 ISR 内部发出,所以:
# * 若压在 sp-8(常规做法),从 ISR 里调用时会正好盖掉 ISR 存好的 s6 / uepc;
# uepc 一坏,uret 就返回到 ISR 内部地址,最后执行到 ISR 尾部的 csrrci 把 MIE
# 永久清零,之后再也不响应中断(实测就是这个现象);
# * 放在 sp+256 越过了中断可能压栈的 192 字节区域,嵌套中断打断延时也不会破坏
# 它;每次调用各用自己的 sp+256,嵌套调用互不影响。
#
# 延时计数用"负数向上数到 0"而不是"递减到 0":万一 t2 被嵌套调用改掉,循环只会
# 在 t2 >= 0 时立即退出,不会像递减写法那样回绕成 40 亿次而死等。
# -----------------------------------------------------------------------------
DISP_DLY:
sw ra, 256(sp)
lui a7, 1
sw a0, 0(a7)
lui t2, 0xFFFC0
DISP_DLY_WAIT:
addi t2, t2, 1
blt t2, zero, DISP_DLY_WAIT
lw ra, 256(sp)
jalr zero, ra, 0