마이크로연산과 제어 신호
명령어 하나를 레지스터 사이의 작은 이동과 계산인 마이크로연산으로 나누고, 레지스터 전송 표기에서 같은 에지의 여러 전송이 이전 값으로 함께 계산되는 이유를 확인합니다. LOAD를 여섯 단계의 마이크로연산으로 모의 실행하고, 내부 버스 하나에 값을 하나만 올릴 수 있는 제약을 다룹니다.
LOAD 명령어 하나도 CPU 안에서는 한 번에 일어나지 않습니다. 명령어의 주소를 메모리에 보내고, 읽어 온 명령어를 담고, 그 안의 데이터 주소를 다시 보내고, 읽어 온 데이터를 레지스터에 쓰는 여러 동작으로 나뉩니다. 레지스터 수준에서 한 클록 주기에 하는 이런 작은 이동과 계산을 마이크로연산(micro-operation)이라고 합니다. 이 글에서는 CPU 데이터 경로의 단계를 마이크로연산으로 더 잘게 나누고, 각 마이크로연산을 일으키는 제어 신호를 다룹니다.
레지스터 전송 표기
마이크로연산은 레지스터 전송 표기(register transfer notation)로 적습니다. R2 ← R1은 R1의 값을 R2에 복사한다는 뜻이며, R1의 값은 그대로 남습니다. P: R2 ← R1처럼 앞에 조건을 붙이면 제어 조건 P가 1일 때만 전송합니다. 동기 회로에서 전송은 정해진 클록 에지에 일어나고, 같은 에지에 적은 여러 전송은 모두 에지 직전의 값으로 계산되어 함께 저장됩니다.
그래서 R1 ← R2, R2 ← R1을 같은 에지에 하면 두 값이 서로 바뀝니다. 두 전송 모두 에지 직전의 R1과 R2를 읽기 때문입니다. 프로그램에서 r1 = r2를 실행한 다음 r2 = r1을 실행하는 것과는 다르며, 그렇게 하면 두 변수가 모두 r2의 처음 값이 됩니다. JK 입력식으로 동기 카운터 만들기에서 모든 비트의 다음 값을 이전 값으로 계산해야 했던 것과 같은 이유입니다.
마이크로연산은 하는 일에 따라 레지스터 사이의 전송, 1 더하기나 덧셈과 뺄셈 같은 산술, AND, OR, XOR 같은 논리, 비트를 옮기는 시프트로 나눌 수 있습니다. 오른쪽 시프트에는 두 가지가 있는데, 논리 시프트는 비어 있는 맨 왼쪽 자리를 0으로 채우고, 산술 시프트는 부호 비트를 복제해 채웁니다. 산술 시프트는 2의 보수로 읽은 음수를 2로 나눈 몫을 지키기 위한 것입니다.
LOAD를 여섯 단계로 나눈다
설명을 위한 데이터 경로에 메모리 주소를 담는 MAR, 메모리와 주고받을 데이터를 담는 MDR, 명령어를 담는 IR이 있고, 메모리 읽기는 주소를 보낸 다음 단계에 끝난다고 하겠습니다. 명령어 하나는 메모리 한 칸에 들어 있습니다. 주소 0에 LOAD R1, [20]이 있고 주소 20에 7이 있을 때, 이 명령어는 다음 여섯 단계로 실행됩니다.
| 단계 | 마이크로연산 | 제어 신호가 하는 일 |
|---|---|---|
| T0 | MAR ← PC | PC를 주소 입력으로 고르고 MAR에 쓴다 |
| T1 | MDR ← M[MAR], PC ← PC + 1 | 메모리를 읽어 MDR에 쓰고 PC를 늘린다 |
| T2 | IR ← MDR | 읽어 온 명령어를 IR에 쓴다 |
| T3 | MAR ← IR의 주소 부분 | 명령어 안의 데이터 주소를 MAR에 쓴다 |
| T4 | MDR ← M[MAR] | 데이터를 읽어 MDR에 쓴다 |
| T5 | R1 ← MDR | 읽어 온 데이터를 R1에 쓴다 |
T0부터 T2까지는 명령어를 가져오는 인출 과정이라 모든 명령어에 공통이고, T3부터가 LOAD에만 해당하는 실행 과정입니다. T1에서는 메모리 읽기와 PC 증가라는 서로 다른 두 일을 같은 에지에 합니다. 실제 메모리가 더 느리다면 T1과 T4에서 읽기가 끝날 때까지 기다려야 하므로, 이 표의 고정된 단계 수는 이 모델의 가정입니다.
memory = {0: ("LOAD", 20), 20: 7}
regs = {"PC": 0, "MAR": None, "MDR": None, "IR": None, "R1": 0}
steps = [
("T0", {"MAR": lambda r: r["PC"]}),
("T1", {"MDR": lambda r: memory[r["MAR"]], "PC": lambda r: r["PC"] + 1}),
("T2", {"IR": lambda r: r["MDR"]}),
("T3", {"MAR": lambda r: r["IR"][1]}),
("T4", {"MDR": lambda r: memory[r["MAR"]]}),
("T5", {"R1": lambda r: r["MDR"]}),
]
for name, transfers in steps:
new = {dest: compute(regs) for dest, compute in transfers.items()}
regs.update(new)
print(name, regs)
r = {"R1": 3, "R2": 8}
r.update({"R1": r["R2"], "R2": r["R1"]})
r1, r2 = 3, 8
r1 = r2
r2 = r1
print(r, (r1, r2))T0 {'PC': 0, 'MAR': 0, 'MDR': None, 'IR': None, 'R1': 0}
T1 {'PC': 1, 'MAR': 0, 'MDR': ('LOAD', 20), 'IR': None, 'R1': 0}
T2 {'PC': 1, 'MAR': 0, 'MDR': ('LOAD', 20), 'IR': ('LOAD', 20), 'R1': 0}
T3 {'PC': 1, 'MAR': 20, 'MDR': ('LOAD', 20), 'IR': ('LOAD', 20), 'R1': 0}
T4 {'PC': 1, 'MAR': 20, 'MDR': 7, 'IR': ('LOAD', 20), 'R1': 0}
T5 {'PC': 1, 'MAR': 20, 'MDR': 7, 'IR': ('LOAD', 20), 'R1': 7}
{'R1': 8, 'R2': 3} (8, 8)steps의 각 단계는 목적지 레지스터마다 값을 계산하는 함수를 담고 있습니다. 반복마다 먼저 모든 목적지의 새 값을 지금의 regs로 계산해 new에 모은 뒤, regs.update(new)로 한꺼번에 바꿉니다. 이것이 같은 에지의 여러 전송이 이전 값으로 함께 계산되는 모습입니다. T1에서 MDR과 PC가 함께 바뀌고, T5가 끝나면 R1에 7이 들어 있습니다. 마지막 줄은 같은 방법으로 두 값을 맞바꾼 결과 (8, 3)과, 차례로 대입한 결과 (8, 8)의 차이입니다.
버스 하나에는 값 하나만
부품 사이를 버스 하나로 잇는 데이터 경로에서는 한 단계에 버스에 올릴 수 있는 값이 하나뿐입니다. R1과 R2가 같은 단계에 서로 다른 값을 버스에 내보내면 두 신호가 부딪혀 어느 값도 제대로 전해지지 않습니다. 반면 버스에 올라간 값 하나를 여러 레지스터가 동시에 받아 저장하는 것은 가능하게 설계할 수 있습니다. 그래서 제어 신호를 정할 때는 어느 레지스터가 버스에 값을 내보내는지와 어느 레지스터가 버스의 값을 받아 쓰는지를 따로 정해야 하고, 내보내는 쪽은 단계마다 하나여야 합니다.
위의 표가 이 조건을 지키는지 보면, T1의 PC + 1은 버스를 쓰지 않는 따로 된 증가 회로가 한다고 보아야 합니다. 그렇지 않다면 메모리에서 읽은 값과 PC + 1이 같은 단계에 버스를 두고 다투게 됩니다. 이처럼 데이터 경로에 어떤 연결이 따로 있느냐에 따라 같은 단계에 할 수 있는 마이크로연산이 달라지고, 그래서 명령어 하나에 필요한 단계 수도 달라집니다.
정리
- 마이크로연산은 레지스터 수준에서 한 주기에 하는 이동과 계산이며, 전송, 산술, 논리, 시프트로 나눌 수 있습니다.
R2 ← R1은 복사라서 R1이 그대로 남고, 조건을 붙인 전송은 조건이 1일 때만 일어납니다.- 같은 에지의 여러 전송은 모두 에지 직전의 값으로 계산되어 함께 저장되므로, 같은 에지에 두 값을 맞바꿀 수 있습니다.
- 논리 오른쪽 시프트는 0을, 산술 오른쪽 시프트는 부호 비트를 채웁니다.
- 버스가 하나인 데이터 경로에서는 한 단계에 값을 내보내는 쪽이 하나여야 하며, 받는 쪽은 여럿일 수 있습니다.
연습 문제
1. 버스 위의 충돌
버스 하나인 데이터 경로에서 R3 ← R1과 R4 ← R2를 같은 단계에 하려고 합니다. R1 = 5, R2 = 9일 때 왜 안 되는지 설명하고, 단계 두 개로 고치세요. R3 ← R1과 R4 ← R1은 같은 단계에 할 수 있나요?
해설 보기
첫 조합은 R1의 5와 R2의 9를 같은 단계에 버스에 내보내야 하므로, 내보내는 쪽이 둘이라 할 수 없습니다. T0에 R1을 내보내 R3에 쓰고, T1에 R2를 내보내 R4에 쓰는 두 단계로 나누면 됩니다. 둘째 조합은 R1 하나만 버스에 5를 내보내고 R3과 R4가 함께 받아 쓰므로, 받는 쪽이 여럿인 것을 허용하는 이 모델에서는 같은 단계에 할 수 있습니다. 충돌은 목적지의 수가 아니라 버스에 올라가는 값의 수로 판단합니다.
2. 두 가지 오른쪽 시프트
4비트 값 1100을 오른쪽으로 한 칸 논리 시프트한 결과와 산술 시프트한 결과를 쓰고, 2의 보수로 읽은 값의 변화를 설명하세요.
해설 보기
논리 시프트는 맨 왼쪽을 0으로 채워 0110이 되고, 산술 시프트는 부호 비트 1을 복제해 1110이 됩니다. 부호 없는 수로 보면 12가 6이 되므로 논리 시프트가 2로 나눈 몫이고, 2의 보수로 보면 −4가 −2가 되므로 산술 시프트가 2로 나눈 몫입니다. 논리 시프트를 2의 보수 값에 쓰면 −4가 +6이 되어 부호가 바뀝니다. 그래서 CPU에는 두 시프트 명령어가 따로 있습니다.
3. STORE의 마이크로연산
본문과 같은 데이터 경로에서 STORE R1, [21], 곧 R1의 값을 주소 21에 쓰는 명령어를 마이크로연산 단계로 나누세요. 메모리 쓰기는 MAR과 MDR에 값이 준비된 단계에서 할 수 있다고 합니다.
해설 보기
T0부터 T2까지는 LOAD와 같은 인출 과정으로 MAR ← PC, MDR ← M[MAR]와 PC ← PC + 1, IR ← MDR입니다. 그다음 T3에 MAR ← IR의 주소 부분으로 21을 MAR에 쓰고, T4에 MDR ← R1로 쓸 값을 MDR에 담으며, T5에 M[MAR] ← MDR로 메모리에 씁니다. 버스가 하나라면 T3과 T4는 IR과 R1이라는 서로 다른 출발지를 쓰므로 한 단계로 합칠 수 없습니다. 주소와 데이터가 다른 통로로 들어가는 데이터 경로라면 두 단계를 합칠 수 있습니다.
마이크로연산을 설계할 때는 단계마다 버스에 값을 내보내는 레지스터와 값을 받는 레지스터를 따로 적어, 내보내는 쪽이 하나인지 확인해 보세요. 다음 글 하드와이어드·마이크로프로그램에서는 명령어와 단계에 따라 이 제어 신호들을 만들어 내는 두 가지 방법을 다룹니다.