안녕하세요! 지난 편에서는 LED를 깜빡이며 출력 제어의 기초를 배웠습니다. 이번에는 입력을 다뤄보겠습니다. 버튼을 눌러서 LED를 제어하는 인터랙티브한 프로그램을 만들어보죠!
이번 편의 목표
- 디지털 입력을 읽는 방법 이해
- 풀업 저항의 개념과 필요성 파악
- 디바운싱 기법으로 안정적인 입력 처리
- 버튼과 LED를 조합한 다양한 제어 패턴 구현
회로 구성하기
SimulIDE에서 회로 만들기
지난 편의 LED 회로에 버튼을 추가해보겠습니다:
추가할 부품들:
- Push Button (택트 스위치) 1개
- Resistor 10kΩ 1개 (풀업 저항용)
연결 방법:
- LED 회로 (기존 유지):
- ATmega328P PB5 → 저항 330Ω → LED → GND
- 버튼 회로 (새로 추가):
- ATmega328P PB0을 풀업 저항 10kΩ을 통해 +5V에 연결
- ATmega328P PB0을 버튼의 한쪽 다리에 연결
- 버튼의 다른쪽 다리를 GND에 연결
왜 이렇게 연결할까?
+5V
|
10kΩ (풀업 저항)
|
PB0 ─┴─ 버튼 ─ GND
- 버튼이 안 눌린 상태: PB0은 풀업 저항을 통해 +5V에 연결 → HIGH
- 버튼이 눌린 상태: PB0이 직접 GND에 연결 → LOW
디지털 입력의 기초
PINx 레지스터 사용법
출력에 PORTB
를 사용했다면, 입력에는 PINB
를 사용합니다:
c
// PB0 핀을 입력으로 설정 (기본값이므로 생략 가능)
DDRB &= ~(1 << PB0);
// PB0 핀의 상태 읽기
if (PINB & (1 << PB0)) {
// 버튼이 안 눌린 상태 (HIGH)
} else {
// 버튼이 눌린 상태 (LOW)
}
비트 마스킹으로 특정 핀 읽기
c
uint8_t button_state = PINB & (1 << PB0);
동작 과정:
(1 << PB0)
→0b00000001
(마스크)PINB
의 값과 AND 연산- PB0 비트만 추출, 나머지는 0
PINB 값: 0b10110001
마스크: 0b00000001
결과: 0b00000001 // PB0이 HIGH면 0이 아닌 값
첫 번째 버튼 프로그램
기본 버튼-LED 제어
c
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
// PB5를 출력으로 설정 (LED)
DDRB |= (1 << PB5);
// PB0을 입력으로 설정 (기본값이므로 생략 가능)
DDRB &= ~(1 << PB0);
// PB0에 내부 풀업 저항 활성화
PORTB |= (1 << PB0);
while(1) {
// 버튼이 눌렸는지 확인
if (!(PINB & (1 << PB0))) {
// 버튼이 눌림 (LOW) → LED 켜기
PORTB |= (1 << PB5);
} else {
// 버튼이 안 눌림 (HIGH) → LED 끄기
PORTB &= ~(1 << PB5);
}
_delay_ms(50); // 작은 지연으로 안정성 확보
}
return 0;
}
풀업 저항의 비밀
왜 풀업 저항이 필요할까?
풀업 저항 없이 버튼만 연결하면:
PB0 ─ 버튼 ─ GND
문제점:
- 버튼이 안 눌린 상태에서 PB0이 플로팅 상태
- 주변 노이즈에 의해 HIGH/LOW가 불규칙하게 변함
- 안정적인 입력 읽기 불가능
내부 풀업 저항 vs 외부 풀업 저항
내부 풀업 저항 사용
c
// 입력 모드로 설정 후
DDRB &= ~(1 << PB0);
// 내부 풀업 저항 활성화
PORTB |= (1 << PB0);
장점:
- 외부 부품 불필요
- 회로 간소화
단점:
- 저항값이 크다 (20~50kΩ)
- 노이즈에 상대적으로 민감
외부 풀업 저항 사용
c
// 별도 설정 불필요 (하드웨어에서 해결)
DDRB &= ~(1 << PB0);
장점:
- 저항값 선택 가능 (보통 10kΩ)
- 더 안정적인 신호
단점:
- 추가 부품 필요
디바운싱: 깨끗한 신호 만들기
바운싱 현상이란?
기계적 버튼을 누를 때 접점이 물리적으로 여러 번 접촉하면서 짧은 시간 동안 HIGH/LOW가 반복됩니다:
버튼 누름: HIGH → LOW → HIGH → LOW → LOW (안정)
시간: 0ms 1ms 2ms 3ms 5ms
소프트웨어 디바운싱
c
#include <avr/io.h>
#include <util/delay.h>
// 디바운싱 함수
uint8_t button_debounce(void) {
if (!(PINB & (1 << PB0))) {
// 버튼이 눌린 것 같음, 확인을 위해 잠시 대기
_delay_ms(20);
// 다시 확인
if (!(PINB & (1 << PB0))) {
// 여전히 눌려있으면 진짜 눌림
return 1;
}
}
return 0;
}
int main(void) {
DDRB |= (1 << PB5); // LED 출력 설정
DDRB &= ~(1 << PB0); // 버튼 입력 설정
PORTB |= (1 << PB0); // 내부 풀업 활성화
while(1) {
if (button_debounce()) {
PORTB |= (1 << PB5); // LED 켜기
} else {
PORTB &= ~(1 << PB5); // LED 끄기
}
}
return 0;
}
다양한 버튼 제어 패턴
1. 토글 스위치 (누를 때마다 ON/OFF)
c
#include <avr/io.h>
#include <util/delay.h>
uint8_t led_state = 0; // LED 상태 저장
int main(void) {
DDRB |= (1 << PB5); // LED 출력
DDRB &= ~(1 << PB0); // 버튼 입력
PORTB |= (1 << PB0); // 풀업 활성화
while(1) {
// 버튼이 눌렸는지 확인
if (!(PINB & (1 << PB0))) {
_delay_ms(20); // 디바운싱
// 여전히 눌려있으면
if (!(PINB & (1 << PB0))) {
// LED 상태 토글
led_state = !led_state;
if (led_state) {
PORTB |= (1 << PB5);
} else {
PORTB &= ~(1 << PB5);
}
// 버튼이 떼질 때까지 대기
while (!(PINB & (1 << PB0)));
_delay_ms(20); // 릴리즈 디바운싱
}
}
}
return 0;
}
2. 길게 누르기 감지
c
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
DDRB |= (1 << PB5) | (1 << PB4); // LED 2개 출력
DDRB &= ~(1 << PB0); // 버튼 입력
PORTB |= (1 << PB0); // 풀업 활성화
while(1) {
if (!(PINB & (1 << PB0))) {
_delay_ms(20); // 디바운싱
if (!(PINB & (1 << PB0))) {
uint16_t press_time = 0;
// 버튼이 눌려있는 동안 시간 측정
while (!(PINB & (1 << PB0))) {
_delay_ms(10);
press_time += 10;
if (press_time >= 1000) {
// 1초 이상 누름 - 긴 누르기
PORTB |= (1 << PB4); // PB4 LED 켜기
break;
}
}
if (press_time < 1000) {
// 짧은 누르기
PORTB ^= (1 << PB5); // PB5 LED 토글
PORTB &= ~(1 << PB4); // PB4 LED 끄기
}
// 버튼 릴리즈 대기
while (!(PINB & (1 << PB0)));
_delay_ms(20);
}
}
}
return 0;
}
3. 더블 클릭 감지
c
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
DDRB |= (1 << PB5) | (1 << PB4); // LED 2개
DDRB &= ~(1 << PB0); // 버튼 입력
PORTB |= (1 << PB0); // 풀업 활성화
while(1) {
// 첫 번째 클릭 감지
if (!(PINB & (1 << PB0))) {
_delay_ms(20);
if (!(PINB & (1 << PB0))) {
// 버튼 릴리즈 대기
while (!(PINB & (1 << PB0)));
_delay_ms(20);
// 300ms 안에 두 번째 클릭이 있는지 확인
uint16_t wait_time = 0;
uint8_t double_click = 0;
while (wait_time < 300) {
if (!(PINB & (1 << PB0))) {
_delay_ms(20);
if (!(PINB & (1 << PB0))) {
double_click = 1;
break;
}
}
_delay_ms(10);
wait_time += 10;
}
if (double_click) {
// 더블 클릭 - 두 LED 모두 토글
PORTB ^= (1 << PB5) | (1 << PB4);
} else {
// 싱글 클릭 - PB5만 토글
PORTB ^= (1 << PB5);
}
// 버튼 릴리즈 대기 (더블클릭인 경우)
if (double_click) {
while (!(PINB & (1 << PB0)));
_delay_ms(20);
}
}
}
}
return 0;
}
디버깅과 최적화 팁
1. 시리얼 모니터 활용
c
// 디버깅용 코드 (SimulIDE에서 확인)
printf("Button state: %d\n", (PINB & (1 << PB0)) ? 1 : 0);
2. 매크로로 코드 정리
c
#define BUTTON_PIN PB0
#define LED_PIN PB5
#define BUTTON_INIT() do { \
DDRB &= ~(1 << BUTTON_PIN); \
PORTB |= (1 << BUTTON_PIN); \
} while(0)
#define BUTTON_PRESSED() (!(PINB & (1 << BUTTON_PIN)))
#define LED_INIT() (DDRB |= (1 << LED_PIN))
#define LED_ON() (PORTB |= (1 << LED_PIN))
#define LED_OFF() (PORTB &= ~(1 << LED_PIN))
#define LED_TOGGLE() (PORTB ^= (1 << LED_PIN))
3. 상태 머신 패턴
c
typedef enum {
STATE_IDLE,
STATE_PRESSED,
STATE_RELEASED
} button_state_t;
button_state_t button_state = STATE_IDLE;
void update_button_state(void) {
switch(button_state) {
case STATE_IDLE:
if (BUTTON_PRESSED()) {
button_state = STATE_PRESSED;
}
break;
case STATE_PRESSED:
if (!BUTTON_PRESSED()) {
button_state = STATE_RELEASED;
// 버튼 릴리즈 이벤트 처리
LED_TOGGLE();
}
break;
case STATE_RELEASED:
button_state = STATE_IDLE;
break;
}
}
응용 과제
과제 1: 3단계 밝기 조절
버튼을 누를 때마다 LED가 꺼짐 → 어둡게 → 밝게 → 꺼짐 순으로 변하게 만들어보세요. (힌트: PWM을 사용하거나 빠른 ON/OFF로 밝기 시뮬레이션)
과제 2: 버튼 조합
2개의 버튼을 사용해서:
- 버튼1만: LED1 토글
- 버튼2만: LED2 토글
- 버튼1+2 동시: 모든 LED 토글
과제 3: 게임 만들기
“반응속도 테스트” 게임을 만들어보세요:
- 랜덤한 시간 후 LED 켜짐
- 사용자가 버튼을 최대한 빨리 누름
- 반응시간에 따라 다른 패턴의 LED 표시
다음 편 예고
다음 편에서는 여러 LED로 패턴 만들기를 배워보겠습니다:
- 8개 LED로 만드는 나이트 라이더 효과
- 이진 카운터와 패턴 생성
- 배열과 반복문을 활용한 효율적인 코드
- 더 복잡한 시각적 효과들
버튼 입력을 마스터했다면, 이제 더 화려한 출력 패턴을 만들 차례입니다!
직접 버튼을 눌러가며 코드를 테스트해보세요. 디바운싱의 중요성을 몸소 체험할 수 있을 거예요!