하드와이어드·마이크로프로그램
명령어와 단계에 따라 제어 신호를 만드는 두 방법인 하드와이어드 제어와 마이크로프로그램 제어를 비교하고, 제어 기억장치의 주소인 마이크로 PC가 조건에 따라 다음 주소를 고르는 과정을 LOAD 루틴으로 따라갑니다. 수평형과 수직형 마이크로명령, 두 구현이 같은 결과를 내는지 확인하는 방법을 다룹니다.
마이크로연산과 제어 신호에서 LOAD를 여섯 단계로 나누었습니다. 그렇다면 지금 어느 명령어의 몇 번째 단계인지에 맞추어 알맞은 제어 신호를 켜는 일은 누가 할까요? 이 일을 하는 부분이 제어 장치(control unit)이고, 제어 장치를 만드는 방법은 크게 두 가지입니다. 이 글에서는 제어 신호를 회로로 직접 만드는 하드와이어드 제어와, 제어 신호를 작은 메모리에 적어 두고 읽어 쓰는 마이크로프로그램 제어를 비교합니다.
하드와이어드 제어
하드와이어드 제어(hardwired control)는 명령어의 연산 코드, 지금의 단계를 담은 상태 레지스터, 상태 플래그 같은 조건을 입력으로 받는 조합 회로로 제어 신호를 만듭니다. 예를 들어 "지금 단계가 실행이고 연산 코드가 ADD이면 ALU를 덧셈으로 하고 레지스터 쓰기를 켠다"는 규칙이 그대로 게이트가 됩니다. 다음 단계도 유한 상태 기계처럼 지금의 상태와 조건으로 계산합니다.
신호가 게이트 몇 단만 거치면 나오므로 빠르지만, 명령어를 더하거나 동작을 고치려면 회로를 다시 설계해야 합니다. 그래서 명령어가 단순하고 고른 RISC CPU에서 많이 쓰입니다. 다만 하드와이어드 제어라서 반드시 단순한 ISA만 만들 수 있는 것은 아니며, 실제 설계에서는 성능, 크기, 설계 기간을 함께 따져 고릅니다.
마이크로프로그램 제어
마이크로프로그램 제어(microprogrammed control)는 단계마다 켤 제어 신호를 한 줄의 마이크로명령(microinstruction)으로 적어 제어 기억장치(control store)라는 작은 메모리에 넣어 둡니다. 제어 장치는 마이크로 PC(µPC)가 가리키는 마이크로명령을 읽어 그 안의 신호를 켜고, 다음에 읽을 마이크로명령의 주소를 고릅니다. 다음 주소는 순서대로 넘어가거나, 조건에 따라 분기하거나, 새 명령어의 연산 코드에 맞는 루틴의 처음으로 갑니다. 명령어를 고치거나 더할 때 회로 대신 제어 기억장치의 내용을 바꾸면 되므로, 명령어가 많고 복잡한 CISC CPU에서 많이 쓰였습니다.
프로그램의 PC가 가리키는 것은 ISA의 명령어이고, µPC가 가리키는 것은 그 명령어를 실행하는 마이크로명령입니다. 두 PC는 층이 다르며, LOAD 명령어 하나가 마이크로명령 여러 줄로 실행됩니다. 마이크로프로그램이라는 이름 때문에 사용자가 마음대로 고칠 수 있는 프로그램이라고 생각하기 쉽지만, 대부분은 CPU를 만들 때 정해 두거나 제조사만 바꿀 수 있습니다.
마이크로명령이 데이터 경로의 제어를 선택합니다
µPC 20의 제어어로 MAR를 준비한다. LOAD 결과를 기록하고 fetch로 복귀한다. 각 단계의 값과 전환 이유를 비교합니다.
컴퓨터구조 · 읽는 순서
마이크로명령이 데이터 경로의 제어를 선택합니다
제어 주소→제어 정보→제어 신호→데이터 경로 순서로 따라갑니다.
조건·명령에 따른 다음 제어 주소 선택은 별도 논리
다음 제어 주소는조건·명령으로 선택
ISA 명령 PC와 마이크로 PC는 서로 다른 계층의 위치입니다. 그림은 제어 정보의 흐름이며 모든 내부 연산이 한 사이클이라는 뜻은 아닙니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 주소 설정부터 레지스터 기록까지 진행합니다.
장면별 값과 변화
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 주소 설정 µPC 20의 제어어로 MAR를 준비한다. |
|
|
| 읽기 요청 µPC 21에서 메모리에 한 번 요청한다. |
|
|
| 첫 대기 아직 값이 준비되지 않았다. |
|
|
| 둘째 대기 제어어를 유지해 준비를 기다린다. |
|
|
| 준비 완료 값이 준비된 방문에서 래치한다. |
|
|
| 레지스터 기록 LOAD 결과를 기록하고 fetch로 복귀한다. |
|
|
마이크로명령이 데이터 경로의 제어를 선택합니다
µPC 20의 제어어로 MAR를 준비한다. LOAD 결과를 기록하고 fetch로 복귀한다. 각 단계의 값과 전환 이유를 비교합니다.
컴퓨터구조 · 읽는 순서
마이크로명령이 데이터 경로의 제어를 선택합니다
제어 주소→제어 정보→제어 신호→데이터 경로 순서로 따라갑니다.
조건·명령에 따른 다음 제어 주소 선택은 별도 논리
다음 제어 주소는조건·명령으로 선택
ISA 명령 PC와 마이크로 PC는 서로 다른 계층의 위치입니다. 그림은 제어 정보의 흐름이며 모든 내부 연산이 한 사이클이라는 뜻은 아닙니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 주소 설정부터 레지스터 기록까지 진행합니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 주소 설정 µPC 20의 제어어로 MAR를 준비한다. |
|
|
| 읽기 요청 µPC 21에서 메모리에 한 번 요청한다. |
|
|
| 첫 대기 아직 값이 준비되지 않았다. |
|
|
| 둘째 대기 제어어를 유지해 준비를 기다린다. |
|
|
| 준비 완료 값이 준비된 방문에서 래치한다. |
|
|
| 레지스터 기록 LOAD 결과를 기록하고 fetch로 복귀한다. |
|
|
마이크로명령의 형식에도 두 방향이 있습니다. 수평형(horizontal) 마이크로명령은 제어 신호마다 비트를 하나씩 두어 여러 신호를 한꺼번에 켤 수 있지만 폭이 넓어집니다. 수직형(vertical) 마이크로명령은 동시에 켜지지 않는 신호들을 번호로 묶어 폭을 줄이는 대신, 그 번호를 다시 신호로 바꾸는 디코더를 거쳐야 합니다. 실제 형식은 두 방향을 섞어 쓰는 경우가 많습니다.
더 깊이: 운영체제가 올리는 마이크로코드 갱신
오늘날의 x86 CPU에는 제조사가 내놓은 마이크로코드 갱신을 운영체제가 올리는 경로가 있습니다. Linux의 x86 마이크로코드 로더 문서에 따르면 커널은 부팅할 때 initrd 안의 kernel/x86/microcode/GenuineIntel.bin(Intel)이나 AuthenticAMD.bin(AMD)에서 마이크로코드를 읽어 각 코어에 적용하는 이른 적재(early loading)를 지원합니다. 절전 상태에서 깨어날 때 다시 적용할 수 있도록 맞는 패치를 메모리에 보관해 두기도 합니다. 문서는 OEM의 지원이 끝난 플랫폼의 마이크로코드를 갱신하는 것을 이 기능의 쓰임새로 듭니다.
시스템이 실행 중일 때 /sys/devices/system/cpu/microcode/reload에 써서 갱신하는 늦은 적재(late loading)도 있지만, Linux 5.19부터는 기본으로 켜져 있지 않습니다. 문서는 그 이유로 세 가지를 듭니다. 마이크로코드 엔진을 SMT 형제 스레드가 함께 쓰므로 한 스레드를 갱신하는 동안 다른 스레드가 영향을 받을 수 있고, 머신 체크나 NMI 같은 사건은 갱신하는 동안 멈출 수 없으며, 어떤 갱신이 늦은 적재에 알맞은지 선언할 방법이 없다는 것입니다.
µPC를 따라 LOAD 루틴 실행하기
명령어 인출과 해독이 끝나 LOAD R1, [10]의 실행 루틴에 들어선 상태에서 시작하겠습니다. 처음 상태는 µPC = 20, 명령어의 주소 부분 = 10, M[10] = 7, MDR = 0, R1 = 0이고, 프로그램의 PC는 이미 다음 명령어인 101을 가리키며 이 루틴에서는 바뀌지 않습니다. 주소 0은 다음 명령어를 인출하는 루틴의 시작입니다. 메모리 읽기는 요청을 보낸 뒤 나중에 끝나며, READY 신호가 1이 되면 읽은 값이 준비되었다는 뜻입니다.
| 제어 기억장치 주소 | 마이크로명령이 하는 일 | 다음 µPC |
|---|---|---|
| 20 | MAR ← 명령어의 주소 부분 | 21 |
| 21 | MAR이 가리키는 곳에 읽기 요청을 한 번 보냄 | 22 |
| 22 | READY가 0이면 그대로 있고, 1이면 MDR ← 읽은 값 | READY가 0이면 22, 1이면 23 |
| 23 | R1 ← MDR | 0 |
22번 마이크로명령의 내용은 그대로이고 바뀌는 것은 조건 입력 READY입니다. 제어 장치는 마이크로명령과 READY를 함께 보고 MDR에 쓸지와 다음 주소를 정합니다. 읽기 요청은 21번에서 한 번만 보냈으므로, 22번을 되풀이하는 동안 새 요청을 보내지 않습니다. 실제 메모리 인터페이스의 요청과 완료 규칙은 설계마다 다르므로, 이 표를 모든 CPU의 타이밍으로 넓히면 안 됩니다.
CONTROL_STORE = {
20: {"action": "MAR <- IR.addr", "next": 21},
21: {"action": "메모리 읽기 요청", "next": 22},
22: {"action": "READY 기다리기", "next": (22, 23)},
23: {"action": "R1 <- MDR", "next": 0},
}
def run_load(ready_pattern, wait_for_ready=True):
regs = {"MAR": 0, "MDR": 0, "R1": 0}
memory, ir_addr = {10: 7}, 10
ready = iter(ready_pattern)
upc, trail = 20, []
while upc != 0:
trail.append(upc)
word = CONTROL_STORE[upc]
action, nxt = word["action"], word["next"]
if action == "MAR <- IR.addr":
regs["MAR"] = ir_addr
elif action == "READY 기다리기":
is_ready = next(ready)
if is_ready:
regs["MDR"] = memory[regs["MAR"]]
nxt = nxt[1] if is_ready or not wait_for_ready else nxt[0]
elif action == "R1 <- MDR":
regs["R1"] = regs["MDR"]
upc = nxt
return trail, regs
print(run_load([0, 0, 1]))
print(run_load([0, 1]))
print(run_load([0, 1], wait_for_ready=False))([20, 21, 22, 22, 22, 23], {'MAR': 10, 'MDR': 7, 'R1': 7})
([20, 21, 22, 22, 23], {'MAR': 10, 'MDR': 7, 'R1': 7})
([20, 21, 22, 23], {'MAR': 10, 'MDR': 0, 'R1': 0})CONTROL_STORE가 제어 기억장치이고, 각 항목의 action이 마이크로명령이 켜는 동작, next가 다음 주소입니다. 22번의 next는 READY가 0일 때와 1일 때의 두 주소를 담고 있습니다. run_load는 µPC가 가리키는 마이크로명령을 읽어 동작을 하고 다음 주소로 넘어가는 일을 0에 닿을 때까지 되풀이하며, ready_pattern은 22번에 들를 때마다 READY가 몇이었는지를 차례로 정합니다.
첫 줄은 READY가 두 번 0이었다가 1이 된 경우로, µPC가 20, 21, 22, 22, 22, 23을 지나 R1에 7을 씁니다. 마이크로명령은 여섯 번 실행되었지만 프로그램의 PC는 101 그대로이므로, 두 PC가 서로 다른 층의 일을 한다는 것을 볼 수 있습니다. 둘째 줄은 READY가 한 번만 0이었던 경우입니다. 셋째 줄은 READY가 0인데도 23으로 넘어가도록 잘못 만든 제어 기억장치로, 읽은 값이 들어오기 전의 MDR 0을 R1에 써 버렸습니다. 이때 R1이 0인 것은 메모리가 0을 돌려주었기 때문이 아니라 제어가 기다리지 않았기 때문입니다.
두 구현이 같은 결과를 내는지 확인하기
하드와이어드 제어와 마이크로프로그램 제어는 내부의 단계 수가 달라도, 같은 ISA라면 같은 명령어에 같은 결과를 내야 합니다. 그래서 두 구현을 검사할 때는 처음 레지스터, 메모리, 명령어를 같게 두고 실행이 끝난 뒤의 상태를 비교합니다. 내부에서 몇 단계를 거쳤는지는 같을 필요가 없습니다.
이때 바뀌어야 하는 상태뿐 아니라 바뀌면 안 되는 상태도 함께 확인해야 합니다. 예를 들어 ADD의 마이크로명령에 실수로 메모리 쓰기 신호가 켜져 있다면, 목적 레지스터의 결과는 맞아도 메모리의 어딘가가 망가집니다. 결과 레지스터만 보는 검사는 이런 오류를 놓칩니다.
정리
- 하드와이어드 제어는 연산 코드, 단계, 조건을 입력으로 하는 조합 회로로 제어 신호를 만들어 빠르지만, 고치려면 회로를 다시 설계해야 합니다.
- 마이크로프로그램 제어는 단계마다 켤 신호를 마이크로명령으로 제어 기억장치에 적어 두고, µPC로 읽어 쓰며 다음 주소를 조건에 따라 고릅니다.
- 프로그램의 PC는 ISA의 명령어를, µPC는 그 명령어를 실행하는 마이크로명령을 가리키며, 명령어 하나가 마이크로명령 여러 줄로 실행됩니다.
- 수평형 마이크로명령은 신호마다 비트를 두어 폭이 넓고, 수직형은 신호를 번호로 묶어 폭을 줄이는 대신 디코더를 거칩니다.
- 두 구현이 같은지 검사할 때는 실행 뒤의 상태를 비교하되, 바뀌면 안 되는 상태도 함께 확인합니다.
연습 문제
1. 잘못된 마이크로명령
LOAD 루틴의 마지막 마이크로명령은 R1 ← MDR이고, 그 직전 상태는 M[10] = 7, MDR = 7, R1 = 0, MAR = 10입니다. 정상이라면 실행 뒤 R1과 M[10]은 얼마인가요? 이 마이크로명령이 실수로 M[MAR] ← R1로 적혀 있다면 어떤 오류가 생기고, 어떻게 검사하면 찾을 수 있나요?
해설 보기
정상이라면 R1 = 7이고 M[10]은 읽기만 했으므로 7 그대로입니다. 잘못 적힌 마이크로명령은 R1의 0을 주소 10에 쓰므로 M[10] = 0이 되고, R1은 0으로 남습니다. 결과 레지스터 R1만 확인해도 7이 아니라서 오류가 보이지만, 바뀌면 안 되는 M[10]이 바뀌었다는 것까지 확인해야 오류의 정체를 알 수 있습니다. 마이크로명령을 읽을 때는 줄마다 값을 내보내는 곳, 받아 쓰는 곳, 다음 주소를 표시해 두면 이런 실수를 찾기 쉽습니다.
2. 기다리지 않아도 되는 경우
메모리가 빨라서 22번에 처음 들렀을 때 READY가 이미 1이었습니다. µPC가 지나는 주소의 순서와 실행 뒤의 R1을 쓰세요. 이 경우에는 READY를 보지 않고 23으로 넘어가는 잘못된 제어 기억장치도 맞는 결과를 내나요?
해설 보기
µPC는 20, 21, 22, 23을 지나고, 22에서 READY가 1이라 MDR에 7을 쓴 뒤 23에서 R1 ← MDR로 R1 = 7이 됩니다. 본문의 run_load를 run_load([1])로 불러도 같은 결과가 나옵니다. READY를 보지 않는 잘못된 제어 기억장치도 이 경우에는 우연히 같은 결과를 내므로, 메모리가 빠른 경우만 검사하면 오류를 발견하지 못합니다. 그래서 검사에는 READY가 늦게 1이 되는 경우처럼 기다려야 하는 입력을 반드시 넣어야 합니다.
3. 마이크로명령의 폭
제어 신호가 모두 32개이고, 그 가운데 ALU 연산을 고르는 신호 8개는 언제나 정확히 하나만 켜집니다. 수평형 마이크로명령은 몇 비트인가요? ALU 신호 8개만 번호로 묶으면 몇 비트가 되고, 그 대가는 무엇인가요?
해설 보기
수평형은 신호마다 비트를 하나씩 두므로 32비트입니다. 정확히 하나만 켜지는 신호 8개는 0부터 7까지의 번호로 나타낼 수 있어 3비트면 되므로, 32 − 8 + 3 = 27비트가 됩니다. 대신 이 3비트를 다시 8개의 신호로 바꾸는 3:8 디코더가 필요하고, 디코더를 지나는 시간만큼 신호가 늦게 나옵니다. 묶을 신호가 많을수록 마이크로명령은 짧아지지만, 동시에 켤 수 있는 신호의 조합이 줄어든다는 점도 함께 따져야 합니다.
마이크로프로그램을 추적할 때는 줄마다 µPC, 켜지는 신호, 조건 입력, 다음 µPC를 표로 적고, 프로그램의 PC와 섞지 않도록 따로 적어 보세요. 다음 장의 병렬처리와 Flynn 분류에서는 이런 처리 장치를 여러 개 쓸 때 명령어와 데이터의 흐름이 어떻게 달라지는지 다룹니다.