【转载】哪吒D1开发板RISC-V CLINT编程实践
-
哪吒D1开发板RISC-V CLINT编程实践
原稿来自公众号:嵌入式IoT
原创:bigmagic
链接:哪吒D1开发板RISC-V CLINT编程实践-
1.本文概述
-
2.D1上的软件中断与定时器中断分析
-
3.CLINT的编程模型与实际演示
-3.1 设置中断向量入口地址
-3.2 设置RISCV核的中断使能
-3.3 设置CLINT的寄存器的值
-
4.测试结果
-
5.小结
1.本文概述
当前riscv的中断控制器部分比较简单,不像arm那样复杂,设计的简单分析起来就比较容易理解清楚。相比于ARM的GIC,RISC-V这一套CLINT与PLINT简直太容易理解了。或许是因为ARM迭代的时间很长,积累了很多设计上的经验,RISCV还需要经过实际的市场的考验,才能真正的看到中断控制这一块的设计到底是否简洁并且设计合理。
在RISCV的设计上,其规范《riscv-spec-20191213.pdf》是这样描述中断、异常、陷阱的。
中断:
由RISC-V HART运行的程序,意外被打断,转向执行意外事件的一种机制。例如串口中断,定时器中断等等。
异常:
异常就是指RISC-V HART在正常运行的过程中,突然发生了意外的情况。例如访问了没有分配的内存,或者访问未定义的指令等等。
陷阱:
陷阱就是主动的被唤起去做一件意料之中的事情,比如系统调用,软件中断等等。
上述对RISCV的中断、异常、陷阱的描述都不够完全的覆盖,只是说了大概的意思,深入理解RISCV的中断、异常、陷阱的设计可以直接查看官方文档。
https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf
根据RISC-V架构的定义,当前主流RISC-V芯片设计上的中断控制器。
sifive的芯片基本上采用clint+plic。
gd32vf103(eclic)
d1(clint+plic)
本文分析的d1上的clint编程模型,将能够很好的理解riscv的中断编程的设计。
图片上概述了相对标准的RISCV中断控制部分的机制,对于D1单核的情况来看,CLINT只负责处理软件中断和时钟中断,因为这两个中断是RISC-V架构中定义的。经过CLINT不需要进行任何的仲裁,直接将中断(Software与Timer)送入D1的RISC-V核中。
由于Software与Timer中断不需要任何外设控制,可以直接控制其产生对应的中断。
2.D1上的软件中断与定时器中断分析
CLINT本质上也是一个核内外设,由于D1采用的是平头哥的玄铁C906,所以可以从官方网站下载C906手册。
CLINT的全称(Core-Local Interruptor)核间局部中断。
主要是定义了M-Mode(机器模式)的软件中断和计时器比较中断,S-Mode(超级用户模式)下的软件中断和计时器比较中断。
基本上和官方定义的一样,但是玄铁c906并未实现mtime寄存器,这一点是需要注意的。mtimer寄存器的作用是读取当前的cycle。
软件中断
作为CLINT来说,软件中断只需要向CLINT的MSIP0或者SSIP0寄存器的最高位写1即可,处理完中断后,将其置为0,这样就能够清除掉软件中断的标志位。
定时器中断
作为riscv内核特有的中断,其用法就是往MTIMECMP或者STIMECMP中写特定的值,当mtime达到该值时产生中断,此时继续填写特定的tick就可以继续产生下个中断,反复如此,便可产生周期性的tick中断。
3.CLINT的编程模型与实际演示
原理层面上理解不难,那么实际操作时又该是怎样的编程模型呢?下面详细分析一下CLINT的编程模型。
3.1 设置中断向量入口地址
要想让其产生中断,必须告诉处理器中断的处理的入口地址,这里通过写入
mtvec
,当程序运行在机器模式下时,其程序的入口地址是_trap_handler
。.global table_val_set table_val_set: la t0, _trap_handler csrw mtvec, t0 jr ra
riscv支持向量中断和非向量中断两种编程模型,这里只演示用非向量中断,也就是中断发生后,所有的入口只有一个,不固定偏移。
在
_trap_handler
函数中,需要做的事情其实就是三件:保存现场,判断并执行中断处理函数,恢复现场。
.globl _trap_handler _trap_handler: SAVE_CONTEXT csrr a0,mcause csrr a1,mepc call irq_handle_trap RESTORE_CONTEXT mret
其中判断中断的入口可以通过
mcause
寄存器来判断具体中断发生的原因。对于D1 rv64架构,寄存器的位宽是64位,所以最高位是1表示中断,为0表示异常。
对于irq_handle_trap实际的判断,需要根据中断类型,从而去执行对应的中断逻辑。
这里有个非常关键的地方,就是中断产生后,现场的保护和恢复,以及什么时候开关中断的问题,这些细节可以优化,从而让程序状态调整到最佳。
/* Register ABI Name Description Saver x0 zero Hard-wired zero -- x1 ra Return address Caller x2 sp Stack pointer Caller x3 gp Global pointer -- x4 tp Thread pointer -- x5-7 t0-2 Temporaries Caller x8 s0/fp Saved register/frame pointer Caller x9 s1 Save register Caller x10-11 a0-1 Function arguments/return values Caller x12-17 a2-7 Function arguments Caller x18-27 s2-11 Saved registers Caller x28-31 t3-6 Temporaries Caller ------------------------------------------------------------------------------- f0-7 ft0-7 FP temporaries Caller f8-9 fs0-1 FP save registers Caller f10-11 fa0-1 FP arguments/return values Caller f12-17 fa2-7 FP arguments Caller f18-27 fs2-11 FP saved registers Caller f28-31 ft8-11 FP temporaries Caller */
在这些寄存器中,有些是可以不用压入栈中的,具体哪些,我以后会慢慢分析,只有对riscv寄存器的特性有了足够清晰的认识,设计出最佳压栈入栈的设计,将程序调整到最优。因为在高性能,高实时性的场合下,多一个寄存器的压入都是一笔性能的损失。
那么到底什么时候开关中断,这个问题是非常重要的。默认情况下,中断产生后进入中断处理的第一条指令都是关闭中断的,所以这里可能会有两种模型。
按照正常的处理流程,第一种效率高一些,缩短关闭全局中断的时间,可以很大程度上提高系统的实时性,但是其实第二种才是正确的结果。第一种情况可能会在寄存器出栈的过程中再次产生中断,由于寄存器数据还没有恢复完成,此时又压入寄存器,这样是没有意义的操作,就算处理得当效率反而会下降。
第二种是比较简单和安全的,但是存在时间长度过长的问题。
由于当前的riscv中断编程模型较为简单,不存在咬尾中断,中断嵌套等模型。在目前的riscv中断设计中,其中只见到芯来的ECLIC有咬尾中断的处理过程。下面简述一下原理。
其实就是中断产生后,并不会直接跳转到具体的中断入口函数,由统一的入口进行分发处理。
eclic新增了下面的指令。
csrrw ra, CSR_JALMNXTI, ra
该指令做了两件事
1.判断是否有挂起中断,如果有跳转到中断向量入口,开始执行具体中断,没有则向下执行 2.如果有挂起中断,跳转到中断处理程序后,再次回到该指令,看是否还有中断处理
整个过程的流程稍微复杂一些,但是这样却增加了实时性,中断处理效率更高效。当然,CLINT没有这种特性。所以使用起来比较简单一些。
3.2 设置RISCV核的中断使能
既然需要理解D1的CLINT的使用,那么就必须使能全局中断。
全局中断的使能在
mstatus
寄存器中。.global all_interrupt_disable all_interrupt_disable: csrrci a0, mstatus, 8 ret
其中
mstatus
叫做机器模式处理器状态寄存器,其中记录了机器模式下的状态和控制信息。包括中断有效位,异常特权模式位等等。而第三位则是机器模式下的中断开启或者使能位。
当然,全局中断使能,还不能结束,还要使能机器模式中断使能控制寄存器
MIE
。该寄存器定义了当前处理器需要开启哪些中断类型,C906支持超级用户模式\机器模式下的三种中断。
MEIE:外部中断
MTIE:定时器中断
MSIE:软件中断
比如这里使能定时器中断,此时就需要开启
MSTATUS
的全局中断与中断使能寄存器MIE
寄存器进行开启。3.3 设置CLINT的寄存器的值
进行到这里,基本上riscv中断产生的条件就有了,就差最后一步,配置CLINT寄存器。
#define D1_MSIP0 0x14000000 #define D1_MTIMECMPL0 0x14004000 #define D1_MTIMECMPH0 0x14004004 #define D1_SSIP0 0x1400C000 #define D1_STIMECMPL0 0x1400D000 #define D1_STIMECMPH0 0x1400D004
在D1上,CLINT的寄存器地址如上所示,比如开启定时器,那么只需要保证两点。C906自定义了一个机器模式扩展状态寄存器
MXSTATUS
。保证第17位是1表示可以开启CLINT功能。
另外,还需要将MTIMECMPL0的值设置的大于当前的时间基点。
问题是标准的CLINT上有MTIME寄存器,而C906上可以通过time的csr来获取当前机器的时基。
uint64_t counter(void) { uint64_t cnt; __asm__ __volatile__("csrr %0, time\n" : "=r"(cnt) :: "memory"); return cnt; }
设置定时器中断,主要分为三部分:
1.开启全局中断
通过设置
mstatus
寄存器。2.开启中断使能控制器
通过设置
mie
寄存器开启定时器中断。3.设置clint的MTIMECMP寄存器
让该计数值大于当前时间,即可产生定时器中断。
csr_clear(mie, MIP_MTIP | MIP_MSIP); write32(CLINT + 0x4000, counter() + 1000000); write32(CLINT + 0x4004, 0); csr_set(mie, MIP_MTIP | MIP_MSIP);
这样就可正常产生定时器中断了。
在中断处理程序中不断的添加
MTIMECMP
值即可。4.测试结果
通过对结果的分析,可以看到正常的产生了定时器中断。
mcause表示的是中断的原因,最高位是1表示中断,否则为陷阱或者异常。
实现代码可以参考
https://github.com/bigmagic123/d1-nezha-baremeta
对D1裸机部分进行细致深入的分析。
5.小结
riscv的CLINT使用起来相比arm来说容易一些,掌握其编程模型,也非常容易实现自己的中断处理程序。但是不支持中断嵌套,更多的中断特性还需要实际的产品中使用才能真正的理解设计。
对于CLINT,主要理解有两个中断,软件中断,定时器中断,这样两者几乎不依赖任何的驱动组件IP,所以一般做标准的RISCV核,都会集成这样的设计,对于编写操作系统,做生态软件的开发需要深刻理解。
-
-
@hazelijy 请问D1支持向量中断吗?
Copyright © 2024 深圳全志在线有限公司 粤ICP备2021084185号 粤公网安备44030502007680号