본문 바로가기
임베디드/STM32

14. [STM32] Interrupt 구현하기

by fuhehe 2024. 10. 10.

앞에서 인터럽트와 인터럽트에 사용되는 레지스터에 대해 알아보았다.

이번장에서는 STM32에 구현되어 있는 인터럽트 중 내부 인터럽트와 외부 인터럽트를 나누어 살펴보고 실제 인터럽트를 소스로 구현해보자. 이전 Hello World 글에서 이미 인터럽트를 사용했지만 복습 차원에서 한번더 간단하게 살펴볼 것이다.



 

  • 본 블로그는 STM32를 소프트웨어 엔지니어 관점에서 바라본 블로그입니다. 따라서 회로등의 전자공학 관련 내용은 사실과 다를 수 있습니다.
  • 본 블로그에서 사용된 MCU 및 개발보드는 다음과 같습니다.
    1. NUCLEO-F429ZI (STM32F429ZIT LQFP144)
    2. NUCLEO-F439ZI (STM32F439ZIT LQFP144)

 

 

1. STM32의 인터럽트

STM32의 인터럽트는 레퍼런스 매뉴얼의 375p의 테이블에 나와있다. 우리가 사용하는 STM32F429/439의 경우 378p부터 나오는 테이블을 참조하자.

 

내부 인터럽트는 매뉴얼의 테이블에서 앞쪽에 회색으로 칠해진 몇가지 인터럽트이고 그 외에는 외부 인터럽트(External Interrupt : EXTI)이다.

[STM32F4xx의 인터럽트 일부]

 

내부 인터럽트의 경우  ARM에서 정해놓은 인터럽트로 Exception이라 부르기도 한다.

외부 인터럽트의 경우 위의 표와 같이 STM32에 각 용도별로 인터럽트를 지정해 놓기는 했지만 사용자에 의해 변경가능한 인터럽트이다.

 

 

2. 내부 인터럽트

내부 인터럽트를 하나씩 살펴보자.

  • Reset : 우선순위가 가장 높은 인터럽트로 시스템 On 또는 Reset시 발생되는 인터럽트이다. 이 Reset인터럽트가 발생하면 MCU는 인터럽트 벡터 테이블 정의등 초기화 작업을 진행한다. 자세한 내용은 Entry Point 장을 참조하라.
  • NMI(Non-Maskable Interrupt) : 이름대로 마스크 불가능한, 즉 인터럽트를 발생하지 않도록 마스킹 할 수 없는 인터럽트로 MCU에 치명적인 예외 사항이 발생했을때 발생하는 인터럽트이다.
  • HardFault : NMI보다는 치명적이지 않지만 더이상 수행이 불가능한 상태의 오류발생시 호출된다.
  • MemManage : 메모리의 특정영역에 대한 엑세스를 제한해 놓았을 때 이를 액세스 시도하면 발생하는 인터럽트이다.
  • BusFault : 각 메모리 Read/Write과정에서 예외가 발생하는 경우 이 인터럽트가 호출된다.
  • UsageFault : 실행 프로그램의 Instruction이 실행할 수 없는 Instruction인 경우 발생하는 인터럽트이다.
  • SVCall(Supervisor Call) : SWI명령어에 의해 System Service Call이 발생시 호출되는 인터럽트로서 주로 RTOS상에서 OS커널함수와 장치 드라이버에 엑세스하기 위한 S/W인터럽트이다.
  • DebugMonitor : 디버깅을 할 때 Break Point등을 설정할 때 발생하는 인터럽트이다.
  • PendSV : RTOS에서 각 프로세스간 Context Switching시 발생하는 S/W 인터럽트.
  • SysTick : System Tick Timer에 의해 발생되는 인터럽트.

여기서 다른 인터럽트는 임의로 발생시키기가 어려우나 HardFault 인터럽트의 경우 소스로 발생시킬 수 있으므로 한번 해보자.

CubeMX로 소스를 만들었다면 이미 이들 인터럽트에 대한 핸들러가 작성되어 있는데 stm32f4xx_it.c파일 위쪽에서 찾을 수 있다.

NMI_Handler(), HardFault_Handler(), MemManage_Hander()등 MCU가 더이상 코드를 실행할 수 없는 상태에서 발생하는 인터럽트의 경우 내부에 while(1) 무한루프가 돌도록 코딩되어 있다. 이는 중대한 오류로 인해 MCU가 더이상 코드를 실행하지 못하게 하기 위함이다.

물론 우리는 이쪽에 직접 코딩을 해서 별도 처리를 해도 된다.

 

이 파일에서 우리가 발생시킬 HardFault 인터럽트 핸들러인 HardFault_Handler()함수도 찾을 수 있다.

 

HardFault인터럽트를 발생시키기 위해 우선 main.c의 while루프 안에 다음과 같이 코딩하자. 

수학적으로 불가능한 0으로 나누는 Divide by zero 오류를 발생시키기 위함이다.

  while (1)
  {
	  int z = 10 / 0;
  }

 

코딩 후 F11키를 눌러 디버그 모드에서 코드를 실행해보자(HAL_Init()에서 Break Point가 걸리면 F8을 눌러 계속 진행시키면 된다.)

그러면 MCU는 즉각 HardFault 인터럽트를 발생시킨다. stm32f4xx_it.c의 HardFault_Handler()에 Break가 걸리는지 확인해보라.

 

 

3. 외부 인터럽트 소스 구현

먼저 CubeMX에서 인터럽트를 사용할 Pin의 GPIO Mode를 Interrupt모드로 변경해야 한다.

이 모드를 선택하기 위해서는 STM32이미지가 있는 Pinout View화면에서 해당 핀을 찾아 클릭하면 나오는 팝업메뉴에서 GPIO_EXTIxx 를 선택해야 한다.

그리고 왼쪽 Categories에서 GPIO를 선택하고 GPIO화면에서 해당 핀의 mode를 바꿔주면 된다.

[CubeMX 설정]

 

 

이번 예제에서는 User Button이 연결된 PC13과 다른 하나로 PE3 핀에 대해 인터럽트 핀설정을 하고 소스를 작성해 보겠다. 위의 방식대로 CubeMX에서 PC13과 PE3에 대해 인터럽트를 설정한 뒤 Categories에서 NVIC를 선택하고 PC13과 PE3의 인터럽트 Enabled에 체크를 해야 한다.

[NVIC 설정]

CubeMX설정이 완료되면 저장해서 소스를 생성하자.

그러면 인터럽트 발생시 인터럽트 레지스터 장에서 설명한 함수 호출 순서대로 마지막에 우리가 선언한 HAL_GPIO_EXTI_Callback()함수가 실행될 것이다.

이 상태에서 User Button을 눌러 PC13핀의 EXTI [15:10] 인터럽트가 발생될 때의 순서를 알아보자. 

  • 인터럽트 발생시 MCU는 0x0000 0000부터 시작되는 인터럽트 벡터 테이블에서 해당 IRQ번호에 해당하는 ISR(Interrupt Service Routine)의 주소를 찾는다. 
  • EXTI 15_10인터럽트의 ISR주소는 0x0000 00E0이다. (레퍼런스 매뉴얼 377p)

[EXTI15_10의 ISR주소]

 

  • 실행 후 디버깅을 해보면 아래와 같이 0x0000 00E0의 주소값에는 0x0800 0CA5값이 들어있다.

[인터럽트 벡터 테이블 0x0000 00E0값]

  • 메모리에서 0x0800 0CA5위치에는 stm32f4xx_it.c파일에 정의된 void EXTI15_10_IRQHandler()함수의 코드가 위치해 있다. (아래 이미지에서 EXTI15_10_IRQHandler()함수안에서 HAL_GPIO_EXTI_IRQHandler()호출 부분에 Break Point를 걸어 확인했으므로 실제 디버깅상에 표시되는 주소는 약간의 오차가 있지만 시작주소는 0x0800 0CA5이다.)

[EXTI15_10의 ISR]

  • 일단 EXTI15_10의 ISR인 EXTI15_10_IRQHandler()가 호출되면 이 함수 안에서는 다시 HAL_GPIO_EXTI_IRQHandler()함수를 호출한다.(stm32f4xx_hal_gpio.c)
  • HAL_GPIO_EXTI_IRQHandler()함수에서는 이전장에서 설명한 EXTI_PI(Pending Register)를 리셋하기 위한 __HAL_GPIO_EXTI_CLEAR_IT() 매크로 함수를 실행한 뒤다시 HAL_GPIO_EXTI_Callback()함수를 호출한다.
  • HAL_GPIO_EXTI_Callback()함수는 같은 파일의 바로 밑에 __weak형태로 정의되어 있고 안에는 아무런 코딩이 되어 있지 않다.
  • __weak형태이므로 필요할 경우 우리는 이 함수를 재정의해서 EXTI15_10인터럽트가 발생했을 때 최종적으로 우리가 재정의한 함수를 실행하도록 코딩한다.

현재 우리는 PC13과 PE3핀 두개에 대해 인터럽트 설정을 했는데 PE3도 위와 마찬가지이다.

다른점은 PE3의 경우 EXTI [15:10]이 아니라 EXTI[3] 인터럽트를 사용하므로 인터럽트 벡터테이블의 내용대로 0x0000 0064위치에 있는 ISR이 실행된다는 것이다.

이 ISR은 stm32f4xx_it.c의 EXTI15_10_IRQHandler()함수가 정의된 위치 바로 위에 EXTI3_IRQHandler()로 정의되어 있다.

하지만 이 함수내부를 보면 PC13과 동일하게 HAL_GPIO_EXTI_IRQHandler()함수를 호출하고 있으므로 이 이후부터는 동일한 루틴을 탄다.

따라서 우리가 작성한 HAL_GPIO_EXTI_Callback()함수의 파라미터 GPIO_Pin으로 넘어오는 핀번호를 구분해서 각 인터럽트에 맞는 루틴을 작성해주면 된다.

 

다음은 __weak 형태의 HAL_GPIO_EXTI_Callback()함수를 main.c에 재정의한 예제이다.

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == GPIO_PIN_13) {
		// PC13 인터럽트 처리
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);		
	}
	else if(GPIO_Pin == GPIO_PIN_3) {
		// PE3 인터럽트 처리
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
	}
}

 

인터럽트 처리 루틴은 이게 전부다.

인터럽트 벡터 테이블, 디버깅등의 내용을 좀 더 알고 싶다면 Entry Point 장, 디버깅 장, 인터럽트 개요 장 등을 참조하면서 보기 바란다.

 

 

4. 외부 인터럽트 핸들러에서 HAL_Delay()사용

이번에는 인터럽트 핸들러 HAL_GPIO_EXTI_Callback()함수안에서 다음과 같이 HAL_Delay()함수를 사용해보자.

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == GPIO_PIN_13) {		
		HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
		HAL_Delay(1000);
		HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
	}
}

 

단순히 EXTI [15:10] 인터럽트가 발생했을때 보드의 Green LED를 1초동안 켰다 끄는 소스이다.

하지만 실행해보면 LED가 켜지기는 하지만 꺼지지 않는다.

왜 그럴까?

 

이유를 알려면 먼저 HAL_Delay()가 어떻게 구현되어 있는지를 봐야 한다.

HAL_Delay() 소스를 보면 내부에서 HAL_GetTick()함수를 두번 호출하는데 첫번째 시간을 저장해 두고 while안에서 두번째 시간을 알아낸 뒤 첫번째 시간과의 차이를 판단해 주어진 파라미터 만큼의 시간이 경과했는지를 체크해서 시간이 지났으면 종료하고 그렇지 않으면 계속 while문을 수행한다.

 

이 HAL_GetTick()함수는 전역으로 선언된 uwTick이라는 uint32_t 변수값만 리턴하는데 이 변수는 HAL_IncTick()이라는 함수에서 값을 증가시킨다.

HAL_IncTick()은 stm32f4xx_it.c파일에 보면 SysTick_Handler() 함수가 있는데 이 함수는 SysTick 인터럽트가 발생할 때 마다 호출되는 핸들러이고 이 핸들러에서 HAL_IncTick()을 매번 호출하도록 되어 있다.

 

그런데 이 SysTick인터럽트는 디폴트로 우선순위가 0으로 설정된다. 이 내용은 main()에서 호출하는 HAL_Init()함수를 보면 나오는데 이 함수에서  HAL_InitTick(TICK_INT_PRIORITY); 와 같이 호출한다. 여기서 TICK_INT_PRIORITY는 CubeMX의 NVIC화면에서 설정한 Priority를 그대로 가져온다. 그런데 CubeMX에 표시되는 Priority는 모든 인터럽트가 디폴트값이 0으로 지정되어 있다.

 

HAL_InitTick() 함수에서는 이전장의 레지스트리에서 봤듯 HAL_NVIC_SetPriority()함수를 호출해서 NVIC_IPRx레지스터를 설정한다. 그런데 주어진 파라미터인 TICK_INT_PRIORITY define은 0으로 설정되어 있다.

 

즉, SysTick인터럽트의 우선순위는 0인 것이다.

 

그리고 우리가 CubeMX의 NVIC설정시 EXTI line[15:10]설정에 대해 아무것도 건드리지 않았다면 이 인터럽트의 Preemption Priority도 0으로 설정된다.

동일한 우선순위의 인터럽트가 발생했을 때 선점한 인터럽트의 핸들러가 실행완료되지 않았다면 다른 인터럽트는 실행이 미뤄지게 된다.

그래서 EXTI line[15:10] 인터럽트 핸들러가 실행되고 있고 내부에서 HAL_Delay()는 SysTick인터럽트에 의해 Tick이 1000ms증가 할때 까지 대기를 한다. 하지만 EXTI line[15:10] 인터럽트가 선점을 하고 있으므로 SysTick인터럽트 핸들러는 실행되지 못하므로 HAL_Delay()역시 while문에서 빠져나오지 못한다.

즉 서로가 서로를 기다리는 데드락 상태에 빠져 버린다.

 

이 현상을 해결하기 위해서는 EXTI line[15:10]의 우선순위를 SysTick보다 낮게 설정하거나 HAL_Delay()대신 인터럽트와 무관한 타이머를 사용해 별도 Delay함수를 구현해 사용해야 한다.

 

이게 헷갈리게 되어있는게 레퍼런스 매뉴얼상에는 SysTick 인터럽트의 우선순위가 일반 인터럽트보다 높게 설정되어 있지만 CubeMX에서는 각 인터럽트의 우선순위가 일괄적으로 0이 디폴트로 설정되어 있다. 따라서 이 우선순위 값을 바꿔주지 않으면 이런 현상이 발생할 수 있으므로 주의하자.

 

 

5. 마무리

지금까지 STM32의 인터럽트에 대해 알아보았다.

인터럽트는 일반적으로 main()의 while문 안에서 루프마다 상황을 체크하는 Polling방식과 달리 어떤 이벤트가 발생할 때만 ISR을 실행하므로 MCU의 부담을 줄일 수 있다.

또한 Polling방식에 비해 즉각적인 실행이 가능하므로 중요한 이벤트 체크시에는 Polling방식보다 인터럽트 방식으로 구성하는게 여러모로 유리하다. (GPIO 기초 장에 Polling 방식과 Interrupt방식의 차이에 대한 코딩부분이 있으므로 참조하자)

STM32F4xx의 경우 지원되는 인터럽트의 갯수가 넉넉하긴 하지만 사용할 수 있는 갯수가 정해져 있으므로 특정 신호의 용도에 따라 방식을 잘 선택해야 하며 이때 각 인터럽트의 우선순위도 잘 구성해야 한다.

그렇지 않으면 위의 HAL_Delay()사용 등과 같이 코드가 의도대로 돌아가지 않아 한참 헤매는 경우가 생길 수 있다.(사실 인터럽트 핸들러 내부에서 HAL_Delay()와 같이 인위적으로 실행을 지연시키는 행위는 바람직하지 않다. 핸들러에서는 최대한 빨리 필요한 작업만 하고 다른 인터럽트가 실행될 수 있도록 작성하는 것이 좋다)

 

추후 인터럽트의 실제 사용은 통신관련 장에서 더 살펴보기로 하고 여기서 설명을 마칠까 한다.

 

 

'임베디드 > STM32' 카테고리의 다른 글

16. [STM32] Power - Backup Domain Access  (0) 2024.11.07
15. [STM32] Power - Low Power Mode  (0) 2024.11.04
13. [STM32] Interrupt-Register  (0) 2024.09.26
12. [STM32] Interrupt 개요  (0) 2024.09.26
11. [STM32] SWV/ITM 을 사용한 디버깅  (0) 2024.09.23