
일정 시간 동안 여러 번 샘플링하여 안정적인 상태를 확인하는 방법입니다.
타이머를 이용해 정해진 주기에 들어온 신호만 정상 신호로 판단해 인터럽트로 취급하는 소프트웨어 기반 디바운싱 기법입니다.
동작 원리:
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 */
버튼 상태를 적분하여 임계값을 넘으면 상태 변화로 인식하는 방법입니다.
아날로그 회로에서 적분기는 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. 샘플링 결과를 비트로 저장
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 */
비트 시프트 방식의 장점:
단순히 버튼 상태를 읽는 것이 아니라, 이벤트를 감지하고 처리하는 시스템입니다.
버튼 이벤트 종류:
/* 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 */
/* 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 */
/* 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 */
여러 개의 버튼을 효율적으로 관리하는 방법입니다.
/* 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 */
/* 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 */
여러 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인지 확인
}
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 */
이 패턴은 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 */
복잡한 입력 시퀀스를 처리하기 위한 상태 머신입니다.
상태 머신은 현재 상황(State)에 따라 똑같은 입력(Event)이 들어와도 다른 행동을 하게 만드는 일종의 설계도입니다. 단순히 HAL_GPIO_ReadPin()을 이용했을 때 보다 여러 상태(IDLE, DEBOUNCE, WAIT_TYPE)로 버튼의 입력 상황을 정해놓을 수 있기 때문에 논리적으로 처리하기 용이합니다.
상태 머신의 3요소:
상태(State): 시스템이 현재 처한 상황
이벤트(Event): 상태를 변화시키는 트리거
전이(Transition): 이벤트가 발생했을 때 A 상태에서 B 상태로 넘어가는 규칙
예제: 비밀번호 입력 시스템
/* 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 */
버튼을 사용한 간단한 메뉴 네비게이션 시스템입니다.
/* 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 */
4개의 버튼과 4개의 LED를 사용한 다기능 컨트롤러:
기능:
1. 각 버튼 클릭: 해당 LED 토글
2. 버튼 길게 누르기: 모든 LED 밝기 조절
3. 더블 클릭: LED 패턴 변경
4. 3개 버튼 동시 누르기: 시스템 리셋
/* 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 */
전략:
/* 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 */
/* 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 */
/* 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 */
/* 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 */
/* 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 */
요구사항:
1. 랜덤한 시간 후 LED 켜짐
2. 사용자가 버튼을 빠르게 누름
3. 반응 시간 측정 및 LED로 표시
요구사항:
1. LED가 랜덤 시퀀스로 점등
2. 사용자가 같은 시퀀스로 버튼 입력
3. 시퀀스 길이가 점점 증가
요구사항:
1. Up/Down 버튼으로 볼륨 조절
2. 4개 LED로 볼륨 레벨 표시
3. 길게 누르면 빠르게 증가/감소
4. 음소거 버튼
| 방법 | 장점 | 단점 | 사용 시기 |
|---|---|---|---|
| 타임스탬프 | 간단, 빠름 | 일부 채터링 통과 가능 | 일반적인 경우 |
| 샘플링 | 안정적 | 타이머 필요 | 노이즈 많은 환경 |
| 적분기 | 부드러움, 튜닝 가능 | 복잡 | 정밀한 제어 필요 |
| 비트 시프트 | 메모리 효율 | 구현 복잡도 | 리소스 제한 |