임베디드 면접 용어 사전
C 언어비트 연산

비트 마스킹

|로 세우고 &~로 지우고 ^로 토글해 레지스터의 특정 비트만 다루는 기법.

비트 단위로 값을 읽고 쓰고 검사하는 기법. 하드웨어 레지스터 조작의 기본이다.

네 가지 기본 연산

#define BIT(n)  (1UL << (n))

reg |=  BIT(3);            // 세트   : 3번 비트를 1로 (나머지는 유지)
reg &= ~BIT(3);            // 클리어 : 3번 비트를 0으로
reg ^=  BIT(3);            // 토글   : 뒤집기
if (reg & BIT(3)) { … }    // 검사   : 3번 비트가 1인가

왜 이렇게 되는지

세트   x | 1 = 1,  x | 0 = x   → 1을 OR 하면 켜지고, 0인 자리는 그대로
클리어 x & 0 = 0,  x & 1 = x   → 0을 AND 하면 꺼지고, 1인 자리는 그대로
토글   x ^ 1 = ~x, x ^ 0 = x   → 1을 XOR 하면 뒤집히고, 0인 자리는 그대로

~BIT(3)1111...0111이므로 3번만 0이고 나머지는 1이다. 그래서 AND 하면 3번만 꺼지고 나머지는 보존된다.

여러 비트 필드 — 마스크 후 시프트

// 비트 4~7 에 값 0b1010 을 넣기
#define FIELD_MASK   (0xFUL << 4)
#define FIELD_SET(v) (((v) & 0xFUL) << 4)

reg = (reg & ~FIELD_MASK) | FIELD_SET(0xA);
//     ↑ 기존 필드를 지우고    ↑ 새 값을 넣는다

"지우고 넣기"를 한 문장으로 하는 것이 중요하다. 두 문장으로 나누면 그 사이에 인터럽트가 끼어들 수 있고, 중간 상태가 하드웨어에 반영된다.

// 읽을 때
uint8_t v = (reg & FIELD_MASK) >> 4;

반드시 지켜야 할 규칙

1UL을 쓴다

uint32_t reg;
reg |= (1 << 31);      // ✗ 1은 int(32비트 부호 있음) → 부호 비트 오버플로
                       //   미정의 동작
reg |= (1UL << 31);    // ✓

② read-modify-write는 원자적이 아니다

GPIOA->ODR |= BIT(5);

이 한 줄은 읽기 → OR → 쓰기 3단계다. 중간에 인터럽트가 다른 비트를 바꾸면 그 변경이 사라진다.

메인:   ODR 읽기 (0x00)
ISR:    ODR |= BIT(3)  →  0x08
메인:   0x00 | BIT(5) = 0x20 을 쓴다   ← ISR의 BIT(3)이 사라졌다

해결 — 하드웨어의 원자적 레지스터를 쓴다

GPIOA->BSRR = BIT(5);        // 세트   (STM32) — 한 번의 쓰기. 원자적
GPIOA->BSRR = BIT(5) << 16;  // 클리어

많은 MCU가 이런 set/clear 전용 레지스터를 제공한다. 있으면 반드시 쓴다. 없으면 임계 구역으로 감싼다.

③ volatile을 빠뜨리지 않는다 레지스터 포인터에 volatile이 없으면 컴파일러가 읽기를 생략한다.

유용한 관용구

x & (x - 1)              // 최하위 1비트를 지운다
x & (-x)                 // 최하위 1비트만 남긴다
(x & (x - 1)) == 0       // 2의 거듭제곱인가 (0 제외)
x >> 1                   // 2로 나누기 (부호 없는 경우)
x & (N - 1)              // N이 2의 거듭제곱일 때 x % N 과 동일 (훨씬 빠름)

마지막이 특히 유용하다 — 나눗셈 명령이 없는 MCU에서 링 버퍼 인덱스를 idx = (idx + 1) & (SIZE - 1) 로 처리하면 나머지 연산 비용이 사라진다. 단 SIZE가 2의 거듭제곱이어야 한다.

비트 필드 구조체보다 나은 이유

struct { uint8_t en : 1; uint8_t mode : 3; } reg;   // 이식성 없음

비트 필드는 배치 순서·패딩·엔디안이 구현 정의라 컴파일러를 바꾸면 레지스터 매핑이 어긋난다. 하드웨어 제어에는 마스크와 시프트를 명시적으로 쓰는 것이 안전하고, MISRA C도 이 방향을 권한다.

가독성 확보

// ✗ 매직 넘버
TIM2->CR1 |= 0x0001;

// ✓ 의미가 드러난다
#define TIM_CR1_CEN   (1UL << 0)      // Counter Enable
TIM2->CR1 |= TIM_CR1_CEN;

벤더 헤더(CMSIS)가 이런 매크로를 제공하므로 직접 숫자를 쓰지 말고 제공된 매크로를 쓴다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — C 언어 핵심 — 포인터·메모리클래스·const/volatile·비트·struct 패딩