Navigation

    全志在线开发者论坛

    • Register
    • Login
    • Search
    • Categories
    • Tags
    • 在线文档
    • 社区主页

    【转载】哪吒D1开发板RISC-V CLINT编程实践

    D1系列-RISC-V
    2
    2
    479
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Hazelijy
      Hazelijy LV 4 last edited by

      哪吒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的中断编程的设计。

      de156e50-1893-4172-ace2-80d394a97a95-image.png

      图片上概述了相对标准的RISCV中断控制部分的机制,对于D1单核的情况来看,CLINT只负责处理软件中断和时钟中断,因为这两个中断是RISC-V架构中定义的。经过CLINT不需要进行任何的仲裁,直接将中断(Software与Timer)送入D1的RISC-V核中。

      e8e08af3-c7a3-4c28-a1c3-6b2b327bf5b3-image.png

      由于Software与Timer中断不需要任何外设控制,可以直接控制其产生对应的中断。

      2.D1上的软件中断与定时器中断分析

      CLINT本质上也是一个核内外设,由于D1采用的是平头哥的玄铁C906,所以可以从官方网站下载C906手册。

      CLINT的全称(Core-Local Interruptor)核间局部中断。

      c59a09ef-6f26-46d7-8fae-14f908b0c73e-image.png

      主要是定义了M-Mode(机器模式)的软件中断和计时器比较中断,S-Mode(超级用户模式)下的软件中断和计时器比较中断。

      b172b86c-485a-40c6-b82a-806c3c1a070b-image.png

      基本上和官方定义的一样,但是玄铁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寄存器来判断具体中断发生的原因。

      e5369318-483f-42c4-8299-59187cc217e9-image.png

      对于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寄存器的特性有了足够清晰的认识,设计出最佳压栈入栈的设计,将程序调整到最优。因为在高性能,高实时性的场合下,多一个寄存器的压入都是一笔性能的损失。

      那么到底什么时候开关中断,这个问题是非常重要的。默认情况下,中断产生后进入中断处理的第一条指令都是关闭中断的,所以这里可能会有两种模型。

      217e9926-07cb-4f35-890b-5287554829c5-image.png

      按照正常的处理流程,第一种效率高一些,缩短关闭全局中断的时间,可以很大程度上提高系统的实时性,但是其实第二种才是正确的结果。第一种情况可能会在寄存器出栈的过程中再次产生中断,由于寄存器数据还没有恢复完成,此时又压入寄存器,这样是没有意义的操作,就算处理得当效率反而会下降。

      第二种是比较简单和安全的,但是存在时间长度过长的问题。

      由于当前的riscv中断编程模型较为简单,不存在咬尾中断,中断嵌套等模型。在目前的riscv中断设计中,其中只见到芯来的ECLIC有咬尾中断的处理过程。下面简述一下原理。

      其实就是中断产生后,并不会直接跳转到具体的中断入口函数,由统一的入口进行分发处理。

      eclic新增了下面的指令。

      csrrw ra, CSR_JALMNXTI, ra
      

      该指令做了两件事

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

      a3c5afde-6426-483d-b6c8-31c8410650a3-image.png

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

      3.2 设置RISCV核的中断使能

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

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

      .global all_interrupt_disable
      all_interrupt_disable:
        csrrci a0, mstatus, 8
        ret
      

      其中 mstatus 叫做机器模式处理器状态寄存器,其中记录了机器模式下的状态和控制信息。包括中断有效位,异常特权模式位等等。

      d86f7e9d-6030-4671-869c-a2f4aaef2850-image.png

      而第三位则是机器模式下的中断开启或者使能位。

      当然,全局中断使能,还不能结束,还要使能机器模式中断使能控制寄存器MIE。

      b4bf6f42-3e10-4370-b503-5df7700e6d1d-image.png

      该寄存器定义了当前处理器需要开启哪些中断类型,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。

      ed407d0b-9b94-497e-8496-a8493e3e619d-image.png

      保证第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.测试结果

      通过对结果的分析,可以看到正常的产生了定时器中断。

      0d27ece2-8e7e-4e56-8c25-5712ccc30c51-image.png

      mcause表示的是中断的原因,最高位是1表示中断,否则为陷阱或者异常。

      实现代码可以参考

      https://github.com/bigmagic123/d1-nezha-baremeta
      

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

      5.小结

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

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

      Marleo 1 Reply Last reply Reply Quote Share 0
      • Marleo
        Marleo LV 4 @Hazelijy last edited by

        @hazelijy 请问D1支持向量中断吗?

        1 Reply Last reply Reply Quote Share 0
        • 1 / 1
        • First post
          Last post

        Copyright © 2022 深圳全志在线有限公司 粤ICP备2021084185号 粤公网安备44030502007680号

        行为准则 | 用户协议 | 隐私权政策