3. GPIO

손지웅·2025년 5월 19일

Embedded Software

목록 보기
4/7

STM32F4 Intrduction

임베디드 시스템에 필요한 기능을 탑제. 마이크로프로세서와 입출력 모듈을 하나의 칩으로 micro controller unit

임베디드 시스템에 필요한 기능이 들어가있는 MCU

STM32F429의 내부구조

AHB/APB Bus

  • Cortex-M4 프로세서와 온칩 주변장치들은 AHB와 APB를 통해서 연결된다.

AHB

advanced High-performance Bus
고속의 데이터 전송을 위한 통로, CPU<->메모리 연결

APB

advanced peripheral bus
저속의 데이터 전송을 위한 통로. 온칩 디바이스 레지스터 <-> 주변장치
APB에 연결된 장치들은 AHB/APB Bridge를 통해 프로세서와 통신한다.

GPIO 기본 개념

General Purpose Input Output . 범용 I/O

  • GPIO는 범용적으로 사용할 수 있는 프로세서의 표준 인터페이스
  • 핀들을 직접 직렬/병렬/입력/출력/기타기능 으로 설정하여 사용할 수 있다.
  • 프로세서와 외부장치가 GPIO로 연결될 경우, 사용을 위해서 GPIO 레지스터들을 설정해야한다.
  • 입력과 출력 2가지 모드로 사용 가능
  • 이때의 모드는 동시에 사용 불가하다. 프로그래머가 직접 결정
  • GPIO 30~200개 지원
  • 핀이 40개 -> 200개 기능 지원? pin 하나당 여러개의 기능을 지원하기 때문. Alternate Function AF 기능을 이용한 Multiplexing

STM32F429의 GPIO

  • STM32f429상에는 GPIO_A ~ GPIO_K까지 11개의 포트가 존재
  • 각 포트는 16개의 I/O 제어 가능. 독립적으로 원하는 기능으로 사용이 가능핟.
  • 출력
  • 출력상태 : push-pull(핀이 0 or 1로, 일반적인 출력방식), open drain(핀이 0으로만 연결, 1로 만드려면 풀업 저항을 달아야 함. 여러 장치가 한 선을 공유할 때 사용) + pull up/down(내부에 저항을 연결하여 기본값을 1 or 0 으로 유지)
  • 출력 Data register(GPIOx_ODR)에서 나오는 출력 데이터 또는 온칩 디바이스 출력을 연결할 수 있음. 출력 데이터는 출력 데이터 레지스터에서 결정된다.
  • 각 I/O를 위한 속도 선택 가능
  • 입력
  • 입력상태 : floating(아무것도 연결하지 않음), pull-up/down(내부 저항으로 1(풀업), 0(풀다운) 으로 기본값 유지), analog(아날로그 신호, 즉 센서에서 들어온 데이터를 읽을 때 사용)
  • 입력 Data register(GPIOx_IDR)이나 주변장치로 입력 연결
  • 비트를 GPIOx_ODR에 기록하기 위한 Bit 설정/리셋 레지스터 (GPIOx_BSRR). 입력된 값은 입력 데이터 레지스터에서 읽을 수 있다.
  • I/O 설정을 잠그기 위한 라킹 매커니즘 ( GPIOx_LCKR ).
  • 아날로그 입력 기능
  • 포트마다 최대 16개의 alternate Function 부가 기능 설정 가능

GPIO 사용은, Register를 통하여 이뤄짐

GPIO를 어떻게 제어, 즉 어떤 레지스터를 통해 핀의 동작을 설정하고 데이터를 읽고 쓰는지
포트 A-J는 16비트이다. 즉 포트마다 16개의 핀이 존재
포트 K는 8비트. 즉 8개의 핀만 있다. GPIOK0 - GPIOK7

32-bit configuration register 설정 레지스터

최대 16개의 핀(I/O)를 한 번에 설정할 수 있다.

  • GPIOx_MODER : 각 핀의 입출력 방향 선택
  • GPIOx_OTYPER, GPIOx_OSPEEDR : 출력 타입(pushpull, open drain) 선택, 출력 속도(신호 전환 속도) 설정
  • GPIOx_PUPDR : 풀업, 풀다운 저항 설정. 기본값을 1로 할지 0으로 할지

32-bit data register 데이터 레지스터

  • GPIOx_IDR : 입력 데이터 레지스터, 각 핀에 들어온 신호를 읽을 때 사용
  • GPIOx_ODR : 출력 데이터 레지스터, 각 핀으로 내보낼 값을 쓸 때 사용

32-bit set/reset register 설정/리셋 레지스터

  • GPIOx_BSRR : 각 핀의 출력을 한 번에 여러개 켜거나 끌 때 사용

32-bit locking register

  • GPIOx_LCKR : 핀의 설정을 잠그는 기능, 리셋 전까지 변경 불가

32-bit alternate function selection register 대체 기능 선택 레지스터

  • GPIOx_AFRH / GPIOx_AFRL : 각 핀에 특별한 기능으로 쓸 때

GPIO 메모리 맵

메모리 맵 : 특정 하드웨어가 CPU의 어떤 메모리 주소에 연결되어 있는지
STM32의 경우, 각 GPIO포트 (A-K)가 아래 사진과 같이 특정 메모리 주소에 할당되어 있다.

GPIO 핀의 구조

  • pull-up/ pull-down 기능 : 핀을 기본적으로 1(VDD)이나, 0(VSS)로 유지하도록 내부에 저항을 연결하여, 외부 신호가 없을때도 핀 상태가 유지된다.
  • Protection diode 보호 다이오드 : 핀에 과전압이 걸릴 때 내부 회로를 보호하는 다이오드
  • Push-Pull : 핀을 1,0 으로 확실하게 밀어주는 출력 방식. 내부에 P-MOS, N-MOS 트랜지스터가 번갈아 켜지면서 강하게 신호를 내보낸다. 대부분의 일반출력 시 사용
  • Open-drain 모드 : 내부 N-MOS 트랜지스터만으로 0으로만 당길 수 있다. 1로 만들려면 외부에 풀업 저항을달아야 함. 여러 장치가 한 선을 공유할 때 사용
  • 입력/출력 연결 : 각 핀은 내부 회로나 alternate function 신호선, 소프트웨어로 연결.

VDD : 양의 전압을 의미한다.
VSS : 접지, 0V를 의미한다.

포트 비트 설정표

따라서, GPIO 사용 전에 위에 해당하는 레지스터 비트를 원하는 입출력 구성에 맞게 설정해야 한다.

MODER(i) [1:0]OTYPER(i)OSPEEDR(i) [B:A]PUPDR(i) [1:0]I/O configuration설명
100SPEED00AF, PP대체기능(AF), 푸시풀 출력(Push-Pull), 풀업/풀다운 없음
100SPEED01AF, PP + PU대체기능, 푸시풀 출력, 내부 풀업 저항 연결
100SPEED10AF, PP + PD대체기능, 푸시풀 출력, 내부 풀다운 저항 연결
100SPEED11Reserved예약(사용 금지)
101SPEED00AF, OD대체기능, 오픈드레인 출력(Open Drain), 풀업/풀다운 없음
101SPEED01AF, OD + PU대체기능, 오픈드레인 출력, 내부 풀업 저항 연결
101SPEED10AF, OD + PD대체기능, 오픈드레인 출력, 내부 풀다운 저항 연결
101SPEED11Reserved예약(사용 금지)
00xx00Input, Floating입력 모드, 풀업/풀다운 없음(플로팅 입력)
00xx01Input, PU입력 모드, 내부 풀업 저항 연결
00xx10Input, PD입력 모드, 내부 풀다운 저항 연결
00xx11Reserved예약(플로팅 입력, 사용 금지)
11xxxxInput/output, Analog아날로그 모드(ADC 등 아날로그 입력/출력에 사용)
xxxx11Reserved예약(사용 금지)

MODER(i) 1:0
00 입력
01 일반 출력
10 대체 기능
11 아날로그

OTYPER(i)
0 : push pull
1 : Open drain .. 등

Port mode Register . GPIOx_MODER

포트 x의 각 핀별 Mode -> 입력, 출력, AF, Analog 설정
핀마다 2비트씩 할당하여 포트마다 총 32비트를 사용..

그러니까, 지금 STM32 MCU 레지스터 구조를 보여주는거임
하나의 포트 예를 들어 GPIO_A ~ K 에 있는 16개 또는 K의 경우 8개 ㅇㅇ핀 각각의 동작 모드를 설정하는 데 사용된다.
핀 하나당 2비트씩 (2자리수) 사용되므로, 포트 하나당 총 32비트 사용
MODER 1:0은 0번 핀의 모드를 설정한다.
핀의 모드라고 하면 위에서 말한것처럼 00 01 10 11 등 존재

rw? read write

입력포트로 설정시, 동작

  • output buffer 비활성화. 출력 모드일 때 핀에 전압을 내보내는 회로. 입력때는 끔
  • the schmitt trigger input 활성화. 입력 신호가 애매하게 변할 때 0 or 1로 판단해주는 회로
  • the pull-up, pull-down 저항들은 PUPDR register 값에 따라 활성화된다.
  • 해당 IO pin의 값이 샘플되어 input data register에 저장
  • 프로세서는, 해당 레지스터의 값을 읽어 IO값을 얻는다.

즉 입력포트로 설정 시, 출력 회로는 꺼지고, 입력 회로가 켜짐. 외부 신호가 들어오면 그 값이 Input Data Register에 저장하고 CPU가 해당 레지스터의 값을 읽어서 핀 값을 확인하게 된다.

출력포트로 설정시, 동작

  • output buffer 활성화
  • Open drain mode : 핀에서 0을 내보낼 때만 전기가 통하게 한다. N-MOS라는 핀을 활성화하여 핀이 ground와 연결. 1을 쓰면 핀이 연결되지 않고 전기가 흐르지 않는 Hi-Z 하이 임피던스 상태가 된다.
  • Push-pull mode : 0,1 모두 내보낼 수 있다. 0을 쓰면 N-MOS 장치 활성화, 1을 쓰면 P-MOS 스위치 켜져서 핀이 플러스 전원과 연결된다.
  • 약한 pull-up(아무것도 연결되지 않음), pull down(땅족으로 당겨주는 약한 저항) 저항값들이 활성화. 소프트웨어에서 설정한 값인 GPIOx_PURDR과는 상관없을수도 있다.
  • I/O 핀의 값이 샘플링되어 input data register에 저장.
  • output data register의 값을 읽으면 직전에 read한 값을 얻는다.

주변 장치 사용 절차

  1. 주변장치 구동을 위한 클럭회로 설정 : 기본적으로 절전모드를 위해 개별 주변장치의 클럭신호가 중단되어 있기 때문에 활성화 시켜야함.
  2. I/O핀의 동작 모드 설정
  3. 주변 장치 내부 레지스터 값 설정 : 대부분의 주변장치들은 여러개의 프로그램 가능 레지스터를 보유했다. 즉 주변장치 내부의 레지스터 값을 세팅해야 한다. (드라이버 함수 호출)
  4. 인터럽트 설정 : 인터럽트 우선순위 등 설정

STM32 클럭

clock : MCU가 어느정도의 빠르기로 동작하는지 정해주는 박자 역할
따라서, 프로세서를 구동하기 위해 여러 종류의 클럭을 생성해야 한다.

다양한 클럭 소스를 조합하여 필요 없는 클럭은 끄고 ㅇㅇ 그렇게 사용하믄댐

  • STM32는 다음과 같은 내부 클럭을 이용하여 시스템의 메인클럭(SYSCLK)을 생성한다.
  • HSI oscillator clock : 내부에 내장된 고속 클럭. 외부 부품 없이도 동작 가능
  • HSE oscillotor clock : 외부에서 연결하는 코속 클럭. 정확도가 높음
  • Main PLL clock : PLL은 입력 클럭을 원하는 배수로 증폭! 해서 더 빠른 클럭을 만들어준다.
  • 보조 클럭들도 이용
  • 32 kHz 저속 내부 RC 클럭
  • 저속 외부 수정발진자. RTC는 실시간 시계

C언어를 이용한 GPIO 제어

주변장치 설정 - 주소를 헤더파일에 지정

MCU 안에 있는 GPIO, 타이머, UART 등 .. 기능을 설정하려면 레지스터를 바꿔야됨

모든 주변장치의 초기화 작업은, 주변장치의 레지스터들을 설정하여 수행
주변장치 레지스터 -> 메모리 주소에 매핑되어 있음. 즉 포인터를 이용하여 접근
ex) GPIO 설정 - 헤더파일의 레지스터 주소 정의
헤더파일에는 이런 주소들이 이름으로서 정리되어 있다.

*(unsigned int*)0x40020014 = 0x01; // PA0 핀을 HIGH로 만든다

GPIOA의 ODR 주소가 0x40020014라면 해당 값을 1로 바꾸어 핀에 전기가 나가게 한다.

헤더파일에는 아래처럼 define되어 있으므로,

#define GPIOA_ODR  (*(volatile unsigned int*)0x40020014)

아래처럼 쉽게 접근할 수 있다.

GPIOA_ODR = 0x01;

주변장치의 사용방법 - 주소 직접 지정

위의 설명처럼 #define을 이용하여 주변장치 레지스터에 접근해보자.

GPIO 레지스트 구조체의 정의


struct 구조체의 원소들의 순서는 GPIO 제어를 위한 메모리 맵 상의 레지스터들의 오프셋 주소와 일치하게 된다. 이때 각 원소들의 주소는 레지스터 길이만큼씩 offset 차이가 난다.
offset? 기준점에서 얼마나 떨어져 있는지. 즉 거리

GPIO_LED는 GPIOx 포트의 레지스터들의 시작주소로 설정한다. 포트x의 레지스터들은 C언어의 struct 접근방식으로 read write 가능

프로그래밍 패턴 - I/O 접근

Enum 타입으로 아래와 같이 활용 가능

중간 정리
외부와 소통하는 GPIO에서, LED는 특정 포트와 연결되어 있다. 소프트웨어는 해당 핀의 모드와 상태를 제어해야 하는데 이때 LED를 enum 타입으로 정의하여 유지보수 쉽게 ㅇㅇ

MCU내부의 GPIO (입출력 포트) 를 통하여, LED를 끄고 키는 것을 통제할 수 있는데,
GPIOA 라고 쓰는것만으로도 미리 define 즉 메모리 주소와 매핑시켜놓자

LED1을 켜라 -> GPIOA의 특정 비트에 1을 써라!
버튼을 읽어라 -> GPIOB의 입력값을 읽어라 등

┌───────────────────────────────────────────────┐
│                 MCU(마이크로컨트롤러)         │
│                                               │
│   ┌─────────────┐     ┌──────────────┐        │
│   │   CPU       │     │   메모리     │        │
│   │ (두뇌)      │     │ (RAM/ROM)   │        │
│   └─────────────┘     └──────────────┘        │
│         │                    │                │
│         │                    │                │
│         │                    │                │
│   ┌─────────────────────────────────────┐     │
│   │         주변장치(Peripherals)       │     │
│   │ ┌────────────┐  ┌───────────────┐   │     │
│   │ │  GPIOA     │  │   타이머      │   │ ... │
│   │ │ (입출력)   │  │ (Timer)      │   │     │
│   │ └────────────┘  └───────────────┘   │     │
│   └─────────────────────────────────────┘     │
│         │                                     │
│         │ GPIOA의 특정 핀(예: PA5)            │
│         │                                     │
│   ┌───────────────────────┐                   │
│   │        LED           │  ←←←←←←←←←←←←←←←←─┘
│   │   (불빛 부품)        │
│   └───────────────────────┘
└───────────────────────────────────────────────┘
1.	**CPU(두뇌)**가
2.	**메모리 주소(0x40010800 등)**를 통해
3.	**GPIOA(입출력 회로)**의
4.	**특정 핀(PA5)**에 신호를 보내서
5.	외부의 LED를 켜거나 끈다

GPIO 메모리 맵 정의하기

헤더 파일에 GPIO 메모리 맵을 define으로 정의

PERIPH_BASE는 주변장치 영역이 시작되는 주소, 즉 0x40000000 번지부터 각종 주변장치가 배치되어 있다.
각 포트의 메모리 주소를 정할 수 있다.

GPIO 핀의 정의 : Stm32f4xx_hal_GPIO.h

위에서 이해한 바와 같이, 코드를 읽기 쉽게 하기 위하여 GPIO 관련 핀번호를 레지스터에서 관련된 위치값들로 미리 정의해둔다.

GPIO 모드 관련 #define


GPIO mode 설정값들을 모드 이름으로 정의해준다.

GPIO 레지스터 관련 Macro

매크로를 이용하여, 간단한 체크를 함수처럼 표현한다. 마찬가지로 프로그램이 읽기 쉬워짐
매크로 == 자동치환

주변장치 데이터의 유형

ARM 계열 CPU의 헤더파일에는 주변장치의 레지스터를 가리키는 자료형인 volatile을 정의한 _ _ IO 유형이 들어있다.

레지스터 관련 변수들은 volatile로 선언하여, 단순 변수가 아니라는 것을 컴파일러에게 지시해야 한다.
volatile로 선언하지 않은 변수들은 컴파일러가 메모리로 인식하여 코드를 변환하거나 최적화로 삭제할 수 있다.

volatile 형식 한정자에 대해 설명하시오

레지스터 관련 변수의 값을 항상 메모리에서 직접 읽고 쓸 수 있게 한다. 컴파일러가 변수의 값을 메모리로 인식하여 코드를 변환하거나 최적화하여 삭제/변경할 수 있기 때문이다.

호환성이 높은 코드 작성법

uint8_t  a = 255;      // 8비트 부호 없는 정수
int16_t  b = -12345;   // 16비트 부호 있는 정수
uint32_t c = 100000;   // 32비트 부호 없는 정수

C언어에서 기본 자료형은 컴퓨터나 컴파일러에 의해 크키가 달라질 수 있다. 하지만 임베디드 시스템에서는 정확한 사이즈의 자료형이 필요하다. 따라서 위의 코드를 작성하여 항상 같은 크기로 변수를 쓸 수 있게 한다.

다른 프로세서로 소스코드를 포팅할 때, 해당 파일을 수정하면 변경이 쉬워진다.
-> 포터블 코딩 기법

GPIO 설정 예제

// STM32에서 LED가 연결된 GPIO 핀을 초기화하는 함수 예시

void BSP_LED_Init(Led_TypeDef Led)
{
    GPIO_InitTypeDef GPIO_InitStruct; // 1. GPIO 설정값을 담을 구조체 선언

    // 2. 해당 LED가 연결된 GPIO 포트의 클럭(전원 공급)을 켠다.
    //    (GPIO를 사용하려면 반드시 클럭을 먼저 활성화해야 함)
    LEDx_GPIO_CLK_ENABLE(Led);

    // 3. GPIO 핀 설정값 지정
    GPIO_InitStruct.Pin   = GPIO_PIN[Led];           // (3-1) 사용할 핀 번호 지정 (예: PA5)
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;     // (3-2) 출력 모드로 설정 (Push-Pull)
    GPIO_InitStruct.Pull  = GPIO_PULLUP;             // (3-3) 풀업 저항 활성화 (핀을 기본적으로 1로 유지)
    GPIO_InitStruct.Speed = GPIO_SPEED_FAST;         // (3-4) 빠른 신호 전환 속도 설정

    // 4. 설정한 값으로 실제 GPIO 하드웨어를 초기화
    //    (구조체에 담긴 값대로 레지스터에 적용)
    HAL_GPIO_Init(GPIO_PORT[Led], &GPIO_InitStruct);

    // 5. LED 핀의 초기 출력값을 설정 (여기서는 High로 설정)
    //    (LED 회로에 따라 High가 켜짐 또는 꺼짐이 될 수 있음)
    HAL_GPIO_WritePin(GPIO_PORT[Led], GPIO_PIN[Led], GPIO_PIN_SET);
}

GPIO에서 Alternate Function 설정

/* 
 * STM32 HAL 라이브러리에서 GPIO 핀을 Alternate Function(대체 기능, 예: UART, SPI, I2C 등)으로 설정
 */

/* 만약 GPIO 핀의 모드가 Alternate Function(Push-Pull 또는 Open-Drain)이라면 */
if ((GPIO_Init->Mode == GPIO_MODE_AF_PP) || 
    (GPIO_Init->Mode == GPIO_MODE_AF_OD))
{
    /* 1. Alternate function 값이 올바른지 검사 (디버깅용, 잘못된 값이면 assert 에러) */
    assert_param(IS_GPIO_AF(GPIO_Init->Alternate));

    /* 2. 현재 설정하려는 핀(position)에 해당하는 AFR(Alternate Function Register)의 값을 읽어온다.
     *    - STM32는 한 포트에 16개 핀이 있고, AFR 레지스터는 2개(각각 8핀씩 담당, AFR[0], AFR[1])
     *    - position >> 3: position(0~15)을 8로 나눠서 0이면 AFR[0], 1이면 AFR[1]을 선택
     */
    temp = GPIOx->AFR[position >> 3];

    /* 3. 해당 핀의 4비트(Alternate Function 설정 영역)를 먼저 0으로 클리어한다.
     *    - (position & 0x07): 0~7 중 몇 번째 핀인지
     *    - * 4: 4비트씩 이동 (각 핀마다 4비트 자리)
     *    - 0xF << (위치): 해당 4비트만 1로 만든 마스크
     *    - ~: 반전해서 해당 4비트만 0으로 만들기
     */
    temp &= ~((uint32_t)0xF << ((uint32_t)(position & (uint32_t)0x07) * 4));

    /* 4. 원하는 Alternate Function 값을 해당 위치에 넣는다.
     *    - GPIO_Init->Alternate: 설정하려는 Alternate Function 값(0~15)
     *    - (위치): 해당 핀의 4비트 자리로 이동
     *    - |=: 기존 값에 새 값을 더해 넣기
     */
    temp |= ((uint32_t)(GPIO_Init->Alternate)
            << (((uint32_t)position & (uint32_t)0x07) * 4));

    /* 5. 변경한 값을 AFR 레지스터에 다시 저장한다.
     *    - 이렇게 하면 해당 핀이 원하는 Alternate Function(예: UART, SPI 등)으로 동작하게 됨
     */
    GPIOx->AFR[position >> 3] = temp;
}

테스팅하는 기술

  1. 시리얼 포트로 디버깅 메시지를 많이 출력
  2. LED 출력을 이용하여 상태를 인코딩
  3. assert_param() 함수 사용 -> 특정 조건이 만족되는지 체크, 만족하지 않는다면 assert_failed() 함수 호출. 이때 매개변수로 실패한 file이름과 소스코드의 라인번호를 전달

assert_param()

함수의 입력값이 올바른지 자동으로 검사해주는 디버깅용 매크로

#ifdef USE_FULL_ASSERT
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif

USE_FULL_ASSERT가 정의되어 있고
expr이 참이면 -> 아무일도 하지 않음
expr이 거짓이면 -> assert_failed() 호출. 문제 발생 위치를 알려줌

USE_FULL_ASSERT가 정의되어 있지 않으면,
void 0 즉 아무 동작도 하지 않음

해당 함수를 실행시키려면

define USE_FULL_ASSERT 1 의 주석을 제거하고 프로젝트를 반드시 리빌드 해야 한다.

assert_failed() 함수 구현

void assert_failed(uint8_t* file, uint32_t line) {
    printf("Assert fail at File %s Line %d", file, (int)line);
    while(1); // 무한루프에 빠져서 프로그램이 여기서 멈춤 (개발자가 원하면 다른 동작도 가능)
}

테스트 후에는 USE_FULL_ASSERT를 0으로 하여 동작하지 않게 해야함.

0개의 댓글