이것이 점프 투 공작소

NandToTetris-Hardware simulator / 컴퓨터 아키텍쳐 (밑바닥부터 만드는 컴퓨팅 시스템) 본문

NandToTetris

NandToTetris-Hardware simulator / 컴퓨터 아키텍쳐 (밑바닥부터 만드는 컴퓨팅 시스템)

겅겅겅 2025. 2. 13. 21:34

폰 노이만 구조

튜링기계는 간단한 추장석 컴퓨터 모델로, 주로 이론 컴퓨터 과학에서 계산의 논리적 기초를 분석하는 데 활용됩니다.

반면 폰 노이만 기계는 모든 현태 컴퓨터 플랫폼을 구성하는 실제 모델로서 폰 노이만 구조는 메모리 장치와 통신하고, 입력 장치에서 데이터를 받고, 출력 장치로 데이터를 내보내는 중앙 처리 장치(CPU)를 바탕으로 합니다.

컴퓨터가 조작하는 데이터 외에 컴퓨터가 수행할 작업을 지시하는 명령어메모리에 저장된다는 개념입니다.

메모리

메모리는 물리적관점과 논리적관점에서 바라 볼 수 있습니다.

 

물리적으로는 주소를 지정 할 수 있는 고정된 크기의 레지스터들을 선형적으로 배열한 것 이며, 레지스터들은 각각의 값을 가집니다.

논리적으로는 데이터 저장과 명령어 저장이라는 두가지 용도로 사용됩니다.

명령어와 데이터 모두 동일한 방식, 비트 열로 표현됩니다.

 

데이터가 저장되는 전용 메모리 영역은 데이터 메모리 (data memory), 명령어가 저장되는 전용 메모리 영역은 명령어 메모리(instruction memory)라고 합니다.

폰 노이만 구조를 따르는 모델중에서 데이터 메모리와 명령어 메모리가 필요에 따라 물리적으로 동일한 주소공간에서 동적으로 할당 및 관리되는 모델도 존재합니다.

또 다른 모델은 두 메모리가 개별 주소공간을 가집니다. 

두 모델 모두 장단점이 존재합니다.

 

메모리에서 특정 메모리 레지스터에 접근하려면 레지스터의 주소가 필요합니다.

이 작업을 주소지정 (addressing)이라고 합니다.

데이터 메모리

변수, 배열, 객체와 같은 개념은 모두 2진 숫자열로 변환되어 데이터 메모리에 저장됩니다.

주소지정을 통해 데이터들의 개별적인 읽기, 쓰기작업을 할 수 있습니다.

명령어 메모리

고수준 프로그램은 컴퓨터에서 실행되기 전에 먼저 대상 컴퓨터의 기계어로 변역되어야 합니다.
고수준 명령문들은 항상 하나 이상의 저수준 명령어로 번역 되어서 2진, 또는 실행 가능한 버전 프로그램이라는 파일에 2진 값으로 기록됩니다.

 

프로그램을 실행하려면 먼저 대용량 저장 장치에서 2진 버전 프로그램을 불러온 후
그 명령어를 컴퓨터의 명령어 메모리에 직렬화해야 합니다.

 

순수한 컴퓨터 아키텍처 관점에서 프로그램을 컴퓨터 메모리에
어떻게 불러오는지는 외부의 문제로 취급합니다.

중앙 처리 장치 : CPU (Central Processing Unit)

CPU는 현재 실행 중인 프로그램의 명령어를 실행하는 일을 합니다.
각 명령어는 CPU에 수행할 계산, 접근할 레지스터, 다음에 불러와서 실행할 명령어를 알려줍니다.
CPU는 산술 논리 장치, 레지스터 집합, 제어 장치를 활용해서 이 작업들을 실행합니다.

ALU(산술 논리 장치)

ALU 칩은 컴퓨터에서 지원하는 모든 저수준 산술 및 논리 연산을 수행하는 장치입니다.
일반적인 ALU는 주어진 두 값을 더하거나, 비트 단위 And를 계산하거나, 두 값이 동일한지 비교하는 일 등을 수행하며,
설계에 따라 ALU가 지원하는 함수가 정해집니다.

레지스터

CPU는 연산 도중에 중간 값을 임시로 저장해야 하는 경우가 많습니다.
이론적으로는 이 값들을 메모리 레지스터에 저장할 수 있지만, CPU와 RAM은 별개의 칩이기에 때문에 신호가 이동하려면 거리가 멀다는 단점이 있습니다. 이때 발생하는 신호 지연을 기아상태(stavation)라고 합니다.

 

기아 상태를 방지하고 성능을 향상시키기 위해 일반적으로 CPU에는 프로세서에서 곧바로 접근 가능한 메모리 역할을 하는
고속 레지스터들을 적은 용량으로 탑재합니다. 이 레지스터들은 다양한 용도로 활용됩니다. 

  • 데이터 레지스터 : CPU의 단기 기억 메모리로 CPU계산의 중간값을 임시로 저장할 떄 사용됩니다.
  • 주소 지정 레지스터 (Addressing Register) : CPU가 메모리접근을 위해 사용하는 레지스터입니다. 명령어에 주소가 포함되지 않는 경우 사용합니다.
  • 프로그램 카운터 : 다음에 불러와서 실행해야하는 명령어의 주소를 저장하는 레지스터입니다. 
  • 명령어 레지스터 : 현재 명령어를 저장합니다.

일반 컴퓨터에서는 레지스터가 많이 존재하지만 우리가 만들 HACK에는 어드레스 레지스터, 데이터 레지스터 그리고 프로그램 카운터 3개 입니다.

제어

메모리부터 프로그랭 명령을 순차적으로 꺼내 해독하고, 해석에 따라서 명령어 실행에 필요한 제어 신호를 기억장치, 연산장치, 입출력 장치 등으로 보내는 장치입니다. (프로그램 카운터(PC), 명령 해독기, 부호기, 명령 레지스터 등으로 구성됩니다.)

 

컴퓨터 명령어는 미리 정의되고 구조화된 마이크로코드의 집합으로,

마이크로코드는 여러 장치에 해야 할 일을 알려 주는 1개 이상의 비트열입니다.

따라서 명령어는 실행되기 전에 마이크로코드로 디코딩되어야 합니다.


그 후 각 마이크로 코드는 CPU 내에 지정된 하드웨어 장치(ALU, 레지스터, 메모리)로
전달되어, 전체 명령어가 실행되기 위해 장치가 수행해야 할 동작들을 알려줍니다.

인출-실행 (fetch-execute cycle)

프로그램이 실행되는 각 주기마다 CPU는 명령어 메모리에서 2진 기계 명령어를 인출하고, 디코딩하고, 실행합니다.

 

CPU가 실행할 명령어는 메모리에 존재하고, CPU는 Program Counter(PC)를 통해서 다음번에 실행할 명령어를 알 수 있습니다.

실행할 명령어는 주소 버스를 통해 주소 지정 레지스터 (addressing register)에 copy됩니다. (fetch)

이후 명령어 레지스터가 해당 명령어를 읽고 디코딩하여 ALU, 데이터 레지스터 등을 이용하며 메모리와 상호작용 하며 적절하게 명령을 처리합니다. (execution)

 

하지만 fetch-execution 사이클에는 충돌 문제가 존재합니다.

fetch와 execution이 동시에 일어날 때, fetch 단계에서 pc의 값을 업데이트하는 동안, execution 단계에서 ALU가 연산을 진행하는 중 동일하게 pc에 값에 접근해야할 때 충돌이 일어납니다.

 

예를들어 execution 단계에서 조건부 분기처리나 jump와 같은 명령어가 있있다면 execution이 끝날 때

pc값이 업데이트 되어야 합니다. 

 

이 때 멀티플렉서(Mux)를 통해 충돌을 방지합니다.

Mux를 통해 선택된 명령어는 IR로 이동하여 실행됩니다. (선택되지 않았던 명령어도 IR로 같이 가서 다음 주기 때 실행됩니다.)

입력과 출력 (메모리 매핑 I/O)

수많은 I/O장치를 컴퓨터와 통신할 수 있도록 컴퓨터는 수많은 장치들을 동일하게 처리하는 기법이 필요합니다.

이를 메모리 매핑 I/O라 합니다.

 

I/O장치와 컴퓨터가 연결되면 빈 메모리에 해당 장치가 사용할 공간이 할당되며, 일종의 '메모리 맵' 역할을 하는 전용 메모리 영역을 할당합니다. 

이러한 작업은 보통 I/O장치의 installer 프로그램과  디바이스 드라이버(device driver)가 담당합니다.

 

입력장치에 변동이 존재할 때 RAM에 해당 내용을 기록하고,

CPU는 RAM에서 해당 데이터를 받아와 메모리 맵(RAM)에 저장하거나 연산하는 등 상황에 맞게 처리합니다.

I/O 장치는 RAM에서 해당 데이터를 받아와 화면에 출력하는 등 적절하게 동작합니다.

 

이와 같은 방식으로 I/O장치의 종류와 무관하게 사용이 가능합니다.

핵 하드웨어 플랫폼

이제 핵 하드웨어 플랫폼에 대해 정리하려 합니다.

16bit 폰 노이만 기계로 CPU, 명령어메모리 및 데이터메모리 그리고 두개의 메모리 매핑 I/O 장치(외부장치)인 스크린과 키보드로 구성됩니다.

추가로 핵 플랫폼에서 명령어 메모리는 프로그램이 미리 기록된 ROM 칩으로 물리적으로 구현됩니다.

CPU

핵 기계어로 작성된 명령들을 실행하도록 설계되었습니다.

 

ALU와 데이터 레지스터(D), 주소 레지스터(A), 프로그램 카운터(PC)로 구성되며,

CPU는 실행을 위한 명령어를 인출해 오는 명령어 메모리(RAM)와, 데이터 값을 읽거나 쓰는 데이터 메모리(ROM)에 연결됩니다.

D는 데이터를 저장하고, A는 데이터, RAM/ROM 주소 등을 저장합니다.

rom은 pc값에 따라 계속 해서 명령어를 출력합니다. 여기서 출력되는 명령어가 CPU의 현재명령어가 됩니다.

 

A명령어의 경우 16bit명령어는 A레지스터에 그대로 로드되는 2진값(0--- ---- ---- ---- )으로 취급되며. C명령어는 CPU내의 다양한 칩들이 수행되는 여러 마이크로 연산을 가리키는 제어비트(1xxa cccc ccdd djjj)로 처리됩니다.

추가로 C 명령어의 목적지가 M이면 writeM은 쓰기 명령을 위해 1이되고, 그렇지 않으면 0이되고, ALU의 결과는 outM으로 나갑니다.

M이 아니라면 writeM은 0으로 설정되고, outM은 아무 값이나 될 수 잇습니다.

reset이 1이면 pc를 0으로 설정합니다.

 

fetch-execute cycle 을 통해 CPU가 하는 전반적인 일들을 설명 할 수 있습니다..

 

execute : pc를 통해 현재 명령어가 여러 칩에 전달됩니다.

주소명령어라면 A 레지스터에 값이 전달되고, 계산 명령어라면 ALU와 레지스터가 명령을 수행합니다.

 

fetch : ALU의 출력과 jump bit을 비교해 조건이 틀리면 PC++, 조건이 맞으면 PC = A (A로 jump) 를 수행합니다.

다음 CLK때는 PC가 가리키는 명령어가 ROM의 출력이 된다.

 

 

데이터 메모리

이 칩은 기본적으로 세 개의 16비트 칩 (RAM16K, 스크린, 키보드) 부품으로 되어있습니다.

RAM = RAM16K + 스크린(RAM8K) + 키보드(레지스터, 읽기전용)

 

- RAM16K(16K 레지스터 RAM 칩, 범용 데이터 저장소)

- Screen(내장형 8K RAM 칩, 스크린 메모리 맵, 0x4000-0x5FFF)

- Keyboard(내장형 레지스터 칩, 읽기전용, 키보드 메모리 맵, 0x6000)

RAM16K

address에 주소를 통해 out으로 데이터가 출력됩니다.

in에 값(value), load Bit 1을 설정하면 해당 주소에 전달한 값이 저장됩니다.

데이터메모리는 기본적인 범용 데이터 저장공간, 메모리 맵을 통한 I/O 장치를 위한 영역이 존재합니다.

명령어 메모리

ROM32K라고도 불립니다.

0000 0000 0000 0000 ~ 0111 1111 1111 1111 2^15(32K)만큼 접근할수 있고,

한 레지스터가 16비트이기에 출력은 16비트 크기를 가집니다.

쓰기 작업을 위한 in이나 load 단자는 존재하지 않습니다.

입력/출력

화면, 키보드와 같은 I/O장치는 데이터 메모리 RAM에 매핑되고, 클럭마다 반영됩니다.

핵 컴퓨터 플랫폼에서는 스크린 메모리 맵과 키보드 메모리 맵은 Screen과 Keyboard라는 2개의 내장형 칩으로 구현됩니다.

 

스크린

16비트 레지스터로 된 8K 메모리 칩으로 구현되며, address와 load 입력을 받습니다.

스크린 공간이 8K이기에 주소가 13비트 입력으로 되어있습니다.

칩에 기록한 Bit은 픽셀로 표시됩니다.

키보드

키보드 베이스 어드레스의 위치에 있는 레지스터 하나의 값으로 키보드 입력을 나타내기에,

입력 값이나 주소가 필요없습니다.

키보드에서 키가 눌리면 Keyboard 칩의 output으로 키에 해당하는 문자의 16비트 코드를 출력하고, 아무것도 눌리지 않으면 칩은 0을 출력합니다.

컴퓨터

Hack 시스템의 최상위 칩입니다.

CPU, RAM, ROM, 스크린, 키보드로 구성되어있습니다.

프로그램의 실행을 위해 rom에 미리 Load 해야하고, reset이 0이면 프로그램 실행, reset이 1이면 프로그램을 초기화합니다.

CPU구현

실제 구현에 앞서 잠깐 CPU가 어떤일을 하는지 먼저 정리해보려합니다.

 

1. 디코딩

먼저 명령어가 들어오는 부분부터 보면

들어오는 명령어가 A명령어인지 C명령어인지 op-code를 통해 구분하는 디코딩작업을 수행해야합니다.

 

2. 명령어 실행

A명령어의 경우 MSB를 제외한 15비트는 그대로 A 레지스터에 로드되고,

이후 A레지스터의 출력은 M으로 전달됩니다.

 

C명령어의 경우 111accccccccdddjjj 6개의 제어 비트 ccccccc와 a로 선택된 연산을 수행하고, ddd로 지정한 곳에 연산 결과를 저장합니다.

a비트는 ALU 입력이 A 레지스터 값에서 올지, 아니면 입력된 M 값에서 올지 결정하는 비트입니다.

cccccc 비트는 ALU에서 어떤 함수가 계산될지를 가리키고, ddd 비트는 어떤 레지스터가 ALU 출력을 받아야 할지 지정합니다.

jjj비트는 다음에 인출할 명령어를 결정합니다.

000이 아닌 경우 연산 결과에 따라서 jjj의 조건(0과같거나, 크거나, 작거나 등)에 따라 어드레스 레지스터에 입력된 명령어 주소로 점프합니다.

 

3. 명령어 가져오기

PC는 일반적으로 다음 순서의 명령어의 주소를 정하고 출력한다. ( PC(t)=PC(t-1)+1 )

 

하지만 goto 연산(분기문)의 경우 A 레지스터에 이동할 주소를 저장한 뒤 jump 명령어를 사용하면 됩니다.

( If jump(t) then PC(t)=A(t-1) else PC(t)=PC(t-1)+1 )

즉 레지스터의 출력을 PC의 입력으로 연결시키고, jump를 해야할 때 PC의 load를 활성시키켜 PC에 다음명령어를 지정해주면 됩니다. 

 

추가로 프로그램을 종료하려면 reset Bit을 set하여 PC의 값을 0으로 만들어야합니다.

CHIP CPU {
    IN inM[16],          // 데이터 메모리에서 읽은 값 (M=A인 경우)
        instruction[16], // 현재 실행할 명령어 (ROM에서 가져옴)
        reset;           // 프로그램을 처음부터 실행할지 여부

    OUT outM[16],        // 데이터 메모리에 저장할 값
        writeM,          // 데이터 메모리에 쓰기 여부
        addressM[15],    // 데이터 메모리 주소
        pc[15];          // Program Counter 값 (다음 명령어 주소)

    PARTS:
    
    // 명령어의 최상위 비트를 반전하여 A-명령어인지 C-명령어인지 판별 (A-명령어면 ni=1, C-명령어면 ni=0)
    Not(in=instruction[15],out=ni);
    
    // A-명령어이면 instruction 값을 선택하고, C-명령어이면 outtM 값을 선택하여 A 레지스터에 저장할 값 결정
    Mux16(a=outtM,b=instruction,sel=ni,out=i);
    
    // A 레지스터: A-명령어일 경우 값을 저장하고, 해당 값의 하위 15비트를 addressM에 출력 (메모리 주소로 사용됨)
    Or(a=ni,b=instruction[5],out=intoA);
    ARegister(in=i,load=intoA,out=A,out[0..14]=addressM);

    // C-명령어에서 A 레지스터 또는 메모리 값을 선택하여 ALU의 입력으로 전달
    And(a=instruction[15],b=instruction[12],out=AorM);
    Mux16(a=A,b=inM,sel=AorM,out=AM);
    
    // ALU 실행: 입력된 x(D 레지스터)와 y(A/M 레지스터)를 기반으로 연산 수행, 결과를 outM에 출력
    ALU(x=D,y=AM,zx=instruction[11],nx=instruction[10],zy=instruction[9],ny=instruction[8],f=instruction[7],no=instruction[6],out=outtM,out=outM,zr=zr,ng=ng);

    // C-명령어의 D 레지스터 저장 비트를 확인하여 D 레지스터 업데이트 여부 결정
    And(a=instruction[15],b=instruction[4],out=intoD);
    DRegister(in=outtM,load=intoD,out=D);

    // C-명령어의 메모리 쓰기 비트를 확인하여 writeM 결정 (1이면 메모리에 쓰기 수행)
    And(a=instruction[15],b=instruction[3],out=writeM);

    // 점프 로직: 조건을 판별하여 PC를 업데이트할지 결정
    Not(in=ng,out=pos); // 음수 여부 반전
    Not(in=zr,out=nzr); // 0 여부 반전
    And(a=instruction[15],b=instruction[0],out=jgt); // JGT 비트 체크
    And(a=pos,b=nzr,out=posnzr); // 양수이며 0이 아닐 경우
    And(a=jgt,b=posnzr,out=ld1); // 점프 조건 충족 시 ld1 활성화

    And(a=instruction[15],b=instruction[1],out=jeq); // JEQ 비트 체크
    And(a=jeq,b=zr,out=ld2); // 0일 경우 점프 조건 충족 시 ld2 활성화

    And(a=instruction[15],b=instruction[2],out=jlt); // JLT 비트 체크
    And(a=jlt,b=ng,out=ld3); // 음수일 경우 점프 조건 충족 시 ld3 활성화

    // ld1 (JGT) 또는 ld2 (JEQ) 활성화 여부 확인
    Or(a=ld1,b=ld2,out=ldt);
    // ld3 (JLT) 또는 이전 결과(ldt) 활성화 여부 확인
    Or(a=ld3,b=ldt,out=ld);

    // PC 업데이트: 점프 조건이 충족되면 A 레지스터 값으로 설정, 그렇지 않으면 1 증가
    PC(in=A,load=ld,inc=true,reset=reset,out[0..14]=pc);
}

 

메모리 구현

CHIP Memory {
    IN in[16], load, address[15];
    OUT out[16];

    PARTS:
    DMux(in=load, sel=address[14], a=loadRAM, b=loadScreen);
    RAM16K(in=in, load=loadRAM, address=address[0..13], out=outRAM);
    Screen(in=in, load=loadScreen, address=address[0..12], out=outScreen);
    Keyboard(out=outKeyboard);
    Mux4Way16(a=outRAM, b=outRAM, c=outScreen, d=outKeyboard, sel=address[13..14], out=out);
}CHIP Memory {
    IN in[16], load, address[15];  // 16비트 데이터 입력, 쓰기 신호(load), 15비트 주소 입력
    OUT out[16];                   // 16비트 데이터 출력

    PARTS:
    // address[14] 값에 따라 load 신호를 RAM 또는 Screen으로 분배
    DMux(in=load, sel=address[14], a=loadRAM, b=loadScreen);
    
    // RAM (16K 메모리 블록)
    RAM16K(in=in, load=loadRAM, address=address[0..13], out=outRAM);
    
    // Screen 메모리
    Screen(in=in, load=loadScreen, address=address[0..12], out=outScreen);
    
    // 키보드 입력을 받아 outKeyboard에 출력
    Keyboard(out=outKeyboard);
    
    // address[13..14]에 따라 4개의 데이터 소스 중 하나를 선택하여 out으로 출력
    // a, b -> RAM, c -> Screen, d -> Keyboard
    Mux4Way16(a=outRAM, b=outRAM, c=outScreen, d=outKeyboard, sel=address[13..14], out=out);
}

컴퓨터 구현

최상위 Computer 칩은 CPU, MEMORY, ROM32K 칩으로 구현됩니다.


- 사용자가 reset 입력을 활성화하면, CPU의 pc에서는 0이 출력되고 프로그램의 첫 번째 명령어를 출력합니다.

- CPU는 출력된 명령어를 실행하며, 필요에 따라 적절한 레지스터를 읽고씁니다. 또 다음에 어떤 명령어를 실행할지 결정합니다.

- 다음 명령어가 정해지면 해당 명령어의 주소를 pc 출력으로 내보낸다. (CPU의 PC 출력은 명령어 메모리의 address 입력으로 연결)

- 메모리로 전달된 다음 명령어는 CPU의 instruction 입력으로 연결되어서 fetch-execute cycle을 마무리합니다.

CHIP Computer {

    IN reset;  // 프로그램을 처음부터 실행할지 여부를 결정하는 입력 신호

    PARTS:
    
    // ROM : 프로그램 명령어를 저장하는 32K 크기의 읽기 전용 메모리, PC값을 주소로 명령어를 가져옴
    ROM32K(address=pc, out=instruction);

    // CPU : 명령어를 실행하고, 메모리 및 PC값을 업데이트
    CPU(inM=inM, instruction=instruction, reset=reset, outM=outM, writeM=writeM, addressM=addressM, pc=pc);

    // RAM : CPU 연산결과 저장 및 필요 데이터를 가져옴
    Memory(in=outM, load=writeM, address=addressM, out=inM);
}