1788|3

1484

帖子

2

资源

五彩晶圆(初级)

【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来判断异常类型。

regtable1.png

  上面的 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, 这个寄存器的描述是:

mcause.PNG   状态是遇到了非法指令,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路外部中断请求信号。
extint.png (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 有 mipmie 两个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 中的外部中断源如下:
intsrc.PNG

  以 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 的寄存器很多:
plic_map.PNG   因为 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响应这个中断的大概时间:
20210712220445.png   按照16MHz频率,响应此中断总共经历了70多个时钟周期。回顾代码看来,有一半就用在保存现场上了。因为没有不同异常安排单独的入口,条件分支也消耗了一些CPU时间。

 

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

此帖出自FPGA/CPLD论坛

回复

2万

帖子

71

资源

管理员

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

点评

针对一个具体CPU差不多。 RISC-V是一个开放架构,指令集就有好多种,还可以自定义指令。即使指令集相同的,不同的CPU实现片上总线、中断控制器都可能差异比较大。 可以大致理解为RISC-V制定了一个大框架,里面  详情 回复 发表于 2021-7-26 10:49
个人签名

不管是哪年,都要加油!继续为中国电子行业做出小小的贡献吧! 扣扣 1206973913


回复

1484

帖子

2

资源

五彩晶圆(初级)

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

针对一个具体CPU差不多。

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

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

点评

好的,了解啦。  详情 回复 发表于 2021-7-26 11:24

回复

2万

帖子

71

资源

管理员

cruelfox 发表于 2021-7-26 10:49 针对一个具体CPU差不多。 RISC-V是一个开放架构,指令集就有好多种,还可以自定义指令。即使指令集相 ...

好的,了解啦。

个人签名

不管是哪年,都要加油!继续为中国电子行业做出小小的贡献吧! 扣扣 1206973913


回复
您需要登录后才可以回帖 登录 | 注册

最新文章 更多>>
    关闭
    站长推荐上一条 1/10 下一条

    About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

    站点相关: 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

    北京市海淀区知春路23号集成电路设计园量子银座1305 电话:(010)82350740 邮编:100191

    电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2021 EEWORLD.com.cn, Inc. All rights reserved
    快速回复 返回顶部 返回列表