비트 연산 심화

비트 연산 심화

임베디드 프로그래밍의 핵심인 비트 단위 조작을 완벽하게 익힙니다.

학습 목표

  • 비트 연산자 완벽 이해
  • 비트 마스킹 기법 습득
  • 레지스터 제어 개념 이해
  • volatile 키워드 이해

사전 지식

  • C 언어 기본 문법
  • 2진수, 16진수 표현

1. 왜 비트 연산이 중요한가?

임베디드에서 비트 연산이 필수인 이유

1. 레지스터 제어
   - MCU의 모든 기능은 레지스터(특수 메모리) 제어
   - 레지스터의  비트가 특정 기능 담당

2. 메모리 절약
   - 임베디드는 메모리가 제한적 (KB 단위)
   - 8 플래그를 1바이트로 저장 가능

3. 통신 프로토콜
   - 데이터 패킷의 비트별 해석 필요

4. 성능 최적화
   - 비트 연산은 CPU에서 가장 빠른 연산

예시: LED 제어 레지스터

Arduino Uno의 포트 B 레지스터 (핀 8~13)
┌─────────────────────────────────────────────┐
│  PORTB = 0b00100000                        │
│                                             │
│  비트:  7    6    5    4    3    2    1    0│
│        [ ]  [ ]  [1]  [ ]  [ ]  [ ]  [ ]  [ ]│
│              │    │    │    │    │    │    │ │
│              │   핀13 핀12 핀11 핀10 핀9  핀8│
│              │    ↓                         │
│              │   LED ON (핀 13)             │
│              │                              │
│  비트 5 = 1 → 핀 13에 HIGH 출력            │
└─────────────────────────────────────────────┘

2. 비트 연산자 복습

기본 비트 연산자

// bitwise_operators.c
#include <stdio.h>

void print_binary(unsigned char n) {
    for (int i = 7; i >= 0; i--) {
        printf("%d", (n >> i) & 1);
        if (i == 4) printf(" ");  // 가독성을 위한 공백
    }
    printf("\n");
}

int main(void) {
    unsigned char a = 0b11001010;  // 202
    unsigned char b = 0b10110011;  // 179

    printf("a        = "); print_binary(a);  // 1100 1010
    printf("b        = "); print_binary(b);  // 1011 0011
    printf("\n");

    // AND (&): 둘 다 1일 때만 1
    printf("a & b    = "); print_binary(a & b);   // 1000 0010

    // OR (|): 둘 중 하나라도 1이면 1
    printf("a | b    = "); print_binary(a | b);   // 1111 1011

    // XOR (^): 서로 다르면 1
    printf("a ^ b    = "); print_binary(a ^ b);   // 0111 1001

    // NOT (~): 비트 반전
    printf("~a       = "); print_binary(~a);      // 0011 0101

    // Left Shift (<<): 왼쪽으로 이동, 빈 자리는 0
    printf("a << 2   = "); print_binary(a << 2);  // 0010 1000

    // Right Shift (>>): 오른쪽으로 이동
    printf("a >> 2   = "); print_binary(a >> 2);  // 0011 0010

    return 0;
}

연산자 진리표

AND (&)          OR (|)           XOR (^)
A  B  A&B        A  B  A|B        A  B  A^B
0  0   0         0  0   0         0  0   0
0  1   0         0  1   1         0  1   1
1  0   0         1  0   1         1  0   1
1  1   1         1  1   1         1  1   0

3. 비트 마스킹 기법

3.1 특정 비트 읽기 (GET)

// 특정 비트의 값 확인
// 방법: (value >> bit) & 1

unsigned char reg = 0b10110100;

// 비트 2의 값 읽기
int bit2 = (reg >> 2) & 1;  // 결과: 1

// 비트 3의 값 읽기
int bit3 = (reg >> 3) & 1;  // 결과: 0

// 매크로로 정의
#define GET_BIT(value, bit) (((value) >> (bit)) & 1)

// 사용 예
if (GET_BIT(reg, 5)) {
    printf("비트 5가 설정됨\n");
}

3.2 특정 비트 설정 (SET)

// 특정 비트를 1로 설정
// 방법: value |= (1 << bit)

unsigned char reg = 0b10100000;

// 비트 3을 1로 설정
reg |= (1 << 3);  // 결과: 0b10101000

// 여러 비트 동시 설정
reg |= (1 << 1) | (1 << 4);  // 비트 1, 4 설정

// 매크로로 정의
#define SET_BIT(value, bit) ((value) |= (1 << (bit)))

// 사용 예
SET_BIT(reg, 6);  // 비트 6 설정

동작 원리:

reg       = 1010 0000
1 << 3    = 0000 1000
           ─────────── OR
결과      = 1010 1000

3.3 특정 비트 해제 (CLEAR)

// 특정 비트를 0으로 해제
// 방법: value &= ~(1 << bit)

unsigned char reg = 0b11111111;

// 비트 5를 0으로 해제
reg &= ~(1 << 5);  // 결과: 0b11011111

// 매크로로 정의
#define CLEAR_BIT(value, bit) ((value) &= ~(1 << (bit)))

// 사용 예
CLEAR_BIT(reg, 2);  // 비트 2 해제

동작 원리:

reg       = 1111 1111
1 << 5    = 0010 0000
~(1 << 5) = 1101 1111
           ─────────── AND
결과      = 1101 1111

3.4 특정 비트 토글 (TOGGLE)

// 특정 비트 반전 (0→1, 1→0)
// 방법: value ^= (1 << bit)

unsigned char reg = 0b10101010;

// 비트 4 토글
reg ^= (1 << 4);  // 결과: 0b10111010 (0→1)
reg ^= (1 << 4);  // 결과: 0b10101010 (1→0)

// 매크로로 정의
#define TOGGLE_BIT(value, bit) ((value) ^= (1 << (bit)))

// 사용 예: LED 토글
TOGGLE_BIT(PORTB, 5);  // 핀 13 LED 토글

3.5 비트 마스크 유틸리티 헤더

// bit_utils.h
#ifndef BIT_UTILS_H
#define BIT_UTILS_H

// 비트 조작 매크로
#define BIT(n)                  (1 << (n))
#define SET_BIT(reg, bit)       ((reg) |= BIT(bit))
#define CLEAR_BIT(reg, bit)     ((reg) &= ~BIT(bit))
#define TOGGLE_BIT(reg, bit)    ((reg) ^= BIT(bit))
#define GET_BIT(reg, bit)       (((reg) >> (bit)) & 1)
#define CHECK_BIT(reg, bit)     ((reg) & BIT(bit))

// 여러 비트 조작
#define SET_BITS(reg, mask)     ((reg) |= (mask))
#define CLEAR_BITS(reg, mask)   ((reg) &= ~(mask))
#define TOGGLE_BITS(reg, mask)  ((reg) ^= (mask))

// 비트 필드 조작
#define GET_FIELD(reg, mask, shift)     (((reg) & (mask)) >> (shift))
#define SET_FIELD(reg, mask, shift, val) \
    ((reg) = ((reg) & ~(mask)) | (((val) << (shift)) & (mask)))

#endif

4. 플래그 관리

여러 상태를 하나의 변수로 관리합니다.

플래그 정의

// flags.c
#include <stdio.h>
#include <stdbool.h>

// 각 비트에 의미 부여
#define FLAG_RUNNING    (1 << 0)  // 비트 0: 실행 중
#define FLAG_ERROR      (1 << 1)  // 비트 1: 에러 발생
#define FLAG_CONNECTED  (1 << 2)  // 비트 2: 연결됨
#define FLAG_READY      (1 << 3)  // 비트 3: 준비 완료
#define FLAG_BUSY       (1 << 4)  // 비트 4: 사용 중
#define FLAG_TIMEOUT    (1 << 5)  // 비트 5: 타임아웃

// 전역 상태 플래그
unsigned char system_flags = 0;

// 플래그 설정
void set_flag(unsigned char flag) {
    system_flags |= flag;
}

// 플래그 해제
void clear_flag(unsigned char flag) {
    system_flags &= ~flag;
}

// 플래그 확인
bool is_flag_set(unsigned char flag) {
    return (system_flags & flag) != 0;
}

// 플래그 토글
void toggle_flag(unsigned char flag) {
    system_flags ^= flag;
}

int main(void) {
    // 시스템 시작
    set_flag(FLAG_RUNNING);
    set_flag(FLAG_READY);

    printf("FLAGS: 0x%02X\n", system_flags);

    // 상태 확인
    if (is_flag_set(FLAG_RUNNING)) {
        printf("시스템 실행 중\n");
    }

    if (is_flag_set(FLAG_ERROR)) {
        printf("에러 발생!\n");
    } else {
        printf("정상 동작\n");
    }

    // 에러 발생
    set_flag(FLAG_ERROR);
    printf("에러 플래그 설정 후: 0x%02X\n", system_flags);

    // 에러 해결
    clear_flag(FLAG_ERROR);
    printf("에러 플래그 해제 후: 0x%02X\n", system_flags);

    return 0;
}

Arduino에서 플래그 사용 예

// arduino_flags.ino

// 상태 플래그
#define STATE_IDLE      0x00
#define STATE_RUNNING   0x01
#define STATE_PAUSED    0x02
#define STATE_ERROR     0x04

unsigned char deviceState = STATE_IDLE;

void setup() {
    Serial.begin(9600);
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    // 시리얼 명령 처리
    if (Serial.available()) {
        char cmd = Serial.read();

        switch (cmd) {
            case 'r':  // Run
                deviceState |= STATE_RUNNING;
                deviceState &= ~STATE_PAUSED;
                Serial.println("Running");
                break;

            case 'p':  // Pause
                deviceState |= STATE_PAUSED;
                Serial.println("Paused");
                break;

            case 's':  // Stop
                deviceState = STATE_IDLE;
                Serial.println("Stopped");
                break;

            case 'e':  // Error toggle
                deviceState ^= STATE_ERROR;
                Serial.println("Error toggled");
                break;
        }

        // 상태 출력
        Serial.print("State: 0x");
        Serial.println(deviceState, HEX);
    }

    // 상태에 따른 LED 제어
    if (deviceState & STATE_ERROR) {
        // 에러: 빠른 깜빡임
        digitalWrite(LED_BUILTIN, HIGH);
        delay(100);
        digitalWrite(LED_BUILTIN, LOW);
        delay(100);
    } else if (deviceState & STATE_RUNNING) {
        if (!(deviceState & STATE_PAUSED)) {
            // 실행 중: 느린 깜빡임
            digitalWrite(LED_BUILTIN, HIGH);
            delay(500);
            digitalWrite(LED_BUILTIN, LOW);
            delay(500);
        }
    } else {
        // 대기: LED 꺼짐
        digitalWrite(LED_BUILTIN, LOW);
    }
}

5. 레지스터 개념

MCU 레지스터란?

레지스터는 MCU 내부의 특수한 메모리 위치로, 하드웨어를 제어합니다.

Arduino Uno (ATmega328P) GPIO 관련 레지스터:

┌─────────────────────────────────────────────────────┐
│ DDRx (Data Direction Register)                      │
│ - 핀의 입/출력 방향 설정                            │
│ - 0 = 입력 (INPUT)                                  │
│ - 1 = 출력 (OUTPUT)                                 │
├─────────────────────────────────────────────────────┤
│ PORTx (Port Output Register)                        │
│ - 출력 모드: HIGH/LOW 출력                          │
│ - 입력 모드: 풀업 저항 활성화                       │
├─────────────────────────────────────────────────────┤
│ PINx (Port Input Register)                          │
│ - 핀의 현재 상태 읽기                               │
└─────────────────────────────────────────────────────┘

x = B (핀 8~13), C (아날로그 핀), D (핀 0~7)

Arduino 함수 vs 직접 레지스터 제어

// Arduino 라이브러리 사용 (편리하지만 느림)
pinMode(13, OUTPUT);
digitalWrite(13, HIGH);
digitalWrite(13, LOW);

// 직접 레지스터 제어 (빠름)
DDRB |= (1 << 5);   // 핀 13을 출력으로 설정
PORTB |= (1 << 5);  // 핀 13 HIGH
PORTB &= ~(1 << 5); // 핀 13 LOW

속도 비교

// speed_compare.ino
// digitalWrite vs 직접 레지스터

void setup() {
    Serial.begin(9600);
    DDRB |= (1 << 5);  // 핀 13 출력 설정
}

void loop() {
    unsigned long start, end;

    // digitalWrite 속도 측정
    start = micros();
    for (long i = 0; i < 100000; i++) {
        digitalWrite(13, HIGH);
        digitalWrite(13, LOW);
    }
    end = micros();
    Serial.print("digitalWrite: ");
    Serial.print(end - start);
    Serial.println(" us");

    // 직접 레지스터 속도 측정
    start = micros();
    for (long i = 0; i < 100000; i++) {
        PORTB |= (1 << 5);
        PORTB &= ~(1 << 5);
    }
    end = micros();
    Serial.print("Direct register: ");
    Serial.print(end - start);
    Serial.println(" us");

    delay(3000);
}

// 결과 예시:
// digitalWrite: 625000 us
// Direct register: 12500 us
// → 직접 제어가 약 50배 빠름!

레지스터 직접 접근 예제

// register_access.ino
// 레지스터로 LED 제어

void setup() {
    // DDRB: Data Direction Register B
    // 비트 5 = 1 → 핀 13 출력 모드
    DDRB |= (1 << DDB5);

    Serial.begin(9600);
    Serial.println("Register control demo");
}

void loop() {
    // PORTB 레지스터의 비트 5를 토글
    PORTB ^= (1 << PORTB5);

    // PINB 레지스터로 현재 상태 읽기
    if (PINB & (1 << PINB5)) {
        Serial.println("LED ON");
    } else {
        Serial.println("LED OFF");
    }

    delay(500);
}

6. volatile 키워드

volatile이 필요한 이유

// 문제 상황: 컴파일러 최적화

int flag = 0;

// 인터럽트 핸들러 (하드웨어에 의해 호출)
void interrupt_handler() {
    flag = 1;
}

int main() {
    while (flag == 0) {
        // 대기
    }
    // flag가 1이 되면 여기 실행
}

컴파일러는 flag가 루프 안에서 변경되지 않는다고 판단하여 최적화할 수 있습니다:

// 컴파일러가 최적화한 코드 (문제!)
if (flag == 0) {
    while (1) { }  // 무한 루프로 변환
}

volatile 사용

// 해결: volatile 키워드 사용
volatile int flag = 0;

void interrupt_handler() {
    flag = 1;
}

int main() {
    while (flag == 0) {
        // 매번 메모리에서 flag 값을 읽음
    }
    // 정상 동작
}

volatile의 의미

volatile = "변덕스러운"

컴파일러에게 알려주는 것:
1. 이 변수는 언제든 외부에서 변경될 수 있음
2. 최적화하지 말고 항상 메모리에서 읽어라
3. 레지스터에 캐싱하지 마라

Arduino에서 volatile 사용

// volatile_example.ino
// 인터럽트와 volatile

volatile bool buttonPressed = false;
const int BUTTON_PIN = 2;
const int LED_PIN = 13;

// 인터럽트 서비스 루틴 (ISR)
void buttonISR() {
    buttonPressed = true;
}

void setup() {
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);

    // 핀 2에 인터럽트 연결 (FALLING = HIGH→LOW)
    attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);

    Serial.begin(9600);
}

void loop() {
    if (buttonPressed) {
        Serial.println("Button pressed!");
        digitalWrite(LED_PIN, !digitalRead(LED_PIN));  // LED 토글

        buttonPressed = false;  // 플래그 리셋
        delay(200);  // 디바운스
    }
}

레지스터와 volatile

// MCU 레지스터는 항상 volatile
// 하드웨어가 값을 변경할 수 있으므로

// 실제 Arduino 헤더 파일 정의 (avr/io.h)
#define PORTB (*(volatile uint8_t *)0x25)
#define DDRB  (*(volatile uint8_t *)0x24)
#define PINB  (*(volatile uint8_t *)0x23)

// 설명:
// 0x25 = PORTB 레지스터의 메모리 주소
// (volatile uint8_t *) = 해당 주소를 volatile 포인터로 캐스팅
// * = 해당 주소의 값에 접근

7. 실습: 비트 조작 유틸리티

비트 카운터

// bit_counter.c
#include <stdio.h>

// 1인 비트 개수 세기 (popcount)
int count_ones(unsigned int n) {
    int count = 0;
    while (n) {
        count += n & 1;
        n >>= 1;
    }
    return count;
}

// 더 빠른 방법: Brian Kernighan 알고리즘
int count_ones_fast(unsigned int n) {
    int count = 0;
    while (n) {
        n &= (n - 1);  // 가장 오른쪽 1 비트 제거
        count++;
    }
    return count;
}

int main(void) {
    unsigned int test[] = {0, 1, 7, 255, 0xABCD};

    for (int i = 0; i < 5; i++) {
        printf("0x%04X (%5u): %d ones\n",
               test[i], test[i], count_ones(test[i]));
    }

    return 0;
}

비트 반전 (Reverse)

// bit_reverse.c
#include <stdio.h>

// 8비트 반전
unsigned char reverse_bits(unsigned char n) {
    unsigned char result = 0;
    for (int i = 0; i < 8; i++) {
        result <<= 1;
        result |= (n & 1);
        n >>= 1;
    }
    return result;
}

int main(void) {
    unsigned char val = 0b10110001;  // 177

    printf("원본:   ");
    for (int i = 7; i >= 0; i--) printf("%d", (val >> i) & 1);
    printf(" (0x%02X)\n", val);

    unsigned char reversed = reverse_bits(val);

    printf("반전:   ");
    for (int i = 7; i >= 0; i--) printf("%d", (reversed >> i) & 1);
    printf(" (0x%02X)\n", reversed);

    return 0;
}

비트 스왑

// bit_swap.c
#include <stdio.h>

// 두 비트 위치의 값 교환
unsigned char swap_bits(unsigned char n, int i, int j) {
    // i와 j 위치의 비트가 다를 때만 교환
    if (((n >> i) & 1) != ((n >> j) & 1)) {
        n ^= (1 << i) | (1 << j);  // 둘 다 토글
    }
    return n;
}

// 상위 4비트와 하위 4비트 교환
unsigned char swap_nibbles(unsigned char n) {
    return ((n & 0x0F) << 4) | ((n & 0xF0) >> 4);
}

int main(void) {
    unsigned char val = 0b11001010;

    printf("원본: 0x%02X (0b%d%d%d%d%d%d%d%d)\n", val,
           (val>>7)&1, (val>>6)&1, (val>>5)&1, (val>>4)&1,
           (val>>3)&1, (val>>2)&1, (val>>1)&1, val&1);

    // 비트 1과 6 교환
    unsigned char swapped = swap_bits(val, 1, 6);
    printf("비트 1,6 교환: 0x%02X\n", swapped);

    // 니블 교환
    unsigned char nibble_swapped = swap_nibbles(val);
    printf("니블 교환: 0x%02X\n", nibble_swapped);

    return 0;
}

Arduino 비트 조작 예제

// bit_manipulation.ino
// 비트 조작으로 여러 LED 제어

// LED 연결: 핀 8, 9, 10, 11 (PORTB 비트 0~3)
#define LED_MASK 0x0F  // 하위 4비트

void setup() {
    // 핀 8~11을 출력으로 설정
    DDRB |= LED_MASK;
    PORTB &= ~LED_MASK;  // 모든 LED 끄기

    Serial.begin(9600);
    Serial.println("Bit manipulation demo");
    Serial.println("Commands: 0-F (hex), r (rotate), i (invert)");
}

void loop() {
    if (Serial.available()) {
        char cmd = Serial.read();

        if (cmd >= '0' && cmd <= '9') {
            // 숫자 0-9: 해당 패턴 표시
            PORTB = (PORTB & ~LED_MASK) | (cmd - '0');
        }
        else if (cmd >= 'a' && cmd <= 'f') {
            // a-f: 10-15 패턴 표시
            PORTB = (PORTB & ~LED_MASK) | (cmd - 'a' + 10);
        }
        else if (cmd >= 'A' && cmd <= 'F') {
            // A-F: 10-15 패턴 표시
            PORTB = (PORTB & ~LED_MASK) | (cmd - 'A' + 10);
        }
        else if (cmd == 'r' || cmd == 'R') {
            // 왼쪽 순환 시프트
            unsigned char leds = PORTB & LED_MASK;
            leds = ((leds << 1) | (leds >> 3)) & LED_MASK;
            PORTB = (PORTB & ~LED_MASK) | leds;
        }
        else if (cmd == 'i' || cmd == 'I') {
            // 반전
            PORTB ^= LED_MASK;
        }

        // 현재 상태 출력
        Serial.print("LED pattern: 0b");
        for (int i = 3; i >= 0; i--) {
            Serial.print((PORTB >> i) & 1);
        }
        Serial.println();
    }
}

연습 문제

연습 1: 비트 필드 추출

8비트 값에서 비트 2~5 (4비트)를 추출하는 함수를 작성하세요.

unsigned char extract_bits(unsigned char value, int start, int length);
// extract_bits(0b11010110, 2, 4) → 0b0101 (5)

연습 2: 2의 거듭제곱 확인

주어진 숫자가 2의 거듭제곱인지 확인하는 함수를 비트 연산으로 작성하세요.

int is_power_of_two(unsigned int n);
// is_power_of_two(8) → 1 (true)
// is_power_of_two(6) → 0 (false)

연습 3: 패리티 비트

8비트 값의 1인 비트 개수가 홀수면 1, 짝수면 0을 반환하는 함수를 작성하세요.

int parity(unsigned char n);
// parity(0b10110001) → 0 (1이 4개 = 짝수)
// parity(0b10110011) → 1 (1이 5개 = 홀수)

연습 4: Arduino LED 바

4개의 LED로 0~15 값을 2진수로 표시하고, 버튼을 누를 때마다 값이 1씩 증가하도록 만드세요.


핵심 개념 정리

연산 코드 설명
비트 설정 val \|= (1 << n) n번째 비트를 1로
비트 해제 val &= ~(1 << n) n번째 비트를 0으로
비트 토글 val ^= (1 << n) n번째 비트 반전
비트 확인 (val >> n) & 1 n번째 비트 값
하위 n비트 val & ((1 << n) - 1) 하위 n개 비트만
키워드 의미
volatile 컴파일러 최적화 방지, 항상 메모리에서 읽기
register 레지스터에 저장 요청 (힌트)

다음 단계

비트 연산을 익혔다면 다음 문서로 넘어가세요: - 15. GPIO 제어 - LED와 버튼으로 실습

to navigate between lessons