【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域——所有的异常共用一个入口地址。<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("csrw mtvec, %0\n"::"r"(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> 先造成一个异常——不支持的指令,看看效果如何。在程序中用汇编插入指令编码:<br />
<span style="color:#8e44ad;"><strong><span style="font-family:Courier;"> if(c>2)<br />
{<br />
asm volatile(".word 0x00421E53\n"); // FADD.S instr<br />
}</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;">> halt<br />
halted at 0x80000334 due to debug interrupt<br />
> step<br />
halted at 0x80000334 due to step<br />
> reg a0<br />
a0 (/32): 0x00000002<br />
> 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: fd2a59e3 bge s4,s2,800000ac <main+0x4c><br />
800000de: 00421e53 fadd.s ft8,ft4,ft4,rtz<br />
800000e2: b7e9 j 800000ac <main+0x4c></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 />
static int g;<br />
g=g+ *c;<br />
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 <killer>:<br />
800002c2: 80c1a703 lw a4,-2036(gp) # 9000002c <g.1467><br />
800002c6: 4108 lw a0,0(a0)<br />
800002c8: 953a add a0,a0,a4<br />
800002ca: 80a1a623 sw a0,-2036(gp) # 9000002c <g.1467></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没有——但它内部包含的CoreSight Debug模块就复杂太多了。<br />
<strong><span style="background-color:#f1c40f;">(8)</span></strong> RISC-V 处理中断时自动屏蔽中断,不会发生中断嵌套;要在中断响应中允许其它中断需要软件自行管理。</p>
<p> </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> set_csr(mie, 1<<3|1<<7|1<<11); // enable MSIE, MTIE, MEIE <br />
set_csr(mstatus, 3); // 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 />
if(mcause & 1<<31)<br />
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<1;<br />
else<br />
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<0;</strong></span></span></p>
<p><span style="color:#8e44ad;"><span style="font-family:Courier;"><strong> for(;;)<br />
{<br />
}<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 />
软中断发生后,绿灯亮起,<strong>mcause</strong>=0x80000003. 表示机器模式软件中断。</p>
<p> </p>
<p> 产生定时器中断用 CLINT 的 MTIMECMP 寄存器设置一个值,当定时器走到等于或大于这个值时发生中断。若想在系统运行3秒后产生中断,可以写</p>
<p><span style="color:#8e44ad;"><strong><span style="font-family:Courier;"> *(uint64_t *)&CLINT_REG(CLINT_MTIMECMP)=32768*3;</span></strong></span><br />
定时器中断发生后,绿灯亮起,<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 & 1<<31)
{
if((mcause &15)==7)
{
CLINT_REG(CLINT_MTIMECMP) += 32768;
GPIO_REG(GPIO_OUTPUT_VAL)= GPIO_REG(GPIO_OUTPUT_VAL)^1<<2;
return mepc;
}
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<1;
}
else
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<0;
for(;;)
{
}
}</code></pre>
<p> </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> GPIO_REG(GPIO_INPUT_EN) = 1<<9;<br />
GPIO_REG(GPIO_RISE_IE) = 1<<9; // 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> PLIC_REG(PLIC_ENABLE_OFFSET) = 1<<17; // 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;"> 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 & 1<<31)
{
if((mcause &15)==11) // ext int
{
uint16_t irq = PLIC_REG(PLIC_CLAIM_OFFSET); // claim
if(irq==17)
{
GPIO_REG(GPIO_OUTPUT_VAL) ^= 1<<4;
GPIO_REG(GPIO_RISE_IP) = 1<<9; // clear pending
}
PLIC_REG(PLIC_CLAIM_OFFSET)=irq; // complete
return mepc;
}
elseif((mcause &15)==7)
{
CLINT_REG(CLINT_MTIMECMP) += 32768;
GPIO_REG(GPIO_OUTPUT_XOR) ^= 1<<2;
return mepc;
}
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<1;
}
else
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<0;
for(;;)
{
}
}</code></pre>
<p> 增加了一个LED表示GPIO9的中断触发。注意,在中断处理时还要对GPIO寄存器的pending位作清除操作,否则GPIO不会产生新的中断。</p>
<p><br />
用示波器测一下外部中断信号(GPIO电平变化)到E203响应这个中断的大概时间:<br />
按照16MHz频率,响应此中断总共经历了70多个时钟周期。回顾代码看来,有一半就用在保存现场上了。因为没有不同异常安排单独的入口,条件分支也消耗了一些CPU时间。</p>
<p> </p>
<p> 小结:对于RISC-V系和ARM Cortex-M系MCU的用法差异,中断或异常处理是主要的差异所在。指令集的差异对于C语言开发者并不需要太关注,片上设备实现甚至可以做得兼容。RISC-V是个开放架构,E203实现的功能还是简化了许多的。Perf-V板子可以让学习者关注更多的硬件设计细节,而不是仅仅用一个MCU. </p>
<p>中断或异常处理搞定后,基本就能玩转了RISC-V?</p>
soso 发表于 2021-7-26 09:37
中断或异常处理搞定后,基本就能玩转了RISC-V?
<p>针对一个具体CPU差不多。</p>
<p>RISC-V是一个开放架构,指令集就有好多种,还可以自定义指令。即使指令集相同的,不同的CPU实现片上总线、中断控制器都可能差异比较大。</p>
<p>可以大致理解为RISC-V制定了一个大框架,里面有很多部分是可选的,还有很多部分可以自由发挥的,所以到芯片这里各家产品的CPU有共性也有差异。</p>
cruelfox 发表于 2021-7-26 10:49
针对一个具体CPU差不多。
RISC-V是一个开放架构,指令集就有好多种,还可以自定义指令。即使指令集相 ...
<p>好的,了解啦。</p>
<p>RISC-V是一个开放架构,完全不是一个风格,大致理解为RISC-V制定了一个大框架,各家产品有共性有差异</p>
页:
[1]