인터럽트, 예외, 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 인터럽트로 설정할 수 있습니다.
- Vectored 인터럽트가 발생하면 코어는 인터럽트 벡터 테이블에서 해당 인터럽트에 해당하는 핸들러의 주소를 찾아 점프합니다.
- Non-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은 두 가지 권한 모드를 지원합니다.
- 머신(M) 모드: 코드
3
- 유저(U) 모드: 코드
0
리셋 직후에는 기본적으로 머신 모드입니다. 인터럽트가 발생한 시점의 권한 모드에 따라 인터럽트 처리가 달라지므로 권한 모드에 유의하여야 합니다.
머신 서브 모드
위의 권한 모드에 더해 Nuclei 코어는 별도의 머신 서브 모드를 지원합니다.
- 노멀 머신 모드
- 리셋 후의 기본 서브 모드
- 트랩이 발생하지 않았고 명시적으로 서브 모드를 변경하지 않았을 때의 서브 모드
- 예외 처리 모드
- 예외가 발생하였을 때 전환되는 서브 모드
- NMI 처리 모드
- NMI가 발생하였을 때 전환되는 서브 모드
- 인터럽트 처리 모드
- 인터럽트가 발생하였을 때 전환되는 서브 모드
CSR
RISC-V는 코어별 설정과 상태를 나타내는 CSR(Control and Status Register)를 제공합니다. CSR에 접근하기 위해서는 별도의 명령어 csrr
과 csrw
등을 사용해야 합니다.
Nuclei 코어에서 인터럽트 처리와 크게 관련있는 주요 CSR은 다음과 같습니다.
mtvec
레지스터는 예외 핸들러의 주소와 인터럽트 핸들링 모드를 설정합니다. 이때 주소는 하위 6비트를 빼고 저장하기 때문에 예외 핸들러의 주소는 64바이트로 정렬되어 있어야 합니다.
필드 | 비트 | 설명 |
---|---|---|
ADDR | 31:6 | 예외 핸들러 주소 |
MODE | 5:0 |
인터럽트 핸들링 모드
|
mtvt
레지스터는 CLIC 모드에서 vectored 인터럽트가 사용할 인터럽트 벡터 테이블의 주소를 저장합니다. 여기서 코어가 지원하는 인터럽트 번호의 최대값에 따라mtvt
에 제한이 가해지는데, GD32VF103의 최대 인터럽트 번호인 86을 기준으로mtvt
의 값은 512의 배수여야 합니다.mtvt2
레지스터는 CLIC 모드에서 non-vectored 인터럽트가 사용할 공통 인터럽트 핸들러의 주소를 설정합니다. 또한MTVT2EN
필드를 통해 별도의 공통 인터럽트 핸들러를 정의하는 대신 예외 핸들러와 공통 핸들러를 공유하는 방식도 지원합니다.mtvec
과 같이 주소는 하위 2비트를 빼고 저장하기 때문에 주소를 4바이트로 정렬해야 합니다.
필드 | 비트 | 설명 |
---|---|---|
COMMON-CODE-ENTRY | 31:2 | MTVT2EN 이 1일 때 사용할 non-vectored 인터럽트 핸들러의 주소 |
Reserved | 1 | Reserved 0 |
MTVT2EN | 0 |
mtvt2 활성화
|
mcause
레지스터는 트랩이 발생했을 때 정보를 저장합니다. 즉, 핸들러 내에서 이 레지스터를 확인하여 어떤 상황에서 어떤 이유로 발생했는지 알 수 있습니다.
필드 | 비트 | 설명 |
---|---|---|
INTERRUPT | 31 |
트랩 유형
|
MINHV | 30 | 인터럽트 벡터 테이블 읽기 여부 (CLIC 모드에서만 작동) |
MPP | 29:28 |
발생 시점의 권한 모드
|
MPIE | 27 | 발생 시점의 인터럽트 활성화 여부 |
Reserved | 26:24 | Reserved 0 |
MPIL | 23:16 | 이전 인터럽트 레벨 |
Reserved | 15:12 | Reserved 0 |
EXCCODE | 11:0 | 발생한 트랩 코드 |
mnvec
레지스터는 NMI 핸들러의 주소를 저장합니다. 그런데 이 레지스터는 읽기 전용이므로mmisc_ctl
레지스터를 이용해 값을 수정해야 합니다.mmisc_ctl
레지스터는 NMI 설정을 수정하는 데 사용합니다.
필드 | 비트 | 설명 |
---|---|---|
Reserved | 31:10 | Reserved 0 |
NMI_CAUSE_FFF | 9 |
mnvec 및 NMI의 트랩 코드(mcause.EXCCODE ) 설정
|
Reserved | 8:0 | Reserved 0 |
mepc
레지스터는 트랩이 발생한 시점의 프로그램 카운터를 저장합니다.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)
|
Reserved | 2:0 | Reserved 0 |
mintstatus
레지스터는 현재 인터럽트 레벨인터럽트 레벨은 인터럽트 중첩(nesting)과 관련된 개념으로, 어떤 인터럽트가 발생하여 그 핸들러를 실행하는 도중 그 인터럽트보다 레벨이 높은 인터럽트가 발생하면 원래 핸들러를 멈추고 새 인터럽트의 핸들러로 점프하게 됩니다. 자세한 내용은 ECLIC 레지스터에서 설명하겠습니다.을 저장합니다.
필드 | 비트 | 설명 |
---|---|---|
MIL | 31:24 | 머신 모드 인터럽트 레벨 |
Reserved | 23:8 | Reserved 0 |
UIL | 7:0 | 유저 모드 인터럽트 레벨 |
msubm
레지스터는 머신 서브 모드를 저장합니다.
필드 | 비트 | 설명 |
---|---|---|
Reserved | 31:10 | Reserved 0 |
PTYP | 9:8 |
트랩 발생 전 머신 서브 모드
|
TYP | 7:6 |
현재 머신 서브 모드
|
Reserved | 5:0 | Reserved 0 |
인터럽트 핸들러에 들어갈 때
이제 기본적인 개념을 다 다루었으므로 본격적으로 CLIC 모드에서 인터럽트가 발생할 때 어떤 일들이 일어나는지 살펴보도록 합시다. 인터럽트가 발생하면 코어는 다음 동작을 1 사이클 내에 수행합니다.
- CSR 업데이트
mepc
mstatus
mcause
mintstatus
msubm
- 권한 모드를 머신 모드로 변경
- 머신 서브 모드를 인터럽트 처리 모드로 변경
- 인터럽트 핸들러로 점프
CSR이 어떻게 업데이트되는지 자세히 설명하자면 다음과 같습니다.
mepc
에 현재 PC 값을 저장합니다.mcause.EXCCODE
에 발생한 인터럽트의 코드를 저장합니다.mcause.MPIE
에mstatus.MIE
를 복사하고mstatus.MIE
를 0으로 설정합니다. 즉, 인터럽트를 비활성화합니다.mcause.MPP
에 권한 모드를 저장하고 권한 모드를 머신 모드로 변경합니다.mcause.MPIL
에mintstatus.MIL
을 복사하고mintstatus.MIL
에 발생한 인터럽트의 레벨을 저장합니다.msubm.PTYP
에msubm.TYP
을 복사하고msubm.TYP
에 1을 저장합니다. 즉, 인터럽트 처리 모드로 전환합니다.- 추가로 만약 vectored 인터럽트라면, 인터럽트가 발생한 시점에
mcause.MINHV
를 1로 설정하고 인터럽트 벡터 테이블을 참조하여 핸들러로 점프했을 때 0으로 설정합니다. 이를 활용해 예외 등이 벡터 테이블을 읽는 도중에 발생했는지 판단할 수 있습니다.
인터럽트 핸들러에서 나올 때
인터럽트 핸들러는 종료 시 mret
명령을 실행합니다. 이때 코어는 다음 동작을 1 사이클 내에 수행합니다.
mepc
에 저장된 값을 PC에 불러와 인터럽트가 발생하기 전 위치로 점프합니다.mcause.MPIL
에 저장된 값을mintstatus.MIL
에 불러와 현재 인터럽트 레벨을 복구합니다.mcause.MPIE
에 저장된 값을mstatus.MIE
에 불러와 인터럽트 활성화 여부를 복구합니다.mcause.MPP
에 저장된 값을 불러와 권한 모드를 복구합니다.msubm.PTYP
에 저장된 값을msubm.TYP
에 불러와 머신 서브 모드를 복구합니다.
예외와 NMI 역시 핸들러에 들어갈 때와 핸들러를 나올 때 거의 동일한 동작을 수행합니다. 다만 인터럽트 레벨이라는 개념이 없으므로 mcause.MPIL
과 mintstatus.MIL
을 건드리지 않는다는 차이가 있습니다.
구현
이 글에서는 CLIC 모드를 사용하고, non-vectored 인터럽트 핸들러를 별도로 정의하며 NMI 발생 시 예외 핸들러를 호출하도록 설정하겠습니다. 따라서 리셋 핸들러의 제일 처음에 CSR을 설정하는 코드를 추가합니다.
mmisc_ctl
을 수정하여mnvec
을 설정한다.mtvt
에 인터럽트 벡터 테이블 주소를 저장한다.mtvt2
를 활성화하고 non-vectored 인터럽트 핸들러 주소를 설정한다.mtvec
에 예외 핸들러 주소를 저장하고 CLIC 모드로 전환한다.
#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
섹션도 추가하고요.
SECTIONS
{
/* Flash sections. */
.start :
{
KEEP(*(.start))
} >FLASH
.handler :
{
KEEP(*(.handler))
} >FLASH
.vtable ALIGN(512) :
{
KEEP(*(.vtable))
} >FLASH
/* ... */
}
또한 부트 코드에서 CSR 주소를 사용할 수 있게 GD32VF103이 사용하는 CSR의 주소를 헤더 파일로 정의합니다.
#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
과 같이 입력합니다.
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
테스트
핸들러가 잘 설정되었는지 테스트해보기 위해 예외를 일으키는 코드를 작성하였습니다. 글 제목에 맞게 인터럽트를 테스트하면 더 좋겠지만, 아직 인터럽트 설정을 다루지 않았기 때문에 예외로 대신합니다.
#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에 해당합니다.
참고 문헌
- RISC-V Bytes: Privilege Levels
- Nuclei ISA Spec - 6. Interrupt Handling in Nuclei processor core
- Nuclei ISA Spec - 16. Nuclei processore core CSRs Descriptions