前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >哪吒D1开发板RISC-V CLINT编程实践

哪吒D1开发板RISC-V CLINT编程实践

作者头像
bigmagic
发布2021-07-23 11:11:12
2.5K1
发布2021-07-23 11:11:12
举报
文章被收录于专栏:嵌入式iot嵌入式iot

哪吒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的中断、异常、陷阱的设计可以直接查看官方文档。

代码语言:javascript
复制
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

代码语言:javascript
复制
 .global table_val_set
table_val_set:
    la t0, _trap_handler
    csrw mtvec, t0
    jr ra

riscv支持向量中断和非向量中断两种编程模型,这里只演示用非向量中断,也就是中断发生后,所有的入口只有一个,不固定偏移。

_trap_handler函数中,需要做的事情其实就是三件:

保存现场,判断并执行中断处理函数,恢复现场。

代码语言:javascript
复制
 .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实际的判断,需要根据中断类型,从而去执行对应的中断逻辑。

这里有个非常关键的地方,就是中断产生后,现场的保护和恢复,以及什么时候开关中断的问题,这些细节可以优化,从而让程序状态调整到最佳。

代码语言:javascript
复制
/*
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新增了下面的指令。

代码语言:javascript
复制
csrrw ra, CSR_JALMNXTI, ra

该指令做了两件事

代码语言:javascript
复制
1.判断是否有挂起中断,如果有跳转到中断向量入口,开始执行具体中断,没有则向下执行
2.如果有挂起中断,跳转到中断处理程序后,再次回到该指令,看是否还有中断处理

整个过程的流程稍微复杂一些,但是这样却增加了实时性,中断处理效率更高效。当然,CLINT没有这种特性。所以使用起来比较简单一些。

3.2 设置RISCV核的中断使能

既然需要理解D1的CLINT的使用,那么就必须使能全局中断。

全局中断的使能在mstatus寄存器中。

代码语言:javascript
复制
  .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寄存器。

代码语言:javascript
复制
#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来获取当前机器的时基。

代码语言:javascript
复制
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寄存器

让该计数值大于当前时间,即可产生定时器中断。

代码语言:javascript
复制
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表示中断,否则为陷阱或者异常。

实现代码可以参考

代码语言:javascript
复制
https://github.com/bigmagic123/d1-nezha-baremeta

对D1裸机部分进行细致深入的分析。

5.小结

riscv的CLINT使用起来相比arm来说容易一些,掌握其编程模型,也非常容易实现自己的中断处理程序。但是不支持中断嵌套,更多的中断特性还需要实际的产品中使用才能真正的理解设计。

对于CLINT,主要理解有两个中断,软件中断,定时器中断,这样两者几乎不依赖任何的驱动组件IP,所以一般做标准的RISCV核,都会集成这样的设计,对于编写操作系统,做生态软件的开发需要深刻理解。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-07-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式IoT 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 哪吒D1开发板RISC-V CLINT编程实践
    • 1.本文概述
      • 2.D1上的软件中断与定时器中断分析
        • 3.CLINT的编程模型与实际演示
          • 3.1 设置中断向量入口地址
          • 3.2 设置RISCV核的中断使能
          • 3.3 设置CLINT的寄存器的值
        • 4.测试结果
          • 5.小结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档