cruelfox 发表于 2021-7-25 22:42

【Perf-V评测】蜂鸟E203的异常(中断)处理

<p>  sirv-e-sdk 里的 entry.S 是和异常(中断)有关的。现在要使用中断功能,必须了解如何编写中断处理程序。entry.S 主要内容如下:</p>

<pre>
<code>#include "encoding.h"
#include "sifive/bits.h"

.section      .text.entry       
.align 2
.global trap_entry
trap_entry:
addi sp, sp, -32*REGBYTES

STORE x1, 1*REGBYTES(sp)
STORE x2, 2*REGBYTES(sp)
STORE x3, 3*REGBYTES(sp)
... 此处省略若干行 ...
STORE x30, 30*REGBYTES(sp)
STORE x31, 31*REGBYTES(sp)

csrr a0, mcause
csrr a1, mepc
mv a2, sp
call handle_trap
csrw mepc, a0

# Remain in M-mode after mret
li t0, MSTATUS_MPP
csrs mstatus, t0

LOAD x1, 1*REGBYTES(sp)
LOAD x2, 2*REGBYTES(sp)
LOAD x3, 3*REGBYTES(sp)
... 此处省略若干行 ...
LOAD x30, 30*REGBYTES(sp)
LOAD x31, 31*REGBYTES(sp)

addi sp, sp, 32*REGBYTES
mret

.weak handle_trap
handle_trap:
1:
j 1b</code></pre>

<p>  看起来很陌生,和我熟悉的ARMv7-M (常见的Cortex-m3,m4都是属于这个构架的CPU)完全不是一个风格。原因在于,RISC-V架构的异常处理与ARMv7-M 有很大的不同,需要查资料学习了。仅从软件方面看:</p>

<p><strong><span style="background-color:#f1c40f;">(1)</span></strong> ARMv7-M有一个中断向量表,从表的第二项开始每一项对应一个异常处理程序的入口地址;系统寄存器VTOR指定中断向量表的位置。RISC-V每个模式有一个CSR寄存器存放异常处理程序的入口地址;通过这个寄存器的MODE域设置可以为异步中断在入口地址上加偏移量。E203只实现了机器模式,只有一个 <strong>mtvec</strong>,且不支持MODE域&mdash;&mdash;所有的异常共用一个入口地址。<br />
<strong><span style="background-color:#f1c40f;">(2)</span></strong> ARMv7-M处理每个异常时(Reset除外),自动保存8个寄存器到堆栈中(当前SP寄存器指定栈顶),然后PC设置为中断向量表中的异常处理程序入口开始执行,SP设置为MSP; 中断返回时,保存在堆栈中的8个寄存器被自动装入。RISC-V异常处理时,不会在堆栈中保存任何寄存器。但是返回地址(被中断时候的PC值)必须要保存,RISC-V规定了 <strong>mepc</strong> 等CSR用于保存各模式下的返回地址。<br />
<strong><span style="background-color:#f1c40f;">(3)</span></strong> ARMv7-M处理异常时,LR寄存器有特殊意义,因此BX LR指令可以实现从异常返回;C语言中写异常处理程序等同与一般函数写法。RISC-V需要使用专用指令从异常返回,例如<strong>mret</strong>.<br />
<strong><span style="background-color:#f1c40f;">(4)</span></strong> RISC-V因为每个模式下所有异常共用一个入口(称为Trap Vector),软件要读取CSR寄存器如<strong>mcause</strong>来判断异常类型。</p>

<p></p>

<p>  上面的 entry.S 代码用 STORE/LOAD 保存和恢复了31个通用寄存器,就是因为RISC-V异常处理不会自动保存现场。全部寄存器保存是简单粗暴的做法,耗费机器周期比较多了。然后,代码读取了 <strong>mcause</strong>, <strong>mepc</strong> 两个CSR,分别作为参数,调用了 handle_trap 这个函数(别处定义),再将返回值写入 <strong>mepc</strong>. 也就是说 entry.S 只管了现场保存,如何处理异常是转交给 handle_trap() 函数实现的。如果别处没有定义 handle_trap, 这里默认的就是一个死循环。</p>

<p>  我把 entry.S 内容加到以前的代码工程中去,并在 main() 中先设置好 mtvec CSR:增加一行<br />
<span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>asm volatile(&quot;csrw mtvec, %0\n&quot;::&quot;r&quot;(trap_entry));</strong></span></span><br />
  或者用 encodings.h 定义过的宏来写:<br />
<span style="color:#8e44ad;"><strong><span style="font-family:Courier;">write_csr(mtvec, trap_entry);</span></strong></span><br />
  这样发生异常时 trap_entry 的代码就会执行了。</p>

<p>  先造成一个异常&mdash;&mdash;不支持的指令,看看效果如何。在程序中用汇编插入指令编码:<br />
<span style="color:#8e44ad;"><strong><span style="font-family:Courier;">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(c&gt;2)<br />
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{<br />
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; asm volatile(&quot;.word 0x00421E53\n&quot;);&nbsp;&nbsp; &nbsp;// FADD.S instr<br />
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}</span></strong></span><br />
  0x00421E53 是我在手册中找的一条浮点运算指令的编码,因为 E203 不支持浮点,应当产生异常。重新编译下载、复位CPU后,在调试器中 halt CPU,然后单步执行,确认PC已经不变,即程序在 handle_trap 的死循环状态。然后看下 a0, a1 寄存器。<br />
<span style="color:#27ae60;"><strong><span style="font-family:Courier;">&gt; halt<br />
halted at 0x80000334 due to debug interrupt<br />
&gt; step<br />
halted at 0x80000334 due to step<br />
&gt; reg a0<br />
a0 (/32): 0x00000002<br />
&gt; reg a1<br />
a1 (/32): 0x800000DE</span></strong></span><br />
  a1 对应的是 <strong>mepc</strong>, a0 对应的是 <strong>mcause</strong>.(其实OpenOCD中用 reg 命令查看 CSR 也是可以的)<br />
  异常代码(引发异常的原因)<strong>mcause</strong>=2, 这个寄存器的描述是:</p>

<p>  状态是遇到了非法指令,<strong>mepc</strong>是该指令的地址:0x800000DE. 反汇编看一下:<br />
<span style="color:#27ae60;"><strong><span style="font-family:Courier;">800000da: &nbsp; &nbsp; &nbsp; fd2a59e3 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;bge &nbsp; &nbsp; s4,s2,800000ac &lt;main+0x4c&gt;<br />
800000de: &nbsp; &nbsp; &nbsp; 00421e53 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;fadd.s &nbsp;ft8,ft4,ft4,rtz<br />
800000e2: &nbsp; &nbsp; &nbsp; b7e9 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;j &nbsp; &nbsp; &nbsp; 800000ac &lt;main+0x4c&gt;</span></strong></span><br />
  没错,引起异常的就是我手工插入的 FADD.S 浮点指令。</p>

<p>  尝试其它异常,例如非对齐内存访问,用一个函数测试:<br />
<span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>int killer(int *c)<br />
{<br />
&nbsp;&nbsp; &nbsp;static int g;<br />
&nbsp;&nbsp; &nbsp;g=g+ *c;<br />
&nbsp;&nbsp; &nbsp;return g;<br />
}</strong></span></span><br />
  当传入参数(地址)不是4字节对齐时,发生非对齐读异常(<strong>mcause</strong>=4, <strong>mepc</strong>=0x800002C6)<br />
<span style="color:#27ae60;"><strong><span style="font-family:Courier;">800002c2 &lt;killer&gt;:<br />
800002c2: &nbsp; &nbsp; &nbsp; 80c1a703 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;lw &nbsp; &nbsp; &nbsp;a4,-2036(gp) # 9000002c &lt;g.1467&gt;<br />
800002c6: &nbsp; &nbsp; &nbsp; 4108 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;lw &nbsp; &nbsp; &nbsp;a0,0(a0)<br />
800002c8: &nbsp; &nbsp; &nbsp; 953a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;add &nbsp; &nbsp; a0,a0,a4<br />
800002ca: &nbsp; &nbsp; &nbsp; 80a1a623 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;sw &nbsp; &nbsp; &nbsp;a0,-2036(gp) # 9000002c &lt;g.1467&gt;</span></strong></span></p>

<p><br />
  继续比较 ARMv7-M 和 RISC-V 异常处理方式的区别。<br />
<strong><span style="background-color:#f1c40f;">(5)</span></strong> ARMv7-M 在CPU核内部有一个NVIC中断控制器,负责所有外部中断管理。RISC-V核中没有这个模块,在核外面有PLIC (Platform-Level Interrupt Controller),外部中断请求都连接到PLIC上,PLIC向RISC-V提供1路外部中断请求信号。<br />
<strong><span style="background-color:#f1c40f;">(6)</span></strong> ARMv7-M 有 SysTick, PendSV, SVC 三个中断在CPU内部产生。SysTick中断来源于CPU内部的定时器,SVC中断是SVC指令触发的软件中断。RISC-V除了外部中断还有定时器中断和软件中断,但是由CPU外部的CLINT模块产生(为什么软中断也要外部产生,以多核工作的情形考虑。但是类似ARM SVC指令的功能已经有ECALL指令及有关异常实现);系统内部其它定时器产生的中断就属于PLIC管理的外部中断了。<br />
<strong><span style="background-color:#f1c40f;">(7)</span></strong> RISC-V 还可以有调试中断,ARMv7-M没有&mdash;&mdash;但它内部包含的CoreSight Debug模块就复杂太多了。<br />
<strong><span style="background-color:#f1c40f;">(8)</span></strong> RISC-V 处理中断时自动屏蔽中断,不会发生中断嵌套;要在中断响应中允许其它中断需要软件自行管理。</p>

<p>&nbsp;</p>

<p>  下面实验一下 CLINT 管理的软中断和定时器中断。<br />
  先留意一下,RISC-V 有 <strong>mip</strong> 和 <strong>mie</strong> 两个CSR分别反映中断请求状态和使能状态, 在 <strong>mstatus</strong> CSR中,MIE域控制了全局的中断使能。要用中断,先要操作 CSR 允许中断(异常是不需要的)。<br />
<span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>&nbsp;&nbsp; &nbsp;set_csr(mie, 1&lt;&lt;3|1&lt;&lt;7|1&lt;&lt;11);&nbsp;&nbsp; &nbsp;// enable MSIE, MTIE, MEIE&nbsp;<br />
&nbsp;&nbsp; &nbsp;set_csr(mstatus, 3);&nbsp;&nbsp; &nbsp;// enable MIE</strong></span></span></p>

<p>  修改一下 handle_trap() ,使中断发生时RGB LED亮绿灯,异常发生时 RGB LED 亮红灯。<br />
<span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>void handle_trap(uint32_t mcause, uint32_t mepc)<br />
{<br />
&nbsp;&nbsp; &nbsp;if(mcause &amp; 1&lt;&lt;31)<br />
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;GPIO_REG(GPIO_OUTPUT_XOR)= 1&lt;&lt;1;<br />
&nbsp;&nbsp; &nbsp;else<br />
&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;GPIO_REG(GPIO_OUTPUT_XOR)= 1&lt;&lt;0;</strong></span></span></p>

<p><span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>&nbsp;&nbsp; &nbsp;for(;;)<br />
&nbsp;&nbsp; &nbsp;{<br />
&nbsp;&nbsp; &nbsp;}<br />
}</strong></span></span></p>

<p>  产生软件中断只需要写 CLINT 的 MSIP 寄存器最低位为1<br />
<span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>CLINT_REG(CLINT_MSIP)=1;</strong></span></span><br />
&nbsp;&nbsp; &nbsp;软中断发生后,绿灯亮起,<strong>mcause</strong>=0x80000003. 表示机器模式软件中断。</p>

<p>&nbsp;</p>

<p>&nbsp;&nbsp; &nbsp;产生定时器中断用 CLINT 的 MTIMECMP 寄存器设置一个值,当定时器走到等于或大于这个值时发生中断。若想在系统运行3秒后产生中断,可以写</p>

<p><span style="color:#8e44ad;"><strong><span style="font-family:Courier;">&nbsp;&nbsp; &nbsp;*(uint64_t *)&amp;CLINT_REG(CLINT_MTIMECMP)=32768*3;</span></strong></span><br />
&nbsp;&nbsp; &nbsp;定时器中断发生后,绿灯亮起,<strong>mcause</strong>=0x80000007. 表示机器模式定时器中断。</p>

<p>  改一下异常处理程序,实现定时器周期性中断,让RGB LED的蓝灯亮灭交替:</p>

<pre>
<code class="language-cpp">uint32_t handle_trap(uint32_t mcause, uint32_t mepc)
{
        if(mcause &amp; 1&lt;&lt;31)
        {
                if((mcause &amp;15)==7)
                {
                        CLINT_REG(CLINT_MTIMECMP) += 32768;
                        GPIO_REG(GPIO_OUTPUT_VAL)= GPIO_REG(GPIO_OUTPUT_VAL)^1&lt;&lt;2;
                        return mepc;
                }
                GPIO_REG(GPIO_OUTPUT_XOR)= 1&lt;&lt;1;
        }
        else
                GPIO_REG(GPIO_OUTPUT_XOR)= 1&lt;&lt;0;

        for(;;)
        {
        }
}</code></pre>

<p>&nbsp;</p>

<p>  对于MCU, 片上硬件设备的各种中断才是功能丰富的。RISC-V的PLIC负责管理这些中断请求。E203 SOC 中的外部中断源如下:<br />
</p>

<p>  以 GPIO 中断为例进行试验,在我的板子上就用 GPIO9(分配到Arduino A0脚了)的中断功能作为中断源,对应中断号是17.<br />
  先要在 GPIO 里面配置中断,由 GPIO 给 PLIC 发送中断请求。根据寄存器表,可以用上升沿、下降沿以及高低电平作为中断条件。我用上升沿触发中断功能:<br />
<span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>&nbsp;&nbsp; &nbsp;GPIO_REG(GPIO_INPUT_EN) = 1&lt;&lt;9;<br />
&nbsp;&nbsp; &nbsp;GPIO_REG(GPIO_RISE_IE) = 1&lt;&lt;9;&nbsp;&nbsp; &nbsp;// GPIO9: arduido A0</strong></span></span><br />
  然后还要在 PLIC 中允许这个中断。PLIC 的寄存器很多:<br />
  因为 E203 的PLIC支持1024个外部中断输入,又支持15872个target(实际上只有1个可用,就是CPU),寄存器就很庞大了,实际上有效的只是一小部分。对于唯一的CPU的中断允许位有1024个,分配在32个32-bit寄存器中。我要允许中断17, 只需要对第一个寄存器的bit 17置1就可以了。<br />
<span style="color:#8e44ad;"><span style="font-family:Courier;"><strong>&nbsp;&nbsp; &nbsp;PLIC_REG(PLIC_ENABLE_OFFSET) = 1&lt;&lt;17;&nbsp;&nbsp; &nbsp;// enable int 17 (GPIO9)</strong></span></span><br />
  编译并运行程序。将GPIO和VDD接触一下,然后,确实能看到 PLIC 的 pending 寄存器组中中断17的位变成1了。但是CPU并没有被中断。<br />
  想了想,可能还有优先级没设置。文档中说,最简单的设置是所有中断优先级都设成1, 但我看了优先级寄存器的默认值现在都是0, 可能是问题所在。于是,初始化代码再加一行:<br />
<span style="color:#8e44ad;"><strong><span style="font-family:Courier;">&nbsp;&nbsp; &nbsp;PLIC_REG(PLIC_PRIORITY_OFFSET+17*4) = 1;</span></strong></span></p>

<p>  然后再实验,trap_entry()函数就检测到外部中断发生了。这个时候,还不知道是哪一个中断源引起的,需要从 PLIC 的寄存器中获取信息。唯一的只有 claim/complete 寄存器了,读这个寄存器能得到当前中断号,并且pending标志也清除。在中断处理完之后,还要告诉 PLIC 已经完成中断处理:通过向 claim/complete 寄存器写当前的中断号,这样 PLIC 才会继续下一个中断请求。</p>

<p><br />
  现在 trap_entry() 函数成这样了:</p>

<pre>
<code class="language-cpp">uint32_t handle_trap(uint32_t mcause, uint32_t mepc)
{
        if(mcause &amp; 1&lt;&lt;31)
        {
                if((mcause &amp;15)==11) // ext int
                {
                        uint16_t irq = PLIC_REG(PLIC_CLAIM_OFFSET);        // claim
                        if(irq==17)
                        {
                                GPIO_REG(GPIO_OUTPUT_VAL) ^= 1&lt;&lt;4;
                                GPIO_REG(GPIO_RISE_IP) = 1&lt;&lt;9;        // clear pending

                        }
                        PLIC_REG(PLIC_CLAIM_OFFSET)=irq;        // complete
                        return mepc;
                }
                elseif((mcause &amp;15)==7)
                {
                        CLINT_REG(CLINT_MTIMECMP) += 32768;
                        GPIO_REG(GPIO_OUTPUT_XOR) ^= 1&lt;&lt;2;
                        return mepc;
                }
                GPIO_REG(GPIO_OUTPUT_XOR)= 1&lt;&lt;1;
        }
        else
                GPIO_REG(GPIO_OUTPUT_XOR)= 1&lt;&lt;0;

        for(;;)
        {
        }
}</code></pre>

<p>  增加了一个LED表示GPIO9的中断触发。注意,在中断处理时还要对GPIO寄存器的pending位作清除操作,否则GPIO不会产生新的中断。</p>

<p><br />
  用示波器测一下外部中断信号(GPIO电平变化)到E203响应这个中断的大概时间:<br />
  按照16MHz频率,响应此中断总共经历了70多个时钟周期。回顾代码看来,有一半就用在保存现场上了。因为没有不同异常安排单独的入口,条件分支也消耗了一些CPU时间。</p>

<p>&nbsp;</p>

<p>  小结:对于RISC-V系和ARM Cortex-M系MCU的用法差异,中断或异常处理是主要的差异所在。指令集的差异对于C语言开发者并不需要太关注,片上设备实现甚至可以做得兼容。RISC-V是个开放架构,E203实现的功能还是简化了许多的。Perf-V板子可以让学习者关注更多的硬件设计细节,而不是仅仅用一个MCU.&nbsp;</p>

soso 发表于 2021-7-26 09:37

<p>中断或异常处理搞定后,基本就能玩转了RISC-V?</p>

cruelfox 发表于 2021-7-26 10:49

soso 发表于 2021-7-26 09:37
中断或异常处理搞定后,基本就能玩转了RISC-V?

<p>针对一个具体CPU差不多。</p>

<p>RISC-V是一个开放架构,指令集就有好多种,还可以自定义指令。即使指令集相同的,不同的CPU实现片上总线、中断控制器都可能差异比较大。</p>

<p>可以大致理解为RISC-V制定了一个大框架,里面有很多部分是可选的,还有很多部分可以自由发挥的,所以到芯片这里各家产品的CPU有共性也有差异。</p>

soso 发表于 2021-7-26 11:24

cruelfox 发表于 2021-7-26 10:49
针对一个具体CPU差不多。

RISC-V是一个开放架构,指令集就有好多种,还可以自定义指令。即使指令集相 ...

<p>好的,了解啦。</p>

radish800305 发表于 2022-8-27 15:52

<p>RISC-V是一个开放架构,完全不是一个风格,大致理解为RISC-V制定了一个大框架,各家产品有共性有差异</p>
页: [1]
查看完整版本: 【Perf-V评测】蜂鸟E203的异常(中断)处理