보충 1) 비트 연산 완전 정복 – 마이크로컨트롤러의 핵심 언어

안녕하세요! 3편에서 비트 연산을 간단히 다뤘지만, 4편부터 갑자기 복잡해진 비트 연산 때문에 어려움을 느끼셨을 수도 있습니다. 이번에는 비트 연산을 완전히 정복해보겠습니다. 마이크로컨트롤러 프로그래밍에서 비트 연산은 선택이 아닌 필수입니다!

이 글의 목표

  • 비트 연산이 왜 필요한지 확실히 이해
  • 각 비트 연산자의 동작 원리 완전 파악
  • 복잡한 비트 연산 표현식을 단계별로 분해하기
  • 실전에서 자주 사용되는 비트 연산 패턴 숙달

이진수와 비트의 기초 복습

우리는 왜 이진수를 사용할까?

마이크로컨트롤러는 전압으로만 소통합니다:
0V = 0 (거짓)
5V = 1 (참)

8개 핀의 상태: PB7 PB6 PB5 PB4 PB3 PB2 PB1 PB0
실제 전압:      5V  0V  5V  0V  0V  5V  0V  5V
이진수 표현:     1   0   1   0   0   1   0   1  = 0b10100101

비트 위치와 숫자의 관계

이진수: 0b10100101
위치:    7 6 5 4 3 2 1 0  ← 비트 위치 (오른쪽이 0번)
값:      1 0 1 0 0 1 0 1
가중치:  128+0+32+0+0+4+0+1 = 165 (10진수)

핵심: 각 비트는 해당 핀의 상태를 나타냅니다!

비트 연산자 하나씩 완전 분해

1. AND 연산 (&) – “둘 다 1이어야 1”

c

// 특정 비트만 확인하고 싶을 때 사용
uint8_t portb_value = 0b10100101;  // 현재 PORTB 값
uint8_t mask = 0b00100000;         // PB5만 확인하는 마스크

uint8_t result = portb_value & mask;

단계별 계산:

  10100101  (PORTB 값)
& 00100000  (마스크 - PB5만 1)
-----------
  00100000  (결과 - PB5 비트만 남음)

결과 해석:

  • result가 0이 아니면 → PB5가 HIGH
  • result가 0이면 → PB5가 LOW

c

// 실제 사용 예
if (PINB & (1 << PB0)) {
    // PB0 핀이 HIGH일 때 실행
    printf("버튼이 안 눌림\n");
} else {
    // PB0 핀이 LOW일 때 실행  
    printf("버튼이 눌림\n");
}

2. OR 연산 (|) – “하나라도 1이면 1”

c

// 특정 비트를 1로 설정할 때 사용 (다른 비트는 그대로 유지)
uint8_t portb_value = 0b10100000;  // 현재 PORTB 값
uint8_t mask = 0b00000101;         // PB2와 PB0를 켜는 마스크

PORTB = portb_value | mask;

단계별 계산:

  10100000  (현재 PORTB)
| 00000101  (켜고 싶은 비트들)
-----------
  10100101  (결과 - PB2, PB0가 추가로 켜짐)

핵심 이해:

  • 기존에 켜져있던 비트(PB7, PB5)는 그대로 유지
  • 새로 켜고 싶은 비트(PB2, PB0)만 추가로 켜짐
  • 원래 꺼져있던 다른 비트들은 그대로 유지

3. XOR 연산 (^) – “서로 다르면 1”

c

// 특정 비트를 반전(토글)시킬 때 사용
uint8_t portb_value = 0b10100000;  // 현재 PORTB 값
uint8_t mask = 0b10000001;         // PB7과 PB0를 토글하는 마스크

PORTB = portb_value ^ mask;

단계별 계산:

  10100000  (현재 PORTB)
^ 10000001  (토글하고 싶은 비트들)
-----------
  00100001  (결과 - PB7은 꺼지고, PB0는 켜짐)

토글의 신기한 특성:

c

uint8_t original = 0b10100000;
uint8_t toggled = original ^ 0b11111111;  // 모든 비트 토글
// toggled = 0b01011111

uint8_t back = toggled ^ 0b11111111;      // 다시 토글
// back = 0b10100000 (원래 값으로 돌아옴!)

4. NOT 연산 (~) – “모든 비트 반전”

c

uint8_t value = 0b10100101;
uint8_t inverted = ~value;
// inverted = 0b01011010

주의사항: NOT은 모든 비트를 반전시킵니다!

복잡한 표현식 단계별 분해

자주 등장하는 복잡한 표현식들

표현식 1: PORTB &= ~(1 << PB5)

“특정 비트만 끄기”의 표준 패턴

c

// 단계별 분해
uint8_t bit_position = PB5;           // PB5 = 5
uint8_t shift_result = (1 << 5);      // 0b00100000
uint8_t inverted = ~shift_result;     // 0b11011111  
PORTB = PORTB & inverted;             // PB5만 0으로, 나머지는 유지

시각적 이해:

현재 PORTB:     1 1 1 1 1 1 1 1  (모든 LED 켜져있음)
마스크 생성:    0 0 1 0 0 0 0 0  ← (1 << PB5)
마스크 반전:    1 1 0 1 1 1 1 1  ← ~(1 << PB5)
AND 연산 결과:  1 1 0 1 1 1 1 1  ← PB5만 꺼짐

표현식 2: if (!(PINB & (1 << PB0)))

“버튼이 눌렸는지 확인”의 표준 패턴

c

// 단계별 분해
uint8_t mask = (1 << PB0);           // 0b00000001
uint8_t masked = PINB & mask;        // PB0 비트만 추출
uint8_t inverted = !masked;          // 논리 반전 (0→1, 0이아님→0)
if (inverted) {                      // 결과가 참이면 버튼 눌림

경우별 분석:

경우 1: 버튼이 안 눌림 (PB0 = HIGH)
PINB:           xxxx xxx1  (PB0가 1)
(1 << PB0):     0000 0001  
PINB & mask:    0000 0001  (0이 아닌 값)
!(0이 아님):    0          (거짓)
→ if 조건 실행 안됨

경우 2: 버튼이 눌림 (PB0 = LOW)  
PINB:           xxxx xxx0  (PB0가 0)
(1 << PB0):     0000 0001
PINB & mask:    0000 0000  (0)
!(0):           1          (참)
→ if 조건 실행됨

표현식 3: DDRB |= (1 << PB4) | (1 << PB5)

“여러 핀을 동시에 출력으로 설정”

c

// 단계별 분해
uint8_t mask1 = (1 << PB4);         // 0b00010000
uint8_t mask2 = (1 << PB5);         // 0b00100000  
uint8_t combined = mask1 | mask2;    // 0b00110000
DDRB = DDRB | combined;              // PB4, PB5를 출력으로 설정

시각적 과정:

초기 DDRB:      0 0 0 0 0 0 0 0  (모든 핀이 입력)
PB4 마스크:     0 0 0 1 0 0 0 0
PB5 마스크:     0 0 1 0 0 0 0 0  
결합된 마스크:   0 0 1 1 0 0 0 0
최종 DDRB:      0 0 1 1 0 0 0 0  (PB4, PB5가 출력으로 변경)

비트 연산 사고 훈련

연습 문제 1: 비트 상태 분석

c

uint8_t portb = 0b11010010;

// 다음 연산의 결과를 예측해보세요:
uint8_t result1 = portb & 0b00001111;    // 결과는?
uint8_t result2 = portb | 0b10000000;    // 결과는?  
uint8_t result3 = portb ^ 0b01010101;    // 결과는?

<details> <summary>정답 (클릭해서 확인)</summary> “`c // 원본: 0b11010010

result1 = 0b11010010 & 0b00001111 = 0b00000010 // 하위 4비트만 남김 result2 = 0b11010010 | 0b10000000 = 0b11010010 // 이미 PB7이 1이므로 변화없음 result3 = 0b11010010 ^ 0b01010101 = 0b10000111 // 대응되는 비트끼리 XOR

</details>

### 연습 문제 2: 실전 패턴 만들기

**문제**: PB3과 PB6은 켜고, PB1과 PB4는 끄고, 나머지 비트는 그대로 유지하는 코드를 작성하세요.

<details>
<summary>힌트</summary>

- 켜기: OR 연산 사용
- 끄기: AND + NOT 연산 사용  
- 순서: 먼저 끄기, 그 다음 켜기
</details>

<details>
<summary>정답</summary>
```c
// 방법 1: 단계별 처리
PORTB &= ~((1 << PB1) | (1 << PB4));  // PB1, PB4 끄기
PORTB |= (1 << PB3) | (1 << PB6);     // PB3, PB6 켜기

// 방법 2: 한 번에 처리
PORTB = (PORTB & ~((1 << PB1) | (1 << PB4))) | ((1 << PB3) | (1 << PB6));

</details>

실전 비트 연산 패턴

패턴 1: 특정 범위의 비트만 조작

c

// 하위 4비트만 0으로 만들기
PORTB &= 0xF0;  // 0b11110000와 AND

// 상위 4비트만 1로 만들기  
PORTB |= 0xF0;  // 0b11110000와 OR

// 중간 4비트(PB2~PB5)만 토글
PORTB ^= 0x3C;  // 0b00111100와 XOR

패턴 2: 비트 회전 (로테이트)

c

// 왼쪽으로 1비트 회전
uint8_t rotate_left(uint8_t value) {
    return (value << 1) | (value >> 7);
}

// 단계별 이해:
// value = 0b11010010
// value << 1 = 0b10100100  (왼쪽 시프트, 맨 왼쪽 비트 손실)
// value >> 7 = 0b00000001  (오른쪽 시프트, 원래 맨 왼쪽 비트가 맨 오른쪽으로)  
// 결과 = 0b10100101

패턴 3: 비트 카운팅

c

// 켜진 비트 개수 세기
uint8_t count_bits(uint8_t value) {
    uint8_t count = 0;
    for (uint8_t i = 0; i < 8; i++) {
        if (value & (1 << i)) {
            count++;
        }
    }
    return count;
}

패턴 4: 조건부 비트 설정

c

// 조건에 따라 비트 설정/해제
void conditional_bit_set(uint8_t bit, uint8_t condition) {
    if (condition) {
        PORTB |= (1 << bit);   // 비트 설정
    } else {
        PORTB &= ~(1 << bit);  // 비트 해제
    }
}

// 더 효율적인 방법 (분기 없음)
void conditional_bit_set_efficient(uint8_t bit, uint8_t condition) {
    PORTB = (PORTB & ~(1 << bit)) | (condition ? (1 << bit) : 0);
}

디버깅과 시각화 도구

비트 상태를 보기 쉽게 출력하는 함수

c

void print_bits(uint8_t value, const char* label) {
    printf("%s: ", label);
    for (int8_t i = 7; i >= 0; i--) {
        printf("%d", (value & (1 << i)) ? 1 : 0);
        if (i == 4) printf(" ");  // 가독성을 위한 공백
    }
    printf(" (0x%02X, %d)\n", value, value);
}

// 사용 예:
uint8_t portb_value = 0b11010010;
print_bits(portb_value, "PORTB");
// 출력: PORTB: 1101 0010 (0xD2, 210)

비트 연산 과정을 단계별로 보여주는 매크로

c

#define SHOW_BIT_OP(var, op, mask, desc) do { \
    printf("Before: "); print_bits(var, #var); \
    var op mask; \
    printf("After %s: ", desc); print_bits(var, #var); \
    printf("\n"); \
} while(0)

// 사용 예:
uint8_t test = 0b11110000;
SHOW_BIT_OP(test, |=, (1 << PB2), "setting PB2");

비트 연산 마스터 체크리스트

다음 항목들을 모두 이해했다면 비트 연산을 마스터한 것입니다:

기초 개념

  • 이진수와 16진수 표현을 자유롭게 변환할 수 있다
  • 각 비트가 실제 하드웨어 핀과 대응된다는 것을 이해한다
  • 왜 비트 연산이 필요한지 설명할 수 있다

연산자 이해

  • AND(&) 연산의 “마스킹” 역할을 이해한다
  • OR(|) 연산의 “설정” 역할을 이해한다
  • XOR(^) 연산의 “토글” 역할을 이해한다
  • NOT(~) 연산의 “반전” 역할을 이해한다

복합 표현식

  • PORTB &= ~(1 << PB5)를 단계별로 분해할 수 있다
  • if (!(PINB & (1 << PB0)))의 의미를 정확히 안다
  • 여러 비트를 동시에 조작하는 표현식을 작성할 수 있다

실전 활용

  • 특정 비트만 읽기/쓰기/토글할 수 있다
  • 디버깅용 비트 출력 함수를 작성할 수 있다
  • 효율적인 비트 조작 패턴을 적용할 수 있다

다음 단계

비트 연산을 마스터했다면:

  1. 4편의 디바운싱 코드를 다시 읽어보세요 – 이제 완전히 이해될 겁니다!
  2. 5편의 패턴 조작도 훨씬 쉬워질 거예요
  3. 실전 프로젝트에서 창의적으로 비트 연산을 활용해보세요

비트 연산은 마이크로컨트롤러 프로그래밍의 핵심 기술입니다. 한 번 익숙해지면 하드웨어를 다루는 모든 작업이 훨씬 쉬워집니다!


비트 연산을 마스터하면 마이크로컨트롤러와 1:1로 소통할 수 있습니다. 이제 하드웨어의 언어를 구사할 수 있어요!