GPIO(General Purpose Input/Output)는 MCU가 외부 세계와 신호를 주고받는 가장 기본적인 수단이다.
이름 그대로 특정 기능에 고정되지 않고, 소프트웨어 설정에 따라 입력 또는 출력으로 사용할 수 있다.

  • Input: 외부 신호(버튼, 센서 등)를 MCU가 읽어들임
  • Output: MCU가 외부 장치(LED, 모터 드라이버 등)에 신호를 출력함

출력 모드: Push-Pull vs Open-Drain

Push-Pull

MCU가 핀을 High와 Low 모두 직접 drive하는 방식이다. 내부에 PMOS와 NMOS 트랜지스터가 한 쌍으로 연결되어 있어서, High로 설정하면 PMOS가 켜져 핀을 VDD에 연결하고, Low 명령이 오면 NMOS가 켜져 핀을 GND에 연결한다.

외부 저항 없이도 원하는 레벨을 정확하게 출력할 수 있어서, 일반적인 LED 제어나 디지털 출력에 기본으로 사용한다.

Open-Drain

NMOS 하나만 사용하는 방식이다. Low는 직접 drive할 수 있지만, High는 스스로 만들지 못한다. 대신 핀을 그냥 놔버리는데(Hi-Z), 이때 외부에 연결된 Pull-up 저항이 핀을 High로 끌어올린다.

Push-Pull과 Open-Drain 비교

왜 Open-Drain을 사용할까?

대표적인 예가 I2C다. 여러 장치가 같은 버스 선 하나를 공유할 때, 누군가 Low를 당기면 버스 전체가 Low가 된다.

만약 Push-Pull이었다면 문제가 생긴다. 장치 A가 High를 output하는 동시에 장치 B가 Low를 output하면, VDD와 GND가 직접 연결되는 short가 발생하기 때문이다.

Open-Drain은 Pull-up 저항의 전압을 MCU VDD와 다르게 설정할 수도 있어서, 3.3V MCU와 5V 버스를 연결할 때도 별도 level shift 없이 쓸 수 있다.


입력 모드: Pull-up / Pull-down / Floating

버튼을 GPIO 입력으로 읽는다고 해보자. 버튼이 눌리지 않은 상태에서 핀이 아무 데도 연결되지 않으면 Floating 상태가 된다.

이 상태에서는 Floating 상태의 핀이 주변 noise를 그대로 받아들여서 IDR을 읽으면 0이 될지 1이 될지 예측할 수 없다.

그래서 평상시에 핀을 확실한 레벨로 고정해두는 것이 Pull-up / Pull-down이다.

Pull-up과 Pull-down 개념도

설정평상시버튼 누를 때
Pull-upHIGHLOW
Pull-downLOWHIGH

STM32는 외부 저항 없이 내부 Pull-up / Pull-down을 PUPDR 레지스터로 설정할 수 있다. 이전 글에서 PC13 버튼을 읽을 때 Pull-up을 설정했던 것이 바로 이 이유다.

STM32 GPIO 내부 블록 다이어그램

STM32 레퍼런스 매뉴얼에는 GPIO 핀 하나의 구조를 보여주는 블록 다이어그램이 있다.
GPIO를 이해할 때 가장 중요한 그림 중 하나라서, 한 번 제대로 읽어두면 이후에 훨씬 덜 헷갈린다.

STM32 GPIO 내부 블록 다이어그램

1) 실제 핀 (I/O Pin)

핀 양쪽에는 Protection Diode가 붙어 있다.
핀에 VDD보다 높거나 VSS보다 낮은 전압이 들어왔을 때 내부 회로를 보호하는 역할을 한다.

또한 Pull-up / Pull-down 저항이 스위치 형태로 표현되어 있는데, 이 스위치를 소프트웨어로 켜고 끄는 것이 PUPDR 레지스터다.

2) 입력 경로 (Input Driver)

핀 신호는 TTL Schmitt Trigger를 거쳐 Input Data Register(IDR) 로 들어간다.

Schmitt Trigger가 있는 이유는 noise 때문이다.
신호가 천천히 변하거나 noise가 섞여 있어도, hysteresis 특성을 통해 더 명확하게 0과 1로 해석할 수 있다.

Analog 모드에서는 이 디지털 입력 경로 자체를 끄기도 한다.

즉, 펌웨어에서 입력을 읽는다는 것은 결국 IDR 값을 읽는 것이다.

3) 출력 경로 (Output Driver)

출력은 ODR(Output Data Register)에 값을 쓰거나, BSRR(Bit Set/Reset Register)를 통해 제어된다.
이 값이 Output Control을 거쳐 P-MOS / N-MOS 조합으로 전달된다.

  • Push-Pull 모드: P-MOS와 N-MOS가 번갈아 동작하며 High / Low를 직접 drive
  • Open-Drain 모드: N-MOS만 동작하고 P-MOS는 꺼짐

핵심은 MUX다.
MODER 레지스터를 통해 이 핀이 GPIO로 동작할지, UART / SPI 같은 Alternate Function으로 동작할지 선택한다.
AF(Alternate Function) 모드로 설정하면 MCU 내부 peripheral(USART, SPI, TIM 등)이 해당 핀을 직접 제어한다.

실습: User Button으로 LED 제어 (STM32F411 Nucleo)

보드 기본 핀 정보

코드를 작성하기 전에 보드 회로도에서 먼저 확인해야 할 정보는 다음과 같다.

기능특이사항
User LED (LD2)PA5Active High
User Button (B1)PC13Active Low, 보드 내부 Pull-up 연결

여기서 중요한 점은 버튼이 Active Low라는 것이다.
즉, 버튼을 누르지 않았을 때는 PC13 = High, 버튼을 눌렀을 때는 PC13 = Low가 된다.

따라서 코드에서 GPIO_PIN_RESET은 버튼이 눌린 상태를 의미한다.

STM32F411 Nucleo 보드의 LED와 버튼 위치 예시: STM32F411 Nucleo 보드에서 LD2와 B1의 위치를 표시한 사진

CubeMX 핀 설정

  • PA5GPIO_Output
  • PC13GPIO_Input

PC13의 Pull-up은 보드 하드웨어에 이미 연결되어 있으므로, CubeMX에서는 No pull로 두어도 된다.

CubeMX GPIO 설정 화면 PA5를 출력, PC13을 입력으로 설정한 CubeMX 화면

HAL 코드

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
while (1)
{
    if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
    {
        // 버튼 눌림 (Active Low)
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
    }
    else
    {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
    }
}

버튼 상태를 읽어 눌려 있으면 LED를 켜고, 아니면 끈다.

버튼 입력에 따라 LED가 켜지는 실행 결과 버튼을 누르면 LED가 켜지는 실행 결과

실행 결과

버튼을 누르고 있는 동안 LD2(초록 LED)가 켜지고, 버튼에서 손을 떼면 꺼진다.

이 정도의 간단한 실습에서는 debouncing을 따로 넣지 않았다.
버튼을 누르고 있는 동안 계속 켜는 구조라서 chattering이 큰 문제가 되지 않는다.
다만 버튼을 한 번 눌렀다 뗐을 때 상태가 토글되는 구조로 바꾸면 debouncing이 필요해진다.

추가 학습: OSPEEDR를 왜 설정할까?

출력 속도 설정은 slew rate(신호가 바뀌는 속도) 와 관련이 있다.
속도가 빠를수록 엣지가 가팔라지고, 그만큼 EMI 도 커질 수 있다.

그래서 고속 SPI 클럭처럼 빠른 신호가 필요한 경우에는 High Speed가 필요할 수 있지만,
단순한 LED 제어 같은 저속 신호에 Very High Speed를 쓰면 불필요한 noise만 증가시킬 수 있다.

OSPEEDR 레지스터