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-pull | open-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 µA | 2,000 ns | 버튼·저속 신호 |
| 외부 10 kΩ | 330 µA | 500 ns | 일반 |
| 외부 1 kΩ | 3,300 µA | 50 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