03. 비트 연산과 레지스터 조작 (set·clear·toggle)
하드웨어를 다룬다는 건 결국 레지스터의 특정 비트를 켜고·끄고·읽는 일입니다. 이때 나머지 비트는 절대 건드리면 안 됩니다(옆 비트가 다른 핀을 제어할 수 있으니까요). 비트 연산자(& | ^ ~ << >>)와 세 가지 황금 관용구 set(|=)·clear(&= ~)·toggle(^=), 그리고 test(>> &)를 PC의 gcc 로 익힙니다. 여기서 익힌 관용구는 6편 이후 8051·STM32 레지스터 제어에 그대로 쓰입니다.
이 강의에서 배우는 것
- 1비트 연산자 6종(& | ^ ~ << >>)의 동작을 설명한다
- 2비트 마스크 (1u<<n) 로 원하는 비트를 지정한다
- 3한 비트를 set(|=)·clear(&= ~)·toggle(^=)·test(>> &)한다
- 4나머지 비트를 보존하는 read-modify-write 의미를 이해한다
- 5여러 비트를 마스크로 한꺼번에 조작한다
소개
"GPIO 3번 핀 High", "타이머 플래그 클리어", "UART 송신 완료 비트가 1이 될 때까지 대기" — 모두 한 레지스터 안의 개별 비트를 다루는 작업입니다. 이번 편은 보드 없이 8비트 변수를 "포트 레지스터"로 가정하고 비트 관용구를 익힙니다.
핵심 개념
1) 비트 연산자 6종
| 연산자 | 이름 | 용도 |
|---|---|---|
| & | AND | 비트 끄기/검사(마스킹) |
| | | OR | 비트 켜기 |
| ^ | XOR | 비트 뒤집기(토글) |
| ~ | NOT | 마스크 반전 |
| << | 왼쪽 시프트 | 마스크 생성, ×2ⁿ |
| >> | 오른쪽 시프트 | 비트 추출, ÷2ⁿ |
& | ^ 는 비트 연산이고 && || 는 논리 연산(참/거짓)입니다. 비트를 다룰 때 &&를 쓰면 전혀 다른 결과가 나옵니다.
2) 비트 마스크와 황금 관용구
n번 비트만 1인 값이 마스크입니다: `(1u << n)`. 한 비트만 조작하면서 나머지를 보존하는 것이 핵심(read-modify-write)입니다.
| 동작 | 코드 | 원리 |
|---|---|---|
| set(1로) | reg |= (1u<<n); | OR 1 → 그 비트만 1, 나머지 불변 |
| clear(0으로) | reg &= ~(1u<<n); | AND 0 → 그 비트만 0, 나머지 불변 |
| toggle(반전) | reg ^= (1u<<n); | XOR 1 → 그 비트만 반전 |
clear 의 핵심은 ~(NOT)입니다. `~(1u<<n)` 은 n번만 0이고 나머지는 전부 1인 마스크라, AND하면 n번만 0이 되고 나머지는 보존됩니다.
3) 비트 읽기(test)
if ((reg >> n) & 1u) { ... } /* 정확히 0/1 — 안전 */
if (reg & (1u << n)) { ... } /* 마스킹: 0 또는 (1<<n) → 참/거짓 */핵심 예제
8비트 변수를 "LED 포트"로 보고 관용구를 함수로 떼어내 검증합니다(하드웨어 없이 단위 검증 가능).
#include <stdio.h>
#include <stdint.h>
#define BIT(n) (1u << (n))
static uint8_t set_bit(uint8_t r, unsigned n) { return (uint8_t)(r | BIT(n)); }
static uint8_t clear_bit(uint8_t r, unsigned n) { return (uint8_t)(r & ~BIT(n)); }
static uint8_t toggle_bit(uint8_t r, unsigned n) { return (uint8_t)(r ^ BIT(n)); }
static int test_bit(uint8_t r, unsigned n) { return (int)((r >> n) & 1u); }
int main(void)
{
uint8_t reg = 0x00;
reg = set_bit(reg, 0);
reg = set_bit(reg, 7); /* 1000 0001 */
reg = clear_bit(reg, 0); /* 1000 0000 */
printf("0x%02X bit7=%d\n", reg, test_bit(reg, 7)); /* 0x80 bit7=1 */
return 0;
}자주 하는 실수
Q. 한 비트만 끄려고 reg &= (1u<<n); 했더니 다른 비트가 다 사라졌어요.
A. clear 는 반전 마스크가 필요합니다. (1u<<n) 은 그 비트만 1이라 AND하면 나머지를 전부 0으로 지웁니다. 올바른 형태는 reg &= ~(1u << n); — ~로 "그 비트만 0, 나머지 1" 마스크를 만들어야 합니다.
Q. if (reg & (1u<<7) == 1) 이 항상 거짓이에요.
A. ① ==가 &보다 우선순위가 높아 (1u<<7)==1 이 먼저 계산됩니다 → 괄호 필요. ② reg & (1u<<7) 은 참일 때 0x80 이라 ==1 과 절대 같지 않습니다. if ((reg >> 7) & 1u) 로 쓰세요.
Q. 1 << 31 같은 큰 시프트가 이상해요.
A. 1 은 int 라 부호 비트까지 밀면 미정의 동작입니다. 마스크는 항상 부호 없는 리터럴 1u(필요하면 1UL)로 시작하세요. 32비트 레지스터에서는 1u << 31 이 안전합니다.
정리
- 비트 마스크는 (1u << n) — n번 비트만 1
- set은 |=, clear는 &= ~, toggle은 ^= — 레지스터 제어의 기본 3관용구
- clear의 ~, 시프트의 1u(부호 없는 리터럴)를 빠뜨리지 않는다
- read-modify-write로 한 비트만 다루며 나머지를 보존한다
- 비트 읽기는 (reg >> n) & 1u 가 정확히 0/1을 준다
과제
- 8비트 레지스터에서 bit2·bit5 를 동시에 set 하는 마스크를 만들어 적용
- 주어진 레지스터의 켜진 비트 개수를 세는 count_on() 함수 작성
- toggle 을 같은 비트에 두 번 적용하면 원래대로 돌아옴을 코드로 확인