JINTBEAT Design Life

ARM cortex-M3 ISR(Interrupt Service Routine) 구현 이론 정리(DTM) 본문

🖥️ - ARM

ARM cortex-M3 ISR(Interrupt Service Routine) 구현 이론 정리(DTM)

jintbeat_design 2025. 7. 22. 23:50
반응형
Interrupt Vector Table 이란 ?

 

인터럽트 벡터 테이블(Interrupt Vector Table)  인터럽트 발생 시 실행될 함수의 시작 주소를 모아놓은 테이블이다.
Cortex-M3에서는 이 테이블이 메모리 주소 0x00000000 번지에 위치해 있고, 리셋 벡터, 예외 핸들러, 외부 인터럽트 핸들러 들이 순서대로 저장되어 있다.

 

- Vector Table 구조 (간단 예시)

// 벡터 테이블 예시 (startup_cm3.s 또는 startup.c)
__Vectors:
    .word  _estack                // 초기 Stack Pointer 값
    .word  Reset_Handler          // 리셋 핸들러
    .word  NMI_Handler            // NMI 핸들러
    .word  HardFault_Handler      // 하드폴트 핸들러
    ...
    .word  TIMER0_IRQHandler      // Timer0 인터럽트 핸들러
    ...
  • __Vectors는 실제 Vector Table 이름이며, 링커 스크립트를 통해 0x00000000에 위치한다.
  • 각 .word 항목은 인터럽트나 예외 발생 시 호출할 함수의 주소이다.
  • TIMER0_IRQHandler는 타이머 인터럽트가 발생하면 실행될 함수이다.

- 중요한 조건 : ISR 함수 이름이다.

Cortex-M3에서는 Interrupt 번호가 정해져 있기 때문에, CMSIS를 사용할 경우 다음과 같은 이름이 자동 Mapping된다.

 

- TIMER0_IRQn = Vector Table상 N번 째 항목

-> void TIMER0_IRQHandler(void) 함수가 있어야 해당 인터럽트 발생 시 호출된다.

 

typedef void(*ISR_Handler)(void);

// 예: 실제 메모리 맵 벡터 테이블 (예제용)
__attribute__((section(".isr_vector")))
ISR_Handler vector_table[] = {
    (ISR_Handler)&_estack,        // 초기 스택 포인터
    Reset_Handler,                // 리셋 핸들러
    NMI_Handler,
    HardFault_Handler,
    ...
    TIMER0_IRQHandler,            // 이 자리에 우리가 작성한 ISR 등록
};

 

ISR이 호출되기 위한 조건

 

1. 함수 이름을 정확히 맞춰줘야 함.

2. 링커와 시작 코드가 내 ISR을 Vector Table에 반영해야 한다.

  • 대부분의 CMSIS 스타트업 코드에서는 기본적으로 __attribute__((weak))로 ISR이 선언되어 있고, 우리가 같은 이름으로 재정의하면 자동으로 연결된다.
CMSIS에서는 자동 연결되는 구조

CMSIS 기반 Startup code에서는 다음과 같이 연결되어 있다.

void __attribute__((weak)) TIMER0_IRQHandler(void) {
    while (1);  // 기본 핸들러 (사용자가 override 가능)
}

 

다음과 같은 함수를 작성하면 자동 연결된다.

void TIMER0_IRQHandler(void) {
    // 이 함수가 인터럽트 발생 시 실행됨
}

 

__xxx__ 또는 __xxx 식별자는 왜 쓰는 걸까?

 

- 일반적인 목적은 C/C++ 컴파일러나 라이브러리, CMSIS 같은 저수준 코드에서 특수 목적으로 사용되는 식별자라는 의미이다.

  • 일반 사용자 코드와 구분하기 위해 __를 붙인다.
  • 2개를 붙이는 이유는 충돌 방지 + 내부 구현 명시이다.
예시 별 의미
식별자 의미
__attribute__ GNU C 컴파일러 확장 문법: 함수나 변수에 특별한 속성을 부여함
__Vectors 벡터 테이블의 심볼 이름 (보통 링크 스크립트에서 참조)
__weak 또는 __attribute__((weak)) 약한 심볼(override 가능함)
__IO, __I, __O CMSIS에서 정의한 IO 접근 매크로 (volatile 의미)
__isr_vector 링커 스크립트에서 벡터 테이블이 위치할 섹션 이름

 

CMSIS 및 ARM 환경에서 자주 보이는 식별자들
__attribute__((section(".isr_vector"))) // 특정 섹션에 위치하도록 지정
__attribute__((weak))                   // 기본 구현, 오버라이드 가능
__STATIC_INLINE                        // CMSIS에서 쓰는 정적 인라인 정의
__NVIC_EnableIRQ(...)                 // NVIC 인터럽트 Enable

 

ISR 함수 작성

 

- 목적

ISR은 인터럽트가 발생했을 때 CPU가 자동으로 호출하는 핸들러 함수이다. 
이 함수 안에서 인터럽트 원인을 확인하고, 클리어하고, 원하는 동작을 수행한다. 

 

- 기본 구조

void TIMER0_IRQHandler(void) {
    // 1. 인터럽트 발생 여부 확인
    if (TIMER0->MIS & 0x1) {
        // 2. 인터럽트 클리어
        TIMER0->ICR = 0x1;

        // 3. 원하는 동작 수행
        LED ^= 1;  // 예: LED 토글
    }
}

 

1. 인터럽트 확인 : 인터럽트 발생 상태를 레지스터에서 읽어서 확인(MIS 레지스터 등)

2. 클리어 : ICR, INTCLR 등, 인터럽트 상태를 클리어하지 않으면 계속 인터럽트 발생

3. 사용자 동작 : 타이머가 끝났을 때 실행할 코드: LED 토글, 카운터 증가 등.

 

- 주요 레지스터 (예: ARM Dual Timer 기준)

레지스터 설명
LOAD 타이머 초기값 (카운트 다운 시작값)
VALUE 현재 카운트값
CONTROL 타이머 설정 (Enable, 모드, 인터럽트 등)
INTCLR or ICR 인터럽트 클리어
MIS Masked Interrupt Status (1이면 인터럽트 발생)

 

- 실제 예제(CMSIS + Dual Timer)

void TIMER0_IRQHandler(void) {
    // ① 인터럽트 발생 여부 확인
    if (DTIMER0->MIS & 0x1) {
        // ② 인터럽트 클리어
        DTIMER0->ICR = 0x1;

        // ③ 사용자 동작
        count++;         // 전역 변수 증가
        toggle_LED();    // 함수 호출 (예: GPIO 제어)
    }
}

 

- 주의 할 점

항목 설명
인터럽트 클리어를 꼭 해야 함 클리어 안 하면 ISR이 계속 호출돼 무한 루프에 빠짐
ISR 안에서는 시간 오래 걸리는 연산 피하기 ISR은 짧고 빠르게! 딜레이 루프, printf 등은 피함
전역 변수는 volatile로 선언 ISR과 main에서 공유하는 변수는 volatile 키워드 필수

 

volatile uint32_t count = 0;  // ISR과 main에서 공유

 

- 반복 동작 타이머 예시 흐름

 

  • 타이머가 설정한 시간만큼 흐르면
  • MIS 비트가 1이 됨 → 인터럽트 발생
  • NVIC가 TIMER0_IRQHandler() 호출
  • ISR에서:
    • MIS 비트 확인
    • ICR로 클리어
    • 사용자 정의 동작 수행

 

MIS(Masked Interrupt Status) : NVIC에서 마스크된 결과를 반영해 보여줌. 실제 인터럽트 동작 판단에 사용한다. 실제 인터럽트가 동작할 조건은 MIS가 1인 경우이기 때문에, ISR에서는 보통 MIS를 확인한다. 

 

NVIC에서 Interrupt 활성화

 

- NVIC란 ? 

Cortex-M3에는 NVIC라는 하드웨어 모듈이 있어서, 외부 인터럽트의 활성화, 우선 순위 설정, Pending 상태 관리 등을 담당한다. 

 

- 인터럽트를 사용하려면 꼭 해야 하는 일

1. NVIC에서 해당 인터럽트 "Enable"

2. 인터럽트 우선 순위 설정(필요한 경우)

3. ISR(Handler) 함수 정의는 앞서 완료됨.

 

기본 코드 : NVIC 설정 예시(CMSIS 사용 기준)

 

#include "core_cm3.h"  // NVIC 관련 함수 선언 포함

// 인터럽트 활성화
NVIC_EnableIRQ(TIMER0_IRQn);

// (선택) 인터럽트 우선순위 설정: 낮은 숫자가 높은 우선순위
NVIC_SetPriority(TIMER0_IRQn, 2);

TIMER0_IRQn은 CMSIS에서 제공하는 열거형(enum) 값으로, 해당 인터럽트의 번호를 의미한다. 

 

NVIC 함수 설명(CMSIS 기준)
함수 이름 설명
NVIC_EnableIRQ(IRQn) 해당 인터럽트를 Enable 상태로 설정
NVIC_DisableIRQ(IRQn) Disable 상태로 설정
NVIC_SetPriority(IRQn, priority) 우선순위 설정 (priority 값 작을수록 우선순위 높음)
NVIC_ClearPendingIRQ(IRQn) Pending 상태 제거
NVIC_SetPendingIRQ(IRQn) 강제로 인터럽트 Pending 상태로 만듦 (테스트용)

 

예시 : 타이머 인터럽트를 NVIC에 등록
// 인터럽트 번호는 보통 헤더파일에 이렇게 정의되어 있어요
typedef enum {
    ...
    TIMER0_IRQn = 18,
    ...
} IRQn_Type;

// 초기화 코드
void Timer0_NVIC_Init(void) {
    NVIC_SetPriority(TIMER0_IRQn, 2);   // 우선순위 2로 설정
    NVIC_EnableIRQ(TIMER0_IRQn);        // 인터럽트 활성화
}

 

위 코드는 보통 main() 함수에서 타이머 초기화 직후 호출한다.

 

직접 만든 SoC에서는 ?

 

CMSIS를 쓰지 않는 경우라면, NVIC 레지스터를 직접 제어해야 할 수도 있다. 예를 들면

#define NVIC_ISER0    (*(volatile uint32_t *)0xE000E100)  // Interrupt Set Enable Register
#define IRQ_TIMER0    18  // 인터럽트 번호

NVIC_ISER0 = (1 << IRQ_TIMER0);  // 인터럽트 Enable

하지만 이 방법은 CMSIS가 없는 bare-metal 환경에서 주로 사용된다.

 

인터럽트 우선순위란 ?

 

CM3에선느 인터럽트 우선순위가 0~255까지 표현 가능(8비트, 실제 구현에 따라 다를 수 있음)

값이 작을수록 우선 순위가 높다.

 

주의할 점

 

- ISR이 정의되지 않은 상태에서 NVIC를 Enable하면 HardFault 발생 가능

- 인터럽트 번호(TIMER0_IRQn)는 정확히 매칭되어야 함

- CMSIS가 없는 경우, NVIC 레지스터 직접 접근 시 비트 위치 주의

 

DTM 타이머 설정 - 기본 흐름

 

1. 카운트 시작 값 설정(LOAD)

2. 제어 레지스터 설정(CONTROL)

3. 인터럽트 Enable 설정 포함

4. 타이머 Enable 비트를 ON

5. 이후 타이머 만료 시 자동으로 인터럽트 발생

 

제어 레지스터(CONTROL) 설정 비트 예시
비트 위치 이름 설명
[7] Timer Enable 1: 타이머 동작 시작
[6] Periodic Mode 1: 주기 모드 (0: one-shot)
[5] Interrupt Enable 1: 인터럽트 발생 허용
[2:1] Prescale 분주 비트 (00: /1, 01: /16, 10: /256)
[0] 32-bit Mode 1: 32비트, 0: 16비트

 

타이머 초기화 예제
void DTM_Timer0_Init(uint32_t load_value) {
    DTIMER0->LOAD = load_value;    // 타이머 시작값 설정 (예: 1초 주기)

    DTIMER0->CONTROL =
        (1 << 7) |  // Timer Enable
        (1 << 6) |  // Periodic Mode (주기 반복)
        (1 << 5) |  // Interrupt Enable
        (0 << 2) |  // Prescale = /1
        (1 << 1);   // 32-bit 모드
}

 

예 : 50MHz Clock 기준 1초 주기를 원한다면 ?

load_value = 50,000,000; // 1초 = 50M 클럭
인터럽트 발생 흐름 요약

 

1. LOAD에 설정된 값부터 타이머가 감소(count down)

2. 0이 되면 MIS 비트가 1로 set 됨. 인터럽트가 NVIC로 전달됨.

3. NVIC는 TIMER0_IRQHandler() 호출

4. ISR에서 ICR에 1을 써서 인터럽트 클리어

 

전체 초기화 코드 흐름 정리
void Init_Timer0(void) {
    // 1. 타이머 설정
    DTM_Timer0_Init(50000000);  // 1초 주기 (50MHz 기준)

    // 2. NVIC 등록
    NVIC_SetPriority(TIMER0_IRQn, 2);
    NVIC_EnableIRQ(TIMER0_IRQn);
}

 

동작 테스트 예시(ISR까지 포함)
volatile uint32_t tick = 0;

void TIMER0_IRQHandler(void) {
    if (DTIMER0->MIS & 0x1) {
        DTIMER0->ICR = 0x1;  // 인터럽트 클리어
        tick++;              // 1초마다 tick 증가
    }
}

 

 

전체 흐름 요약!

 

1. 타이머 LOAD 설정 : 원하는 주기만큼 카운트 다운

2. CONTROL 설정 : Enable, Periodic, Interrupt 등 비트 설정

3. NVIC 설정 : 인터럽트 등록 및 우선 순위 설정

4. ISR 호출 : 인터럽트 발생 시 함수 실행

5. 인터럽트 클리어 : ICR에 1 써야 다음 인터럽트도 받을 수 있음.

 

 

반응형