레지스터·카운터·상태기계
플립플롭을 묶은 레지스터에 적재 신호로 값을 넣거나 유지하고, 시프트 레지스터로 직렬 데이터를 병렬 값으로 모읍니다. 다음 상태를 계산해 저장하는 카운터와, 상태와 입력으로 다음 상태를 정하는 유한 상태 기계를 잠금 장치 예제로 설계합니다.
덧셈 결과를 다음 명령까지 보관하려면 비트마다 플립플롭이 하나씩 필요합니다. 4비트 값이라면 플립플롭 네 개를 나란히 두고 같은 클록으로 움직이게 하면 됩니다. 그런데 클록이 올 때마다 무조건 새 값을 받아들이면 보관한 값이 금방 지워지므로, 언제 새 값을 넣을지 정하는 제어 신호가 함께 필요합니다. 이 글에서는 래치·플립플롭·클록의 플립플롭을 묶어 여러 비트를 기억하는 레지스터를 만들고, 상태가 정해진 규칙으로 바뀌는 카운터와 상태 기계를 다룹니다.
레지스터와 적재 신호
같은 클록을 쓰는 플립플롭 여러 개를 묶어 한 값처럼 읽고 쓰는 것을 레지스터(register)라고 합니다. 4비트 레지스터에 적재(load) 신호를 두어, load = 1이면 에지에서 입력 D를 받아들이고 load = 0이면 지금의 값 Q를 유지하게 할 수 있습니다. 플립플롭은 에지마다 무언가를 받아들이므로, 유지하려면 자기 자신의 값을 다시 넣어 주어야 합니다. 그래서 각 플립플롭의 입력 앞에 MUX를 두고, load 신호로 새 입력 D와 지금의 출력 Q 가운데 하나를 고르게 합니다.
레지스터를 처음 상태로 되돌리는 리셋(reset) 신호에는 두 가지 방식이 있습니다. 동기 리셋은 리셋 신호가 켜져 있으면 다음 클록 에지에서 0을 저장하고, 비동기 리셋은 클록과 상관없이 신호가 켜지는 즉시 0으로 만듭니다. 전원을 켠 직후처럼 클록이 아직 안정되지 않았을 때도 회로를 정해진 상태에서 시작하려면 리셋이 필요합니다.
시프트 레지스터
시프트 레지스터(shift register)는 에지마다 모든 비트를 이웃 자리로 한 칸씩 옮기는 레지스터입니다. 오른쪽으로 옮기면서 비어 있는 맨 왼쪽 자리에 바깥에서 들어온 한 비트를 넣으면, 한 번에 한 비트씩 들어오는 직렬(serial) 데이터를 모아 여러 비트를 한꺼번에 읽는 병렬(parallel) 값으로 바꿀 수 있습니다. 반대로 병렬 값을 넣고 한 비트씩 밀어내면 직렬 데이터로 바꿀 수 있습니다. 직렬 전송은 선이 적게 들지만 한 비트씩 보내야 하고, 병렬 전송은 선이 많이 들지만 여러 비트를 한 번에 보냅니다.
def load_register(q, load, d):
return d if load else q
def shift_right(q, serial_in, width=4):
return (serial_in << (width - 1)) | (q >> 1)
q = 0b0000
for load, d in ((1, 0b1010), (0, 0b1111), (1, 0b0110)):
q = load_register(q, load, d)
print(f"load={load} d={d:04b} -> q={q:04b}")
q = 0b0000
for bit in (1, 0, 1, 1):
q = shift_right(q, bit)
print(f"입력 {bit} -> {q:04b}")
def next_state(state, event):
if state == "LOCKED":
return "OPEN" if event == "VALID_CARD" else "LOCKED"
if state == "OPEN":
return "LOCKED" if event == "CLOSED" else "OPEN"
raise ValueError("알 수 없는 상태")
state = "LOCKED"
for event in ("INVALID_CARD", "VALID_CARD", "NONE", "CLOSED"):
state = next_state(state, event)
print(event, "->", state)load=1 d=1010 -> q=1010
load=0 d=1111 -> q=1010
load=1 d=0110 -> q=0110
입력 1 -> 1000
입력 0 -> 0100
입력 1 -> 1010
입력 1 -> 1101
INVALID_CARD -> LOCKED
VALID_CARD -> OPEN
NONE -> OPEN
CLOSED -> LOCKED반복 한 번이 클록 에지 하나입니다. 처음 세 줄에서 레지스터는 load = 1일 때만 입력을 받아들이고, load = 0인 둘째 에지에서는 입력이 1111이어도 1010을 유지합니다. 다음 네 줄은 시프트 레지스터에 1, 0, 1, 1을 차례로 넣은 결과입니다. 넣은 비트가 맨 왼쪽으로 들어와 에지마다 오른쪽으로 밀려나므로, 네 번 뒤에는 처음 넣은 비트가 맨 오른쪽 자리에 와서 1101이 됩니다. 직렬로 보낸 비트 순서를 오른쪽 끝부터 읽으면 그대로 1, 0, 1, 1입니다. 마지막 네 줄은 아래에서 설명할 상태 기계입니다.
카운터는 다음 상태를 계산해 저장한다
카운터(counter)는 에지마다 정해진 순서로 상태를 바꾸는 레지스터입니다. 2비트 업 카운터는 00, 01, 10, 11을 지나 다시 00으로 돌아갑니다. 지금의 값에 1을 더하는 조합 회로를 두고 그 결과의 아래 2비트를 다시 레지스터에 넣으면 되는데, 11에 1을 더한 100의 아래 2비트가 00이라서 저절로 처음으로 돌아갑니다.
카운터를 만드는 방법은 두 가지입니다. 리플 카운터(ripple counter)는 아래 비트의 출력을 위 비트의 클록으로 쓰므로, 비트가 하나씩 차례로 바뀌어 출력이 잠깐씩 틀린 값을 거칩니다. 동기 카운터(synchronous counter)는 모든 비트가 같은 클록 에지에서 함께 바뀌므로 이런 문제가 없습니다. 동기 카운터의 각 비트에 무엇을 넣어야 하는지는 다음 글에서 설계합니다.
유한 상태 기계로 잠금 장치 만들기
유한 상태 기계(finite state machine, FSM)는 상태가 유한한 개수로 정해져 있고, 지금의 상태와 입력에 따라 다음 상태와 출력이 정해지는 모델입니다. 유효한 카드를 대면 문이 열리고, 문이 닫혔다는 센서 신호가 오면 다시 잠기는 장치를 생각해 봅시다. 상태는 잠김(LOCKED)과 열림(OPEN)의 두 가지면 됩니다.
| 지금의 상태 | 입력 | 다음 상태 | 다음 상태의 잠금 출력 |
|---|---|---|---|
| LOCKED | 유효한 카드 | OPEN | 해제 |
| LOCKED | 그 밖의 입력 | LOCKED | 잠금 |
| OPEN | 닫힘 센서 | LOCKED | 잠금 |
| OPEN | 그 밖의 입력 | OPEN | 해제 |
코드의 next_state가 이 표를 그대로 옮긴 것이고, 마지막 네 줄이 입력 순서에 따라 바뀐 상태입니다. 무효한 카드로는 열리지 않고, 유효한 카드로 열린 뒤 아무 입력이 없으면 열린 채로 있다가, 닫힘 센서에서 다시 잠깁니다. 두 상태는 1비트 레지스터 하나에 저장할 수 있고, 표는 그 레지스터의 다음 값을 계산하는 조합 회로가 됩니다.
이 표에서 잠금 출력은 상태만 보고 정해집니다. 유효한 카드가 들어온 순간에도 아직 상태가 LOCKED이므로 잠겨 있다가, 다음 클록 에지에서 상태가 OPEN이 된 뒤에 해제됩니다. 이렇게 출력이 상태로만 정해지는 기계를 무어 기계(Moore machine)라고 합니다. 출력이 상태와 지금의 입력에 함께 따라 정해지는 기계는 밀리 기계(Mealy machine)라 하며, 입력에 더 빨리 반응하지만 입력이 흔들리면 출력도 같은 주기 안에서 흔들릴 수 있습니다. 실제 문이라면 비상 해제나 센서 고장도 다루어야 하지만, 이 예는 상태가 바뀌는 기본 규칙만 다룹니다.
정리
- 레지스터는 같은 클록을 쓰는 플립플롭의 묶음이며, 적재 신호로 MUX가 새 입력과 지금의 값 가운데 하나를 골라 넣게 해 값을 유지합니다.
- 리셋은 회로를 정해진 처음 상태로 되돌리며, 다음 에지에서 동작하는 동기 방식과 즉시 동작하는 비동기 방식이 있습니다.
- 시프트 레지스터는 에지마다 비트를 한 칸씩 옮겨 직렬 데이터와 병렬 값을 서로 바꿉니다.
- 카운터는 다음 상태를 계산해 저장하며, 모든 비트가 같은 에지에서 바뀌는 동기 카운터는 리플 카운터의 중간 값 문제가 없습니다.
- 유한 상태 기계는 상태와 입력으로 다음 상태를 정하며, 출력이 상태로만 정해지면 무어 기계, 입력에도 따라 정해지면 밀리 기계입니다.
연습 문제
1. 세 번 누르면 완료
버튼을 누를 때마다 상태가 0, 1, 2, DONE 순서로 나아가고, DONE에서는 버튼을 더 눌러도 그대로입니다. reset은 어느 상태에서든 0으로 되돌립니다. 입력이 press, press, reset, press, press, press, press일 때 상태의 순서와, 상태를 저장하는 데 필요한 최소 비트 수를 구하세요.
해설 보기
상태는 차례로 1, 2, 0, 1, 2, DONE, DONE입니다. 마지막 press에서도 DONE에 머무르는 것이 규칙이므로, DONE 다음에 0으로 돌아가는 카운터와는 다릅니다. 상태가 0, 1, 2, DONE의 네 가지이므로 2비트면 구분할 수 있습니다. 상태 개수가 k이면 인 가장 작은 b비트가 필요합니다.
2. 직렬로 1바이트 받기
본문의 shift_right를 8비트로 바꾸어 바이트 0xA5의 비트를 가장 아래 비트부터 한 비트씩 넣으면, 8번의 에지 뒤 레지스터의 값은 무엇인가요? 같은 바이트를 병렬로 받으면 에지가 몇 번 필요한가요?
해설 보기
처음 넣은 비트가 에지마다 오른쪽으로 밀려 마지막에는 맨 오른쪽 자리에 오므로, 가장 아래 비트부터 넣으면 8번 뒤에는 각 비트가 원래 자리에 돌아와 0xA5 그대로입니다. 실제로 함수를 8비트로 바꾸어 실행하면 0xa5가 나옵니다. 병렬로 받으면 8비트를 한 번에 적재하므로 에지 한 번이면 됩니다. 직렬 전송은 선 하나로 보낼 수 있는 대신 비트 수만큼의 에지가 필요하다는 차이가 있습니다.
3. 카드를 대는 순간 열리게 하려면
잠금 장치가 유효한 카드를 읽는 그 순간 바로 잠금을 해제하게 하려면 무어 기계와 밀리 기계 가운데 어느 쪽으로 만들어야 하나요?
해설 보기
밀리 기계로 만들어야 합니다. 출력이 지금의 입력에도 따라 정해지므로, LOCKED 상태에서 유효한 카드 입력이 들어오면 상태가 바뀌기 전인 같은 주기 안에 해제 출력을 낼 수 있습니다. 무어 기계는 출력이 상태로만 정해지므로, 다음 클록 에지에서 상태가 OPEN이 될 때까지 한 주기를 기다려야 합니다. 대신 밀리 기계는 카드 입력이 흔들리면 해제 출력도 함께 흔들릴 수 있어서, 입력이 깨끗한지 더 신경 써야 합니다.
상태 기계를 설계할 때는 모든 상태에서 모든 입력이 어디로 가는지 표의 빈칸 없이 채웠는지 먼저 확인해 보세요. 다음 글 JK 입력식으로 동기 카운터 만들기에서는 원하는 상태 변화에서 거꾸로 플립플롭의 입력식을 이끌어 냅니다.