Week 1 Day 4: 디바운싱 기법과 복잡한 입출력 처리

학습 목표

  • 고급 디바운싱 기법 이해 및 구현
  • 여러 버튼 입력을 효율적으로 처리하는 방법
  • 버튼 이벤트 시스템 설계
  • 비트 마스킹을 활용한 GPIO 제어
  • 상태 머신을 활용한 복잡한 입력 처리

1. 고급 디바운싱 기법

1.1 샘플링 기반 디바운싱

일정 시간 동안 여러 번 샘플링하여 안정적인 상태를 확인하는 방법입니다.

타이머를 이용해 정해진 주기에 들어온 신호만 정상 신호로 판단해 인터럽트로 취급하는 소프트웨어 기반 디바운싱 기법입니다.

동작 원리:
1. 버튼 상태를 일정 주기로 샘플링 (예: 10ms마다)
2. 연속으로 동일한 값이 N번 나오면 안정 상태로 판단
3. 안정 상태 확인 후 이벤트 발생

장점:

  • 채터링에 매우 강함
  • 노이즈에 강함
  • 안정적인 동작

단점:

  • 샘플링을 위한 타이머 필요
  • 메모리 사용량 증가 (샘플 히스토리 저장)
/* USER CODE BEGIN 0 */
#define DEBOUNCE_SAMPLES 5  // 연속 5번 동일한 값

typedef struct {
  GPIO_TypeDef* port;
  uint16_t pin;
  uint8_t sample_count;
  GPIO_PinState last_state;
  GPIO_PinState stable_state;
} Button_t;

Button_t user_button = {
  .port = GPIOA,
  .pin = GPIO_PIN_0,
  .sample_count = 0,
  .last_state = GPIO_PIN_SET,
  .stable_state = GPIO_PIN_SET
};

void Button_Sample(Button_t* btn)
{
  GPIO_PinState current_state = HAL_GPIO_ReadPin(btn->port, btn->pin);
  
  if(current_state == btn->last_state) {
    // 같은 상태가 연속되면 카운트 증가
    btn->sample_count++;
    
    if(btn->sample_count >= DEBOUNCE_SAMPLES) {
      // 안정 상태 확인
      if(btn->stable_state != current_state) {
        btn->stable_state = current_state;
        
        // 상태 변화 이벤트 발생
        if(current_state == GPIO_PIN_RESET) {
          // 버튼 눌림
          HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
        }
      }
    }
  } else {
    // 상태가 바뀌면 카운트 리셋
    btn->last_state = current_state;
    btn->sample_count = 0;
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
// 10ms 타이머 설정 필요 (다음 강의에서 학습)
/* USER CODE END 2 */

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

  /* USER CODE BEGIN 3 */
  Button_Sample(&user_button);
  HAL_Delay(10);  // 10ms 샘플링 주기
}
/* USER CODE END 3 */

1.2 적분기(Integrator) 방식 디바운싱

버튼 상태를 적분하여 임계값을 넘으면 상태 변화로 인식하는 방법입니다.

아날로그 회로에서 적분기는 OP-Amp 회로이지만, 디바운싱에서 말하는 적분기는 RC회로 즉, 저항과 커패시터를 이용해 축전 임계점을 이용하는 회로를 의미합니다. 따라서 물리적으로 버튼을 누른 후 일정 전압 이상으로 V값이 높아지면 소프트웨어에서 버튼 입력으로 동작하게 됩니다.

동작 원리:
1. 버튼이 눌린 상태면 카운터 증가
2. 버튼이 떼어진 상태면 카운터 감소
3. 카운터가 임계값을 넘으면 상태 변경

장점:

  • 부분적인 채터링에도 강함
  • 부드러운 상태 전환
  • 튜닝 가능 (임계값 조정)
/* USER CODE BEGIN 0 */
#define DEBOUNCE_MAX 10
#define DEBOUNCE_THRESHOLD 7

typedef struct {
  GPIO_TypeDef* port;
  uint16_t pin;
  uint8_t integrator;
  GPIO_PinState output;
} ButtonIntegrator_t;

ButtonIntegrator_t btn = {
  .port = GPIOA,
  .pin = GPIO_PIN_0,
  .integrator = 0,
  .output = GPIO_PIN_SET
};

void Button_Integrate(ButtonIntegrator_t* btn)
{
  GPIO_PinState input = HAL_GPIO_ReadPin(btn->port, btn->pin);
  
  if(input == GPIO_PIN_RESET) {
    // 버튼이 눌림 - 적분 증가
    if(btn->integrator < DEBOUNCE_MAX) {
      btn->integrator++;
    }
  } else {
    // 버튼이 떼어짐 - 적분 감소
    if(btn->integrator > 0) {
      btn->integrator--;
    }
  }
  
  // 임계값 확인
  if(btn->integrator >= DEBOUNCE_THRESHOLD) {
    if(btn->output != GPIO_PIN_RESET) {
      btn->output = GPIO_PIN_RESET;
      // 버튼 눌림 이벤트
      HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
    }
  } else if(btn->integrator <= (DEBOUNCE_MAX - DEBOUNCE_THRESHOLD)) {
    if(btn->output != GPIO_PIN_SET) {
      btn->output = GPIO_PIN_SET;
      // 버튼 떼어짐 이벤트
    }
  }
}
/* USER CODE END 0 */

적분기 방식의 특징:

  • 일시적인 노이즈는 무시
  • 지속적인 신호 변화만 인식
  • 임계값으로 민감도 조정 가능

1.3 비트 시프트 방식 디바운싱

비트 시프트 레지스터를 사용하여 최근 샘플 히스토리를 저장하는 방법입니다.

동작 원리:
1. 샘플링 결과를 비트로 저장
2. 최근 N개 샘플을 비트 시프트 레지스터에 유지
3. 모든 비트가 같으면 안정 상태로 판단

8비트 또는 16비트 단위로 샘플링하며, 0000 0001, 0000 0010 처럼 패킷 내 비트가 모두 일치하는지 여부를 판단함으로써 디바운싱하는 방법입니다. 8비트로 샘플링 했을 경우를 예로 들면 1111 1111로 모든 비트가 1로 일치하는 경우에만 소프트웨어가 버튼 입력으로 인식하고 동작하는 방법입니다.

/* USER CODE BEGIN 0 */
typedef struct {
  GPIO_TypeDef* port;
  uint16_t pin;
  uint8_t shift_register;  // 8비트 히스토리
  GPIO_PinState state;
} ButtonShift_t;

ButtonShift_t btn_shift = {
  .port = GPIOA,
  .pin = GPIO_PIN_0,
  .shift_register = 0xFF,  // 초기값: 모두 1 (버튼 안 눌림)
  .state = GPIO_PIN_SET
};

void Button_ShiftDebounce(ButtonShift_t* btn)
{
  // 시프트 레지스터 갱신
  btn->shift_register = (btn->shift_register << 1);
  
  // 현재 입력 추가
  if(HAL_GPIO_ReadPin(btn->port, btn->pin) == GPIO_PIN_SET) {
    btn->shift_register |= 0x01;
  }
  
  // 안정 상태 확인
  if(btn->shift_register == 0x00) {
    // 8번 연속 0 (버튼 눌림)
    if(btn->state != GPIO_PIN_RESET) {
      btn->state = GPIO_PIN_RESET;
      // 버튼 눌림 이벤트
      HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
    }
  } else if(btn->shift_register == 0xFF) {
    // 8번 연속 1 (버튼 안 눌림)
    if(btn->state != GPIO_PIN_SET) {
      btn->state = GPIO_PIN_SET;
      // 버튼 떼어짐 이벤트
    }
  }
}
/* USER CODE END 0 */

비트 시프트 방식의 장점:

  • 메모리 효율적 (1바이트로 8개 샘플 저장)
  • 빠른 처리 속도
  • 구현이 간단

2. 버튼 이벤트 시스템

2.1 이벤트 기반 설계

단순히 버튼 상태를 읽는 것이 아니라, 이벤트를 감지하고 처리하는 시스템입니다.

버튼 이벤트 종류:

  • BUTTON_PRESSED: 버튼이 눌렸을 때
  • BUTTON_RELEASED: 버튼이 떼어졌을 때
  • BUTTON_CLICKED: 짧게 클릭했을 때
  • BUTTON_DOUBLE_CLICKED: 더블 클릭
  • BUTTON_LONG_PRESSED: 길게 눌렀을 때
  • BUTTON_HOLDING: 계속 누르고 있을 때

2.2 버튼 이벤트 구조체 설계

/* USER CODE BEGIN 0 */
typedef enum {
  BUTTON_EVENT_NONE,
  BUTTON_EVENT_PRESSED,
  BUTTON_EVENT_RELEASED,
  BUTTON_EVENT_CLICKED,
  BUTTON_EVENT_DOUBLE_CLICKED,
  BUTTON_EVENT_LONG_PRESSED,
  BUTTON_EVENT_HOLDING
} ButtonEvent_t;

typedef enum {
  BUTTON_STATE_IDLE,
  BUTTON_STATE_DEBOUNCING,
  BUTTON_STATE_PRESSED,
  BUTTON_STATE_WAIT_RELEASE,
  BUTTON_STATE_WAIT_DOUBLE_CLICK,
  BUTTON_STATE_LONG_PRESS
} ButtonState_t;

typedef struct {
  GPIO_TypeDef* port;
  uint16_t pin;
  ButtonState_t state;
  uint32_t press_time;
  uint32_t release_time;
  uint32_t debounce_time;
  uint8_t click_count;
  GPIO_PinState active_level;  // GPIO_PIN_RESET or GPIO_PIN_SET
} Button_EventSystem_t;

// 버튼 초기화
Button_EventSystem_t btn_event = {
  .port = GPIOA,
  .pin = GPIO_PIN_0,
  .state = BUTTON_STATE_IDLE,
  .active_level = GPIO_PIN_RESET,  // Active LOW
  .debounce_time = 50,
  .click_count = 0
};
/* USER CODE END 0 */

2.3 이벤트 처리 함수 구현

/* USER CODE BEGIN 0 */
#define LONG_PRESS_TIME 1000   // 1초
#define DOUBLE_CLICK_TIME 300  // 300ms

ButtonEvent_t Button_Process(Button_EventSystem_t* btn)
{
  ButtonEvent_t event = BUTTON_EVENT_NONE;
  uint32_t current_time = HAL_GetTick();
  GPIO_PinState current_pin = HAL_GPIO_ReadPin(btn->port, btn->pin);
  
  switch(btn->state) {
    case BUTTON_STATE_IDLE:
      if(current_pin == btn->active_level) {
        btn->state = BUTTON_STATE_DEBOUNCING;
        btn->press_time = current_time;
      }
      break;
      
    case BUTTON_STATE_DEBOUNCING:
      if((current_time - btn->press_time) >= btn->debounce_time) {
        if(current_pin == btn->active_level) {
          btn->state = BUTTON_STATE_PRESSED;
          event = BUTTON_EVENT_PRESSED;
        } else {
          btn->state = BUTTON_STATE_IDLE;
        }
      }
      break;
      
    case BUTTON_STATE_PRESSED:
      if(current_pin != btn->active_level) {
        // 버튼 떼어짐
        btn->release_time = current_time;
        btn->state = BUTTON_STATE_WAIT_RELEASE;
        event = BUTTON_EVENT_RELEASED;
      } else if((current_time - btn->press_time) >= LONG_PRESS_TIME) {
        // 길게 누름
        btn->state = BUTTON_STATE_LONG_PRESS;
        event = BUTTON_EVENT_LONG_PRESSED;
      }
      break;
      
    case BUTTON_STATE_WAIT_RELEASE:
      // 더블 클릭 대기
      if(current_pin == btn->active_level) {
        // 다시 눌림 - 더블 클릭
        if((current_time - btn->release_time) < DOUBLE_CLICK_TIME) {
          btn->state = BUTTON_STATE_WAIT_RELEASE;
          event = BUTTON_EVENT_DOUBLE_CLICKED;
        } else {
          btn->state = BUTTON_STATE_DEBOUNCING;
          btn->press_time = current_time;
        }
      } else if((current_time - btn->release_time) >= DOUBLE_CLICK_TIME) {
        // 더블 클릭 타임아웃 - 싱글 클릭
        event = BUTTON_EVENT_CLICKED;
        btn->state = BUTTON_STATE_IDLE;
      }
      break;
      
    case BUTTON_STATE_LONG_PRESS:
      if(current_pin != btn->active_level) {
        btn->state = BUTTON_STATE_IDLE;
        event = BUTTON_EVENT_RELEASED;
      } else {
        // 계속 누르고 있음
        event = BUTTON_EVENT_HOLDING;
      }
      break;
  }
  
  return event;
}
/* USER CODE END 0 */

2.4 이벤트 핸들러 사용 예제

/* USER CODE BEGIN 0 */
void Button_EventHandler(ButtonEvent_t event)
{
  switch(event) {
    case BUTTON_EVENT_PRESSED:
      // 버튼이 눌렸을 때
      break;
      
    case BUTTON_EVENT_RELEASED:
      // 버튼이 떼어졌을 때
      break;
      
    case BUTTON_EVENT_CLICKED:
      // 짧게 클릭했을 때
      HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
      break;
      
    case BUTTON_EVENT_DOUBLE_CLICKED:
      // 더블 클릭했을 때
      HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
      break;
      
    case BUTTON_EVENT_LONG_PRESSED:
      // 길게 눌렀을 때
      HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, GPIO_PIN_SET);
      break;
      
    case BUTTON_EVENT_HOLDING:
      // 계속 누르고 있을 때
      static uint32_t hold_counter = 0;
      hold_counter++;
      if(hold_counter % 10 == 0) {
        HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15);
      }
      break;
      
    default:
      break;
  }
}
/* USER CODE END 0 */

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

  /* USER CODE BEGIN 3 */
  ButtonEvent_t event = Button_Process(&btn_event);
  if(event != BUTTON_EVENT_NONE) {
    Button_EventHandler(event);
  }
  
  HAL_Delay(10);  // 10ms 주기로 처리
}
/* USER CODE END 3 */

3. 여러 버튼 관리

3.1 버튼 배열 관리

여러 개의 버튼을 효율적으로 관리하는 방법입니다.

/* USER CODE BEGIN 0 */
#define MAX_BUTTONS 4

typedef struct {
  GPIO_TypeDef* port;
  uint16_t pin;
  uint32_t last_change_time;
  GPIO_PinState last_stable_state;
  void (*callback)(void);  // 콜백 함수 포인터
} SimpleButton_t;

// 버튼 콜백 함수들
void Button1_Callback(void) {
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}

void Button2_Callback(void) {
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
}

void Button3_Callback(void) {
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14);
}

void Button4_Callback(void) {
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15);
}

// 버튼 배열
SimpleButton_t buttons[MAX_BUTTONS] = {
  {GPIOA, GPIO_PIN_0, 0, GPIO_PIN_SET, Button1_Callback},
  {GPIOA, GPIO_PIN_1, 0, GPIO_PIN_SET, Button2_Callback},
  {GPIOA, GPIO_PIN_2, 0, GPIO_PIN_SET, Button3_Callback},
  {GPIOA, GPIO_PIN_3, 0, GPIO_PIN_SET, Button4_Callback}
};

void Buttons_Process(SimpleButton_t* btns, uint8_t count)
{
  uint32_t current_time = HAL_GetTick();
  
  for(uint8_t i = 0; i < count; i++) {
    GPIO_PinState current_state = HAL_GPIO_ReadPin(btns[i].port, btns[i].pin);
    
    // 상태가 변경되었는지 확인
    if(current_state != btns[i].last_stable_state) {
      // 디바운싱 체크
      if((current_time - btns[i].last_change_time) > 50) {
        btns[i].last_stable_state = current_state;
        btns[i].last_change_time = current_time;
        
        // 버튼이 눌렸을 때만 콜백 호출
        if(current_state == GPIO_PIN_RESET) {
          if(btns[i].callback != NULL) {
            btns[i].callback();
          }
        }
      }
    }
  }
}
/* USER CODE END 0 */

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

  /* USER CODE BEGIN 3 */
  Buttons_Process(buttons, MAX_BUTTONS);
  HAL_Delay(10);
}
/* USER CODE END 3 */

3.2 인터럽트 기반 다중 버튼 처리

/* USER CODE BEGIN 0 */
typedef struct {
  uint16_t pin;
  uint32_t last_interrupt_time;
  void (*callback)(void);
} InterruptButton_t;

void Button_PA0_Handler(void) {
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}

void Button_PA1_Handler(void) {
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
}

InterruptButton_t int_buttons[] = {
  {GPIO_PIN_0, 0, Button_PA0_Handler},
  {GPIO_PIN_1, 0, Button_PA1_Handler}
};

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  uint32_t current_time = HAL_GetTick();
  
  // 버튼 배열에서 해당 핀 찾기
  for(uint8_t i = 0; i < sizeof(int_buttons) / sizeof(InterruptButton_t); i++) {
    if(GPIO_Pin == int_buttons[i].pin) {
      // 디바운싱 체크
      if((current_time - int_buttons[i].last_interrupt_time) > 200) {
        int_buttons[i].last_interrupt_time = current_time;
        
        // 콜백 호출
        if(int_buttons[i].callback != NULL) {
          int_buttons[i].callback();
        }
      }
      break;
    }
  }
}
/* USER CODE END 0 */

4. 비트 마스킹을 활용한 GPIO 제어

4.1 비트 마스킹 기초

여러 GPIO 핀을 동시에 효율적으로 제어하는 방법입니다.

비트 연산 복습:

// SET (OR 연산)
GPIOD->ODR |= (1 << 12);  // PD12를 1로 설정

// CLEAR (AND NOT 연산)
GPIOD->ODR &= ~(1 << 12);  // PD12를 0으로 클리어

// TOGGLE (XOR 연산)
GPIOD->ODR ^= (1 << 12);  // PD12 반전

// CHECK (AND 연산)
if(GPIOD->IDR & (1 << 0)) {
  // PD0가 1인지 확인
}

4.2 여러 핀 동시 제어

BSRR(Bit Set Reset Register)을 이용하면 핀이 많아져도 입력을 동시에 제어할 수 있습니다. TMI지만 이 방법을 이용해 격투 게임의 "잡기 동작" 및 "필살기" 등을 처리합니다.

/* USER CODE BEGIN 0 */
// LED 핀 정의
#define LED1_PIN  GPIO_PIN_12
#define LED2_PIN  GPIO_PIN_13
#define LED3_PIN  GPIO_PIN_14
#define LED4_PIN  GPIO_PIN_15

#define ALL_LEDS  (LED1_PIN | LED2_PIN | LED3_PIN | LED4_PIN)

void LEDs_SetBinary(uint8_t value)
{
  // 모든 LED 끄기
  HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
  
  // 비트별로 LED 설정
  if(value & 0x01) HAL_GPIO_WritePin(GPIOD, LED1_PIN, GPIO_PIN_SET);
  if(value & 0x02) HAL_GPIO_WritePin(GPIOD, LED2_PIN, GPIO_PIN_SET);
  if(value & 0x04) HAL_GPIO_WritePin(GPIOD, LED3_PIN, GPIO_PIN_SET);
  if(value & 0x08) HAL_GPIO_WritePin(GPIOD, LED4_PIN, GPIO_PIN_SET);
}

// 더 효율적인 방법: 직접 레지스터 제어
void LEDs_SetBinary_Fast(uint8_t value)
{
  // 모든 LED 클리어
  GPIOD->BSRR = (ALL_LEDS << 16);
  
  // 비트에 따라 LED 설정
  uint16_t led_bits = 0;
  if(value & 0x01) led_bits |= LED1_PIN;
  if(value & 0x02) led_bits |= LED2_PIN;
  if(value & 0x04) led_bits |= LED3_PIN;
  if(value & 0x08) led_bits |= LED4_PIN;
  
  // 한 번에 설정
  GPIOD->BSRR = led_bits;
}
/* USER CODE END 0 */

4.3 비트 시프트를 이용한 패턴

이 패턴은 LED 전광판처럼 최대 비트(아래 예제엔 8비트)의 공간 제약을 두고, for 이나 while을 이용해 주어진 조건대로 입력을 비트 시프팅(왼쪽 또는 오른쪽으로 비트 이동) 시키는 방법입니다.

/* USER CODE BEGIN 0 */
void LED_Pattern_Shift(void)
{
  static uint8_t pattern = 0x01;  // 0001
  
  LEDs_SetBinary_Fast(pattern);
  
  // 왼쪽으로 시프트 (순환)
  pattern = pattern << 1;
  if(pattern > 0x08) {
    pattern = 0x01;
  }
}

void LED_Pattern_Knight_Rider(void)
{
  static uint8_t pattern = 0x01;
  static int8_t direction = 1;
  
  LEDs_SetBinary_Fast(pattern);
  
  if(direction == 1) {
    pattern = pattern << 1;
    if(pattern == 0x08) {
      direction = -1;
    }
  } else {
    pattern = pattern >> 1;
    if(pattern == 0x01) {
      direction = 1;
    }
  }
}
/* USER CODE END 0 */

5. 상태 머신을 활용한 복잡한 입력 처리

5.1 상태 머신 설계

복잡한 입력 시퀀스를 처리하기 위한 상태 머신입니다.

상태 머신은 현재 상황(State)에 따라 똑같은 입력(Event)이 들어와도 다른 행동을 하게 만드는 일종의 설계도입니다. 단순히 HAL_GPIO_ReadPin()을 이용했을 때 보다 여러 상태(IDLE, DEBOUNCE, WAIT_TYPE)로 버튼의 입력 상황을 정해놓을 수 있기 때문에 논리적으로 처리하기 용이합니다.

상태 머신의 3요소:
상태(State): 시스템이 현재 처한 상황
이벤트(Event): 상태를 변화시키는 트리거
전이(Transition): 이벤트가 발생했을 때 A 상태에서 B 상태로 넘어가는 규칙

예제: 비밀번호 입력 시스템

  • 버튼 4개로 특정 시퀀스 입력
  • 올바른 시퀀스: Button1 → Button2 → Button1 → Button3
/* USER CODE BEGIN 0 */
typedef enum {
  PASSWORD_STATE_IDLE,
  PASSWORD_STATE_STEP1,  // Button1 입력 대기
  PASSWORD_STATE_STEP2,  // Button2 입력 대기
  PASSWORD_STATE_STEP3,  // Button1 입력 대기
  PASSWORD_STATE_STEP4,  // Button3 입력 대기
  PASSWORD_STATE_SUCCESS,
  PASSWORD_STATE_FAIL
} PasswordState_t;

PasswordState_t password_state = PASSWORD_STATE_IDLE;
uint32_t last_input_time = 0;
const uint32_t timeout = 5000;  // 5초 타임아웃

void Password_Reset(void)
{
  password_state = PASSWORD_STATE_IDLE;
  // 모든 LED 끄기
  HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
}

void Password_ProcessButton(uint8_t button_id)
{
  uint32_t current_time = HAL_GetTick();
  
  // 타임아웃 체크
  if((current_time - last_input_time) > timeout) {
    Password_Reset();
  }
  
  last_input_time = current_time;
  
  switch(password_state) {
    case PASSWORD_STATE_IDLE:
      if(button_id == 1) {
        password_state = PASSWORD_STATE_STEP1;
        HAL_GPIO_WritePin(GPIOD, LED1_PIN, GPIO_PIN_SET);
      } else {
        password_state = PASSWORD_STATE_FAIL;
      }
      break;
      
    case PASSWORD_STATE_STEP1:
      if(button_id == 2) {
        password_state = PASSWORD_STATE_STEP2;
        HAL_GPIO_WritePin(GPIOD, LED2_PIN, GPIO_PIN_SET);
      } else {
        password_state = PASSWORD_STATE_FAIL;
      }
      break;
      
    case PASSWORD_STATE_STEP2:
      if(button_id == 1) {
        password_state = PASSWORD_STATE_STEP3;
        HAL_GPIO_WritePin(GPIOD, LED3_PIN, GPIO_PIN_SET);
      } else {
        password_state = PASSWORD_STATE_FAIL;
      }
      break;
      
    case PASSWORD_STATE_STEP3:
      if(button_id == 3) {
        password_state = PASSWORD_STATE_SUCCESS;
        HAL_GPIO_WritePin(GPIOD, LED4_PIN, GPIO_PIN_SET);
        
        // 성공 애니메이션
        for(int i = 0; i < 5; i++) {
          HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_SET);
          HAL_Delay(100);
          HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
          HAL_Delay(100);
        }
        Password_Reset();
      } else {
        password_state = PASSWORD_STATE_FAIL;
      }
      break;
      
    case PASSWORD_STATE_FAIL:
      // 실패 - 빠르게 깜박임
      for(int i = 0; i < 3; i++) {
        HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_SET);
        HAL_Delay(50);
        HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
        HAL_Delay(50);
      }
      Password_Reset();
      break;
      
    default:
      Password_Reset();
      break;
  }
}
/* USER CODE END 0 */

5.2 메뉴 시스템 구현

버튼을 사용한 간단한 메뉴 네비게이션 시스템입니다.

/* USER CODE BEGIN 0 */
typedef enum {
  MENU_MAIN,
  MENU_OPTION1,
  MENU_OPTION2,
  MENU_OPTION3
} MenuState_t;

MenuState_t current_menu = MENU_MAIN;

void Menu_Display(MenuState_t menu)
{
  // LED로 현재 메뉴 표시
  HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
  
  switch(menu) {
    case MENU_MAIN:
      // 메인 메뉴: LED1만 켜기
      HAL_GPIO_WritePin(GPIOD, LED1_PIN, GPIO_PIN_SET);
      break;
      
    case MENU_OPTION1:
      // 옵션1: LED1, LED2 켜기
      HAL_GPIO_WritePin(GPIOD, LED1_PIN | LED2_PIN, GPIO_PIN_SET);
      break;
      
    case MENU_OPTION2:
      // 옵션2: LED1, LED3 켜기
      HAL_GPIO_WritePin(GPIOD, LED1_PIN | LED3_PIN, GPIO_PIN_SET);
      break;
      
    case MENU_OPTION3:
      // 옵션3: LED1, LED4 켜기
      HAL_GPIO_WritePin(GPIOD, LED1_PIN | LED4_PIN, GPIO_PIN_SET);
      break;
  }
}

void Menu_Navigate(uint8_t button_id)
{
  switch(button_id) {
    case 1:  // UP 버튼
      if(current_menu > MENU_MAIN) {
        current_menu--;
      }
      break;
      
    case 2:  // DOWN 버튼
      if(current_menu < MENU_OPTION3) {
        current_menu++;
      }
      break;
      
    case 3:  // SELECT 버튼
      // 선택된 메뉴 실행
      Menu_Execute(current_menu);
      break;
      
    case 4:  // BACK 버튼
      current_menu = MENU_MAIN;
      break;
  }
  
  Menu_Display(current_menu);
}

void Menu_Execute(MenuState_t menu)
{
  switch(menu) {
    case MENU_OPTION1:
      // 옵션1 실행
      // 예: LED 전체 켜기
      HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_SET);
      HAL_Delay(1000);
      break;
      
    case MENU_OPTION2:
      // 옵션2 실행
      // 예: LED 순차 점등
      for(int i = 0; i < 4; i++) {
        HAL_GPIO_WritePin(GPIOD, (1 << (12 + i)), GPIO_PIN_SET);
        HAL_Delay(200);
      }
      break;
      
    case MENU_OPTION3:
      // 옵션3 실행
      break;
      
    default:
      break;
  }
}
/* USER CODE END 0 */

6. 실전 프로젝트: 다기능 버튼 컨트롤러

6.1 프로젝트 요구사항

4개의 버튼과 4개의 LED를 사용한 다기능 컨트롤러:

기능:
1. 각 버튼 클릭: 해당 LED 토글
2. 버튼 길게 누르기: 모든 LED 밝기 조절
3. 더블 클릭: LED 패턴 변경
4. 3개 버튼 동시 누르기: 시스템 리셋


6.2 전체 코드 구현

/* USER CODE BEGIN 0 */
// 전역 변수
typedef struct {
  Button_EventSystem_t buttons[4];
  uint8_t brightness;
  uint8_t pattern_mode;
  uint32_t last_update;
} Controller_t;

Controller_t controller = {
  .brightness = 5,
  .pattern_mode = 0,
  .last_update = 0
};

void Controller_Init(Controller_t* ctrl)
{
  for(int i = 0; i < 4; i++) {
    ctrl->buttons[i].port = GPIOA;
    ctrl->buttons[i].pin = GPIO_PIN_0 << i;
    ctrl->buttons[i].state = BUTTON_STATE_IDLE;
    ctrl->buttons[i].active_level = GPIO_PIN_RESET;
    ctrl->buttons[i].debounce_time = 50;
  }
}

void Controller_ProcessButtons(Controller_t* ctrl)
{
  for(int i = 0; i < 4; i++) {
    ButtonEvent_t event = Button_Process(&ctrl->buttons[i]);
    
    switch(event) {
      case BUTTON_EVENT_CLICKED:
        // 해당 LED 토글
        HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12 << i);
        break;
        
      case BUTTON_EVENT_DOUBLE_CLICKED:
        // 패턴 모드 변경
        ctrl->pattern_mode = (ctrl->pattern_mode + 1) % 3;
        break;
        
      case BUTTON_EVENT_LONG_PRESSED:
        // 밝기 증가
        ctrl->brightness = (ctrl->brightness + 1) % 11;
        break;
        
      default:
        break;
    }
  }
}

void Controller_UpdatePattern(Controller_t* ctrl)
{
  uint32_t current_time = HAL_GetTick();
  
  if((current_time - ctrl->last_update) < 200) {
    return;  // 200ms마다 업데이트
  }
  
  ctrl->last_update = current_time;
  static uint8_t pattern_step = 0;
  
  switch(ctrl->pattern_mode) {
    case 0:  // 기본 모드 (변경 없음)
      break;
      
    case 1:  // 순차 점등
      HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12 << pattern_step, GPIO_PIN_SET);
      pattern_step = (pattern_step + 1) % 4;
      break;
      
    case 2:  // 점멸
      HAL_GPIO_TogglePin(GPIOD, ALL_LEDS);
      break;
  }
}

void Controller_CheckCombination(Controller_t* ctrl)
{
  // 3개 버튼 동시 누르기 체크
  uint8_t pressed_count = 0;
  
  for(int i = 0; i < 4; i++) {
    if(HAL_GPIO_ReadPin(ctrl->buttons[i].port, ctrl->buttons[i].pin) == GPIO_PIN_RESET) {
      pressed_count++;
    }
  }
  
  if(pressed_count >= 3) {
    // 시스템 리셋
    Controller_Reset(ctrl);
  }
}

void Controller_Reset(Controller_t* ctrl)
{
  ctrl->brightness = 5;
  ctrl->pattern_mode = 0;
  
  // 모든 LED 끄기
  HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
  
  // 리셋 애니메이션
  for(int i = 0; i < 3; i++) {
    HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_SET);
    HAL_Delay(100);
    HAL_GPIO_WritePin(GPIOD, ALL_LEDS, GPIO_PIN_RESET);
    HAL_Delay(100);
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
Controller_Init(&controller);
/* USER CODE END 2 */

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

  /* USER CODE BEGIN 3 */
  Controller_ProcessButtons(&controller);
  Controller_UpdatePattern(&controller);
  Controller_CheckCombination(&controller);
  
  HAL_Delay(10);
}
/* USER CODE END 3 */

7. 최적화 기법

7.1 인터럽트와 Polling 혼합

전략:

  • 중요한 버튼: 인터럽트 사용
  • 일반 버튼: Polling 사용
/* USER CODE BEGIN 0 */
// 긴급 버튼 (인터럽트)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if(GPIO_Pin == GPIO_PIN_0) {
    // 긴급 정지 등 즉시 처리 필요
    Emergency_Stop();
  }
}

// 일반 버튼 (Polling)
void Normal_Buttons_Process(void)
{
  // 일반적인 UI 버튼들
  Buttons_Process(buttons, MAX_BUTTONS);
}
/* USER CODE END 0 */

7.2 메모리 최적화

/* USER CODE BEGIN 0 */
// 비효율적인 방법
typedef struct {
  GPIO_TypeDef* port;        // 4 bytes
  uint16_t pin;              // 2 bytes
  uint16_t padding;          // 2 bytes (패딩)
  uint32_t last_time;        // 4 bytes
  GPIO_PinState state;       // 4 bytes
  void (*callback)(void);    // 4 bytes
} Button_Unoptimized_t;      // 총 20 bytes

// 최적화된 방법
typedef struct {
  uint32_t last_time;        // 4 bytes
  void (*callback)(void);    // 4 bytes
  GPIO_TypeDef* port;        // 4 bytes
  uint16_t pin;              // 2 bytes
  uint8_t state;             // 1 byte
  uint8_t padding;           // 1 byte (패딩)
} Button_Optimized_t;        // 총 16 bytes (20% 절약)

// 더 최적화 (작은 시스템용)
typedef struct {
  uint16_t last_time_ms;     // 2 bytes (65초까지)
  uint8_t port_pin;          // 1 byte (포트 3비트 + 핀 4비트)
  uint8_t state;             // 1 byte
} Button_Compact_t;          // 총 4 bytes (80% 절약)
/* USER CODE END 0 */

7.3 처리 속도 최적화

/* USER CODE BEGIN 0 */
// 느린 방법
void Slow_Button_Check(void)
{
  for(int i = 0; i < MAX_BUTTONS; i++) {
    if(HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin) == GPIO_PIN_RESET) {
      // 처리
    }
  }
}

// 빠른 방법: 포트 전체를 한 번에 읽기
void Fast_Button_Check(void)
{
  uint32_t port_state = GPIOA->IDR;  // 한 번만 읽기
  
  if(!(port_state & GPIO_PIN_0)) {
    // Button 0 처리
  }
  if(!(port_state & GPIO_PIN_1)) {
    // Button 1 처리
  }
  if(!(port_state & GPIO_PIN_2)) {
    // Button 2 처리
  }
  if(!(port_state & GPIO_PIN_3)) {
    // Button 3 처리
  }
}
/* USER CODE END 0 */

8. 디버깅 및 테스트

8.1 버튼 입력 시각화

/* USER CODE BEGIN 0 */
void Debug_PrintButtonStates(void)
{
  static uint32_t last_print = 0;
  uint32_t current_time = HAL_GetTick();
  
  if((current_time - last_print) > 100) {
    last_print = current_time;
    
    // LED로 버튼 상태 표시
    for(int i = 0; i < 4; i++) {
      GPIO_PinState btn_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0 << i);
      HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12 << i, 
                        btn_state == GPIO_PIN_RESET ? GPIO_PIN_SET : GPIO_PIN_RESET);
    }
  }
}
/* USER CODE END 0 */

8.2 이벤트 로깅

/* USER CODE BEGIN 0 */
#define LOG_SIZE 16

typedef struct {
  uint32_t timestamp;
  uint8_t button_id;
  ButtonEvent_t event;
} EventLog_t;

EventLog_t event_log[LOG_SIZE];
uint8_t log_index = 0;

void Log_Event(uint8_t button_id, ButtonEvent_t event)
{
  event_log[log_index].timestamp = HAL_GetTick();
  event_log[log_index].button_id = button_id;
  event_log[log_index].event = event;
  
  log_index = (log_index + 1) % LOG_SIZE;
}

void Debug_PrintLog(void)
{
  // 디버거 브레이크포인트에서 event_log 배열 확인
  for(int i = 0; i < LOG_SIZE; i++) {
    // 로그 내용 확인
  }
}
/* USER CODE END 0 */

9. 실습 과제

9.1 과제 1: 리액션 게임

요구사항:
1. 랜덤한 시간 후 LED 켜짐
2. 사용자가 버튼을 빠르게 누름
3. 반응 시간 측정 및 LED로 표시


9.2 과제 2: 사이먼 게임

요구사항:
1. LED가 랜덤 시퀀스로 점등
2. 사용자가 같은 시퀀스로 버튼 입력
3. 시퀀스 길이가 점점 증가


9.3 과제 3: 볼륨 컨트롤러

요구사항:
1. Up/Down 버튼으로 볼륨 조절
2. 4개 LED로 볼륨 레벨 표시
3. 길게 누르면 빠르게 증가/감소
4. 음소거 버튼


10. 정리

10.1 오늘 배운 내용

  1. 고급 디바운싱 기법 (샘플링, 적분기, 비트 시프트)
  2. 버튼 이벤트 시스템 설계 및 구현
  3. 여러 버튼의 효율적인 관리 방법
  4. 비트 마스킹을 활용한 GPIO 제어
  5. 상태 머신을 활용한 복잡한 입력 처리
  6. 실전 프로젝트 구현
  7. 최적화 기법

10.2 디바운싱 방법 비교

방법장점단점사용 시기
타임스탬프간단, 빠름일부 채터링 통과 가능일반적인 경우
샘플링안정적타이머 필요노이즈 많은 환경
적분기부드러움, 튜닝 가능복잡정밀한 제어 필요
비트 시프트메모리 효율구현 복잡도리소스 제한

10.3 참고 자료

profile
당신의 코딩 메이트

0개의 댓글