
GPIO 입력 모드는 외부 신호를 읽어들이는 모드입니다. 버튼, 스위치, 센서 등의 디지털 신호를 마이크로컨트롤러로 입력받을 때 사용합니다.
입력 모드의 특징:
STM32F4의 디지털 입력 임계값:
VIH (Input HIGH): 2.0V ~ 3.3V
VIL (Input LOW): 0V ~ 0.8V
주의사항:
일반적인 버튼 연결 방법:
VCC (3.3V)
|
[R] 10kΩ (Pull-up)
|
+------- GPIO 핀
|
[Button]
|
GND
VCC (3.3V)
|
[Button]
|
+------- GPIO 핀
|
[R] 10kΩ (Pull-down)
|
GND
Pull-up 저항:
Pull-down 저항:
STM32 내부 저항:
입력 핀의 현재 상태를 읽는 레지스터입니다.
uint32_t input_value = GPIOA->IDR;
입력 모드로 설정하려면 MODER 레지스터를 00으로 설정합니다.
// PA0를 입력 모드로 설정
GPIOA->MODER &= ~(0x3 << (0 * 2)); // 00: Input mode
Pull-up 또는 Pull-down 저항을 설정합니다.
// PA0에 Pull-up 설정
GPIOA->PUPDR &= ~(0x3 << (0 * 2));
GPIOA->PUPDR |= (0x1 << (0 * 2)); // 01: Pull-up
// PA0에 Pull-down 설정
GPIOA->PUPDR &= ~(0x3 << (0 * 2));
GPIOA->PUPDR |= (0x2 << (0 * 2)); // 10: Pull-down
// PA0에 Pull-up/Pull-down 없음
GPIOA->PUPDR &= ~(0x3 << (0 * 2)); // 00: No pull-up, pull-down
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 클럭 활성화
__HAL_RCC_GPIOA_CLK_ENABLE();
// GPIO 설정
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_PinState button_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
if(button_state == GPIO_PIN_RESET) {
// 버튼이 눌렸을 때 (Active LOW)
} else {
// 버튼이 눌리지 않았을 때
}
/* USER CODE BEGIN 2 */
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 버튼 핀 초기화 (PA0, Pull-up)
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 버튼 상태 읽기
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// 버튼이 눌렸을 때 LED 켜기
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
} else {
// 버튼이 눌리지 않았을 때 LED 끄기
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);
}
HAL_Delay(10); // 10ms 딜레이
}
/* USER CODE END 3 */
Polling 방식의 문제점:
인터럽트는 특정 이벤트가 발생했을 때 현재 실행 중인 프로그램을 중단하고, 미리 정의된 함수(ISR)를 실행하는 메커니즘입니다.
인터럽트의 장점:
EXTI는 GPIO 핀에서 발생하는 외부 신호 변화를 감지하여 인터럽트를 발생시키는 기능입니다.
EXTI 특징:
EXTI 라인 매핑:
EXTI0: PA0, PB0, PC0, PD0, ... (각 포트의 0번 핀)
EXTI1: PA1, PB1, PC1, PD1, ... (각 포트의 1번 핀)
...
EXTI15: PA15, PB15, PC15, PD15, ...
주의: 하나의 EXTI 라인은 한 번에 하나의 핀만 사용 가능합니다.
Rising Edge (상승 에지):
Falling Edge (하강 에지):
Both Edges (양 에지):
NVIC는 ARM Cortex-M의 인터럽트 컨트롤러입니다.
NVIC의 역할:
우선순위:
Configuration → GPIO:
GPIO mode: External Interrupt Mode with Falling edge trigger detection
GPIO Pull-up/Pull-down: Pull-up
User Label: USER_BUTTON (선택사항)
Configuration → NVIC:
EXTI line0 interrupt: Enabled
Priority: 0 (기본값)
Project Manager → Generate Code
STM32 HAL에서는 HAL_GPIO_EXTI_Callback() 함수를 사용합니다.
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
// PA0 인터럽트 발생 시 처리
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
}
/* USER CODE END 0 */
1. GPIO 핀 상태 변화 감지
2. EXTI 하드웨어가 인터럽트 플래그 설정
3. NVIC가 인터럽트 요청 처리
4. EXTIx_IRQHandler() 호출 (HAL에서 자동 생성)
5. HAL_GPIO_EXTI_IRQHandler() 호출
6. HAL_GPIO_EXTI_Callback() 호출 (사용자 정의)
7. 인터럽트 플래그 클리어
8. 메인 프로그램으로 복귀
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
// 버튼이 눌릴 때마다 LED 토글
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
}
/* USER CODE END 0 */
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
/* 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 */
}
stm32f4xx_it.c 파일:
void EXTI0_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_IRQn 0 */
/* USER CODE END EXTI0_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
/* USER CODE BEGIN EXTI0_IRQn 1 */
/* USER CODE END EXTI0_IRQn 1 */
}
버튼을 누르거나 놓을 때 기계적 접점이 여러 번 붙었다 떨어지는 현상입니다.
정의는 위와 같은데 더 깊게 살펴보면 우리가 손으로 버튼을 눌렀을 때 1회만 전원이 연결 되고 끝날 것 같지만 사실은 실제 버튼 내부에서 작은 진동에 의해 버튼이 수차례 눌리는 것 처럼 진동하게 됩니다. 이 현상을 채터링이라고 하며, 이 채터링 현상을 '필터링'해 여러 번 튀는 신호를 단 한번의 입력으로 처리하는 기술을 디바운싱이라고 합니다.
채터링 특징:
채터링 파형:
버튼 누름 시:
_____ _ _ _________
|___|_|_|_|_|
↑
여러 번 토글됨 (채터링)
RC 필터 사용:
Button ---+--- RC 필터 ---+--- GPIO
| |
GND Pull-up
가장 간단한 방법: 일정 시간 대기 후 재확인
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
HAL_Delay(50); // 50ms 대기 (채터링 안정화)
// 버튼이 여전히 눌려있는지 확인
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
}
}
문제점:
마지막 인터럽트 시간을 저장하여 일정 시간 내 중복 인터럽트 무시
/* USER CODE BEGIN 0 */
uint32_t last_interrupt_time = 0;
const uint32_t debounce_delay = 200; // 200ms
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
uint32_t current_time = HAL_GetTick();
// 마지막 인터럽트로부터 충분한 시간이 지났는지 확인
if((current_time - last_interrupt_time) > debounce_delay) {
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
last_interrupt_time = current_time;
}
}
}
/* USER CODE END 0 */
장점:
/* USER CODE BEGIN 0 */
typedef enum {
BUTTON_IDLE,
BUTTON_PRESSED,
BUTTON_DEBOUNCING
} ButtonState_t;
ButtonState_t button_state = BUTTON_IDLE;
uint32_t debounce_start_time = 0;
const uint32_t debounce_time = 50;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
if(button_state == BUTTON_IDLE) {
button_state = BUTTON_DEBOUNCING;
debounce_start_time = HAL_GetTick();
}
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(button_state == BUTTON_DEBOUNCING) {
if((HAL_GetTick() - debounce_start_time) > debounce_time) {
// 디바운싱 시간 경과 후 버튼 상태 확인
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// 실제 버튼이 눌린 것으로 확인
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
button_state = BUTTON_PRESSED;
} else {
button_state = BUTTON_IDLE;
}
}
} else if(button_state == BUTTON_PRESSED) {
// 버튼이 놓이길 기다림
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
button_state = BUTTON_IDLE;
}
}
}
/* USER CODE END 3 */
일반적인 디바운싱 시간:
실험으로 최적값 찾기:
1. 50ms로 시작
2. 채터링 발생 시 증가
3. 반응이 느리면 감소
/* USER CODE BEGIN 0 */
uint8_t brightness = 0; // 0 ~ 10
uint32_t last_interrupt_time = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
uint32_t current_time = HAL_GetTick();
if((current_time - last_interrupt_time) > 200) {
brightness = (brightness + 1) % 11; // 0 ~ 10 순환
last_interrupt_time = current_time;
}
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 소프트웨어 PWM으로 밝기 조절
for(int i = 0; i < 100; i++) {
if(i < brightness * 10) {
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);
}
HAL_Delay(1);
}
}
/* USER CODE END 3 */
/* USER CODE BEGIN 0 */
uint32_t last_button1_time = 0;
uint32_t last_button2_time = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
uint32_t current_time = HAL_GetTick();
if(GPIO_Pin == GPIO_PIN_0) { // Button 1
if((current_time - last_button1_time) > 200) {
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12); // LED 1 토글
last_button1_time = current_time;
}
}
if(GPIO_Pin == GPIO_PIN_1) { // Button 2
if((current_time - last_button2_time) > 200) {
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13); // LED 2 토글
last_button2_time = current_time;
}
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN 0 */
uint32_t button_press_time = 0;
uint8_t button_long_press = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// 버튼이 눌림
button_press_time = HAL_GetTick();
button_long_press = 0;
} else {
// 버튼이 놓임
uint32_t press_duration = HAL_GetTick() - button_press_time;
if(press_duration > 1000 && !button_long_press) {
// 1초 이상 눌렀을 때
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, GPIO_PIN_SET);
} else if(press_duration < 1000) {
// 짧게 눌렀을 때
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
}
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// 버튼을 1초 이상 누르고 있는지 확인
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
uint32_t press_duration = HAL_GetTick() - button_press_time;
if(press_duration > 1000 && !button_long_press) {
// 길게 누름 처리 (한 번만)
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, GPIO_PIN_SET);
button_long_press = 1;
}
}
}
/* USER CODE END 3 */
확인 사항:
1. GPIO 클럭이 활성화되었는가?
2. EXTI 라인이 올바르게 설정되었는가?
3. NVIC에서 인터럽트가 활성화되었는가?
4. Pull-up/Pull-down 설정이 올바른가?
5. 트리거 모드가 적절한가? (Falling/Rising/Both)
디버깅 코드:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
// 인터럽트가 호출되는지 확인
static uint32_t counter = 0;
counter++;
// 디버거로 counter 값 확인
}
}
원인:
해결:
우선순위 설정:
// EXTI0 우선순위: 1
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
// EXTI1 우선순위: 2 (낮음)
HAL_NVIC_SetPriority(EXTI1_IRQn, 2, 0);
절대 하지 말 것:
권장 방법:
/* USER CODE BEGIN 0 */
volatile uint8_t button_pressed = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
button_pressed = 1; // 플래그만 설정
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(button_pressed) {
button_pressed = 0;
// 여기서 실제 처리 수행
// 복잡한 로직, HAL_Delay 등 사용 가능
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}
}
/* USER CODE END 3 */
버튼을 누를 때마다 LED 상태를 토글하는 프로그램을 작성하세요.
요구사항:
버튼을 누를 때마다 카운터를 증가시키고, 4개의 LED로 2진수 표시
요구사항:
힌트:
uint8_t counter = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_0) {
counter = (counter + 1) % 16;
// LED 업데이트
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, (counter & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, (counter & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, (counter & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, (counter & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
}
버튼을 빠르게 두 번 누르는 것을 감지하는 프로그램 작성
요구사항:
| 구분 | Polling | Interrupt |
|---|---|---|
| CPU 사용 | 지속적으로 확인 필요 | 이벤트 발생 시만 동작 |
| 전력 소모 | 높음 | 낮음 |
| 응답 속도 | 폴링 주기에 의존 | 빠름 |
| 구현 복잡도 | 낮음 | 중간 |
| 놓칠 가능성 | 있음 | 없음 |