임베디드 학습 노트 목차

GPIO — 핀 하나에 담긴 전기

LED 를 켜는 코드는 세 줄이면 된다. 그런데 왜 저항을 달아야 하는지, 왜 어떤 핀은 풀업이 필요한지, 왜 버튼이 한 번 눌렀는데 세 번 눌린 것으로 읽히는지 를 모르면 회로가 조금만 달라져도 막힌다.

GPIO 는 소프트웨어와 전기가 만나는 첫 지점이다.


출력 모드 — 밀어내느냐 당기기만 하느냐

  push-pull                       open-drain
                                  (외부 풀업이 있어야 HIGH 가 된다)

     VDD                              VDD
      |                                |
   [P-MOS]  <- HIGH 일 때 켠다        [ R ]  <- 외부 풀업
      |                                |
      +----- 핀                        +----- 핀
      |                                |
   [N-MOS]  <- LOW 일 때 켠다       [N-MOS]  <- LOW 일 때만 켠다
      |                                |
     GND                              GND

  HIGH : VDD 로 밀어낸다            HIGH : 아무것도 안 한다 (놓아 버린다)
  LOW  : GND 로 당긴다              LOW  : GND 로 당긴다
push-pullopen-drain
HIGH 출력능동적으로 밀어낸다놓아 버린다(하이임피던스)
LOW 출력당긴다당긴다
외부 풀업불필요필수
여러 핀 연결금지 — 하나가 HIGH, 하나가 LOW 면 단락가능 — 하나라도 LOW 면 LOW
속도빠르다풀업 저항 × 정전용량만큼 느리다

여러 장치가 한 선을 공유해야 하면 open-drain 이어야 한다. push-pull 두 개를 붙이면 하나가 VDD 로 밀고 하나가 GND 로 당겨 관통 전류가 흘러 칩이 탄다.

I²C 가 open-drain 을 쓰는 이유가 이것이다 — 여러 장치가 같은 SDA·SCL 선에 붙으므로 push-pull 은 애초에 불가능하다.

// STM32 — OTYPER 로 정한다
GPIOB->OTYPER &= ~(1 << 8);     // 0 = push-pull
GPIOB->OTYPER |=  (1 << 9);     // 1 = open-drain

입력 모드 — 아무것도 안 붙이면 값이 흔들린다

핀을 입력으로 두고 아무 데도 연결하지 않으면 떠 있는(floating) 상태가 된다. 주변 전자기 잡음에 따라 0과 1을 무작위로 오간다.

버튼 회로 — 풀업 방식

VDD
 |
[ R ]  10k 풀업
 |
 +------ 핀 (입력)
 |
 o  o   버튼 (누르면 붙는다)
 |
GND

버튼 안 누름 → 풀업이 핀을 HIGH 로 유지
버튼 누름    → GND 로 직결되어 LOW

버튼을 누르면 0 이 되는 것이 이 방식의 특징이다. 직관과 반대라 헷갈리기 쉽다.

// 내부 풀업을 켜면 외부 저항이 필요 없다
GPIOA->PUPDR &= ~(0b11 << (0 * 2));
GPIOA->PUPDR |=  (0b01 << (0 * 2));    // 01 = 풀업, 10 = 풀다운

if ((GPIOA->IDR & (1 << 0)) == 0) {     // LOW = 눌림
    on_button_press();
}

내부 풀업은 대개 수십 kΩ 로 약하다. 잡음이 심한 환경이나 선이 긴 경우에는 외부에 더 낮은 저항(4.7k~10k)을 다는 편이 안정적이다.

채터링 — 버튼 한 번이 여러 번으로 읽힌다

기계식 스위치는 접점이 붙을 때 미세하게 튕긴다. 그 시간이 보통 1~20ms 인데, MCU 는 그 사이를 수천 번 읽으므로 여러 번 눌린 것으로 본다.

                누름
                 v
HIGH  -----.  .-.  .--.
           |  | |  |  |
LOW        '--' '--'  '-------------------
           |<---------->|
            채터링 1~20ms

MCU 가 읽으면   1 0 1 0 1 0 0 1 0 0 0 0 0
                ^ 한 번 눌렀는데 여러 번으로 보인다

소프트웨어 디바운싱 — 폴링 방식

#define DEBOUNCE_MS  20

bool button_pressed(void) {
    static uint32_t last_change;
    static bool stable = true, last_raw = true;

    bool raw = (GPIOA->IDR & (1 << 0)) != 0;

    if (raw != last_raw) {              // 변화가 보이면 타이머를 리셋
        last_raw = raw;
        last_change = HAL_GetTick();
        return false;
    }
    if ((HAL_GetTick() - last_change) > DEBOUNCE_MS && stable != raw) {
        stable = raw;                   // 20ms 동안 안 변했으면 진짜다
        return (stable == false);       // 눌림(LOW) 확정
    }
    return false;
}

원리는 "변하면 기다리고, 일정 시간 안 변해야 인정한다" 이다. delay 를 쓰지 않고 시각을 비교하는 것이 요점 — 슈퍼루프가 멈추면 안 되기 때문이다.

인터럽트를 쓸 때는 더 조심한다

void EXTI0_IRQHandler(void) {
    EXTI->PR = (1 << 0);                        // 플래그 클리어 — 먼저 한다

    uint32_t now = HAL_GetTick();
    if (now - last_irq < DEBOUNCE_MS) return;   // 너무 빨리 또 왔으면 무시
    last_irq = now;

    button_event = 1;                            // 플래그만 세우고 나온다
}

채터링 하나하나가 인터럽트를 부르므로, 필터가 없으면 20ms 동안 인터럽트가 수십 번 들어온다.

하드웨어 디바운싱도 있다 — 핀에 커패시터(100nF)를 달아 전기적으로 완만하게 만드는 방법이다. 부품이 하나 늘지만 소프트웨어가 단순해진다. 정밀한 타이밍이 필요한 곳에서는 슈미트 트리거를 함께 쓴다.

속도와 슬루율 — 빠를수록 좋은 게 아니다

GPIOA->OSPEEDR |= (0b11 << (5 * 2));    // 최고 속도

핀의 전환 속도를 올리면 파형의 상승·하강이 가팔라진다. 그 급격한 변화가 곧 고주파 성분이라, 주변 선에 잡음을 주고 EMI 시험에서 걸린다.

  느린 슬루율              빠른 슬루율

        ...----              .------
      ..                     |
  ----                    ---'

  완만 — 고주파 성분 적음    급격 — 고주파 성분 많음, 링잉 발생

원칙은 "필요한 만큼만" 이다. LED 나 릴레이 제어는 가장 낮은 속도로 충분하고, 고속 SPI 클럭 핀만 높게 잡는다. 기본값을 그대로 두고 EMI 시험에서 떨어지는 경우가 실제로 흔하다.

풀업 저항 값이 만드는 두 가지 대가

내부 풀업은 편하지만 저항값이 크다(STM32 기준 약 40kΩ). 그 대가가 수로 보인다 (VDD 3.3V, 부하 정전용량 50pF 기준).

풀업LOW 일 때 소비 전류상승 시정수 τ = R×C쓸 곳
내부 40 kΩ82.5 µA2,000 ns버튼·저속 신호
외부 10 kΩ330 µA500 ns일반
외부 1 kΩ3,300 µA50 ns고속·잡음 환경

두 값이 정반대로 움직인다. 저항을 낮추면 파형은 빨라지지만 LOW 마다 전류를 더 먹고, 높이면 전류는 줄지만 신호가 느려진다. 어느 쪽이 중요한지는 용도가 정한다 — 배터리 기기는 전류를, 고속 신호는 시정수를 우선한다.

함정 — 내부 풀업(τ 2µs)으로 수백 kHz 신호를 받으려 하면 신호가 HIGH 까지 못 올라간 채 다음 엣지가 온다. "내부 풀업으로 I²C 를 돌렸더니 400kHz 에서 깨진다" 가 대표적이다 (13편의 풀업 계산과 같은 이야기 — I²C 는 반드시 외부 풀업을 쓴다).

반대로 전원이 꺼진 상대 장치에 풀업을 물리면 그 장치의 보호 다이오드를 통해 전류가 새어 나가 장치가 어중간하게 켜지는(팬텀 전원) 현상이 생긴다. 저항이 작을수록 심하다.

전류 — 핀에서 얼마나 뽑을 수 있나

LED 회로

3.3V ---[ R ]---|>|--- GND
                 LED   (Vf 약 2.0V, If 10mA 로 쓴다면)

R = (3.3 - 2.0) / 0.010 = 130Ω

저항 없이 LED 를 달면 안 된다. LED 는 전압이 조금만 올라도 전류가 급격히 늘어(지수적) 핀과 LED 가 함께 탄다.

MCU 핀에는 두 가지 상한이 있다.

  • 핀 하나당 — 보통 20~25mA
  • 칩 전체 합 — 100~150mA (VDD·VSS 핀의 용량)

핀별 상한을 지켜도 여러 개를 동시에 켜면 전체 상한을 넘는다. LED 를 8개 달고 전부 켜니 리셋이 걸리는 상황이 여기서 나온다. 전류가 많이 필요하면 트랜지스터나 드라이버 IC 로 스위칭하고 MCU 핀은 신호만 준다.

대체 기능 — 핀 하나가 여러 역할

// PA9 를 UART1_TX 로 쓴다
GPIOA->MODER &= ~(0b11 << (9 * 2));
GPIOA->MODER |=  (0b10 << (9 * 2));      // 10 = 대체 기능(alternate function)

GPIOA->AFR[1] &= ~(0xF << ((9 - 8) * 4));
GPIOA->AFR[1] |=  (0x7 << ((9 - 8) * 4)); // AF7 = USART1

MCU 는 핀이 부족하므로 한 핀에 여러 주변장치를 배정해 두고 고르게 한다. 어느 AF 번호가 어느 기능인지는 데이터시트의 Alternate function mapping 표를 봐야 한다 — 레퍼런스 매뉴얼이 아니라 데이터시트다.

AFR[0] 은 핀 0~7, AFR[1] 은 핀 8~15 를 담당한다. 그래서 위 코드에서 9 - 8 로 인덱스를 조정한다.

핀 충돌은 컴파일 시점에 안 잡힌다. SPI1 과 I²C1 이 같은 핀을 쓰는데 둘 다 초기화하면, 나중에 설정한 쪽이 이긴다. 보드를 설계할 때 핀 배정표를 만들어 두는 것이 유일한 예방이다.

실무에서 데는 곳

// ✗ 클럭을 안 켜면 레지스터 쓰기가 그냥 무시된다
GPIOA->MODER |= ...;            // 아무 일도 안 일어난다

// ✓ 주변장치 클럭을 먼저 켠다
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
__DSB();                         // 클럭이 실제로 켜질 때까지 기다린다
GPIOA->MODER |= ...;

"코드는 맞는데 핀이 안 움직인다" 의 1순위 원인이 클럭 미설정이다. 오류도 경고도 없이 그냥 안 된다. 전력을 아끼려고 기본이 꺼져 있기 때문이다.

// ✗ 읽기·수정·쓰기라 인터럽트에 깨진다
GPIOA->ODR |= (1 << 5);

// ✓ BSRR 은 원자적이다
GPIOA->BSRR = (1 << 5);          // SET
GPIOA->BSRR = (1 << (5 + 16));   // RESET
// 입력 핀을 아날로그 모드로 두면 전류가 덜 샌다 (저전력 설계)
GPIOA->MODER |= (0b11 << (n * 2));   // 11 = 아날로그

쓰지 않는 핀을 떠 있는 입력으로 두면 문턱 전압 근처에서 진동해 누설 전류가 흐른다. 배터리 제품에서 무시할 수 없는 양이라, 미사용 핀은 아날로그 모드나 풀다운으로 고정한다.


한눈에 정리

  • push-pull 은 밀고 당기고, open-drain 은 당기기만 한다. 여러 장치가 한 선을 공유하면 open-drain 이어야 한다(I²C)
  • push-pull 두 개를 붙이면 관통 전류로 칩이 탄다
  • 입력 핀을 떠 있게 두면 값이 무작위로 흔들린다풀업/풀다운으로 고정한다
  • 풀업 방식 버튼은 누르면 0 이다
  • 채터링은 1~20ms — "변하면 기다리고 일정 시간 안 변해야 인정" 이 디바운싱의 원리. delay 대신 시각 비교
  • 인터럽트 방식은 채터링마다 ISR 이 불린다필터가 필수
  • 슬루율은 필요한 만큼만 — 빠를수록 고주파 잡음이 늘어 EMI 시험에 걸린다
  • 핀당 20~25mA, 칩 전체 100~150mA — 핀별로 지켜도 합에서 넘칠 수 있다
  • AF 번호는 레퍼런스 매뉴얼이 아니라 데이터시트의 매핑 표에 있다
  • 클럭을 안 켜면 레지스터 쓰기가 조용히 무시된다 — "핀이 안 움직인다" 의 1순위 원인
  • 미사용 핀은 아날로그 모드나 풀다운 — 떠 있으면 누설 전류가 흐른다

꼬리질문 대비

  • "푸시풀과 오픈드레인의 차이는?" → 푸시풀은 HIGH·LOW 를 다 구동, 오픈드레인은 LOW 만 구동하고 HIGH 는 풀업이 만든다 — 와이어드 AND 가 가능
  • "내부 풀업으로 I²C 400kHz 가 깨지는 이유는?" → 약 40kΩ 이라 시정수가 2µs 수준 — 상승이 못 따라간다. 외부 풀업이 필수
  • "핀 속도를 최고로 두면 안 되는 이유는?" → 엣지가 가팔라져 고주파 성분이 늘고 EMI·링잉이 생긴다. 필요한 만큼만

출처 — STMicroelectronics, RM0090 STM32F4 Reference Manual §8 GPIO(MODER·OTYPER·OSPEEDR·PUPDR·BSRR·AFR) · STMicroelectronics, DS8626 STM32F407 Datasheet — 절대 최대 정격(핀 전류)·Alternate function mapping · Arm, Cortex-M4 Devices Generic User Guide(DUI 0553) — __DSB

링커 스크립트와 빌드 — 무엇이 어디에 놓이는가인터럽트와 NVIC — 하던 일을 멈추고 달려가기