Repository navigation
Chapter 14
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 ToggleBoard:
B1 → PC13
LD1 → PB0이번 NUCLEO-F439ZI의 B1은:
Released → LOW
Pressed → HIGH이므로 Rising Edge를 사용한다.
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을 계속 읽을 필요가 없다.
이번 실습에서 필요한 구성:
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
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를 재사용한다.
#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
#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->AHB1ENR |= GPIOCEN;
/*
* --------------------------------------------------------
* Step 3. Configure PC13 as Input
* --------------------------------------------------------
*
* MODER uses 2 bits per pin.
*
* PC13:
*
* MODER<span>[27:26]</span>
*
* Input mode:
*
* 00
*/
GPIOC->MODER &= ~(1U << 26);
GPIOC->MODER &= ~(1U << 27);
/*
* --------------------------------------------------------
* Step 4. Enable SYSCFG Clock
* --------------------------------------------------------
*
* SYSCFG is on APB2.
*/
RCC->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->EXTICR<span>[3]</span> |= (1U << 5);
*
* Reset value is 0000, so setting bit5 produces:
*
* 0010 = Port C
*/
SYSCFG->EXTICR<span>[3]</span> |= (1U << 5);
/*
* --------------------------------------------------------
* Step 6. Unmask EXTI13
* --------------------------------------------------------
*
* IMR:
*
* 0 → interrupt masked
* 1 → interrupt allowed
*/
EXTI->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->RTSR |= LINE13;
/*
* Optional defensive configuration:
*
* Make sure falling-edge trigger is disabled.
*/
EXTI->FTSR &= ~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();
}
#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();
}
}
원본 실습 코드는:
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한다.
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
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까지 전달된다.
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 대신 이 함수가 사용된다.
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)
{
...
}
처럼 구분할 수 있다.
이번 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에 따라 결정해야 한다.
ISR:
if (EXTI->PR & LINE13)
{
EXTI->PR = LINE13;
exti_callback();
}처럼 pending flag를 callback 전에 지운다.
이유:
IRQ 발생
↓
PR = 1
↓
ISR 진입
↓
PR clear
↓
Application processingPR을 안 지우면 ISR 종료 후에도:
Pending = 1이 남을 수 있어 다시 interrupt가 발생할 수 있다.
이번 실습에서는 이해를 위해 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에서 바로 수행했다.
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는 추가하지 않는다.
기존 실습:
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됐다.
make clean
makeExpected:
Build PASS
text = 1272
data = 0
bss = 2560
Debugger:
GPIOC->MODERExpected:
PC13 MODER[27:26]
00
확인:
SYSCFG->EXTICR[3]EXTI13 field:
0010
Port C
즉:
EXTI13 ← PC13확인:
EXTI->IMR
EXTI->RTSR
EXTI->PRExpected:
IMR[13] = 1
RTSR[13] = 1button press 직후:
PR[13] = 1ISR 처리 후:
PR[13] = 0Breakpoint:
void EXTI15_10_IRQHandler(void)B1을 누르면 CPU가 해당 ISR로 진입하는지 확인한다.
Watch:
EXTI->PR
GPIOC->IDR
GPIOB->ODRExpected:
BTN Pressed...button을 누를 때마다 출력된다.
Expected:
Press #1
→ LD1 ON
Press #2
→ LD1 OFF
Press #3
→ LD1 ON
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 TogglePC13
↓
SYSCFG
↓
EXTI13
↓
Rising Edge
↓
PR[13]
↓
NVIC
↓
Vector Table
↓
EXTI15_10_IRQHandler
↓
LED + UARTCh07 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까지 검증 완료했다.