STM32 #2

홍태준·2026년 1월 15일

STM32

목록 보기
2/15
post-thumbnail

Week 1 Day 2: GPIO 출력 - LED 제어 (HAL 라이브러리)

학습 목표

  • GPIO의 동작 원리와 레지스터 구조 이해
  • STM32 HAL 라이브러리를 사용한 GPIO 제어
  • LED를 통한 디지털 출력 구현
  • Blink 프로그램 작성 및 디버깅

1. GPIO 기초 이론

1.1 GPIO란?

GPIO(General Purpose Input/Output)는 범용 입출력 핀으로, 마이크로컨트롤러에서 외부 장치와 디지털 신호를 주고받기 위한 핀입니다.

1.2 GPIO의 기본 개념

STM32F4의 각 GPIO 핀은 다음과 같은 특징을 가집니다:

  • 입력 또는 출력으로 설정 가능
  • 출력 시 HIGH(3.3V) 또는 LOW(0V) 상태 설정
  • 최대 출력 전류: 25mA (핀당)
  • Pull-up/Pull-down 저항 내장

1.3 GPIO 포트 구조

STM32F407은 다음과 같은 GPIO 포트를 제공합니다:

  • GPIOA ~ GPIOI (총 9개 포트)
  • 각 포트는 16개의 핀 보유 (GPIOx_0 ~ GPIOx_15)
  • 예: GPIOD_12, GPIOD_13, GPIOD_14, GPIOD_15

Discovery 보드의 LED 연결:

LED4 (Green)  : PD12
LED3 (Orange) : PD13
LED5 (Red)    : PD14
LED6 (Blue)   : PD15

2. GPIO 레지스터 구조

GPIO를 제어하기 위해서는 내부 레지스터를 설정해야 합니다. HAL 라이브러리가 이를 추상화하지만, 동작 원리를 이해하는 것이 중요합니다.

2.1 GPIOx_MODER (Mode Register)

각 핀의 동작 모드를 설정합니다 (2비트씩 할당).

00: Input mode
01: General purpose output mode
10: Alternate function mode
11: Analog mode

예시: GPIOD_12를 출력으로 설정

GPIOD->MODER &= ~(0x3 << (12 * 2));  // 해당 비트 클리어
GPIOD->MODER |= (0x1 << (12 * 2));   // 출력 모드 설정

2.2 GPIOx_OTYPER (Output Type Register)

출력 타입을 설정합니다 (1비트씩 할당).

0: Push-pull (기본값)
1: Open-drain

Push-pull: HIGH/LOW를 모두 능동적으로 출력
Open-drain: LOW만 출력, HIGH는 외부 Pull-up 필요

open-drain 모드는 내부 스위치가 GND(LOW)에만 연결돼있어 0을 쓰면 GND와 연결돼 0V가 되지만 1을 쓰면 스위치가 그냥 열려버리는 상태를 의미합니다. 따라서 high 출력을 위해 외부(또는 내부)에서 저항을 통해 VCC접압에 묶어두는 것을 풀업(Pull-up)이라고 합니다.

다소 복잡하지만 오픈 드레인을 쓰면 Wired-AND 연결 상태가 되어 여러개의 핀을 하나의 선에 묶을 수 있습니다. 하나라도 0을 출력하면 전체 선에 0이 되기 때문에 주로 I2C통신에서 이용됩니다.

추가로 오픈 드레인 모드를 이용하면 전압 레벨을 변환하기 용이합니다. 따라서 서로 다른 전압을 사용하는 장치끼리 대화할 수 있게 전압을 호환시킬 수 있습니다.

2.3 GPIOx_OSPEEDR (Output Speed Register)

출력 속도를 설정합니다 (2비트씩 할당).

00: Low speed
01: Medium speed
10: High speed
11: Very high speed

2.4 GPIOx_PUPDR (Pull-up/Pull-down Register)

내부 풀업/풀다운 저항을 설정합니다 (2비트씩 할당).

00: No pull-up, pull-down
01: Pull-up
10: Pull-down
11: Reserved

2.5 GPIOx_ODR (Output Data Register)

실제 출력 값을 설정합니다 (1비트씩 할당).

0: Low level (0V)
1: High level (3.3V)

예시: GPIOD_12를 HIGH로 설정

GPIOD->ODR |= (1 << 12);   // Set
GPIOD->ODR &= ~(1 << 12);  // Clear

2.6 GPIOx_BSRR (Bit Set/Reset Register)

원자적(atomic) 연산으로 출력을 설정/해제합니다.

하위 16비트: Set 비트 (1 쓰면 해당 핀 HIGH)
상위 16비트: Reset 비트 (1 쓰면 해당 핀 LOW)

예시:

GPIOD->BSRR = (1 << 12);       // PD12 Set (HIGH)
GPIOD->BSRR = (1 << (12 + 16)); // PD12 Reset (LOW)

BSRR을 사용하는 이유:

  • 읽기-수정-쓰기 없이 한 번의 쓰기로 처리 가능
  • 인터럽트에서 안전 (Race condition 방지)
  • 더 빠른 실행 속도

3. STM32 HAL 라이브러리

HAL(Hardware Abstraction Layer)은 하드웨어를 추상화하여 쉽게 제어할 수 있도록 도와주는 라이브러리입니다.

3.1 HAL 라이브러리 장단점

장점:

  • 코드 가독성 향상
  • 이식성 좋음 (다른 STM32로 쉽게 이전)
  • 초보자 친화적
  • ST 공식 지원

단점:

  • 레지스터 직접 제어보다 느림
  • 코드 크기 증가
  • 내부 동작 이해 필요

실무에서는 HAL과 레지스터 직접 제어를 혼용합니다:

  • 초기 설정: HAL 사용
  • 성능 critical 구간: 레지스터 직접 제어

3.2 GPIO 초기화 함수

3.2.1 HAL_GPIO_Init

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

GPIO_InitTypeDef 구조체:

typedef struct {
    uint32_t Pin;       // 핀 번호 (GPIO_PIN_0 ~ GPIO_PIN_15)
    uint32_t Mode;      // 동작 모드
    uint32_t Pull;      // Pull-up/Pull-down
    uint32_t Speed;     // 출력 속도
} GPIO_InitTypeDef;

3.2.2 Mode 옵션

  • GPIO_MODE_INPUT: 입력 모드
  • GPIO_MODE_OUTPUT_PP: 출력 Push-Pull
  • GPIO_MODE_OUTPUT_OD: 출력 Open-Drain
  • GPIO_MODE_AF_PP: Alternate Function Push-Pull
  • GPIO_MODE_AF_OD: Alternate Function Open-Drain

3.2.3 Pull 옵션

  • GPIO_NOPULL: 풀업/풀다운 없음
  • GPIO_PULLUP: 풀업
  • GPIO_PULLDOWN: 풀다운

3.2.4 Speed 옵션

  • GPIO_SPEED_FREQ_LOW
  • GPIO_SPEED_FREQ_MEDIUM
  • GPIO_SPEED_FREQ_HIGH
  • GPIO_SPEED_FREQ_VERY_HIGH

3.3 GPIO 출력 제어 함수

3.3.1 HAL_GPIO_WritePin

void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

PinState:

  • GPIO_PIN_RESET: LOW (0V)
  • GPIO_PIN_SET: HIGH (3.3V)

3.3.2 HAL_GPIO_TogglePin

void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

현재 상태를 반전시킵니다 (HIGH ↔ LOW).

3.3.3 HAL_GPIO_ReadPin

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

반환값: GPIO_PIN_SET 또는 GPIO_PIN_RESET


4.1 STM32CubeMX 프로젝트 생성

  1. STM32CubeMX 실행
  2. New Project → Board Selector → STM32F4DISCOVERY
  3. Start Project

4.2 GPIO 핀 설정

Pinout & Configuration → System Core → GPIO

Discovery 보드는 이미 LED 핀이 설정되어 있습니다:

  • PD12 ~ PD15: LED4 ~ LED6

추가 설정이 필요하다면:

  1. Pinout view에서 PD12 클릭
  2. GPIO_Output 선택
  3. Configuration → GPIO:
    • GPIO output level: Low
    • GPIO mode: Output Push Pull
    • GPIO Pull-up/Pull-down: No pull-up and no pull-down
    • Maximum output speed: Low

4.3 클럭 설정

Clock Configuration 탭:

  • Input frequency: 8 MHz (HSE)
  • HCLK: 168 MHz (최대 성능)

4.4 프로젝트 생성

Project Manager:

Project Name: led_blink
Toolchain/IDE: Makefile

Generate Code 클릭

생성된 프로젝트의 Core/Src/main.c 파일을 엽니다.

/* USER CODE BEGIN Includes */
/* USER CODE END Includes */

/* Private variables */
/* USER CODE BEGIN PV */
/* USER CODE END PV */

int main(void)
{
  /* USER CODE BEGIN 1 */
  /* USER CODE END 1 */

  /* MCU Configuration */
  HAL_Init();
  SystemClock_Config();
  
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  
  /* USER CODE BEGIN 2 */
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    // LED4 (PD12) 토글
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
    
    // 500ms 지연
    HAL_Delay(500);
  }
  /* USER CODE END 3 */
}

중요: USER CODE BEGINUSER CODE END 사이에만 코드를 작성해야 합니다. 이 영역 밖의 코드는 CubeMX 재생성 시 삭제됩니다.

4.6 빌드 및 플래싱

# 빌드
make

# 플래싱 (VSCode Task 또는 터미널)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
  -c "program build/led_blink.elf verify reset exit"

4.7 실행 결과

녹색 LED(PD12)가 0.5초 간격으로 깜박입니다.


5. 심화 예제

5.1 4개 LED 순차 점등

/* USER CODE BEGIN 2 */
uint8_t led_index = 0;
uint16_t leds[4] = {GPIO_PIN_12, GPIO_PIN_13, GPIO_PIN_14, GPIO_PIN_15};
/* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  // 모든 LED 끄기
  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET);
  
  // 현재 LED 켜기
  HAL_GPIO_WritePin(GPIOD, leds[led_index], GPIO_PIN_SET);
  
  // 다음 LED로 이동
  led_index = (led_index + 1) % 4;
  
  // 200ms 지연
  HAL_Delay(200);
}
/* USER CODE END 3 */

실행 결과: 녹색 → 주황 → 빨강 → 파랑 순서로 LED가 순환합니다.


6. HAL_Delay 함수 분석

6.1 HAL_Delay 구현

__weak void HAL_Delay(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  while((HAL_GetTick() - tickstart) < wait)
  {
  }
}

6.2 SysTick 타이머

HAL_Delay는 SysTick 타이머를 사용합니다:

  • SysTick: ARM Cortex-M의 기본 시스템 타이머
  • 1ms마다 인터럽트 발생
  • HAL_GetTick(): 부팅 후 경과한 ms 반환

초기화 코드 (stm32f4xx_hal.c):

HAL_StatusTypeDef HAL_Init(void)
{
  // SysTick을 1ms 주기로 설정
  HAL_SYSTICK_Config(SystemCoreClock / 1000U);
  
  // SysTick 인터럽트 우선순위 설정
  HAL_NVIC_SetPriority(SysTick_IRQn, TICK_INT_PRIORITY, 0U);
  
  return HAL_OK;
}

6.3 HAL_Delay의 한계

문제점:

  • Busy-waiting: CPU가 대기 중에도 계속 동작
  • 다른 작업 불가 (블로킹)
  • 정확도 제한 (1ms 단위)

대안:

  • 타이머 인터럽트 사용 (다음 강의)
  • RTOS 사용 (나중에 학습)

RTOS에서도 GPIO를 켜거나 UART로 데이터를 보낼 때는 여전히 HAL_GPIO_WritePin()이나 HAL_UART_Transmit()같은 함수를 사용합니다. 다만, 시간을 기다려야 하는 상황에서는 HAL 대신 RTOS 방식으로 교체하는 것입니다.

초기화 단계에선 RTOS 커널이 시작되기 전이기 때문에 HAL_Delay()를 써도 무방합니다. 다만 태스크 실행 이후에는 osDelay() 또는 vTaskDelay()를 써야 다른 태스크들이 멈추지 않고 작동하게 됩니다.


7. 레지스터 직접 제어 vs HAL 비교

7.1 레지스터 직접 제어 예시

// GPIO 클럭 활성화
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;

// PD12를 출력으로 설정
GPIOD->MODER &= ~(0x3 << (12 * 2));
GPIOD->MODER |= (0x1 << (12 * 2));

// Push-pull 설정
GPIOD->OTYPER &= ~(1 << 12);

// Low speed 설정
GPIOD->OSPEEDR &= ~(0x3 << (12 * 2));

// No pull-up, pull-down
GPIOD->PUPDR &= ~(0x3 << (12 * 2));

// 무한 루프
while(1)
{
  GPIOD->BSRR = (1 << 12);        // Set
  for(volatile int i = 0; i < 1000000; i++);
  
  GPIOD->BSRR = (1 << (12 + 16)); // Reset
  for(volatile int i = 0; i < 1000000; i++);
}

7.2 HAL 사용 예시

// GPIO 클럭 활성화
__HAL_RCC_GPIOD_CLK_ENABLE();

// GPIO 초기화
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

// 무한 루프
while(1)
{
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
  HAL_Delay(500);
}

7.3 성능 비교

간단한 토글 테스트 (1,000,000회):

레지스터 직접: ~0.8초
HAL 함수: ~1.2초

차이가 크지 않으므로 대부분의 경우 HAL 사용을 권장합니다.


8. 디버깅 팁

8.1 LED가 켜지지 않을 때

확인 사항:

  • GPIO 클럭이 활성화되었는가?
  • 핀 번호가 정확한가?
  • 출력 모드로 설정되었는가?

디버거로 확인:

// 브레이크포인트 설정 후 레지스터 확인
// RCC->AHB1ENR의 GPIODEN 비트 확인
// GPIOD->MODER의 해당 비트 확인
// GPIOD->ODR의 값 확인

8.2 깜박임이 너무 빠르거나 느릴 때

  • HAL_Delay() 값 조정
  • SysTick 설정 확인

8.3 CubeMX 재생성 시 코드 삭제 문제

항상 USER CODE BEGIN/END 블록 안에 코드 작성:

/* USER CODE BEGIN 2 */
// 여기에 초기화 코드
/* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  // 여기에 메인 루프 코드
  /* USER CODE END 3 */
}

9. 실습 과제

9.1 과제 1: LED 밝기 조절 (소프트웨어 PWM)

HAL_Delay를 사용해 LED 밝기를 조절해보세요.

힌트:

// 밝기 50%
for(int i = 0; i < 100; i++) {
  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
  HAL_Delay(5);  // ON 시간
  HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);
  HAL_Delay(5);  // OFF 시간
}

9.2 과제 2: 교통 신호등 구현

3개의 LED를 사용해 교통 신호등을 구현하세요:

  • 빨강: 5초
  • 노랑: 2초
  • 초록: 5초

9.3 과제 3: SOS 신호

모스 부호로 SOS 신호를 LED로 표현하세요:

S: · · · (짧-짧-짧)
O: --- (길-길-길)
S: · · · (짧-짧-짧)

10. 정리

10.1 오늘 배운 내용

  1. GPIO의 기본 개념과 레지스터 구조
  2. STM32 HAL 라이브러리를 사용한 GPIO 제어
  3. LED 제어를 통한 디지털 출력 구현
  4. HAL_Delay 함수의 동작 원리
  5. 레지스터 직접 제어와 HAL 라이브러리 비교

10.2 참고 자료

profile
당신의 코딩 메이트

0개의 댓글