【Perf-V评测】蜂鸟E203的异常(中断)处理
[复制链接]
sirv-e-sdk 里的 entry.S 是和异常(中断)有关的。现在要使用中断功能,必须了解如何编写中断处理程序。entry.S 主要内容如下:
#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
看起来很陌生,和我熟悉的ARMv7-M (常见的Cortex-m3,m4都是属于这个构架的CPU)完全不是一个风格。原因在于,RISC-V架构的异常处理与ARMv7-M 有很大的不同,需要查资料学习了。仅从软件方面看:
(1) ARMv7-M有一个中断向量表,从表的第二项开始每一项对应一个异常处理程序的入口地址;系统寄存器VTOR指定中断向量表的位置。RISC-V每个模式有一个CSR寄存器存放异常处理程序的入口地址;通过这个寄存器的MODE域设置可以为异步中断在入口地址上加偏移量。E203只实现了机器模式,只有一个 mtvec,且不支持MODE域——所有的异常共用一个入口地址。
(2) ARMv7-M处理每个异常时(Reset除外),自动保存8个寄存器到堆栈中(当前SP寄存器指定栈顶),然后PC设置为中断向量表中的异常处理程序入口开始执行,SP设置为MSP; 中断返回时,保存在堆栈中的8个寄存器被自动装入。RISC-V异常处理时,不会在堆栈中保存任何寄存器。但是返回地址(被中断时候的PC值)必须要保存,RISC-V规定了 mepc 等CSR用于保存各模式下的返回地址。
(3) ARMv7-M处理异常时,LR寄存器有特殊意义,因此BX LR指令可以实现从异常返回;C语言中写异常处理程序等同与一般函数写法。RISC-V需要使用专用指令从异常返回,例如mret.
(4) RISC-V因为每个模式下所有异常共用一个入口(称为Trap Vector),软件要读取CSR寄存器如mcause来判断异常类型。
上面的 entry.S 代码用 STORE/LOAD 保存和恢复了31个通用寄存器,就是因为RISC-V异常处理不会自动保存现场。全部寄存器保存是简单粗暴的做法,耗费机器周期比较多了。然后,代码读取了 mcause, mepc 两个CSR,分别作为参数,调用了 handle_trap 这个函数(别处定义),再将返回值写入 mepc. 也就是说 entry.S 只管了现场保存,如何处理异常是转交给 handle_trap() 函数实现的。如果别处没有定义 handle_trap, 这里默认的就是一个死循环。
我把 entry.S 内容加到以前的代码工程中去,并在 main() 中先设置好 mtvec CSR:增加一行
asm volatile("csrw mtvec, %0\n"::"r"(trap_entry));
或者用 encodings.h 定义过的宏来写:
write_csr(mtvec, trap_entry);
这样发生异常时 trap_entry 的代码就会执行了。
先造成一个异常——不支持的指令,看看效果如何。在程序中用汇编插入指令编码:
if(c>2)
{
asm volatile(".word 0x00421E53\n"); // FADD.S instr
}
0x00421E53 是我在手册中找的一条浮点运算指令的编码,因为 E203 不支持浮点,应当产生异常。重新编译下载、复位CPU后,在调试器中 halt CPU,然后单步执行,确认PC已经不变,即程序在 handle_trap 的死循环状态。然后看下 a0, a1 寄存器。
> halt
halted at 0x80000334 due to debug interrupt
> step
halted at 0x80000334 due to step
> reg a0
a0 (/32): 0x00000002
> reg a1
a1 (/32): 0x800000DE
a1 对应的是 mepc, a0 对应的是 mcause.(其实OpenOCD中用 reg 命令查看 CSR 也是可以的)
异常代码(引发异常的原因)mcause=2, 这个寄存器的描述是:
状态是遇到了非法指令,mepc是该指令的地址:0x800000DE. 反汇编看一下:
800000da: fd2a59e3 bge s4,s2,800000ac <main+0x4c>
800000de: 00421e53 fadd.s ft8,ft4,ft4,rtz
800000e2: b7e9 j 800000ac <main+0x4c>
没错,引起异常的就是我手工插入的 FADD.S 浮点指令。
尝试其它异常,例如非对齐内存访问,用一个函数测试:
int killer(int *c)
{
static int g;
g=g+ *c;
return g;
}
当传入参数(地址)不是4字节对齐时,发生非对齐读异常(mcause=4, mepc=0x800002C6)
800002c2 <killer>:
800002c2: 80c1a703 lw a4,-2036(gp) # 9000002c <g.1467>
800002c6: 4108 lw a0,0(a0)
800002c8: 953a add a0,a0,a4
800002ca: 80a1a623 sw a0,-2036(gp) # 9000002c <g.1467>
继续比较 ARMv7-M 和 RISC-V 异常处理方式的区别。
(5) ARMv7-M 在CPU核内部有一个NVIC中断控制器,负责所有外部中断管理。RISC-V核中没有这个模块,在核外面有PLIC (Platform-Level Interrupt Controller),外部中断请求都连接到PLIC上,PLIC向RISC-V提供1路外部中断请求信号。
(6) ARMv7-M 有 SysTick, PendSV, SVC 三个中断在CPU内部产生。SysTick中断来源于CPU内部的定时器,SVC中断是SVC指令触发的软件中断。RISC-V除了外部中断还有定时器中断和软件中断,但是由CPU外部的CLINT模块产生(为什么软中断也要外部产生,以多核工作的情形考虑。但是类似ARM SVC指令的功能已经有ECALL指令及有关异常实现);系统内部其它定时器产生的中断就属于PLIC管理的外部中断了。
(7) RISC-V 还可以有调试中断,ARMv7-M没有——但它内部包含的CoreSight Debug模块就复杂太多了。
(8) RISC-V 处理中断时自动屏蔽中断,不会发生中断嵌套;要在中断响应中允许其它中断需要软件自行管理。
下面实验一下 CLINT 管理的软中断和定时器中断。
先留意一下,RISC-V 有 mip 和 mie 两个CSR分别反映中断请求状态和使能状态, 在 mstatus CSR中,MIE域控制了全局的中断使能。要用中断,先要操作 CSR 允许中断(异常是不需要的)。
set_csr(mie, 1<<3|1<<7|1<<11); // enable MSIE, MTIE, MEIE
set_csr(mstatus, 3); // enable MIE
修改一下 handle_trap() ,使中断发生时RGB LED亮绿灯,异常发生时 RGB LED 亮红灯。
void handle_trap(uint32_t mcause, uint32_t mepc)
{
if(mcause & 1<<31)
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<1;
else
GPIO_REG(GPIO_OUTPUT_XOR)= 1<<0;
for(;;)
{
}
}
产生软件中断只需要写 CLINT 的 MSIP 寄存器最低位为1
CLINT_REG(CLINT_MSIP)=1;
软中断发生后,绿灯亮起,mcause=0x80000003. 表示机器模式软件中断。
产生定时器中断用 CLINT 的 MTIMECMP 寄存器设置一个值,当定时器走到等于或大于这个值时发生中断。若想在系统运行3秒后产生中断,可以写
*(uint64_t *)&CLINT_REG(CLINT_MTIMECMP)=32768*3;
定时器中断发生后,绿灯亮起,mcause=0x80000007. 表示机器模式定时器中断。
改一下异常处理程序,实现定时器周期性中断,让RGB LED的蓝灯亮灭交替:
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(;;)
{
}
}
对于MCU, 片上硬件设备的各种中断才是功能丰富的。RISC-V的PLIC负责管理这些中断请求。E203 SOC 中的外部中断源如下:
以 GPIO 中断为例进行试验,在我的板子上就用 GPIO9(分配到Arduino A0脚了)的中断功能作为中断源,对应中断号是17.
先要在 GPIO 里面配置中断,由 GPIO 给 PLIC 发送中断请求。根据寄存器表,可以用上升沿、下降沿以及高低电平作为中断条件。我用上升沿触发中断功能:
GPIO_REG(GPIO_INPUT_EN) = 1<<9;
GPIO_REG(GPIO_RISE_IE) = 1<<9; // GPIO9: arduido A0
然后还要在 PLIC 中允许这个中断。PLIC 的寄存器很多:
因为 E203 的PLIC支持1024个外部中断输入,又支持15872个target(实际上只有1个可用,就是CPU),寄存器就很庞大了,实际上有效的只是一小部分。对于唯一的CPU的中断允许位有1024个,分配在32个32-bit寄存器中。我要允许中断17, 只需要对第一个寄存器的bit 17置1就可以了。
PLIC_REG(PLIC_ENABLE_OFFSET) = 1<<17; // enable int 17 (GPIO9)
编译并运行程序。将GPIO和VDD接触一下,然后,确实能看到 PLIC 的 pending 寄存器组中中断17的位变成1了。但是CPU并没有被中断。
想了想,可能还有优先级没设置。文档中说,最简单的设置是所有中断优先级都设成1, 但我看了优先级寄存器的默认值现在都是0, 可能是问题所在。于是,初始化代码再加一行:
PLIC_REG(PLIC_PRIORITY_OFFSET+17*4) = 1;
然后再实验,trap_entry()函数就检测到外部中断发生了。这个时候,还不知道是哪一个中断源引起的,需要从 PLIC 的寄存器中获取信息。唯一的只有 claim/complete 寄存器了,读这个寄存器能得到当前中断号,并且pending标志也清除。在中断处理完之后,还要告诉 PLIC 已经完成中断处理:通过向 claim/complete 寄存器写当前的中断号,这样 PLIC 才会继续下一个中断请求。
现在 trap_entry() 函数成这样了:
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;
}
else if((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(;;)
{
}
}
增加了一个LED表示GPIO9的中断触发。注意,在中断处理时还要对GPIO寄存器的pending位作清除操作,否则GPIO不会产生新的中断。
用示波器测一下外部中断信号(GPIO电平变化)到E203响应这个中断的大概时间:
按照16MHz频率,响应此中断总共经历了70多个时钟周期。回顾代码看来,有一半就用在保存现场上了。因为没有不同异常安排单独的入口,条件分支也消耗了一些CPU时间。
小结:对于RISC-V系和ARM Cortex-M系MCU的用法差异,中断或异常处理是主要的差异所在。指令集的差异对于C语言开发者并不需要太关注,片上设备实现甚至可以做得兼容。RISC-V是个开放架构,E203实现的功能还是简化了许多的。Perf-V板子可以让学习者关注更多的硬件设计细节,而不是仅仅用一个MCU.
|