ECLIC과 SysTick 인터럽트

2023년 6월 21일

개요

앞 글에서 인터럽트 핸들러를 사용할 수 있도록 CSR 및 벡터 테이블 등을 설정하는 방법을 알아보았습니다. 다만 인터럽트 활성화는 다루지 않아 대신 예외를 일으켜 예외 핸들러로 진입하는 것만 관찰했습니다. 이 글에서는 본격적으로 인터럽트를 활성화해보고, SysTick 타이머 인터럽트 핸들러를 작성해보겠습니다.

ECLIC

ECLIC(Enhanced Core Local Interrupt Controller)는 Nuclei 코어에서 제공하는 고속 인터럽트 처리용 컨트롤러로, CLIC 모드 하에서 모든 인터럽트 처리와 인터럽트 소스별 활성화 및 우선순위 설정을 담당합니다. ECLIC 레지스터들은 메모리 주소 0xD200_0000에 매핑되어 있습니다.

ECLIC 레지스터 목록

ECLIC 레지스터 목록
오프셋 크기(비트) 이름
0x0000 8 cliccfg
0x0004 32 clicinfo
0x000b 8 mth
0x1000 + 4×i 8 clicintip[i]
0x1001 + 4×i 8 clicintie[i]
0x1002 + 4×i 8 clicintattr[i]
0x10003 + 4×i 8 clicintctl[i]
cliccfg 레지스터
필드 비트 설명
Reserved 7:5 Reserved 0
nlbits 4:1 clicintctl[i] 레지스터의 필드별 비트 폭 설정
Reserved 0 Reserved 0
clicinfo 레지스터
필드 비트 설명
Reserved 31:25 Reserved 0
CLICINTCTLBITS 24:21 clicintctl[i] 레지스터의 유효 비트 폭(GD32VF103 기준 4비트)
VERSION 20:13 하드웨어 버전
NUM_INTERRUPT 12:0 지원하는 인터럽트 소스 개수(GD32VF103 기준 87개)

이하 네 레지스터는 인터럽트 소스별로 존재합니다. GD32VF103은 인터럽트 소스가 87개이므로, 각각 [0]부터 [86]까지 존재한다고 생각하면 됩니다.

clicintip[i] 레지스터
필드 비트 설명
Reserved 7:1 Reserved 0
IP 0 인터럽트 보류 여부(기본값 0)
clicintie[i] 레지스터
필드 비트 설명
Reserved 7:1 Reserved 0
IE 0 인터럽트 활성화 여부(기본값 0)
clicintattr[i] 레지스터
필드 비트 설명
Reserved 7:3 Reserved to 0b11000
trig 2:1 인터럽트 발생 조건 설정(기본값 0)
shv 0 인터럽트 모드 설정
  • 0: non-vectored 인터럽트로 설정(기본값)
  • 1: vectored 인터럽트로 설정

인터럽트 활성화

인터럽트 활성화에는 앞에서 살펴본 CSR 중 mstatus.MIE 플래그와 ECLIC의 clicintie[i].IE 플래그가 관여합니다. 전자는 전체 인터럽트 활성화 여부를 결정하고, 후자는 개별 인터럽트 소스의 활성화 여부를 결정하므로 두 플래그가 모두 1이어야 인터럽트가 활성화됩니다.

인터럽트 발생 조건

코어에는 각 인터럽트 소스별로 인터럽트 신호선이 존재하여 소스로부터 high(1) 또는 low(0) 신호를 받도록 되어있습니다. clicintattr[i].trig는 이 신호의 어떤 시점에 인터럽트가 발생하는지 결정합니다.

인터럽트 레벨과 우선순위

ECLIC는 인터럽트 소스별로 레벨과 우선순위를 가집니다. 레벨은 인터럽트 선점에 관여하는 요소로, 이미 인터럽트를 처리 중이더라도 레벨이 더 높은 인터럽트가 발생하면 기존 인터럽트 처리를 중단하고 새 인터럽트를 처리합니다. 반대로 레벨이 더 낮은 인터럽트가 발생한 경우엔 기존 인터럽트 처리가 끝날 때까지 기다렸다가, 대기하던 인터럽트 중 가장 레벨이 높은 것을 처리합니다. 만약 그런 인터럽트가 여러 개라면, 그 때는 인터럽트 우선순위를 비교하여 우선순위가 제일 높은 것을 처리하게 됩니다.

인터럽트 레벨과 우선순위는 clicintctl[i] 레지스터로 설정하는데, 방식이 다소 복잡합니다. 이 레지스터는 원래 8비트이지만, 실제로 사용하는 비트는 상위 clicinfo.CLICINTCTLBITS비트만 사용하고 나머지 하위 비트는 모두 1입니다. 그 중에서도 레벨은 상위 cliccfg.nlbits비트에 해당하고 나머지가 우선순위입니다. 예를 들어 CLICINTCTLBITS가 5고 nlbits가 3인 경우에 clicintctl[i]는 다음과 같은 구조를 가집니다.

clicintctl[i] 레지스터 구조 예시

비교 시 사용하는 실제 레벨 값은 상위 nlbits비트를 제외한 나머지 비트를 모두 1로 채웠을 때의 값입니다. 즉, 위 예시에서 레벨 필드를 0b101로 설정하면 실제 레벨 값은 우선순위가 어떻든 0b10111111로 해석한 191입니다. nlbits에 따른 가능한 레벨 값의 목록은 다음과 같습니다.

cliccfg.nlbits에 따른 가능한 레벨 값 목록
nlbits 가능한 레벨 값
0 255
1 127, 255
2 63, 127, 191, 255
3 31, 63, 95, 127, 159, 191, 223, 225

nlbits가 0이면 레벨 값이 항상 255인 점에 주의합시다. 레벨과 유사하게 우선순위 또한 하위 비트를 모두 1로 채운 값을 사용합니다. 위 예시에서 우선순위 필드를 0b01로 설정하면 실제 우선순위 값은 0b01111로 해석한 15입니다. 마찬가지로 nlbitsCLICINTCTLBITS이 같으면 우선순위는 항상 28−CLICINTCTLBITS−1입니다.

GD32VF103에서는 CLICINTCTLBITS가 4로 고정되어 있습니다.

SysTick 타이머

ARM 마이크로프로세서처럼 Nuclei 코어 또한 SysTick 타이머를 지원합니다. SysTick 타이머는 TIMER 모듈이 담당하며 레지스터는 메모리 주소 0xD100_0000에 매핑되어 있습니다.

TIMER 레지스터 목록

TIMER 레지스터 목록
오프셋 크기(비트) 이름
0x000 32 mtime_lo
0x004 32 mtime_hi
0x008 32 mtimecmp_lo
0x00C 32 mtimecmp_hi
0xFF8 32 mtimectl
0xFFC 32 msip
mtimectl 레지스터
필드 비트 설명
Reserved 31:1 Reserved 0
TIMESTOP 0 카운터 동작 설정
  • 0: 카운터 동작(기본값)
  • 1: 카운터 정지
msip 레지스터
필드 비트 설명
Reserved 31:1 Reserved 0
MSIP 0 1로 설정하면 소프트웨어 인터럽트가 발생

구현

먼저 항상 하던대로 헤더 파일을 만듭니다. 인터럽트 소스별로 존재하는 레지스터들은 저렇게 ECLIC_TypeDef 구조체 내부에 다른 구조체로 묶어서 배열로 선언했습니다. clicintip[i]가 아니라 clicint[i].ip로 접근해야 한다는 건 불편하지만, C에서 큰 트릭 없이 구조체만으로 접근할 수 있도록 하기엔 이게 최선인 것 같습니다. 인터럽트 번호는 열거형 IRQn_Type으로 정의했습니다.

gd32vf103_eclic.h

#define MAX_IRQN 87

typedef struct {
    volatile uint8_t cliccfg;
    uint8_t reserved0[3];
    volatile uint32_t clicinfo;
    uint8_t reserved1[3];
    volatile uint8_t mth;
    uint8_t reserved2[4084];
    struct {
        volatile uint8_t ip;
        volatile uint8_t ie;
        volatile uint8_t attr;
        volatile uint8_t ctl;
    } clicint[MAX_IRQN];
} ECLIC_TypeDef;

typedef enum {
    /* Internal interrupts. */

    SFT_IRQn            = 3,
    TMR_IRQn            = 7,
    BUS_ERR_IRQn        = 17,
    PERF_MON_IRQn       = 18,

    /* External interrupts. */

    WWDGT_IRQn          = 19,
    LVD_IRQn            = 20,
    TAMPER_IRQn         = 21,

    // ...

    USBFS_IRQn          = 86,
} IRQn_Type;

#define ECLIC ((ECLIC_TypeDef *)0xD2000000)

#define ECLIC_CLICCFG_NLBITS_Pos            (1U)
#define ELCIC_CLICCFG_NLBITS_Msk            (0xFUL << ECLIC_CLICCFG_NLBITS_Pos)
#define ECLIC_CLICCFG_NLBITS                ELCIC_CLICCFG_NLBITS_Msk

// ...
gd32vf103_timer.h

typedef struct {
    volatile uint32_t mtime_lo;
    volatile uint32_t mtime_hi;
    volatile uint32_t mtimecmp_lo;
    volatile uint32_t mtimecmp_hi;
    uint32_t reserved0[1018];
    volatile uint32_t mtimectl;
    volatile uint32_t msip;
} TIMER_TypeDef;

#define TIMER ((TIMER_TypeDef *)0xD1000000)

#define TIMER_MTIMECTL_TIMESTOP_Pos     (0U)
#define TIMER_MTIMECTL_TIMESTOP_Msk     (0x1UL << TIMER_MTIMECTL_TIMESTOP_Pos)
#define TIMER_MTIMECTL_TIMESTOP         TIMER_MTIMECTL_TIMESTOP_Msk

#define TIMER_MSIP_MSIP_Pos             (0U)
#define TIMER_MSIP_MSIP_Msk             (0x1UL << TIMER_MSIP_MSIP_Pos)
#define TIMER_MSIP_MSIP                 TIMER_MSIP_MSIP_Msk

main에서 먼저 할 일은 ECLIC을 설정한 뒤 인터럽트를 활성화하는 것입니다. 그 다음에는 타이머 인터럽트가 잘 작동하는지 볼 수 있게 LED를 1초마다 깜빡이는 코드를 작성했습니다. 위에서 말했듯이 카운터는 별도로 클럭 설정을 하지 않으면 2 MHz로 작동하고, 0부터 시작하여 mtimecmp보다 커지면 인터럽트가 발생하므로 mtimecmp의 값을 1999로 설정하면 인터럽트가 1 ms마다 발생하게 만들 수 있습니다.

이전 글에서 벡터 테이블을 만들 때 타이머 인터럽트(3번)에는 tmr_irq_handler라는 이름으로 인터럽트 핸들러를 선언하였으므로 동일한 이름으로 C 코드에 정의해야 합니다. 단, 인터럽트 핸들러는 일반적인 함수처럼 처음과 끝에서 레지스터를 스택에 저장하고 복구하는 작업을 하는 대신 mret 등의 특별한 처리를 요구하므로 __attribute__((interrupt))를 붙여 컴파일러가 인터럽트 핸들러라고 인식하게 해야 합니다. 핸들러에서는 인터럽트 횟수를 세는 변수 systick을 증가시키고, 카운터를 0으로 초기화하는 작업을 해줍니다.

마지막으로 대기용 함수 delay를 이전처럼 nop를 반복하는 게 아니라 타이머에 따라 ms 단위로 대기할 수 있게 수정했습니다. 여기서 wfi는 다음 인터럽트가 발생할 때까지 코어가 슬립 모드에서 대기하는 명령어입니다.

main.c

#define DELAY_MS 1000

static volatile uint32_t systick;

static void delay(uint32_t count);

int main(void) {
    /* Enable interrupt globally. */
    __asm__ volatile ("csrs 0x300, 0x8");
    /* Set nlbits to 3. (3 level bits and 1 priority bit) */
    ECLIC->cliccfg = 0x3 << ECLIC_CLICCFG_NLBITS_Pos;

    /* Set SysTick interrupt frequency to 1000 Hz. */
    TIMER->mtimecmp_lo = 2000 - 1;
    TIMER->mtimecmp_hi = 0;
    /* Reset the counter. */
    TIMER->mtime_lo = 0;
    TIMER->mtime_hi = 0;

    /* Enable SysTick interrupt. */
    ECLIC->clicint[TMR_IRQn].ie = 1;
    /* Set SysTick interrupt level and priority to their minimum. */
    ECLIC->clicint[TMR_IRQn].ctl = 0x0F;
    /* Make SysTick interrupt be a vectored interrupt. */
    ECLIC->clicint[TMR_IRQn].attr |= 1 << ECLIC_CLICINTATTR_SHV_Pos;

    /* Enable GPIOC. */
    RCU->APB2EN |= RCU_APB2EN_PCEN;

    /* Output, max speed 50 MHz, push-pull. */
    GPIOC->CTL1 &= ~(GPIO_CTL1_MD13 | GPIO_CTL1_CTL13);
    GPIOC->CTL1 |= (0x3U << GPIO_CTL1_MD13_Pos);

    while (1) {
        /* Turn on red. */
        GPIOC->BC = GPIO_BC_CR13;
        delay(DELAY_MS);

        /* Turn off red. */
        GPIOC->BOP = GPIO_BOP_BOP13;
        delay(DELAY_MS);
    }
}

static void delay(uint32_t ms) {
    uint32_t start = systick;

    while (systick - start < ms) {
        __asm__ volatile ("wfi");
    }
}

__attribute__((interrupt)) void tmr_irq_handler(void) {
    systick++;

    /* Reset the counter. */
    TIMER->mtime_lo = 0;
    TIMER->mtime_hi = 0;
}

참고 문헌