안녕하세요! 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가 HIGHresult
가 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)))
의 의미를 정확히 안다- 여러 비트를 동시에 조작하는 표현식을 작성할 수 있다
실전 활용
- 특정 비트만 읽기/쓰기/토글할 수 있다
- 디버깅용 비트 출력 함수를 작성할 수 있다
- 효율적인 비트 조작 패턴을 적용할 수 있다
다음 단계
비트 연산을 마스터했다면:
- 4편의 디바운싱 코드를 다시 읽어보세요 – 이제 완전히 이해될 겁니다!
- 5편의 패턴 조작도 훨씬 쉬워질 거예요
- 실전 프로젝트에서 창의적으로 비트 연산을 활용해보세요
비트 연산은 마이크로컨트롤러 프로그래밍의 핵심 기술입니다. 한 번 익숙해지면 하드웨어를 다루는 모든 작업이 훨씬 쉬워집니다!
비트 연산을 마스터하면 마이크로컨트롤러와 1:1로 소통할 수 있습니다. 이제 하드웨어의 언어를 구사할 수 있어요!