첫 번째 프로그램) LED 깜빡이기로 배우는 레지스터 기초

안녕하세요! 지난 편에서 개발 환경을 구축하고 간단한 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. (1 << PB5)(1 << 5)0b00100000
  2. PORTB의 현재 값과 OR 연산
  3. 5번째 비트만 1로 설정, 나머지는 변화 없음
현재 PORTB: 0b01010000
OR 연산값:  0b00100000
결과:       0b01110000  // PB5가 추가로 켜짐

2. 특정 비트 끄기 (CLEAR)

c

PORTB &= ~(1 << PB5);

동작 과정:

  1. (1 << PB5)0b00100000
  2. ~(1 << PB5)0b11011111 (비트 반전)
  3. PORTB와 AND 연산
  4. 5번째 비트만 0으로 설정, 나머지는 변화 없음
현재 PORTB: 0b01110000
AND 연산값: 0b11011111
결과:       0b01010000  // PB5만 꺼짐

3. 특정 비트 토글 (TOGGLE)

c

PORTB ^= (1 << PB5);

동작 과정:

  1. XOR 연산으로 비트 상태 반전
  2. 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에서 레지스터 확인하기

  1. 시뮬레이션 실행 중 ATmega328P 우클릭하기
  2. “Open Serial Monitor” 또는 “Properties” 선택하기
  3. 레지스터 탭에서 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를 제어하는 인터랙티브한 프로그램

레지스터와 비트 연산에 대한 이해가 생겼다면, 이제 마이크로컨트롤러와 소통할 준비가 된 거예요!


코드를 직접 실행해보시고, 응용 과제도 꼭 도전해보세요! 질문이 있으시면 댓글로 남겨주세요.