← Embedded C 강의 목록으로
🔌
STM32
STM32 · 선수: 17강 · STM32 · 선수: 18강 · STM32 · 선수: 19강

18–20. NVIC 인터럽트와 EXTI · USART 시리얼 통신 · ADC 아날로그 입력

버튼을 폴링으로 읽으면 CPU가 묶이고 짧은 입력을 놓칠 수 있습니다. 인터럽트는 평소 메인이 제 일을 하다가 사건이 생기면 즉시 핸들러(ISR)로 점프했다 돌아옵니다. Cortex-M에서 인터럽트를 총괄하는 NVIC와, 외부 핀 변화를 인터럽트로 바꾸는 EXTI를 다룹니다. PA0 버튼을 EXTI0 인터럽트로 받아 LED를 토글하고, ISR에서 펜딩 플래그(PR)를 반드시 클리어하는 패턴을 익힙니다. 이제 칩이 사람·PC와 텍스트로 대화합니다. UART(범용 비동기 직렬)는 디버그 메시지, 센서 모듈, 부트로더 등 어디에나 쓰입니다. USART1을 9600bps로 설정해 인사 문자열을 보내고, 받은 문자를 그대로 되돌리는 에코를 구현합니다. μVision Serial 창에서 송수신을 직접 볼 수 있어, UART는 시뮬레이터로 검증하기 가장 좋은 주변장치입니다. 지금까지 다룬 핀은 모두 디지털(0/1)이었습니다. 세상의 신호 대부분은 온도·밝기·소리처럼 연속적인 아날로그입니다. ADC(Analog-to-Digital Converter)는 아날로그 전압을 숫자로 바꿔 MCU가 다룰 수 있게 합니다. STM32F103의 12비트 ADC로 PA0의 전압을 0~4095로 읽고 밀리볼트로 환산합니다. STM32 주변장치 시리즈(13~20편)의 마지막으로, 클럭·핀·폴링이 한 번에 쓰입니다.

인터럽트NVICEXTIAFIOEXTICR펜딩USARTUARTBRRTXERXNE에코ADC12비트EOC보정ADCPRE밀리볼트
소요 시간
⏱ 약 1.5~2시간 + 약 1.5시간 + 약 1.5~2시간
난이도
📊 중급
선수 조건
🎯 17강
결과물
AFIO->EXTICR로 핀을 EXTI 라인에 매핑하고 RTSR/IMR·NVIC_EnableIRQ로 인터럽트를 켜며, ISR에서 PR을 클리어해 외부 핀 변화에 즉시 반응할 수 있습니다. / USART1(PA9/PA10)을 BRR=fCK/baud로 설정하고 TXE/RXNE 폴링으로 문자열을 송신·에코할 수 있습니다. / ADC 클럭(≤14MHz)·채널·샘플시간을 설정하고 F1의 보정(RSTCAL/CAL) 후 EOC 폴링으로 12비트 값을 읽어 밀리볼트로 환산할 수 있습니다.

이 강의에서 배우는 것

  • 1폴링과 인터럽트의 차이와 장단점을 설명한다
  • 2NVIC의 역할과 NVIC_EnableIRQ()를 이해한다
  • 3EXTI로 핀의 에지(상승/하강)를 인터럽트로 만든다
  • 4AFIO->EXTICR로 핀을 EXTI 라인에 매핑한다
  • 5ISR을 작성하고 펜딩 플래그(PR)를 올바르게 클리어한다
  • 6UART의 비동기 통신 원리와 프레임(8N1)을 이해한다
  • 7USART1의 핀(PA9/PA10)과 클럭을 설정한다
  • 8BRR로 보율을 계산·설정한다
  • 9TXE/RXNE 플래그로 폴링 송수신을 구현한다
  • 10μVision Serial 창에서 송수신을 확인한다
  • 11ADC의 12비트 분해능(0~4095)과 기준 전압 개념을 이해한다
  • 12ADC 클럭 제약(≤14MHz)과 프리스케일러를 설정한다
  • 13채널·샘플 시간·변환 시퀀스를 설정한다
  • 14F1 ADC의 보정(RSTCAL/CAL)과 변환 완료(EOC) 폴링을 구현한다
  • 15ADC 카운트를 밀리볼트로 환산한다

18. NVIC 인터럽트와 EXTI

버튼을 폴링으로 읽으면 CPU가 묶이고 짧은 입력을 놓칠 수 있습니다. 인터럽트는 평소 메인이 제 일을 하다가 사건이 생기면 즉시 핸들러(ISR)로 점프했다 돌아옵니다. Cortex-M에서 인터럽트를 총괄하는 NVIC와, 외부 핀 변화를 인터럽트로 바꾸는 EXTI를 다룹니다. PA0 버튼을 EXTI0 인터럽트로 받아 LED를 토글하고, ISR에서 펜딩 플래그(PR)를 반드시 클리어하는 패턴을 익힙니다.

소개

NVIC(Nested Vectored Interrupt Controller)는 각 인터럽트의 활성화·우선순위·펜딩을 관리하고, EXTI(External Interrupt Controller)는 16개 라인(EXTI0~15)의 상승/하강 에지를 감지해 인터럽트를 냅니다.

핵심 개념

1) NVIC와 EXTI 레지스터

레지스터역할
EXTI->IMR인터럽트 마스크(라인 허용)
EXTI->RTSR/FTSR상승/하강 에지 트리거
EXTI->PR펜딩 플래그 — 1을 써서 클리어
c
NVIC_EnableIRQ(EXTI0_IRQn);   /* CMSIS: NVIC에서 인터럽트 활성화 */

2) AFIO->EXTICR — 핀→라인 매핑

c
/* EXTI 라인 n 은 EXTICR[n/4] 의 (n%4)*4 위치 4비트. 0=PA, 1=PB, 2=PC... */
AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;   /* EXTI0 ← PA0 */
/* 매핑에는 AFIO 클럭(RCC_APB2ENR_AFIOEN)이 필요하다 */

3) ISR과 펜딩 클리어

c
void EXTI0_IRQHandler(void) {
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR = EXTI_PR_PR0;        /* 1 을 써야 클리어! 안 하면 무한 재진입 */
        GPIOC->ODR ^= (1u << 13);
    }
}
⚠️

핸들러 이름은 startup 벡터 테이블과 일치해야 합니다(EXTI0_IRQHandler). 펜딩(PR)을 클리어하지 않으면 핸들러를 나가자마자 다시 같은 인터럽트가 걸립니다.

핵심 예제

c
#include "stm32f10x.h"
void EXTI0_IRQHandler(void){
    if (EXTI->PR & EXTI_PR_PR0) { EXTI->PR = EXTI_PR_PR0; GPIOC->ODR ^= (1u<<13); }
}
int main(void){
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
    GPIOA->CRL &= ~0xFu; GPIOA->CRL |= 0x8u; GPIOA->ODR &= ~1u;  /* PA0 입력 풀다운 */
    GPIOC->CRH &= ~(0xFu<<20); GPIOC->CRH |= (0x2u<<20);          /* PC13 출력 */
    AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;   /* EXTI0 ← PA0 */
    EXTI->RTSR |= EXTI_RTSR_TR0;              /* 상승 에지 */
    EXTI->IMR  |= EXTI_IMR_MR0;               /* 마스크 해제 */
    NVIC_EnableIRQ(EXTI0_IRQn);
    while (1) { /* 메인은 한가하다 */ }
}

시뮬레이터 Command 창에서 `PORTA |= 0x0001`(PA0 상승 에지)을 실행하면 EXTI0_IRQHandler로 진입해 PC13이 토글됩니다.

자주 하는 실수

Q. 인터럽트가 한 번 걸리고 영원히 핸들러에서 못 나와요.

A. EXTI->PR을 클리어하지 않았습니다. PR은 1을 써야 클리어됩니다(EXTI->PR = EXTI_PR_PR0;). 빠뜨리면 나가자마자 재진입합니다.

Q. 핸들러가 아예 호출되지 않아요.

A. ① AFIO 클럭(AFIOEN), ② EXTI->IMR 마스크 해제, ③ NVIC_EnableIRQ, 그리고 핸들러 이름이 EXTI0_IRQHandler로 정확한지 확인하세요.

Q. ISR 안에서 긴 작업을 해도 되나요?

A. 안 됩니다. ISR은 짧게 유지하고, 긴 작업은 플래그만 세워 메인에서 처리하세요(22편 링버퍼에서 활용).

정리

  • 인터럽트는 사건 발생 시 자동 호출돼 CPU를 효율적으로 쓴다
  • NVIC가 인터럽트를 총괄하고 NVIC_EnableIRQ로 켠다
  • EXTI는 핀 에지를 인터럽트로 바꾸며 IMR/RTSR/FTSR/PR로 제어한다
  • 핀→라인 매핑은 AFIO->EXTICR(AFIO 클럭 필요)로 한다
  • ISR에서 펜딩(PR)을 반드시 클리어하고 핸들러는 짧게 유지한다

과제

  1. FTSR로 하강 에지 트리거로 바꿔 동작 차이를 관찰
  2. PB0(EXTICR 매핑을 PB로) 버튼으로 EXTI0을 설정해 보기
  3. ISR에서는 volatile 플래그만 세우고 메인 루프에서 LED를 처리하도록 리팩터링

19. USART 시리얼 통신

이제 칩이 사람·PC와 텍스트로 대화합니다. UART(범용 비동기 직렬)는 디버그 메시지, 센서 모듈, 부트로더 등 어디에나 쓰입니다. USART1을 9600bps로 설정해 인사 문자열을 보내고, 받은 문자를 그대로 되돌리는 에코를 구현합니다. μVision Serial 창에서 송수신을 직접 볼 수 있어, UART는 시뮬레이터로 검증하기 가장 좋은 주변장치입니다.

소개

UART는 클럭선 없이 양쪽이 약속한 보율로 데이터를 주고받습니다. 송신측 TX가 수신측 RX에 교차 연결되고 GND를 공유합니다. 가장 흔한 설정은 8N1(데이터8·패리티 없음·정지1).

핵심 개념

1) USART1 핀·클럭과 보율

신호핀모드
TXPA9AF 푸시풀(0xB)
RXPA10플로팅 입력(0x4)
c
/* USART1 은 APB2(RCC_APB2ENR_USART1EN). fCK = PCLK2 = 72MHz */
BRR = fCK / baud;   /* 9600 → 72000000/9600 = 7500 */

2) 송수신(폴링)

c
while (!(USART1->SR & USART_SR_TXE)) { }   /* 송신 버퍼 빌 때까지 */
USART1->DR = c;                            /* 한 글자 송신 */

if (USART1->SR & USART_SR_RXNE) c = USART1->DR;   /* 읽으면 RXNE 자동 클리어 */

CR1에 TE(송신)·RE(수신)·UE(USART 인에이블)를 켭니다: `CR1 = TE | RE | UE`.

핵심 예제

c
#include "stm32f10x.h"
static void usart1_init(void){
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_USART1EN;
    GPIOA->CRH &= ~((0xFu<<4) | (0xFu<<8));
    GPIOA->CRH |= (0xBu<<4);   /* PA9 TX = AF 푸시풀 */
    GPIOA->CRH |= (0x4u<<8);   /* PA10 RX = 플로팅 입력 */
    USART1->BRR = 7500u;       /* 9600 @ 72MHz */
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}
static void tx(char c){ while(!(USART1->SR & USART_SR_TXE)){} USART1->DR=(uint16_t)c; }
int main(void){
    usart1_init();
    const char *s = "Hello, STM32 UART!\r\n"; while(*s) tx(*s++);
    while (1) { if (USART1->SR & USART_SR_RXNE) tx((char)USART1->DR); }  /* 에코 */
}

BRR 검산: 9600→7500, 19200→3750, 115200→625. 시뮬레이터 View → Serial Windows → UART #1 에 인사 문자열이 출력되고, 입력 문자가 에코됩니다.

자주 하는 실수

Q. 글자가 깨져서 나와요.

A. 보율 불일치입니다. BRR 계산에 쓴 fCK(USART1=PCLK2=72MHz)와 터미널 보율이 같은지 확인하세요.

Q. 첫 글자만 나오고 멈춰요.

A. TXE를 기다리지 않고 DR에 연속으로 쓰면 이전 글자를 덮어씁니다. 매 글자 전에 while(!(SR & TXE))로 대기하세요.

Q. PA9/PA10을 일반 입출력으로 뒀어요.

A. TX(PA9)는 AF 푸시풀(0xB), RX(PA10)는 입력이어야 USART 신호가 핀에 연결됩니다.

정리

  • UART는 보율을 맞춘 두 선(TX/RX)으로 비동기 통신한다(8N1 흔함)
  • USART1은 PA9(TX)/PA10(RX), APB2 버스, BRR=fCK/baud
  • 송신은 TXE 대기 후 DR 쓰기, 수신은 RXNE 확인 후 DR 읽기
  • 시뮬레이터 UART #1 창으로 송수신 텍스트를 직접 본다
  • TX는 AF 푸시풀로 설정해야 한다

과제

  1. 정수를 받아 10진 문자열로 보내는 print_num() 작성
  2. 받은 문자가 소문자면 대문자로 바꿔 에코하는 변환 추가
  3. 115200bps 의 BRR 값을 계산하고 적용

20. ADC 아날로그 입력

지금까지 다룬 핀은 모두 디지털(0/1)이었습니다. 세상의 신호 대부분은 온도·밝기·소리처럼 연속적인 아날로그입니다. ADC(Analog-to-Digital Converter)는 아날로그 전압을 숫자로 바꿔 MCU가 다룰 수 있게 합니다. STM32F103의 12비트 ADC로 PA0의 전압을 0~4095로 읽고 밀리볼트로 환산합니다. STM32 주변장치 시리즈(13~20편)의 마지막으로, 클럭·핀·폴링이 한 번에 쓰입니다.

소개

12비트 ADC는 입력 전압 범위(0~Vref, 보통 3.3V)를 4096단계로 나눕니다. 한 단계(LSB)는 약 0.806mV. mV = adc × 3300 / 4095. 가변저항·조도센서·온도센서가 모두 ADC로 들어옵니다.

핵심 개념

1) 클럭 제약과 채널

c
RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;   /* ADC 클럭 ≤14MHz: 72/6 = 12MHz */
ADC1->SQR3  = 0u;                    /* 1차 변환 채널 0 (PA0) */
ADC1->SMPR2 |= (7u << 0);            /* 채널0 샘플시간 최대(239.5 cycle) */
/* ADC 핀은 아날로그 입력(0x0) — 디지털 버퍼/풀업 끄기 */

2) F1 보정과 변환(EOC)

c
ADC1->CR2 |= ADC_CR2_ADON;            /* 전원 ON (tSTAB 대기) */
ADC1->CR2 |= ADC_CR2_RSTCAL; while(ADC1->CR2 & ADC_CR2_RSTCAL){}
ADC1->CR2 |= ADC_CR2_CAL;    while(ADC1->CR2 & ADC_CR2_CAL){}   /* 보정 */

ADC1->CR2 |= ADC_CR2_ADON;            /* ADON 재설정 = 변환 시작 */
while (!(ADC1->SR & ADC_SR_EOC)) { }  /* 변환 완료 대기 */
uint16_t v = ADC1->DR & 0x0FFFu;      /* 12비트 결과 (읽으면 EOC 클리어) */
⚠️

F1은 켠 뒤 보정(RSTCAL→CAL)을 한 번 해야 정확합니다. 밀리볼트 환산은 반드시 곱한 뒤 나누고 32비트로 계산하세요: (uint32_t)adc * 3300 / 4095.

핵심 예제

c
#include "stm32f10x.h"
static void adc1_init(void){
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
    RCC->CFGR    |= RCC_CFGR_ADCPRE_DIV6;          /* 12MHz */
    GPIOA->CRL &= ~(0xFu << 0);                    /* PA0 아날로그 입력(0x0) */
    ADC1->SQR3 = 0u; ADC1->SMPR2 |= (7u << 0);
    ADC1->CR2 |= ADC_CR2_ADON; for(volatile int i=0;i<10000;i++){}
    ADC1->CR2 |= ADC_CR2_RSTCAL; while(ADC1->CR2 & ADC_CR2_RSTCAL){}
    ADC1->CR2 |= ADC_CR2_CAL;    while(ADC1->CR2 & ADC_CR2_CAL){}
}
static uint16_t adc_read(void){
    ADC1->CR2 |= ADC_CR2_ADON; while(!(ADC1->SR & ADC_SR_EOC)){}
    return (uint16_t)(ADC1->DR & 0x0FFFu);
}
int main(void){
    adc1_init();
    while (1) {
        uint16_t adc = adc_read();
        volatile uint32_t mv = (uint32_t)adc * 3300u / 4095u;
        (void)mv;
    }
}

환산 검산: ADC=0→0mV, 2048→1650mV, 4095→3300mV.

자주 하는 실수

Q. EOC 대기에서 멈춰요.

A. ADC 전원(ADON)을 켰는지, 변환을 시작(ADON 재설정/SWSTART)했는지, ADC 클럭(ADC1EN)이 활성인지 확인하세요.

Q. 값이 들쭉날쭉하고 부정확해요.

A. ① 보정(RSTCAL/CAL)을 빠뜨림, ② 샘플 시간이 짧음(고임피던스 소스), ③ ADC 클럭이 14MHz 초과 — ADCPRE를 확인하세요.

Q. mV 계산이 0이 나와요.

A. adc/4095*3300 순서면 정수에서 0이 됩니다. 반드시 곱한 뒤 나누기: (uint32_t)adc * 3300 / 4095.

정리

  • ADC는 아날로그 전압을 12비트 정수(0~4095)로 바꾼다
  • mV = adc × 3300 / 4095 (곱한 뒤 나누고 32비트로 계산)
  • ADC 클럭은 ≤14MHz — ADCPRE로 나눈다
  • F1은 보정(RSTCAL/CAL) 후 변환, EOC 폴링 뒤 DR을 읽는다
  • ADC 핀은 아날로그 입력(0x0)으로 설정한다

과제

  1. 읽은 ADC 값을 19편 USART로 "adc=NNNN mv=MMMM" 형식으로 출력
  2. 여러 번 읽어 평균을 내 노이즈를 줄이는 간단한 필터 추가
  3. 채널1(PA1)을 추가로 읽도록 SQR3/SMPR 설정 변경
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드(과제·정답 포함)는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침