Skip to content

Chapter 14

Hongjayeong-02 edited this page Oct 5, 2026 · 1 revision

14. EXTI & NVIC — External Interrupt

Goal

Ch07에서는 while(1) 안에서 PC13 button을 계속 읽는 Polling 방식을 사용했다.

이번에는 B1을 누르는 순간 hardware interrupt가 발생하도록 구성한다.

B1 Press
   ↓
PC13 LOW → HIGH
   ↓
EXTI13
   ↓
Pending Request
   ↓
NVIC
   ↓
Cortex-M4
   ↓
EXTI15_10_IRQHandler()
   ↓
UART Message
+
LD1 Toggle

Board:

B1  → PC13
LD1 → PB0

이번 NUCLEO-F439ZI의 B1은:

Released → LOW
Pressed  → HIGH

이므로 Rising Edge를 사용한다.


1. Polling vs Interrupt

Ch07:

while (1)
{
    if (get_btn_state())
    {
        led_on();
    }
}

CPU가 계속:

Button?
Button?
Button?
Button?
...

을 확인한다.

이번 Ch14:

CPU doing other work
       ↓

Button Event ↓ Interrupt ↓ ISR

CPU가 button을 계속 읽을 필요가 없다.


2. Interrupt Components

이번 실습에서 필요한 구성:

GPIO
↓
SYSCFG
↓
EXTI
↓
NVIC
↓
Vector Table
↓
ISR

각 역할:

Block 역할
GPIO PC13 pin input
SYSCFG PC13을 EXTI13에 연결
EXTI edge 검출 및 pending 생성
NVIC CPU interrupt enable / priority 관리
Vector Table IRQ와 ISR 주소 연결
ISR 실제 interrupt 처리 코드

이번 설정:

IMR13  = 1
→ EXTI13 interrupt unmask

RTSR13 = 1 → Rising edge detect

FTSR13 = 0 → Falling edge unused


6. Practice Code

Project:

Inc/
├── gpio.h
├── gpio_exti.h
└── uart.h

Src/ ├── gpio.c ├── gpio_exti.c ├── uart.c └── main.c

Ch07 gpio.c와 Ch10 uart.c를 재사용한다.


7. gpio_exti.h

#ifndef GPIO_EXTI_H__
#define GPIO_EXTI_H__

#include <stdint.h> #include "stm32f4xx.h"

/*

  • EXTI line 13 mask.
  • PC13 is connected to:
  • EXTI13
    

*/ #define LINE13 (1U << 13)

/*

  • Configure PC13 as an external interrupt source.
  • Board:
  • B1 → PC13
    
  • Trigger:
  • Rising Edge
    

*/ void pc13_exti_init(void);

#endif


8. gpio_exti.c

#include "gpio_exti.h"

/*

  • ============================================================
  • RCC Clock Enable Bits
  • ============================================================ */

/*

  • RCC AHB1ENR bit2:
  • GPIOC clock enable.
  • B1 is connected to PC13. */ #define GPIOCEN (1U << 2)

/*

  • RCC APB2ENR bit14:
  • SYSCFG peripheral clock enable.
  • SYSCFG is required to connect a GPIO port
  • to an EXTI line. */ #define SYSCFGEN (1U << 14)

/*

  • ============================================================
  • PC13 EXTI Initialization
  • ============================================================ */

void pc13_exti_init(void) { /* * -------------------------------------------------------- * Step 1. Disable Global Interrupts Temporarily * -------------------------------------------------------- * * Several registers must be configured in sequence. * * We do not want an interrupt to occur while * configuration is only partially complete. */ __disable_irq();

/*
 * --------------------------------------------------------
 * Step 2. Enable GPIOC Clock
 * --------------------------------------------------------
 *
 * PC13 belongs to GPIOC.
 */
RCC-&gt;AHB1ENR |= GPIOCEN;


/*
 * --------------------------------------------------------
 * Step 3. Configure PC13 as Input
 * --------------------------------------------------------
 *
 * MODER uses 2 bits per pin.
 *
 * PC13:
 *
 *     MODER<span>[27:26]</span>
 *
 * Input mode:
 *
 *     00
 */

GPIOC-&gt;MODER &amp;= ~(1U &lt;&lt; 26);
GPIOC-&gt;MODER &amp;= ~(1U &lt;&lt; 27);


/*
 * --------------------------------------------------------
 * Step 4. Enable SYSCFG Clock
 * --------------------------------------------------------
 *
 * SYSCFG is on APB2.
 */
RCC-&gt;APB2ENR |= SYSCFGEN;


/*
 * --------------------------------------------------------
 * Step 5. Connect PC13 → EXTI13
 * --------------------------------------------------------
 *
 * EXTI12 ~ EXTI15 are controlled by:
 *
 *     EXTICR<span>[3]</span>
 *
 * EXTI13 field selects which GPIO port
 * supplies line13.
 *
 * Port encoding:
 *
 *     A = 0000
 *     B = 0001
 *     C = 0010
 *
 * For EXTI13:
 *
 *     EXTICR3<span>[7:4]</span>
 *
 * Port C = 0010
 *
 * Original practice code:
 *
 *     SYSCFG-&gt;EXTICR<span>[3]</span> |= (1U &lt;&lt; 5);
 *
 * Reset value is 0000, so setting bit5 produces:
 *
 *     0010 = Port C
 */
SYSCFG-&gt;EXTICR<span>[3]</span> |= (1U &lt;&lt; 5);


/*
 * --------------------------------------------------------
 * Step 6. Unmask EXTI13
 * --------------------------------------------------------
 *
 * IMR:
 *
 *     0 → interrupt masked
 *     1 → interrupt allowed
 */
EXTI-&gt;IMR |= LINE13;


/*
 * --------------------------------------------------------
 * Step 7. Select Rising Edge
 * --------------------------------------------------------
 *
 * NUCLEO-F439ZI B1:
 *
 *     Released = LOW
 *     Pressed  = HIGH
 *
 * Button press therefore produces:
 *
 *     LOW → HIGH
 *
 * so Rising Trigger Selection Register
 * is used.
 */
EXTI-&gt;RTSR |= LINE13;


/*
 * Optional defensive configuration:
 *
 * Make sure falling-edge trigger is disabled.
 */
EXTI-&gt;FTSR &amp;= ~LINE13;


/*
 * --------------------------------------------------------
 * Step 8. Enable IRQ in NVIC
 * --------------------------------------------------------
 *
 * EXTI lines 10 ~ 15 share:
 *
 *     EXTI15_10_IRQn
 */
NVIC_EnableIRQ(EXTI15_10_IRQn);


/*
 * --------------------------------------------------------
 * Step 9. Enable Global Interrupts
 * --------------------------------------------------------
 *
 * Initialization is complete.
 */
__enable_irq();

}


9. main.c

#include "gpio.h"
#include "gpio_exti.h"
#include "uart.h"

/*

  • ============================================================

  • EXTI Callback

  • ============================================================

  • This function contains the application action

  • performed when B1 is pressed. / static void exti_callback(void) { /

    • Send a message through USART3.
    • Expected terminal output:
    • BTN Pressed...
      

    */ uart_write_string("BTN Pressed...\r\n");

    /*

    • Toggle LD1 connected to PB0.
    • Every valid button interrupt changes
    • the LED state. */ led_toggle(); }

int main(void) { /* * -------------------------------------------------------- * Step 1. Initialize USART3 * -------------------------------------------------------- * * Used to print the button event. */ uart_init();

/*
 * --------------------------------------------------------
 * Step 2. Initialize LD1
 * --------------------------------------------------------
 *
 * PB0 → Output
 */
led_init();


/*
 * --------------------------------------------------------
 * Step 3. Configure PC13 EXTI
 * --------------------------------------------------------
 *
 * B1 press:
 *
 *     PC13 Rising Edge
 *        ↓
 *     EXTI13
 *        ↓
 *     NVIC
 *        ↓
 *     ISR
 */
pc13_exti_init();


while (1)
{
    /*
     * No button polling.
     *
     * The CPU does NOT repeatedly execute:
     *
     *     get_btn_state()
     *
     * Button processing happens only when
     * an interrupt occurs.
     *
     * A later low-power version could use:
     *
     *     __WFI();
     *
     * here to sleep until an interrupt arrives.
     */
}

}

/*

  • ============================================================
  • EXTI10 ~ EXTI15 Shared Interrupt Handler
  • ============================================================ */

void EXTI15_10_IRQHandler(void) { /* * -------------------------------------------------------- * Step 1. Identify the Interrupt Source * -------------------------------------------------------- * * EXTI10 ~ EXTI15 share one ISR. * * Therefore check whether EXTI13 is actually pending. / if ((EXTI->PR & LINE13) != 0U) { / * ---------------------------------------------------- * Step 2. Clear Pending Flag * ---------------------------------------------------- * * EXTI PR is W1C: * * Write 1 → clear pending bit * Write 0 → no effect * * IMPORTANT: * * Use direct assignment: * * EXTI->PR = LINE13; * * instead of: * * EXTI->PR |= LINE13; * * because '|=' performs a Read-Modify-Write. * * Since PR is W1C, reading multiple pending bits * and writing them back as 1 could accidentally * clear other pending EXTI lines. */ EXTI->PR = LINE13;

    /*
     * ----------------------------------------------------
     * Step 3. Execute Application Callback
     * ----------------------------------------------------
     *
     * Pending is cleared BEFORE the callback.
     *
     * If another edge occurs later,
     * hardware can set PR again.
     */
    exti_callback();
}

}


10. Important Correction — PR Is W1C

원본 실습 코드는:

EXTI->PR |= LINE13;

이었다.

기능적으로 line13 하나만 pending인 단순 실습에서는 정상 동작했고 실제 board test도 PASS했다.

하지만 register 특성을 고려하면 최종 코드는:

EXTI->PR = LINE13;

이 더 적절하다.

왜냐하면 PR은:

W1C

Write 1 to Clear

register이기 때문이다.

|=는 실제로:

Read PR
↓
OR LINE13
↓
Write PR

를 수행한다.

만약 읽은 순간:

PR[13] = 1
PR[12] = 1

이었다면:

Read
→ bit13 = 1
→ bit12 = 1

Write back → bit13 = 1 → clear → bit12 = 1 → clear

가 되어 처리하지 않은 EXTI12까지 clear될 위험이 있다.

그래서 W1C register에서는 보통:

EXTI->PR = LINE13;

처럼 clear하려는 bit에만 1을 직접 write한다.


11. Complete Interrupt Flow

B1 Released
PC13 = 0
  ↓ User presses B1

PC13 = 1

  ↓

LOW → HIGH

  ↓

EXTI13 Rising Edge Detect

  ↓

PR[13] = 1

  ↓

EXTI15_10 IRQ Request

  ↓

NVIC

  ↓

Cortex-M4 pauses normal execution

  ↓

Vector Table Lookup

  ↓

EXTI15_10_IRQHandler()

  ↓

Check PR[13]

  ↓

Clear PR[13]

  ↓

exti_callback()

  ├── UART Print
  └── LED Toggle

  ↓

Return From Interrupt

  ↓

main() resumes


12. NVIC

NVIC:

Nested
Vectored
Interrupt
Controller

역할:

Interrupt Enable
Interrupt Disable
Priority
Pending
Nested Interrupt

이번 코드는:

NVIC_EnableIRQ(EXTI15_10_IRQn);

을 사용한다.

즉:

EXTI peripheral

에서 interrupt request가 만들어져도 NVIC에서 해당 IRQ가 enable되어 있어야 CPU까지 전달된다.


13. Vector Table

IRQ가 들어오면 Cortex-M4는 함수 이름을 검색하는 것이 아니다.

Startup file에 있는 Vector Table에 ISR address가 저장되어 있다.

개념적으로:

Vector Table

... EXTI15_10_IRQn ↓ EXTI15_10_IRQHandler address ...

따라서 우리가:

void EXTI15_10_IRQHandler(void)

를 구현하면 startup의 weak handler 대신 이 함수가 사용된다.


14. Why One Handler for 6 Lines?

EXTI10~15는 하나의 IRQ를 공유한다.

EXTI10 ─┐
EXTI11 ─┤
EXTI12 ─┤
EXTI13 ─┼── EXTI15_10_IRQHandler
EXTI14 ─┤
EXTI15 ─┘

따라서 ISR 내부에서:

if ((EXTI->PR & LINE13) != 0U)

처럼 실제 pending source를 확인해야 한다.

여러 line을 사용할 경우:

if (EXTI->PR & LINE10)
{
...
}

if (EXTI->PR & LINE13) { ... }

if (EXTI->PR & LINE15) { ... }

처럼 구분할 수 있다.


15. Rising Edge

이번 board:

Not Pressed
PC13 = LOW

Pressed PC13 = HIGH

따라서 button press:

0 → 1

이므로:

EXTI->RTSR |= LINE13;

을 사용한다.

원서처럼 active-LOW button이면:

1 → 0

이므로 FTSR을 사용했을 수 있다.

즉 interrupt edge는 board 회로 polarity에 따라 결정해야 한다.


16. Why Clear Pending First?

ISR:

if (EXTI->PR & LINE13)
{
EXTI->PR = LINE13;
exti_callback();
}

처럼 pending flag를 callback 전에 지운다.

이유:

IRQ 발생
↓
PR = 1
↓
ISR 진입
↓
PR clear
↓
Application processing

PR을 안 지우면 ISR 종료 후에도:

Pending = 1

이 남을 수 있어 다시 interrupt가 발생할 수 있다.


17. ISR Should Be Short

이번 실습에서는 이해를 위해 ISR 안에서:

UART Output
LED Toggle

까지 수행한다.

하지만 일반적인 embedded design에서는 ISR을 짧게 유지하는 것이 좋다.

예:

volatile uint8_t g_btn_pressed;

void EXTI15_10_IRQHandler(void) { if (EXTI->PR & LINE13) { EXTI->PR = LINE13;

    g_btn_pressed = 1U;
}

}

main:

while (1)
{
if (g_btn_pressed)
{
g_btn_pressed = 0U;
    uart_write_string("BTN Pressed...\r\n");
    led_toggle();
}

}

이 방식은:

ISR
→ Event capture only

main → Heavy processing

으로 역할을 분리한다.

이번 Ch14 원본 실습은 interrupt 동작을 직접 확인하기 위한 단순 구조이므로 callback을 ISR에서 바로 수행했다.


18. Button Bounce

EXTI에서는 mechanical switch bounce가 더 눈에 띌 수 있다.

한 번 누른다고 해도 실제 waveform:

LOW
↓
HIGH
↓
LOW
↓
HIGH
↓
LOW
↓
HIGH

처럼 여러 edge가 생길 수 있다.

따라서 실제로는 한 번 눌렀는데:

BTN Pressed...
BTN Pressed...
BTN Pressed...

가 여러 번 발생할 가능성이 있다.

이를 막으려면:

Software Debounce
Timer Debounce
State Machine

등을 추가할 수 있다.

이번 chapter의 핵심은 EXTI + NVIC 구조라 debounce는 추가하지 않는다.


19. Build / Board Result

기존 실습:

Build    : PASS
Download : PASS
Run      : PASS

실제 board:

B1 Press
↓
LD1 Toggle
↓
UART:
BTN Pressed...

확인 완료.

Program size:

text = 1272 bytes
data = 0 bytes
bss  = 2560 bytes

경고 없이 build됐다.


20. Verification

1. Build

make clean
make

Expected:

Build PASS

text = 1272 data = 0 bss = 2560


2. GPIO

Debugger:

GPIOC->MODER

Expected:

PC13 MODER[27:26]

00


3. SYSCFG

확인:

SYSCFG->EXTICR[3]

EXTI13 field:

0010

Port C

즉:

EXTI13 ← PC13

4. EXTI

확인:

EXTI->IMR
EXTI->RTSR
EXTI->PR

Expected:

IMR[13]  = 1
RTSR[13] = 1

button press 직후:

PR[13] = 1

ISR 처리 후:

PR[13] = 0

5. Breakpoint

Breakpoint:

void EXTI15_10_IRQHandler(void)

B1을 누르면 CPU가 해당 ISR로 진입하는지 확인한다.

Watch:

EXTI->PR
GPIOC->IDR
GPIOB->ODR

6. UART

Expected:

BTN Pressed...

button을 누를 때마다 출력된다.


7. LED

Expected:

Press #1
→ LD1 ON

Press #2 → LD1 OFF

Press #3 → LD1 ON


21. Screenshots to Add

images/ch14_build.png
images/ch14_exticr.png
images/ch14_exti_registers.png
images/ch14_isr_breakpoint.png
images/ch14_uart.png
images/ch14_button_led.gif

가장 좋은 증거 구성:

B1 Press
+
ISR Breakpoint
+
PR[13]
+
UART "BTN Pressed..."
+
LD1 Toggle

22. Key Points

PC13
↓
SYSCFG
↓
EXTI13
↓
Rising Edge
↓
PR[13]
↓
NVIC
↓
Vector Table
↓
EXTI15_10_IRQHandler
↓
LED + UART
  • Ch07 polling을 EXTI interrupt 구조로 변경했다.

  • PC13은 EXTI13을 사용한다.

  • SYSCFG->EXTICR[3]에서 EXTI13을 Port C에 연결한다.

  • F439ZI B1은 active-HIGH이므로 Rising Edge를 사용한다.

  • IMR에서 EXTI13 interrupt를 unmask한다.

  • NVIC_EnableIRQ()로 CPU interrupt input을 enable한다.

  • EXTI10~15는 EXTI15_10_IRQHandler()를 공유한다.

  • ISR에서 PR을 확인해 실제 interrupt source를 구분한다.

  • PR은 W1C register이므로 EXTI->PR = LINE13 형태로 clear하는 것이 안전하다.

  • 실제 board에서 B1 → LD1 toggle + UART output까지 검증 완료했다.

Clone this wiki locally