CPU 데이터 경로
PC, 명령어 레지스터, 레지스터 파일, ALU, 메모리가 이어진 데이터 경로에서 값이 어떻게 움직이는지 따라가고, 인출·해독·실행·기록 단계와 그 단계를 고르는 제어 신호를 구분합니다. 제어 신호가 하나만 틀려도 결과가 달라지는 이유와 자원이 겹칠 때의 문제를 다룹니다.
R1 = R1 + R2라는 계산이 CPU 안에서 일어나려면, R1과 R2의 값을 꺼내 ALU로 보내고, ALU가 더한 결과를 다시 R1에 써야 합니다. 값이 지나가는 이 길과 그 길 위의 부품들을 데이터 경로(datapath)라 하고, 지금 단계에서 어느 길을 열고 어떤 계산을 할지 고르는 부분을 제어(control)라고 합니다. 이 글에서는 앞 장의 레지스터와 MUX를 이어 만든 데이터 경로에서 명령어 하나가 실행되는 과정을 따라갑니다.
데이터 경로의 부품
프로그램 카운터(PC)는 다음에 실행할 명령어의 주소를 담고, 명령어 레지스터(instruction register, IR)는 메모리에서 가져온 지금의 명령어를 담습니다. 계산에 쓸 값은 일반 레지스터들이 담는데, 여러 레지스터를 묶어 번호로 고르게 만든 것을 레지스터 파일(register file)이라고 합니다. 레지스터 파일은 보통 두 값을 동시에 읽고 한 값을 쓸 수 있어서, R1 + R2의 두 입력을 한 번에 ALU로 보낼 수 있습니다.
메모리와 주고받을 주소와 데이터를 잠시 담는 자리로 MAR(memory address register)과 MDR(memory data register)을 두기도 합니다. 이 이름들은 데이터의 흐름을 설명하기 좋아서 교재에서 자주 쓰지만, 실제 CPU가 꼭 이런 이름의 레지스터 하나씩으로 만들어져 있다는 뜻은 아닙니다. 부품들 사이에는 어느 값을 보낼지 고르는 MUX가 곳곳에 있고, 제어 신호가 각 MUX의 선택과 레지스터의 쓰기 여부, ALU가 할 연산을 정합니다.
두 입력을 읽고 계산한 값을 목적지에 씁니다
ADD의 두 값을 읽는다. 결과를 목적 레지스터에 쓴다. 각 단계의 값과 전환 이유를 비교합니다.
컴퓨터구조 · 읽는 순서
두 입력을 읽고 계산한 값을 목적지에 씁니다
이 그림은 ADD R1,R2의 값 이동만 보여 줍니다. 명령 인출과 제어 신호는 본문에서 따로 추적합니다.
원래 R1=7은 입력으로 읽고 새 R1=12는 계산 뒤 기록합니다. 레지스터를 복사해서 읽어도 R2=5는 유지됩니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 레지스터 읽기부터 R1 쓰기까지 진행합니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 레지스터 읽기 ADD의 두 값을 읽는다. |
|
|
| ALU 덧셈 선택한 연산으로 두 값을 더한다. |
|
|
| R1 쓰기 결과를 목적 레지스터에 쓴다. |
|
|
두 입력을 읽고 계산한 값을 목적지에 씁니다
ADD의 두 값을 읽는다. 결과를 목적 레지스터에 쓴다. 각 단계의 값과 전환 이유를 비교합니다.
컴퓨터구조 · 읽는 순서
두 입력을 읽고 계산한 값을 목적지에 씁니다
이 그림은 ADD R1,R2의 값 이동만 보여 줍니다. 명령 인출과 제어 신호는 본문에서 따로 추적합니다.
원래 R1=7은 입력으로 읽고 새 R1=12는 계산 뒤 기록합니다. 레지스터를 복사해서 읽어도 R2=5는 유지됩니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 레지스터 읽기부터 R1 쓰기까지 진행합니다.
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 레지스터 읽기 ADD의 두 값을 읽는다. |
|
|
| ALU 덧셈 선택한 연산으로 두 값을 더한다. |
|
|
| R1 쓰기 결과를 목적 레지스터에 쓴다. |
|
|
그림은 ADD R1, R2의 값 이동만 보여 줍니다. 원래 R1의 7과 R2의 5를 읽어 ALU가 12를 만들고, 그 결과를 R1에 씁니다. 원래 R1의 값은 입력으로 읽히고 새 값 12는 계산이 끝난 뒤에 쓰이며, 읽기만 한 R2는 5 그대로입니다.
명령어 하나를 단계별로 따라가기
설명을 단순하게 하려고, 명령어 메모리의 한 칸에 명령어 하나가 들어 있고 PC가 1씩 늘어난다고 하겠습니다. 실제 CPU는 명령어가 4바이트라면 PC를 4씩 늘리는 식으로 다릅니다. 처음에 PC = 0, R1 = 0, R2 = 5, 데이터 메모리 M[40] = 7이고, 0번 명령어는 LOAD R1, [40]입니다. 대괄호는 안의 수 40을 값이 아니라 주소로 보고 그 자리의 값을 읽는다는 뜻이며, 이 표기는 주소지정·스택·호출에서 다른 방식들과 함께 정리합니다.
| 단계 | 하는 일 | 바뀌는 상태 |
|---|---|---|
| 인출(fetch) | IR ← 명령어 메모리[PC], PC ← PC + 1 | IR에 LOAD, PC = 1 |
| 해독(decode) | LOAD, 목적 레지스터 R1, 주소 40을 읽어 냄 | 제어가 메모리 읽기를 고름 |
| 실행(execute) | MDR ← 데이터 메모리[40] | MDR = 7 |
| 기록(write-back) | R1 ← MDR | R1 = 7 |
이어서 ADD R1, R2는 R1의 7과 R2의 5를 ALU로 보내 R1에 12를 쓰고, STORE R1, [41]은 R1의 12를 데이터 메모리의 주소 41에 씁니다. LOAD와 ADD의 결과는 레지스터에, STORE의 결과는 메모리에 쓰인다는 점이 다릅니다. 세 명령어를 실행하며 단계마다 켜지는 제어 신호와 바뀌는 상태를 출력해 보겠습니다.
regs = {"R1": 0, "R2": 5}
memory = {40: 7}
program = [("LOAD", "R1", 40), ("ADD", "R1", "R2"), ("STORE", "R1", 41)]
pc = 0
while pc < len(program):
ir = program[pc]
pc += 1
op, reg, operand = ir
if op == "LOAD":
mdr = memory[operand]
regs[reg] = mdr
signals = "MemRead=1 RegWrite=1"
elif op == "ADD":
regs[reg] = regs[reg] + regs[operand]
signals = "ALUOp=ADD RegWrite=1"
elif op == "STORE":
memory[operand] = regs[reg]
signals = "MemWrite=1"
print(f"PC={pc} {op:5} {signals:20} R1={regs['R1']} M[41]={memory.get(41)}")PC=1 LOAD MemRead=1 RegWrite=1 R1=7 M[41]=None
PC=2 ADD ALUOp=ADD RegWrite=1 R1=12 M[41]=None
PC=3 STORE MemWrite=1 R1=12 M[41]=12반복 한 번이 명령어 하나이고, 그 안에서 ir = program[pc]와 pc += 1이 인출, op, reg, operand = ir가 해독, 그 뒤의 분기가 실행과 기록입니다. signals는 그 명령어에서 켜지는 제어 신호입니다. LOAD는 메모리 읽기(MemRead)와 레지스터 쓰기(RegWrite)를, ADD는 ALU의 덧셈(ALUOp)과 레지스터 쓰기를, STORE는 메모리 쓰기(MemWrite)만 켭니다. memory.get(41)은 주소 41에 아직 값이 없으면 None을 돌려주므로, STORE 전까지 메모리가 바뀌지 않았다는 것이 출력에 드러납니다.
인출, 해독, 실행, 기록은 명령어 하나가 거치는 일을 기능별로 나눈 명령어 사이클(instruction cycle)입니다. 단계마다 꼭 클록 한 주기가 걸리는 것은 아니며, 한 주기에 모두 끝내는 단일 사이클 설계, 단계마다 한 주기씩 쓰는 다중 사이클 설계, 여러 명령어의 단계를 겹치는 파이프라인 설계는 같은 일을 서로 다른 시간 배치로 해냅니다.
데이터와 제어 신호를 구분한다
R1 = 12는 데이터의 상태이고, RegWrite = 1은 레지스터 쓰기를 허락하는 제어 신호입니다. ALU가 덧셈 결과를 만들어도 RegWrite가 0이면 그 결과는 어디에도 저장되지 않고 다음 주기에 사라집니다. 반대로 RegWrite가 켜졌더라도 쓸 값을 고르는 MUX의 선택이 틀리면, 예를 들어 ALU의 결과 대신 MDR의 값을 고르면, 엉뚱한 값이 레지스터에 쓰입니다. 그래서 명령어가 제대로 실행되려면 데이터 경로의 연결과 제어 신호가 모두 맞아야 합니다.
여러 일을 같은 주기에 하려면 쓰는 부품이 겹치지 않아야 합니다. 예를 들어 메모리에 출입구가 하나뿐이라면, 다음 명령어를 인출하는 일과 LOAD가 데이터를 읽는 일을 같은 주기에 할 수 없습니다. 이 문제는 명령어용과 데이터용 메모리 통로를 나누거나, 둘 가운데 하나를 다음 주기로 미루어 해결합니다. 같은 문제는 파이프라인과 위험에서 다시 만납니다.
정리
- 데이터 경로는 PC, IR, 레지스터 파일, ALU, 메모리와 그 사이의 MUX로 이루어진 값의 길이고, 제어는 단계마다 어느 길을 열고 무엇을 계산할지 고릅니다.
- 명령어 하나는 인출, 해독, 실행, 기록의 단계를 거치며, 인출할 때 PC가 다음 명령어로 넘어갑니다.
- LOAD는 메모리의 값을 레지스터에, STORE는 레지스터의 값을 메모리에 쓰며, ADD의 결과는 목적 레지스터에 쓰입니다.
- 데이터가 만들어져도 쓰기 제어 신호가 꺼져 있으면 저장되지 않고, MUX의 선택이 틀리면 엉뚱한 값이 저장됩니다.
- 같은 주기에 같은 부품을 두 가지 일에 쓸 수는 없으므로, 부품을 나누거나 일을 다른 주기로 미룹니다.
연습 문제
1. 마지막 결과만 보지 않기
처음에 R1 = 2, R2 = 6, M[10] = 9입니다. LOAD R1, [10], ADD R1, R2, STORE R1, [11]을 차례로 실행할 때 각 명령어 뒤의 R1, R2, M[10], M[11]을 쓰세요. 마지막 STORE에서 제어가 실수로 MemWrite를 끄고 RegWrite를 켰다면 M[11]은 어떻게 되나요?
해설 보기
R1은 LOAD 뒤 9, ADD 뒤 15, STORE 뒤에도 15입니다. R2는 계속 6이고, M[10]은 읽기만 했으므로 계속 9이며, M[11]은 STORE 뒤에 15가 됩니다. 마지막 STORE에서 MemWrite가 꺼지면 메모리 쓰기가 일어나지 않으므로 M[11]은 처음 값 그대로이고 15가 되지 않습니다. 잘못 켜진 RegWrite는 MUX가 무엇을 고르고 있었느냐에 따라 R1에 엉뚱한 값을 쓸 수도 있어서, 제어 신호 하나가 틀리면 여러 상태가 함께 틀어질 수 있습니다.
2. ADD에 필요한 제어 신호
ADD R3, R1, R2, 곧 R3 ← R1 + R2를 실행할 때 RegWrite, MemRead, MemWrite, ALU의 연산은 각각 어떻게 정해야 하나요?
해설 보기
결과를 레지스터 R3에 써야 하므로 RegWrite = 1이고, 쓸 레지스터 번호로 3을 골라야 합니다. 메모리는 쓰지 않으므로 MemRead = 0, MemWrite = 0입니다. ALU의 연산은 덧셈으로 정하고, 레지스터에 쓸 값을 고르는 MUX는 MDR이 아니라 ALU의 결과를 고르게 합니다. MemWrite를 실수로 켜면 메모리의 어떤 주소가 덮어써질 수 있으므로, 쓰지 않는 신호를 확실히 0으로 두는 것도 중요합니다.
3. 출입구가 하나인 메모리
명령어와 데이터가 출입구가 하나뿐인 같은 메모리에 있습니다. LOAD가 데이터를 읽는 주기에 다음 명령어도 함께 인출할 수 있나요?
해설 보기
할 수 없습니다. 출입구가 하나이므로 한 주기에 한 번만 메모리에 접근할 수 있어서, 데이터 읽기와 명령어 인출이 같은 주기에 겹치면 하나는 다음 주기로 미뤄야 합니다. 이런 충돌을 피하려고 실제 CPU는 명령어용과 데이터용 캐시를 따로 두어 두 접근을 동시에 처리합니다. 이것이 프로그램이 하드웨어를 움직이는 과정에서 본 하버드 구조의 생각을 일부 빌려 쓰는 이유입니다.
데이터 경로를 추적할 때는 단계마다 어느 값이 어디에서 어디로 갔고 어떤 제어 신호가 켜졌는지를 함께 적어 보세요. 다음 글 명령어 집합과 프로그램 실행에서는 명령어의 종류와 형식을 정하고, 분기가 있는 작은 CPU를 실행해 봅니다.