이진수와 비트 마스크
100분 안팎
학습 목표
8비트 값과 마스크를 입력받아 지정한 비트만 설정·해제합니다.
개념
하나의 비트만 바꾸려는 이유
출력 레지스터 0xA0에 여러 상태가 함께 있다고 가정합니다. bit0을 켜려고 전체에 1을 대입하면 0xA0에 있던 다른 상태까지 지워집니다. 표시등 한 개의 변경이 다른 출력까지 끄는 현상이 생깁니다. 레지스터의 숫자를 새로 만드는 대신 선택한 비트만 바꾸어야 합니다. 비트 마스크는 어느 자리를 바꿀지를 표시하는 숫자입니다. 이 레슨에서는 하드웨어 접근 전에 계산 자체가 다른 비트를 보존하는지 Python으로 확인합니다.
8비트 부호 없는 모형의 값은 0부터 255입니다. 오른쪽 끝이 bit0이고 그 값은 1입니다. 왼쪽으로 갈수록 bit1은 2, bit2는 4, bit7은 128입니다. 160은 128과 32의 합이므로 10100000입니다. 십진수 160, 16진수 0xA0, 이진수 10100000은 서로 다른 값이 아니라 같은 값을 다른 표기로 나타낸 것입니다. 입력과 출력 표기를 정하지 않으면 값은 맞아도 자동 채점의 형식이 어긋납니다.
OR로 켜고 AND-NOT으로 끕니다
설정은 value | mask입니다. 마스크의 1 자리에서는 결과가 1이 되고 마스크의 0 자리에서는 기존 value가 유지됩니다. 160과 1을 OR하면 161, 즉 10100001입니다. 이미 켜진 자리에 다시 OR해도 그대로입니다. 같은 명령을 재시도해도 두 번째에 꺼지지 않는 성질을 멱등이라고 부릅니다. 통신 재시도나 초기화에서 켜라는 요청을 여러 번 받을 수 있으므로 토글과 설정을 구분하는 것이 필요합니다.
해제는 value & (~mask & 255)입니다. 마스크를 뒤집으면 선택 자리가 0이고 나머지는 1인 보존 마스크가 됩니다. 기존 값과 AND하면 선택 자리만 0으로 내려갑니다. 161에서 bit0을 끄면 160입니다. value = 0으로 전체를 지우면 관련 없는 비트가 손실됩니다. value - mask 역시 해제 연산이 아닙니다. 선택 비트가 이미 0일 때 뺄셈은 빌림을 만들어 다른 자리를 바꿀 수 있습니다.
Python 정수는 이 실습의 8비트 상자에 자동으로 제한되지 않습니다. ~1은 254가 아니라 -2입니다. 여기에서는 ~mask & 255로 하위 8비트를 명시합니다. 이 절단 규칙은 범위를 벗어난 사용자 입력을 받아도 된다는 뜻은 아닙니다. 입력 256을 0으로 조용히 바꾸면 사용자의 잘못된 마스크가 정상 요청처럼 보입니다. 먼저 0~255를 검사하고, 유효한 입력의 계산 안에서만 폭을 제한합니다.
C로 옮기면 uint8_t도 연산 과정에서 정수 승격을 거칠 수 있습니다. 저장 형의 폭과 계산 중 형이 같다고 단정하지 않습니다. 다음 실습은 uint8_t 마스크와 결과 변환을 명시해 하위 비트 보존 의도를 드러냅니다. 비트 연산은 부호 없는 값으로 표현하고 결과 폭을 확인하는 습관을 기릅니다. 모든 signed 연산의 오버플로나 시프트가 이 모형처럼 순환한다고 일반화하지 않습니다.
읽기와 토글은 다른 질문입니다
value & mask는 선택된 비트를 추출합니다. 결과가 0이 아니면 선택 비트 중 하나 이상이 켜져 있습니다. 모든 선택 비트가 켜졌는지는 (value & mask) == mask로 판단합니다. mask가 3이고 value가 1이면 하나 이상은 참이지만 모두는 거짓입니다. mask 0에서 모두 조건은 참이므로 업무 규칙이 빈 선택을 허용하는지도 따로 정해야 합니다. 이번 채점은 변경 연산만 요구하며 이 두 조건은 읽기 판단을 위한 관찰입니다.
XOR는 1인 마스크 자리의 상태를 뒤집습니다. value ^ mask는 켜라는 요청이 아니라 반전 요청입니다. 두 번 적용하면 처음 값으로 돌아옵니다. LED를 켜 두어야 하는 코드에 XOR를 넣으면 같은 호출이 반복될 때 깜박이게 됩니다. 따라하기에서 OR를 두 번 적용한 값과 XOR를 두 번 적용한 값을 비교합니다. 연산자 모양이 비슷하다는 이유로 바꾸지 말고 요구사항의 동사를 먼저 읽습니다.
핀 하나의 마스크는 1을 해당 위치로 옮겨 만들 수 있습니다. 이 모형에서는 pin이 0~7일 때 1 << pin입니다. 음수 시프트는 Python에서 오류이고 C에서는 폭 이상의 시프트도 안전한 표현이 아닙니다. 핀 검사를 마스크 생성보다 먼저 합니다. 마지막 비트의 마스크 128과 전체 마스크 255를 모두 시험해야 왼쪽 끝 처리를 놓치지 않습니다. 여러 핀은 마스크의 비트를 함께 켜 표현할 수 있습니다.
선택되지 않은 비트 보존을 수식으로 확인하면 (result & (~mask & 255)) == (value & (~mask & 255))입니다. 설정과 해제 모두 이 조건을 만족해야 합니다. 값 하나의 정답만 확인하는 대신 변경하지 않을 부분도 비교하는 관점입니다. 나중에 LED bit1을 바꾸면서 0xA0가 그대로 남는지를 검증할 때 같은 원리를 씁니다. 선택 영역과 보존 영역의 책임을 나누면 출력 덮어쓰기 오류를 빠르게 좁힐 수 있습니다.
브라우저 실습의 입출력 계약
입력은 공백으로 구분한 십진 정수 value, 십진 정수 mask, 동작 SET 또는 CLEAR 세 토큰입니다. 예를 들어 160 1 SET은 bit0 설정입니다. 접두사 0xA0를 받는 파서는 제공하지 않으므로 입력에는 160을 씁니다. 출력은 십진 결과, 공백 하나, 앞을 0으로 채운 8자리 이진 문자열입니다. 0은 0 00000000으로 출력합니다. 소수점이나 0b 접두사를 추가하지 않습니다.
토큰 수가 세 개가 아니거나 정수 변환이 실패하거나 범위를 벗어나면 ERROR 한 줄을 출력합니다. SET과 CLEAR는 대문자만 허용합니다. 같은 입력에 ERROR와 정상값을 함께 출력하지 않습니다. 파서와 예외 처리는 starter에 있으므로 TODO의 연산을 수정하는 데 집중합니다. solution은 정답을 확인할 때 펼치고 먼저 마스크의 1 자리와 0 자리에서 각각 무엇이 남아야 하는지 손으로 설명합니다.
ValueError: invalid literal for int라는 메시지는 비트 수식이 아니라 숫자 변환 단계의 문제입니다. 이 실습의 처리 코드는 이를 잡아 ERROR로 바꿉니다. SyntaxError가 나면 괄호와 들여쓰기부터 확인합니다. & 대신 and를 쓰면 논리 피연산자를 반환해 비트별 계산과 달라집니다. 출력이 1 00000001인데 161이 기대되면 마스크 전체 대입으로 기존 0xA0를 잃었는지 봅니다. 오류 위치와 계산 목적을 연결해 읽습니다.
경계 사례로 수정 여부를 판단합니다
마스크 0은 어떤 비트도 고르지 않으므로 설정과 해제 모두 원래 값이 유지됩니다. 전체 마스크 255로 설정하면 255이고 해제하면 0입니다. 이미 켜진 비트 설정과 이미 꺼진 비트 해제는 값을 그대로 둡니다. 값의 상한과 하한, 최상위 bit7, 복수 선택, 잘못된 입력을 함께 확인합니다. 정상 입력 두 개만 맞는 구현보다 변경 대상 밖을 지킨다는 근거를 제출하는 구현이 필요합니다.
계산이 통과했다고 실제 장치에 같은 식을 바로 쓰지는 않습니다. 이 레슨은 일반 값의 비트 변환만 검증했습니다. 상태 플래그를 지울 때 0을 쓰는지 1을 쓰는지는 접근 규칙에 따라 달라집니다. 다음 레슨에서는 쓰기 버스에 보낸 값과 장치 내부에 남은 값을 분리해 W1C를 모델링합니다. 자료 표현의 더 넓은 내용은 더 읽기로 보충하고 여기에서는 마스크에 의한 보존을 자신의 숫자로 설명합니다.
따라하기
8자리로 같은 값 비교
숫자의 표기와 bit0 설정 결과를 확인합니다.
value = 160
mask = 1
print(format(value, "08b"), format(mask, "08b"))
result = value | mask
print(result, format(result, "08b"))실행 결과
10100000 00000001 161 10100001
보존 마스크 생성
~의 Python 결과와 8비트로 제한한 결과를 구분합니다.
value = 161
mask = 1
print(~mask, (~mask & 255))
result = value & (~mask & 255)
print(result, format(result, "08b"))실행 결과
-2 254 160 10100000
설정과 토글 재시도 비교
반복 요청이 같은 결과를 내는지 판단합니다.
value = 160
mask = 3
print(bool(value & mask), (value & mask) == mask)
print(value | mask, value ^ mask)
print((value | mask) | mask, (value ^ mask) ^ mask)실행 결과
False False 163 163 163 160
채점 사례 적용
브라우저에 160 1 SET을 입력한 뒤 161 1 CLEAR, 255 255 CLEAR를 각각 넣고 테스트를 실행합니다.
확인 문제
실습
TODO의 설정·해제 수식을 구현합니다. 입력은 십진 value mask SET|CLEAR 세 토큰이며 두 정수는 0~255입니다. 결과를 십진수와 8자리 이진수로 출력합니다. 형식·범위 오류는 ERROR 한 줄입니다. 선택되지 않은 비트를 보존하고 마스크 0·255 및 bit7을 시험합니다.
모범 답안
import sys
parts = sys.stdin.read().split()
try:
if len(parts) != 3:
raise ValueError()
value, mask = int(parts[0]), int(parts[1])
action = parts[2]
if not (0 <= value <= 255 and 0 <= mask <= 255) or action not in ("SET", "CLEAR"):
raise ValueError()
result = value | mask if action == "SET" else value & (~mask & 255)
print(result, format(result, "08b"))
except ValueError:
print("ERROR")
더 읽기
면접 질문
- 특정 레지스터 비트만 바꾸는 코드를 설명해 주시면 됩니다.