인터럽트 핸들러

2023년 6월 6일

인터럽트, 예외, NMI

RISC-V의 인터럽트, 예외, NMI는 모두 코어가 하던 일을 멈추고 이들을 다루기 위한 핸들러를 실행한다는 점에서 같지만 발생 원인에 따라 구분합니다. 인터럽트는 요청(request)에 의해 발생하고, 예외는 코어가 비정상적인 상황(abnormal event)을 만났을 때 발생하며, NMI는 외부 하드웨어 오류와 같은 입력 신호(input signal)에 의해 발생합니다. 이 글에서는 인터럽트를 주로 다루겠지만, 예외와 NMI 처리도 많은 부분을 공유하므로 같이 언급합니다. 이 셋을 통틀어 트랩(trap)이라고 부릅니다.

인터럽트 핸들링 모드

CLINT와 CLIC

RISC-V의 인터럽트 핸들링 모드는 CLINT(Core Local INTerruptor)와 CLIC(Core Local Interrupt Controller) 두 가지가 존재합니다. CLINT에 비하면 CLIC은 상대적으로 복잡하지만 세세한 설정이 가능하고 성능상 이점이 있어 실시간성이 중요한 경우 추천됩니다. 리셋 시에는 기본적으로 CLINT 모드입니다. 이하 CLIC을 기준으로 설명합니다.

Vectored와 non-vectored 인터럽트

CLIC 모드에서 각 인터럽트는 vectored 또는 non-vectored 인터럽트로 설정할 수 있습니다.

CLIC 모드의 인터럽트 벡터 테이블은 ARM과 동일하게 핸들러 주소의 배열입니다. 인터럽트 번호는 0부터 최대 4095까지 존재하는데 GD32VF103은 86까지 사용합니다. 그 중에 0부터 18은 Nuclei에서 예약한 내부 인터럽트이고, 19부터가 GD32VF103에서 정의하는 외부 인터럽트입니다. 각 번호가 어떤 인터럽트인지는 데이터시트를 확인하시기 바랍니다. 사용하지 않는 번호에 유의합시다.

RISC-V Privileged Architecture

RISC-V privileged architecture 표준은 이름 그대로 프로그램의 하드웨어 접근 권한과 그에 따른 인터럽트 등의 처리 및 추가 명령어와 레지스터를 다룹니다.

권한 모드

권한 모드(privilege mode)는 글을 하나 써야 할 정도로 다룰 내용이 많지만, 인터럽트 처리에 필수적인 개념이니 간단히 짚고 넘어가겠습니다. RISC-V는 권한 모드의 개념을 도입하여 각 모드별로 접근 가능한 영역을 제한하도록 하였습니다. GD32VF103은 두 가지 권한 모드를 지원합니다.

리셋 직후에는 기본적으로 머신 모드입니다. 인터럽트가 발생한 시점의 권한 모드에 따라 인터럽트 처리가 달라지므로 권한 모드에 유의하여야 합니다.

머신 서브 모드

위의 권한 모드에 더해 Nuclei 코어는 별도의 머신 서브 모드를 지원합니다.

CSR

RISC-V는 코어별 설정과 상태를 나타내는 CSR(Control and Status Register)를 제공합니다. CSR에 접근하기 위해서는 별도의 명령어 csrrcsrw 등을 사용해야 합니다.

Nuclei 코어에서 인터럽트 처리와 크게 관련있는 주요 CSR은 다음과 같습니다.

mtvec 레지스터
필드 비트 설명
ADDR 31:6 예외 핸들러 주소
MODE 5:0 인터럽트 핸들링 모드
  • 000011: CLIC 모드
  • 기타: CLINT 모드
mtvt2 레지스터
필드 비트 설명
COMMON-CODE-ENTRY 31:2 MTVT2EN이 1일 때 사용할 non-vectored 인터럽트 핸들러의 주소
Reserved 1 Reserved 0
MTVT2EN 0 mtvt2 활성화
  • 0: non-vectored 인터럽트 핸들러를 예외 핸들러와 공유
  • 1: non-vectored 인터럽트 핸들러의 주소로 COMMON-CODE-ENTRY를 사용
mcause 레지스터
필드 비트 설명
INTERRUPT 31 트랩 유형
  • 0: 예외 및 NMI 발생
  • 1: 인터럽트 발생
MINHV 30 인터럽트 벡터 테이블 읽기 여부 (CLIC 모드에서만 작동)
MPP 29:28 발생 시점의 권한 모드
  • 3: 머신 모드
  • 0: 유저 모드
MPIE 27 발생 시점의 인터럽트 활성화 여부
Reserved 26:24 Reserved 0
MPIL 23:16 이전 인터럽트 레벨
Reserved 15:12 Reserved 0
EXCCODE 11:0 발생한 트랩 코드
mmisc_ctl레지스터
필드 비트 설명
Reserved 31:10 Reserved 0
NMI_CAUSE_FFF 9 mnvec 및 NMI의 트랩 코드(mcause.EXCCODE) 설정
  • 0: mnvec을 0으로, NMI의 코드를 1로 설정
  • 1: mnvecmtvec.ADDR로, NMI의 코드를 0xFFF로 설정
Reserved 8:0 Reserved 0
mstatus 레지스터
필드 비트 설명
SD 31 FS == 3 || XS == 3
Reserved 30:17 Reserved 0
XS 16:15 유저 모드 확장 유닛 상태
FS 14:13 부동 소수점 유닛 상태
MPP 12:11 mcause.MPP와 동일
Reserved 10:8 Reserved 0
MPIE 7 mcause.MPIE와 동일
Reserved 6:4 Reserved 0
MIE 3 인터럽트 활성화 여부(기본값 0)
  • 0: 비활성화
  • 1: 활성화
Reserved 2:0 Reserved 0
mintstatus 레지스터
필드 비트 설명
MIL 31:24 머신 모드 인터럽트 레벨
Reserved 23:8 Reserved 0
UIL 7:0 유저 모드 인터럽트 레벨
msubm 레지스터
필드 비트 설명
Reserved 31:10 Reserved 0
PTYP 9:8 트랩 발생 전 머신 서브 모드
  • 0: 노멀 머신 모드
  • 1: 인터럽트 처리 모드
  • 2: 예외 처리 모드
  • 3: NMI 처리 모드
TYP 7:6 현재 머신 서브 모드
  • 0: 노멀 머신 모드
  • 1: 인터럽트 처리 모드
  • 2: 예외 처리 모드
  • 3: NMI 처리 모드
Reserved 5:0 Reserved 0

인터럽트 핸들러에 들어갈 때

이제 기본적인 개념을 다 다루었으므로 본격적으로 CLIC 모드에서 인터럽트가 발생할 때 어떤 일들이 일어나는지 살펴보도록 합시다. 인터럽트가 발생하면 코어는 다음 동작을 1 사이클 내에 수행합니다.

CSR이 어떻게 업데이트되는지 자세히 설명하자면 다음과 같습니다.

인터럽트 핸들러에서 나올 때

인터럽트 핸들러는 종료 시 mret 명령을 실행합니다. 이때 코어는 다음 동작을 1 사이클 내에 수행합니다.

예외와 NMI 역시 핸들러에 들어갈 때와 핸들러를 나올 때 거의 동일한 동작을 수행합니다. 다만 인터럽트 레벨이라는 개념이 없으므로 mcause.MPILmintstatus.MIL을 건드리지 않는다는 차이가 있습니다.

구현

이 글에서는 CLIC 모드를 사용하고, non-vectored 인터럽트 핸들러를 별도로 정의하며 NMI 발생 시 예외 핸들러를 호출하도록 설정하겠습니다. 따라서 리셋 핸들러의 제일 처음에 CSR을 설정하는 코드를 추가합니다.

  1. mmisc_ctl을 수정하여 mnvec을 설정한다.
  2. mtvt에 인터럽트 벡터 테이블 주소를 저장한다.
  3. mtvt2를 활성화하고 non-vectored 인터럽트 핸들러 주소를 설정한다.
  4. mtvec에 예외 핸들러 주소를 저장하고 CLIC 모드로 전환한다.
gd32vf103cbt6.S

#include "gd32vf103_csr.h"

// ...

reset_handler:
    /* Set mnvec to be the mtvec address; share the exception handler for the NMI. */
    li t0, 0x200
    csrw CSR_MMISC_CTL, t0

    /* Set interrupt vector table base address in mtvt. */
    la t0, vtable
    csrw CSR_MTVT, t0

    /* Set non-vectored interrupt handler address in mtvt2 and enable mtvt2. */
    la t0, non_vectored_irq_handler
    ori t0, t0, 0x1
    csrw CSR_MTVT2, t0

    /* Set exception handler address in mtvec and set to CLIC mode. */
    la t0, exception_handler
    ori t0, t0, 0x3
    csrw CSR_MTVEC, t0

    // ...

인터럽트 벡터 테이블과 핸들러도 CLIC 모드에서 요구하는대로 정의합시다. 예외 핸들러와 non-vectored 인터럽트 핸들러의 정렬에 주의합시다.


.section .vtable, "ax", %progbits

.globl vtable
.type vtable, %object
vtable:
    /* Internal interrupts. */
    .word 0
    .word 0
    .word 0
    .word sft_irq_handler
    .word 0
    .word 0
    .word 0
    .word tmr_irq_handler
    .word 0
    .word 0
    .word 0
    .word 0
    .word 0
    .word 0
    .word 0
    .word 0
    .word 0
    .word bus_err_irq_handler
    .word perf_mon_irq_handler

    /* External interrupts. */
    .word wwdgt_irq_handler
    .word lvd_irq_handler
    .word tamper_irq_handler
    .word rtc_irq_handler
    .word fmc_irq_handler
    .word rcu_irq_handler
    .word exti0_irq_handler
    .word exti1_irq_handler
    .word exti2_irq_handler
    .word exti3_irq_handler
    .word exti4_irq_handler
    .word dma0_channel0_irq_handler
    .word dma0_channel1_irq_handler
    .word dma0_channel2_irq_handler
    .word dma0_channel3_irq_handler
    .word dma0_channel4_irq_handler
    .word dma0_channel5_irq_handler
    .word dma0_channel6_irq_handler
    .word adc0_1_irq_handler
    .word can0_tx_irq_handler
    .word can0_rx0_irq_handler
    .word can0_rx1_irq_handler
    .word can0_ewmc_irq_handler
    .word exti5_9_irq_handler
    .word timer0_brk_irq_handler
    .word timer0_up_irq_handler
    .word timer0_trg_cmt_irq_handler
    .word timer0_channel_irq_handler
    .word timer1_irq_handler
    .word timer2_irq_handler
    .word timer3_irq_handler
    .word i2c0_ev_irq_handler
    .word i2c0_er_irq_handler
    .word i2c1_ev_irq_handler
    .word i2c1_er_irq_handler
    .word spi0_irq_handler
    .word spi1_irq_handler
    .word usart0_irq_handler
    .word usart1_irq_handler
    .word usart2_irq_handler
    .word exti10_15_irq_handler
    .word rtc_alarm_irq_handler
    .word usbfs_wkup_irq_handler
    .word 0
    .word 0
    .word 0
    .word 0
    .word 0
    .word exmc_irq_handler
    .word 0
    .word timer4_irq_handler
    .word spi2_irq_handler
    .word uart3_irq_handler
    .word uart4_irq_handler
    .word timer5_irq_handler
    .word timer6_irq_handler
    .word dma1_channel0_irq_handler
    .word dma1_channel1_irq_handler
    .word dma1_channel2_irq_handler
    .word dma1_channel3_irq_handler
    .word dma1_channel4_irq_handler
    .word 0
    .word 0
    .word can1_tx_irq_handler
    .word can1_rx0_irq_handler
    .word can1_rx1_irq_handler
    .word can1_ewmc_irq_handler
    .word usbfs_irq_handler

.weak sft_irq_handler
.set sft_irq_handler, default_handler
.weak tmr_irq_handler
.set tmr_irq_handler, default_handler
// ...

.section .handler, "ax", %progbits

.globl default_handler
.type default_handler, %function
default_handler:
    mret

/* Exception handler must be aligned to 64 bytes. */
.align 6

.globl exception_handler
.type exception_handler, %function
exception_handler:
    mret

/* Non-vectored interrupt handler must be aligned to 4 bytes. */
.align 2

.globl non_vectored_irq_handler
.type non_vectored_irq_handler, %function
non_vectored_irq_handler:
    mret

당장은 핸들러에서 처리할 수 있는 것이 없으므로 vectored 인터럽트 핸들러는 모두 default_handler의 alias로 설정하고, 각 핸들러는 전부 mret을 수행하여 핸들러에서 바로 빠져나오게 하였습니다.

추가로 앞에서 언급했듯이 GD32VF103에서 인터럽트 벡터 테이블의 주소는 512바이트로 정렬되어 있어야 하므로 벡터 테이블의 섹션 vtable을 링커 스크립트에서 정렬하도록 설정합니다. 핸들러가 있는 handler 섹션도 추가하고요.

gd32vf103cbt6.ld

SECTIONS
{
    /* Flash sections. */

    .start :
    {
        KEEP(*(.start))
    } >FLASH

    .handler :
    {
        KEEP(*(.handler))
    } >FLASH

    .vtable ALIGN(512) :
    {
        KEEP(*(.vtable))
    } >FLASH

    /* ... */
}

또한 부트 코드에서 CSR 주소를 사용할 수 있게 GD32VF103이 사용하는 CSR의 주소를 헤더 파일로 정의합니다.

gd32vf103_csr.h

#ifndef GD32VF103_CSR_H
#define GD32VF103_CSR_H

/* Standard machine-mode CSRs. */

#define CSR_MVENDORID                   0xF11
#define CSR_MARCHID                     0xF12
#define CSR_MIMPID                      0xF13
#define CSR_MHARTID                     0xF14
#define CSR_MSTATUS                     0x300
#define CSR_MISA                        0x301
#define CSR_MIE                         0x304
#define CSR_MTVEC                       0x305
#define CSR_MTVT                        0x307
#define CSR_MSCRATCH                    0x340
#define CSR_MEPC                        0x341
#define CSR_MCAUSE                      0x342
#define CSR_MTVAL                       0x343
#define CSR_MIP                         0x344
#define CSR_MNXTI                       0x345
#define CSR_MINTSTATUS                  0x346
#define CSR_MSCRATCHCSW                 0x348
#define CSR_MSCRATCHCSWL                0x349
#define CSR_MCYCLE                      0xB00
#define CSR_MCYCLEH                     0xB80
#define CSR_MINSTRET                    0xB02
#define CSR_MINSTRETH                   0xB82

/* Standard user-mode CSRs. */

#define CSR_CYCLE                       0xC00
#define CSR_TIME                        0xC01
#define CSR_INSTRET                     0xC02
#define CSR_CYCLEH                      0xC80
#define CSR_TIMEH                       0xC81
#define CSR_INSTRETH                    0xC82

/* Customized CSRs. */

#define CSR_MCOUNTINHIBIT               0x320
#define CSR_MNVEC                       0x7C3
#define CSR_MSUBM                       0x7C4
#define CSR_MMISC_CTL                   0x7D0
#define CSR_MSAVESTATUS                 0x7D6
#define CSR_MSAVEEPC1                   0x7D7
#define CSR_MSAVECAUSE1                 0x7D8
#define CSR_MSAVEEPC2                   0x7D9
#define CSR_MSAVECAUSE2                 0x7DA
#define CSR_PUSHMSUBM                   0x7EB
#define CSR_MTVT2                       0x7EC
#define CSR_JALMNXTI                    0x7ED
#define CSR_PUSHMCAUSE                  0x7EE
#define CSR_PUSHMEPC                    0x7EF
#define CSR_SLEEPVALUE                  0x811
#define CSR_TXEVT                       0x812
#define CSR_WFE                         0x810

#endif

참고로 Customized CSRs라고 표시한 CSR들은 사실 RISC-V 표준에 없고 Nuclei에서 정의한 확장 CSR입니다. 이런 CSR은 GDB에서 기본적으로 확인할 수 없기 때문에 OpenOCD 설정 파일에서 수동으로 추가해주어야 합니다. 아래와 같이 설정을 수정하면 일반적인 레지스터(a0, a1 등)와 같이 GDB에서 CSR에 접근할 수 있습니다. 대신, 이름 앞에 csr_을 붙여주어야 합니다. 예를 들어 msubm 레지스터의 값을 출력하려면 print $csr_msubm과 같이 입력합니다.

ft2232.cfg

adapter speed 1000

adapter driver ftdi
ftdi vid_pid 0x0403 0x6010

transport select jtag
ftdi layout_init 0x0008 0x001b
ftdi layout_signal nSRST -oe 0x0020 -data 0x0020

set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 5

set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME riscv -chain-position $_TARGETNAME
$_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size 10000 -work-area-backup 1

set _FLASHNAME $_CHIPNAME.flash
flash bank $_FLASHNAME stm32f1x 0x08000000 0 0 0 $_TARGETNAME

$_TARGETNAME riscv expose_csrs 800=mcountinhibit, 1987=mnvec, 1988=msubm, 2000=mmisc_ctl, \
    2006=msavestatus, 2007=msaveepc1, 2008=msavecause1, 2009=msaveepc2, 2010=msavecause2, \
    2027=pushmsubm, 2028=mtvt2, 2029=jalmnxti, 2030=pushmcause, 2031=pushmepc, \
    2064=wfe, 2065=sleepvalue, 2066=txevt

init

halt

테스트

핸들러가 잘 설정되었는지 테스트해보기 위해 예외를 일으키는 코드를 작성하였습니다. 글 제목에 맞게 인터럽트를 테스트하면 더 좋겠지만, 아직 인터럽트 설정을 다루지 않았기 때문에 예외로 대신합니다.

main.c

#include "gd32vf103.h"

#define UNIMP

int main(void) {
#if defined(UNIMP)
    __asm__ volatile ("unimp");
#elif defined(ECALL)
    __asm__ volatile ("ecall");
#elif defined(MISALIGN)
    int arr[10];
    *(int *)((char *)arr + 1) = 42;
#else
    #error "Please select the exception source"
#endif

    while (1) {}
}

셋째 줄의 매크로 정의를 UNIMP, ECALL, MISALIGN으로 바꾸면 각각 잘못된 명령어, ecall(environment call) 명령어, 정렬되지 않은 메모리 접근을 이유로 예외가 발생합니다. 일단 위 코드대로 컴파일하고, OCD 및 GDB를 실행하여 확인하면 예외 핸들러로 진입한 것을 확인할 수 있습니다.


(gdb) load
Loading section .start, size 0x98 lma 0x8000000
Loading section .handler, size 0x7c lma 0x80000c0
Loading section .vtable, size 0x15c lma 0x8000200
Loading section .text, size 0xc lma 0x800035c
Start address 0x00000000, load size 636
Transfer rate: 1 KB/sec, 159 bytes/write.
(gdb) b main
Breakpoint 1 at 0x8000362: file main.c, line 7.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) c
Continuing.

Breakpoint 1, main () at main.c:7
7           __asm__ volatile ("unimp");
(gdb) si
exception_handler () at gd32vf103cbt6.S:308
308         mret
(gdb) si

Breakpoint 1, main () at main.c:7
7           __asm__ volatile ("unimp");
(gdb) si
exception_handler () at gd32vf103cbt6.S:308
308         mret

또한 mret은 예외가 일어났을 때의 위치로 복귀하는데, 복귀 후에도 다시 잘못된 명령어를 만나 예외가 발생하므로 결국 무한 루프에 빠지게 됩니다. 한편 mcause 레지스터를 확인하면 예외가 일어나기 전 코어는 머신 모드에 있었으며, 잘못된 명령어에 해당하는 코드(EXCCODE)는 2입니다.


(gdb) p/x $mcause
$1 = 0x30000002

ecall의 경우에는 코드가 12이고, 정렬되지 않은 접근은 6에 해당합니다.

참고 문헌