GPIO(General Purpose Input/Output)는 범용 입출력 핀으로, 마이크로컨트롤러에서 외부 장치와 디지털 신호를 주고받기 위한 핀입니다.
STM32F4의 각 GPIO 핀은 다음과 같은 특징을 가집니다:
STM32F407은 다음과 같은 GPIO 포트를 제공합니다:
Discovery 보드의 LED 연결:
LED4 (Green) : PD12
LED3 (Orange) : PD13
LED5 (Red) : PD14
LED6 (Blue) : PD15
GPIO를 제어하기 위해서는 내부 레지스터를 설정해야 합니다. HAL 라이브러리가 이를 추상화하지만, 동작 원리를 이해하는 것이 중요합니다.
각 핀의 동작 모드를 설정합니다 (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)); // 출력 모드 설정
출력 타입을 설정합니다 (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비트씩 할당).
00: Low speed
01: Medium speed
10: High speed
11: Very high speed
내부 풀업/풀다운 저항을 설정합니다 (2비트씩 할당).
00: No pull-up, pull-down
01: Pull-up
10: Pull-down
11: Reserved
실제 출력 값을 설정합니다 (1비트씩 할당).
0: Low level (0V)
1: High level (3.3V)
예시: GPIOD_12를 HIGH로 설정
GPIOD->ODR |= (1 << 12); // Set
GPIOD->ODR &= ~(1 << 12); // Clear
원자적(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을 사용하는 이유:
HAL(Hardware Abstraction Layer)은 하드웨어를 추상화하여 쉽게 제어할 수 있도록 도와주는 라이브러리입니다.
장점:
단점:
실무에서는 HAL과 레지스터 직접 제어를 혼용합니다:
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;
GPIO_MODE_INPUT: 입력 모드GPIO_MODE_OUTPUT_PP: 출력 Push-PullGPIO_MODE_OUTPUT_OD: 출력 Open-DrainGPIO_MODE_AF_PP: Alternate Function Push-PullGPIO_MODE_AF_OD: Alternate Function Open-DrainGPIO_NOPULL: 풀업/풀다운 없음GPIO_PULLUP: 풀업GPIO_PULLDOWN: 풀다운GPIO_SPEED_FREQ_LOWGPIO_SPEED_FREQ_MEDIUMGPIO_SPEED_FREQ_HIGHGPIO_SPEED_FREQ_VERY_HIGHvoid 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)void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
현재 상태를 반전시킵니다 (HIGH ↔ LOW).
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
반환값: GPIO_PIN_SET 또는 GPIO_PIN_RESET
Pinout & Configuration → System Core → GPIO
Discovery 보드는 이미 LED 핀이 설정되어 있습니다:
추가 설정이 필요하다면:
Clock Configuration 탭:
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 BEGIN과 USER CODE END 사이에만 코드를 작성해야 합니다. 이 영역 밖의 코드는 CubeMX 재생성 시 삭제됩니다.
# 빌드
make
# 플래싱 (VSCode Task 또는 터미널)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c "program build/led_blink.elf verify reset exit"
녹색 LED(PD12)가 0.5초 간격으로 깜박입니다.
/* 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가 순환합니다.
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
while((HAL_GetTick() - tickstart) < wait)
{
}
}
HAL_Delay는 SysTick 타이머를 사용합니다:
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;
}
문제점:
대안:
RTOS에서도 GPIO를 켜거나 UART로 데이터를 보낼 때는 여전히 HAL_GPIO_WritePin()이나 HAL_UART_Transmit()같은 함수를 사용합니다. 다만, 시간을 기다려야 하는 상황에서는 HAL 대신 RTOS 방식으로 교체하는 것입니다.
초기화 단계에선 RTOS 커널이 시작되기 전이기 때문에 HAL_Delay()를 써도 무방합니다. 다만 태스크 실행 이후에는 osDelay() 또는 vTaskDelay()를 써야 다른 태스크들이 멈추지 않고 작동하게 됩니다.
// 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++);
}
// 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);
}
간단한 토글 테스트 (1,000,000회):
레지스터 직접: ~0.8초
HAL 함수: ~1.2초
차이가 크지 않으므로 대부분의 경우 HAL 사용을 권장합니다.
확인 사항:
디버거로 확인:
// 브레이크포인트 설정 후 레지스터 확인
// RCC->AHB1ENR의 GPIODEN 비트 확인
// GPIOD->MODER의 해당 비트 확인
// GPIOD->ODR의 값 확인
HAL_Delay() 값 조정항상 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 */
}
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 시간
}
3개의 LED를 사용해 교통 신호등을 구현하세요:
모스 부호로 SOS 신호를 LED로 표현하세요:
S: · · · (짧-짧-짧)
O: --- (길-길-길)
S: · · · (짧-짧-짧)