안녕하세요! 지난 편에서 개발 환경을 구축하고 간단한 LED 깜빡이는 프로그램을 만들어봤습니다. 이번 편에서는 그 코드를 자세히 분석하면서 레지스터와 비트 연산의 핵심 개념들을 배워보겠습니다.
지난 편 코드를 다시 보면서 시작합시다
c
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
DDRB |= (1 << PB5); // PB5를 출력으로 설정
while(1) {
PORTB |= (1 << PB5); // LED 켜기
_delay_ms(1000);
PORTB &= ~(1 << PB5); // LED 끄기
_delay_ms(1000);
}
return 0;
}
이 간단한 코드 안에는 마이크로컨트롤러 프로그래밍의 핵심 개념들이 모두 들어있습니다. 하나씩 파헤쳐보죠!
레지스터란 무엇인가?
레지스터의 정의
레지스터(Register)는 마이크로컨트롤러 내부의 특별한 메모리 공간입니다. 각 레지스터는 8비트(1바이트) 크기이며, 하드웨어의 동작을 제어하는 스위치 역할을 합니다.
왜 레지스터를 직접 제어해야 할까?
Arduino를 써봤다면 digitalWrite(13, HIGH)
같은 함수를 사용해봤을 텐데요, 실제로는 이 함수 내부에서 레지스터를 조작하고 있습니다. 레지스터를 직접 제어하면:
- 더 빠른 실행 속도 (함수 호출 오버헤드 없음)
- 정밀한 타이밍 제어
- 메모리 사용량 절약
- 하드웨어의 모든 기능 활용 가능
ATmega328P의 GPIO 레지스터들
ATmega328P에는 포트별로 3개씩의 레지스터가 있습니다:
DDRx (Data Direction Register)
DDRB = 0b00000000 // 모든 핀이 입력 (기본값)
DDRB = 0b11111111 // 모든 핀이 출력
DDRB = 0b00100000 // PB5만 출력, 나머지는 입력
- 0: 입력 모드
- 1: 출력 모드
PORTx (Port Output Register)
PORTB = 0b00100000 // PB5에 HIGH 출력
PORTB = 0b00000000 // 모든 핀에 LOW 출력
- 출력 모드일 때: 0=LOW(0V), 1=HIGH(5V)
- 입력 모드일 때: 0=풀업저항 비활성화, 1=풀업저항 활성화
PINx (Port Input Register)
uint8_t button_state = PINB; // 포트 B의 현재 상태 읽기
- 핀의 실제 전압 상태를 읽음 (읽기 전용)
비트 연산의 마법
왜 비트 연산을 사용할까?
레지스터는 8개의 핀을 제어하는데, 우리는 보통 한 개의 핀만 조작하고 싶습니다. 다른 핀들의 상태는 그대로 두면서 말이죠.
c
// 잘못된 방법 - 다른 핀들도 영향받음
PORTB = 0b00100000; // PB5만 켜려 했는데 다른 핀들은 모두 꺼짐
// 올바른 방법 - PB5만 켜고 다른 핀들은 그대로
PORTB |= (1 << PB5);
핵심 비트 연산들
1. 특정 비트 켜기 (SET)
c
PORTB |= (1 << PB5);
동작 과정:
(1 << PB5)
→(1 << 5)
→0b00100000
PORTB
의 현재 값과 OR 연산- 5번째 비트만 1로 설정, 나머지는 변화 없음
현재 PORTB: 0b01010000
OR 연산값: 0b00100000
결과: 0b01110000 // PB5가 추가로 켜짐
2. 특정 비트 끄기 (CLEAR)
c
PORTB &= ~(1 << PB5);
동작 과정:
(1 << PB5)
→0b00100000
~(1 << PB5)
→0b11011111
(비트 반전)PORTB
와 AND 연산- 5번째 비트만 0으로 설정, 나머지는 변화 없음
현재 PORTB: 0b01110000
AND 연산값: 0b11011111
결과: 0b01010000 // PB5만 꺼짐
3. 특정 비트 토글 (TOGGLE)
c
PORTB ^= (1 << PB5);
동작 과정:
- XOR 연산으로 비트 상태 반전
- 0이었으면 1로, 1이었으면 0으로
🔍 코드 한 줄씩 분석
헤더 파일들
c
#include <avr/io.h> // 레지스터 정의 (DDRB, PORTB 등)
#include <util/delay.h> // 지연 함수 (_delay_ms)
핀 설정
c
DDRB |= (1 << PB5);
PB5
는 상수로5
를 의미(1 << 5)
는0b00100000
- DDRB의 5번째 비트만 1로 설정 → PB5를 출력 모드로
LED 켜기
c
PORTB |= (1 << PB5);
- PORTB의 5번째 비트를 1로 설정
- PB5 핀에 5V 출력 → LED 켜짐
지연
c
_delay_ms(1000);
- 1000밀리초(1초) 대기
- CPU는 이 시간동안 아무것도 하지 않음
LED 끄기
c
PORTB &= ~(1 << PB5);
- PORTB의 5번째 비트만 0으로 설정
- PB5 핀에 0V 출력 → LED 꺼짐
실습: 코드 변형해보기
실습 1: 깜빡이는 속도 바꾸기
c
// 빠른 깜빡임 (0.2초)
_delay_ms(200);
// 느린 깜빡임 (2초)
_delay_ms(2000);
실습 2: 다른 핀 사용해보기
c
// PB0 핀 사용 (Arduino의 8번 핀)
DDRB |= (1 << PB0);
while(1) {
PORTB |= (1 << PB0);
_delay_ms(500);
PORTB &= ~(1 << PB0);
_delay_ms(500);
}
실습 3: 두 개의 LED 번갈아 깜빡이기
c
#include <avr/io.h>
#include <util/delay.h>
int main(void) {
// PB4, PB5 두 핀을 출력으로 설정
DDRB |= (1 << PB4) | (1 << PB5);
while(1) {
// 첫 번째 LED 켜고 두 번째 LED 끄기
PORTB |= (1 << PB4);
PORTB &= ~(1 << PB5);
_delay_ms(500);
// 첫 번째 LED 끄고 두 번째 LED 켜기
PORTB &= ~(1 << PB4);
PORTB |= (1 << PB5);
_delay_ms(500);
}
return 0;
}
Arduino와의 차이점
Arduino 코드와 비교해보면:
Arduino 방식
c
void setup() {
pinMode(13, OUTPUT);
}
void loop() {
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
}
직접 레지스터 방식
c
int main(void) {
DDRB |= (1 << PB5);
while(1) {
PORTB |= (1 << PB5);
_delay_ms(1000);
PORTB &= ~(1 << PB5);
_delay_ms(1000);
}
return 0;
}
차이점 정리
구분 | arduino | 직접 레지스터 |
---|---|---|
가독성 | 높음 | 낮음 |
실행 속도 | 느림 | 빠름 |
메모리 사용 | 많음 | 적음 |
학습 난이도 | 쉬움 | 어려움 |
제어 정밀도 | 제한적 | 완전함 |
디버깅 팁
SimulIDE에서 레지스터 확인하기
- 시뮬레이션 실행 중 ATmega328P 우클릭하기
- “Open Serial Monitor” 또는 “Properties” 선택하기
- 레지스터 탭에서 DDRB, PORTB 값 실시간 확인하기
일반적인 실수들
c
// 실수 1: 방향 설정 없이 출력 시도
PORTB |= (1 << PB5); // DDRB 설정 없음
// 실수 2: 잘못된 비트 연산
PORTB = (1 << PB5); // 다른 핀들이 모두 꺼짐
// 실수 3: 지연 없는 무한 루프
while(1) {
PORTB ^= (1 << PB5); // 너무 빨라서 깜빡임을 볼 수 없음
}
여기서 한걸음 더! 매크로 활용하기
코드를 더 읽기 쉽게 만들어보겠습니다:
c
#include <avr/io.h>
#include <util/delay.h>
// 매크로 정의로 가독성 향상
#define LED_PIN PB5
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_INIT() (LED_DDR |= (1 << LED_PIN))
#define LED_ON() (LED_PORT |= (1 << LED_PIN))
#define LED_OFF() (LED_PORT &= ~(1 << LED_PIN))
#define LED_TOGGLE() (LED_PORT ^= (1 << LED_PIN))
int main(void) {
LED_INIT(); // LED 초기화
while(1) {
LED_ON(); // LED 켜기
_delay_ms(1000);
LED_OFF(); // LED 끄기
_delay_ms(1000);
}
return 0;
}
응용 과제
이제 다음 과제들을 직접 해보세요:
과제 1: SOS 신호 만들기
모스 부호로 SOS(… — …)를 LED로 표현해보세요.
- 짧은 신호: 200ms 켜짐 + 200ms 꺼짐
- 긴 신호: 600ms 켜짐 + 200ms 꺼짐
과제 2: 하트비트 패턴
심장 박동처럼 두 번 빠르게 깜빡이고 잠시 쉬는 패턴을 만들어보세요.
과제 3: 3개의 LED로 신호등 만들기
PB3(빨강), PB4(노랑), PB5(초록)을 사용해서 실제 신호등처럼 동작하게 해보세요.
다음 편 예고
다음 편에서는 버튼 입력 처리를 배워보겠습니다.
- 디지털 입력을 읽는 방법
- 풀업 저항의 개념과 활용
- 디바운싱 기법으로 안정적인 버튼 입력 처리
- 버튼으로 LED를 제어하는 인터랙티브한 프로그램
레지스터와 비트 연산에 대한 이해가 생겼다면, 이제 마이크로컨트롤러와 소통할 준비가 된 거예요!
코드를 직접 실행해보시고, 응용 과제도 꼭 도전해보세요! 질문이 있으시면 댓글로 남겨주세요.