지난 글에서 컴퓨터 HACK을 만들기 위해서

 

1. CPU

2. ROM 32K

3. Screen

4. Keyboard

5. RAM

6. Computer

 

이렇게 6가지로 나누어서 설명했다.

 

 

1. CPU 구현하기

CPU는 ALU와 세 레지스터 - 어드레스 레지스터, 데이터 레지스터, 프로그램 카운터로 만들면 된다. 각각이 뭐하는건지는 직접 만들기도 하고 수차례 시뮬레이션도 돌렸으니 넘어가고 고맙게도 아래의 그림처럼 연결하면 된다고 알려준다. 동작 과정은 다음과 같다.

 

 1) 명령어 해독 : CPU 입력 - 명령어 instruction은 A 명령어(MSB가 0, op-code라고도 했던거같은데), C 명령어(MSB가 1일때) 올수 있고, C 명령인 경우 111accccccccdddjjj 6개의 제어 비트 ccccccc와 a로 선택된 연산을 수행하고, ddd로 지정한 곳에 연산 결과를 저장한다. jjj가 000이 아닌 경우 연산 결과에 따라서 jjj의 조건(0과같거나, 크거나, 작거나 등)에 따라 어드레스 레지스터에 입력된 명령어 주소로 점프한다.

 

 2) 명령어 실행 : 들어온 명령어가 A 명령어인 경우 A 레지스터에 담는다. C 명령어의 경우 해독된 명령어의 연산을 수행한다.

 

 3) 명령어 가져오기 : 명령어가 실행되면 프로그램 카운터는 다음에 실행할 명령어의 주소를 준비하고, 현재 명령어가 끝나면 PC에 지정된 주소의 명령어가 실행된다. 하지만 앞서 보았듯이 "@LOOP\n 0;JMP" 처럼 라벨 심볼로 점프할때는  어드레스 레지스터 A에 담겨진 라벨 심볼 (LOOP)의 주소로 점프하여 명령어를 수행하게 되고, PC도 이를 따라가 다음 실행할 주소를 담아서 수행한다. 

 

이제 CPU 구현에 필요한 내용은 대강 정리했는데

어디부터 시작하는게 좋을지가 고민이다.

프로그램 카운터나 각 먹스, 레지스터에는 제어 비트를 어떻게 넣어야 하는건지

 

 

 위 그림을 보고 사용해야할 회로들의 인터페이스를 정리해보자

CPU = ALU + D/A registor + PC +(Mux 16 2개)

1. ALU

- 입력 : 16비트 x(D), y(M/A),   제어 비트 6개

- 출력 : 상태 출력 비트 2개 zr ng, alu 연산 출력 16비트 outM

2. PC

- 입력 : 16비트 입력(다음 실행 주소 A-점프시),  제어 비트 3개(reset, load, inc-이 순서대로 조건에따라 수행) 

- 출력 : 제어 비트에 따라 리셋시 0, 로드시 점프할 주소, 증산시 다음 명령어, 이도저도 아닐땐 상태 유지

3. Regsitor

- 입력 : 16비트 입력 in, 로드 제어비트 load

- 출력 : 현재 값 out, 입력 로드시 out[t+1] = in[t]가 된다.

4. MUX 16

- 입력 ; 16비트 a, b

- 출력 : sel이 0인경우 a, sel == 1일때 b

 

CPU

- 입력 : inM(데이터 메모리에서 읽어온 값), instruction(현재 실행할 명령어)

- 출력

 outM : RAM[addressM]에 쓰여진 결과

 addressM : 값이 쓰여질 주소, A 레지스터의 출력

 writeM : 메모리에 쓸지 읽을지 여부

 pc : 다음 명령의 주소

 

 

 

1.1 PC 구현하기

1. reset 입력 : PC의 제어비트 같은 경우에는 reset을 바로 연결시켜주면 될거같기는 한데 ..  

2: inc 입력 : inc는 reset이 0이 아니면 1이니까 Not(in=reset, out=resetNotOut) 해서 넣어주면 될거같다.

3. load 입력 : load 의 경우에는 명령어의 j 비트가 000인 경우에만 점프하는게 아니니까 3bit or한 결과가 0이면 load는 0, 1이면 1로해야겠다. 일단 instruction의 LSB 3비트만 받아서 or 연산 후 jmp123or을 출력하여 load 자리에 넣는다고 생각하고 이 이름을 입력받도록 하자.

4. in 입력 : A 레지스터의 출력을 써야하니 일단 A 레지스터를 어떻게 할지 생각하지는 않았지만 aRegistorOut이란 이름으로 해놓자.

5. 출력 : PC의 출력은 pc란 이름으로 출력되어야한다.

 

일단 프로그램 카운터 쪽 파트는 이렇게 정리했다.

    //PC PARTS
    Or(a=instruction[0], b=instruction[1], out=jmp12or);
    Or(a=jmp12or, b=instruction[2], out=jmp123or)
    Not(in=reset, out=resetNotOut)
    PC(in=aRegistorOut, load=jmp123or, inc=resetNotOut, reset=reset, out=pc);

 

 

 

 

 

1.2 A registor 구현하기

가장 먼저 PC를 만들면서 A의 입력을 받도록 했었으니까 이번에는 A 레지스터를 한번 보자

A Registor는 일단 좌측의 Mux16의 결과를 받아 우측의 Mux16과 PC로 보내고 있으며, 제어비트 c가 0인지 1인지에 따라 값을 읽기만 하거나 저장을 한다.

1. in 입력 : in의 경우 좌측 먹스의 출력을 받으니 일단 leftMuxOut 정도로 해놓자

2. 출력 : A 레지스터의 출력은 우측 먹스의 입력으로, PC의 입력으로도 사용하는데 일단 aRegistorOut 정도로 해놓자.

3. c 입력 : A 레지스터에 값을 쓸지 말지 여부는 명령어가 A 명령어인지 C 명령어인지에 따라 결정되었었다. 라벨 심볼이든, 변수든, 상수가 오던간에 A 명령어 이므로 instruction의 MSB를 보고 0이면 c에는 1, MSB가 1이면 c에는 0을 놓도록 Not 게이트(출력명은 opcodeOut)를 추가하여 연결해보자.

    //A registor PARRTS
    Not(in=instruction[15], out=opcodeOut);
    //if opcode is 0 (== A instruction) -> opcodeOut = 1 -> load = 1
    Register(in=leftMuxOut, load=opcodeOut, out=aRegistorOut);

+ 오류 발견1 ) D registor 구현 중 A에 로드해야하는 경우 추가 발견

A 레지스터에는 A 명령어가 들어올때 외에도 C 명령어일 때, 첫번째 d비트의 값이 1인 경우 instruction[5]==1, 연산 결과를 A 레지스터에 담는다. 명령어가 C 명령어고 첫번째 d비트가 1인지 여부 ((instruction[15] && instruction[5]) 한 결과를 loadA라 하면, opcodeOut과 loadA 둘중 하나가 1이면 1이되도록 or 연산을 한 후 aRegistorLoad 라는 이름으로 전달해주자.

    //A registor PARRTS
    Not(in=instruction[15], out=opcodeOut); // A instruction == store to A registor
    And(a=instruction[15], b=instruction[5], out=loadA); //if instruction is C, store outM to A registor
    Or(a=opcodeOut, b=loadA, out=aRegistorLoad);
    //if opcode is 0 (== A instruction) -> opcodeOut = 1 -> load = 1
    Register(in=leftMuxOut, load=aRegistorLoad, out=aRegistorOut);

 

 

 

1.3 D registor

이번에는 D registor를 구현해보자. D 레지스터는 A명령어로 입력하지는 못하고, C 명령어의 ddd 값에 따라서 대입하도록 되어있었다. 그러면 D 레지스터에 입력하는 경우는 어떤 경우가 있었나?

 위 표를 보면 2번째 d비트가 1일때만 D 레지스터에 입력하도록 되어있었다. 그러니까 A D M 순서니까 instruction[5], instruction[4], instruction[3] 순이 되겠다. 근데 지금보니까 d의 첫번째 비트가 1인 경우에도 A 레지스터에 값을 저장하도록 하고 있다. 그러니까 A 명령어인 경우 외에도 instruction[5] == 1일때도 A 레지스터의 load = 1이 되도록 해줘야 하는거같다. 일단 처음에 쓴 부분 뒤에다가 추가로 표기해놔야겠다.

 

1. in 입력 : D Registor는 ALU의 연산 결과를 담을수 있으므로 outM을 연결해주면 될거같은데, outM은 외부로 나가므로 루프할수 없으니 outMLoop 라는 루프 출력을 만들어서 in에다가 넣자.

2. c 입력 : 앞서 설명한것 처럼 명령어가 C 명령어이고 5번째 비트가 1일때 c에 넣도록 구현하자.

3. 출력 : A 레지스터때 처럼 dRegistorOut 정도로만 하자.

    //D registor PARTS
    And(a=instruction[15], b=instruction[4], out=loadD); //if instruction is C and dest is d, store outM to A registor
    Register(in=outMLoop, load=loadD, out=dRegistorOut);

 

이제 PC, A, D, 레지스터는 전부 구현했고, ALU 하기전에 먹스부터 정리해보자.

 

1.4 좌측 MUX16(한참 삽질하여 뒤에 다시 정리함)

 일단 좌측 먹스부터 생해보자. 왼쪽 먹스의 경우에는 a 자리에 ALU의 출력 outM, 이건 루프가 안되니 D 레지스터의 입력으로한 outMLoop를 사용하고, b 자리에는 instruction을 그대로 넣어주면 될거같다. MUX16은 sel이 0일때 a, 1 일때 b를 내보냈었는데 평소에는 A 레지스터에 A 명령어를 넣고, 아니다 C instruction[5]가 1일때만 outM을 A 레지스터에 넣었으니 이걸 sel 기준으로 잡으면 될거같다.

 

 먹스는 sel이 0일때 a를 출력으로 하지만, C instruction[5] == 1일때 A레지스터에 저장해야하니 Not(C instruction[5]) 한것을 sel에 넣어야 alu의 출력이 A 레지스터로 넣어지고, C instruction[5]가 0이라면 not 연산으로 sel에 들어가는 값이 1이되고 instruction이 A 레지스터로 전달 되겠다.

 

 잠깐 A 레지스터는 값과 주소를 저장한다 했는데, 값은 지금까지 한걸보면 A 명령어와 C 명령어의 목적지에 따라 ALU의 결과를 값으로 넣어주도록 했다. 하지만 C 명령어인데 instruction[5]가 아닌 경우는 A registor에 넣어도 되는건지 햇갈리기 시작했다. 

 

 지난번에 ALU, PC 구현할때는 완벽하게 동작을 이해하지 않더라도 표만 보고, 동작 조건만 따라서 연결만 해줬어도 어떤 흐름인지 따라가지는 못하지만 원하는 동작을 하기는 했었다. 지금도 A 레지스터에 먹스 a를 넣는 조건을 찾았으니 전처럼  해야할까? 그냥 그랬다가는 나중에 놓친 부분이 있으면 엄청 해맬거같은데 고민된다.

 

 C 명령어가 (A=)가 아닐때 잠깐 곱셈 어셈블리어로 CPU 에뮬레이터를 보니

 

@1     //이때 A에는 1이 들어가고,    현재 1번지의 값은 20이다.

D=M   //그러면 M에는 20이 들어갈 것이고, A 레지스터는 명령어를 저장하는게 아니라 기존의 1을 유지해야한다.

D=M 연산한 결과 A 레지스터는 1로 그대로 유지되고 있고,

D의 값이 RAM[1]의 값으로 덮어씌워졌다.

 

아 지금 보니까 생각난게 이래서 A 레지스터의 load를 조절해줘야 되구나.

아까 수정한 A 레지스터에는 C 명령어이고, instruction[5]가 1인 경우에만 load=1이 되도록 했으니

어셈블리어에서 A=이 아닐때는 load에 0이들어가 기존 값을 유지하는게 된다.

 

그리고 위에 사진에는 @22에 노란줄이 되어있는데 이 명령어는 A 명령어이고, A 레지스터는 순차 논리 회로이므로 다음 클럭때 A는 22로 반영이 될거같다.

실제로 A 레지스터의 값이 22로 변했다. 

그런데 아직 ALU아 하단 입력 M/A가 20 그대로 유지되고 있다.

현재 22이니까 22이여야 하는데 안바뀐건 시간이 안지나서 그런걸까?

아니면 우측 먹스에서 inM이 들어와 아직까지는 아래에 20이 들어가는거같다.

8번 명령어는 값을 저장하는 A 명령어라 M이 반영이 안된건지

 

 

아 다시 처음부터 보니까 이해가 되는게

내가 그동안 노란줄을 현재 실행한 명령어로 오해하고 있었다. 실제로는 프로그램 카운터가 가리키는 주소라

다음에 실행하는 명령어인데도

다시 이 코드를 한번씩 실행하면

 

 

PC가 0일때 모두 0이지만, 0번 명령어를 실행하는 즉시 PC는 1, A에는 A 명령어를 통해 2가 저장된다. 

registor(load=1) out(t+1) = in(t) 이었으므로 아직 ALU의 M/A 입력이 0이고,

다음 클럭때(PC가 2를 가리킬때) 2를 출력하게 된다.

 

 

일단 오케이 아까보다는 PC니 A 레지스터 값 저장이니 더 이해된거같네

아까 햇갈렷던 A 레지스터의 값이 22인데 ALU에 반영안된 이유는

현재 실행한 명령어는 8번 명령어라 A에는 즉시 저장되었지만 출력이 아직 안되었기 때문이고

8번 시점에 M/A 입력이 20인건, 7번 시점에서는 A가 1이지만 우측 먹스에 의해서 inM이 전달되서 그런거같다.

 

좌측 먹스를 정리하면서 좀 많이 해매버렸는데, A에 A 명령어든 C 명령어 연산 결과를 저장하든 말든간에 우측 먹스에 의해서 alu 아래 단자 값이 정해지니 이게 중요한거같다.

 

먹스 하나가 내용 정리하는데 어쩌다 보니 다른 회로 합친거보다 길어졌다.

 

 

잠깐만 다시 정리하자

 

outMLoop와 instruction을 A 레지스터로 저장 조건과 전달 조건

 

A 레지스터 저장 조건

1. 명령어가 A레지스터인 경우 - instruction을 저장한다.

2. 명령어가 C 명령어이고, instruction[5] = 1인 경우 - outMLoop를 저장한다.

 

 

A 레지스터 전달 조건

1. 명령어가 A레지스터인 경우 - instruction을 저장한다.

 -> 좌측 먹스로 명령어를 전달한다.

2. 명령어가 C 명령어이고, instruction[5] = 1인 경우 - outMLoop를 저장한다.

 -> 좌측 먹스로 outMLoop를 전달한다.

3. 그외 경우 : C 명령어이나 instruction[5] = 0인 경우

 -> 저장 하지 않아 기존의 저장된 값을 출력하므로 무관하다.

 

 그러므로 ((instruction[5] == 1) && (instruction[15] == 1)) ==1일때만 outMLoop를 A 레지스터로 전달하도록 하고

그외는 명령어를 전달하자.

 

그러면 이제 좌측 먹스 구현 조건을 다 정리된거같다.

    //left Mux16
    And(a=instruction[5], b=instruction[15], out=isOutMLoop)
    Mux16(a=instruction, b=outMLoop, sel=isOutMLoop);

 

 

 

하.. 오늘 안에 다 구현할수 있을줄 알았는데

밤에 좀 자고 너무 놀아서 결국에 5장도 마무리 못했다.

매 챕터가 다 금방 끝낼거같아보이긴 한데 막상 하면 막히는 부분에서 한참 해매니까 하루에는 못끝내고 못해도 이틀은 걸린다 ㅜㅜ 내일은 마무리해야지

오늘 새벽에 4장 어샘블리어 구현 마무리하고

잠을 너무 늦게자서 피곤하긴한데

 

오늘 안에 5장을 끝낼수 있을지는 모르겠다.

 

 

범용 목적 컴퓨터와 단일 목적 컴퓨터 

일단 이번 장에서는 

1-3장에서 만든 렘, 프로그램 카운터, ALU, 레지스터 등을 가지고

CPU와 컴퓨터를 만드는게 목표이다.

책에서 말하기를 여기서 만드는게 범용 목적 컴퓨터라 하는데,

 

초등학생 때 컴활 준비했을때였던가

정처기 준비할때였던가. 

그때 범용 목적 컴퓨터가 무엇인지에 대해 잘 몰랐는데,

 

뒤에 또 말할거지만

범용 목적 컴퓨터는 우리가 사용하는 PC나 휴대폰처럼 게임이든, 인터넷이든, 음악이든 하나의 목적이 아닌 다양한 용도로 사용할수 있는 컴퓨터를 말하며,

 

 범용 목적 컴퓨터 외 다른 분류로 특수 목적/단일 목적 컴퓨터가 있다.

단일 목적 컴퓨터의 경우 처음 이 단어를 보는 사람에게는 막연할거같은데 나도 그랬었고,

 

 우리 주위를 보면 신호등, 엘리베이터, 밥솥, 냉장고 등 다양한 기계, 전자장치들이 존재한다.

아직 시퀀스에 대해서 잘 아는건 아니지만 이런 기계 중에서는 엘리베이터나 자동 수양장치, 자기 유지 회로 등 프로세서없이 만들어서 사용할 수 있는 시퀀스 시스템도 있고, 

 

 아날로그 시퀀스 만으로는 구현하기는 어려워 프로세서를 이용한 디지털 시스템이 있는데 우리가 사용하는 PC나 휴대폰 외에도 TV나 밥솥, 냉장고도 내부에 프로세서가 들어가 있고 프로그래밍을 하여 사람이 쉽게 사용할수 있고, 원하는 동작을 하도록 되어있다.

 

 이런 냉장고, 식기세척기, 밥솥 등과 같이 아날로그 회로만으로는 구현하기 어려워 사용한 프로세서를 특수 목적/단일 목적 컴퓨터라 하며, 단일 목적 컴퓨터는 PC같은 범용 목적 컴퓨터와는 다르게 밥솥은 밥솥역활만 하도록, 냉장고는 냉장고 역활만 하도록 특정 용도에 한정한 컴퓨터를 의미한다.

 

 범용 목적 컴퓨터와 차이점이라면 다양한 작업을 할 필요가 없으니 계산 성능이나 메모리 공간, 주변 장치등이 범용 목적 컴퓨터에 비해 적이며 저렴하다는 점이다.

 

 정도로 이해하고 있는데, 당장은 이 정도만 이해해도 답답하거나 막히는 일은 없었다.

 

 

외장 프로그램 방식과 내장 프로그램 방식

 컴퓨터 구조를 공부하다 나오면 자주 보는 폰 노이만 구조니 하버드 구조랑 더불어 내장 프로그램 방식이란 단어를 종종 보곤 했었다. 그런데 처음 컴퓨터를 공부 할때는 당연히 프로그램은 컴퓨터 안에 있으니까 원래 내장된거 아닌가? 왜 내장 프로그램이라는 용어가 나온건지 잘 이해가 되지를 않았었다.

 

 전기랑 디지털 논리 회로를 배우면서 동기 카운터를 만들고, 난드투 태트리스에서 ALU, 램 만들면서 이전보다는 좀 더 와닿았는게, 특히 직접 만든 ALU를 시뮬레이터로 테스트를 할때/기계어 어셈블리어 작성한걸 돌리면서 상태 비트 레지스터/C명령어에 따라 +1 연산하기도 -1 연산하기도하고 D+A M+D 든 연산을 하는걸 봤었다.

 

 지난 장에서는 어셈블리어로 곱셈기 프로그램을 짜서 CPU 에뮬레이터로 돌렸었는데, 그 때는 직접 만든 바이너리 코드를 ROM에다가 저장해서 돌린덕에 PC로 지정한 명령어 실행하고, 그다음 명령어 가져와서 ALU에 넣어 실행하고를 반복했었다.

 

 하지만 이런식으로 프로그램을 기억장치에 넣어서 사용하기 전에는 직접 프로세서가 원하는 동작을 하도록, 원하는 값을 넣을 수 있도록 하드웨어를 조작(선을 뺏다 꽂앗다)하여 만들었으며 이를 하드웨어로 프로그래밍 하는 방식을 외장 프로그램 방식이라 하더라.

 잠깐 찾아보니 최초의 전자식 컴퓨터인 애니악이 이런 외장 프로그래밍 방식이라 한다. 디지털 논리회로를 지금 만큼 모르고 애니악이 최초의 컴퓨터니 에드박이니 하는걸 들을때는 그냥 연도 외우는 문젠갑다 싶어 억지로 외웠었는데 애니악과 우리가 현재 쓰는 컴퓨터가 이런 차이가 있다더라. 

 

 당장에 FPGA로 앞서 만든 ALU를 구현한다 하더라도 입력 두개나 상태 입력 비트에다가 +1, -1, +M, 0, not 연산을 하도록 전선을 일일이 연결해서 전원을 줬다면 얼마나 어려웠을까?

 

 

 

하드웨어를 이용한 동작 구현과 소프트웨어를 이용한 동작 구현

 

 그리고 내장 프로그래밍 방식의 장점은 간단한 명령어들을 합쳐서 복잡한 명령어를 구현할 수 있다는 점이다. 가장 최근에 본 예시로 곱셈 연산일거같은데, 위의 ALU 제어 테이블이나 HACK의 어셈블리어 명령어 테이블을 봐도 하드웨어적으로는 곱셈 연산을 만든 적이 없고, 덧셈 뺄샘 그리고 논리 현산 몇개 뿐이다. 

 

@R2
M=0
@R0
D=M
@END
D;JEQ
@R1
D=M
@END
D;JEQ
  (LOOP)
@R1
D=M
@R2
M=M+D
@R0
M=M-1
@R0
D=M
@END
D;JEQ
@LOOP
0;JMP
  (END)
@END
0;JMP

 

 그런데 어떻게 곱셈 연산을 해냈던가? sum = D+M 연산을 D 횟수 만큼 하도록 루프를 돌면서 곱셈한것과 동일한 결과가 나오도록 어셈블리어를 만들었다. 거기다가 명령어 테이블에도 없던 입출력 제어도 ROM에다가 넣어둔 어셈블리어로 할수 있었다. 명령어 테이블에 없는 동작을 어셈블리어, 그러니까 소프트웨어 적으로 구현했는데 하드웨어로 구현할수 없을까?

곱셈기

 잠깐 찾아보면 곱셈이나 나눗셈도 논리 곱셈기, 논리 나눗셈기가 나오는데 직접 하드웨어로 구현할수가 있다. 그런데도 사용하고자 하는 모든 연산을 하드웨어적으로 구현안하는 이유가 ALU로 원하는 모든 연산을 할수 있도록 각 연산들을 하드웨어로 구현해 짚어넣으면 하드웨어로 구현하기 힘든 연산도 있을 것이고, 그 만큼 비용도 비싸지고, 크기도 커진다.

 

 하지만 곱샘 연산 구현때와 같이 ALU에서 제공하는 단순한 연산들로 소프트웨어 적으로 구현하는 것이 하드웨어로 구현하는것 보다 쉽고, 프로세서가 커질 필요도 없으며 소모하는 비용도 늘진 않는다. 지금 당장은 이정도로 이해하고 있고, 이게 RISC와 CISC의 차이인거 같은데 뒤에 또 보자

 

 

컴퓨터 구조 : 튜링 머신과 폰 노이만 구조

아 내장 프로그램 생각 정리하다가

외장 프로그램으로 넘어가고, RISC CISC 얘기까지 가버렸는데,

 

결국에는 이 내장 프로그램 방식이 대표적인 컴퓨터 모델인 튜링 머신이나 폰 노이만 구조같은 컴퓨터 구조의 핵심이 된다.

 

 튜링 머신은 컴퓨터 공부하면서 몇번 들어봤지만 자주 까먹던 개념인데, 실제 물리적인 컴퓨터는 아니고 어떻게 프로그램을 읽고 처리할지 판단하는 추상적인 개념의 컴퓨터라 한다(만든 것도 있긴한데).

 

 잠깐 나무 위키를 봤는데 (위 그림과는 조금 다르지만) 튜링머신은 테이프, 헤드, 상태 기록기, 행동표 등으로 구성되어있다고 한다. 지금 하는거와 빗댄다면 테이프는 기억 장치, 메모리 역활, 헤드는 어드레스 레지스터 역할, 상태 기록기는 상태 레지스터 쯤 되는거 같고, 행동표는 행동을 지시한다니까 프로그램 카운터쯤? 비슷한게 아닌가 싶기도 하다.

 

 결국에는 헤드를 통해 테이프의 값을 읽거나 쓸수 있다는 점에서 튜링 머신도 내장 프로그램 방식이라고 하는거같다.

 

 폰노이만 구조는 지난번에 하버드 구조와 같이 정리했던거 같은데, 내장 프로그램 방식인 만큼 메모리가 내장 되어있으며, 하버드 구조(데이터 메모리와 명령어 메모리가 다른 버스를 이용)와는 다르게 데이터 메모리와 명령어 메모리가 같은 버스를 통해서 CPU에서 읽고 썻었던게 특징이었다.

 

 

메모리

 아무튼 우리가 만든 컴퓨터 HACK은 계속 봤지만 데이터 메모리 RAM과 명령어 메모리 ROM 두개로 나눠져 있으며 어드레스 레지스터 A를 이용하여 데이터 메모리의 값 M == RAM[A] 에 접근하기도 했었다. 근데 아직도 잘 이해안가는 건 어드레스 레지스터 A가 명령어 메모리의 현재 명령을 가리키고 있다고 하는데,

12: @23

13: D=A

위 연산에서 어드레스 레지스터 A는 23이란 값을 저장하고 있지, D=A란 연산의 주소를 가지고 있지는 않았던거같다.

다음에 실행할 거긴 하지만 명령어의 주소를 가지고 있는건 프로그램 카운터가 아니었나?

 

 

어드레스 레지스터 A가 어떻게 명령어 메모리의 명령어 주소를 가리키는가?

아 어셈블리어 천천히 돌리고 나서야 이해가 된다.

 

위 사진을 보면 지금 ROM의 16번지에서 A레지스터에다가 0을 담고

17번 명령어에는 RAM[0]의 값을 데이터 레지스터에 넣도록 하고 있다.

 

그런데 그 다음 줄을 보면 A 명령어와 C명령어로

@22

D; JEQ

가 있는데,

 

이 명령어를 진행하면 @22라는 A 명령어에 의해 A 레지스터에 22가 담기게 되고,

D에 있는 값에 따라 ROM의 22번지로 점프하게 된다.

 

그러니까 여기서 어드레스 레지스터 A는 점프해서 갈 명령어의 주소를 가리키고 있다.

해당 부분의 실제 어셈블리 코드는

@R0
D=M
@END
D;JEQ

인데 어셈블리어에서 작성한 @END가 어셈블리로인해 @22가 된후 기계어로 되어 롬에 올라갔고,

어드레스 레지스터에 담겨져 주소 역활을 한게 되었다.

 

이제야 이해된다!

그래서 어드레스 레지스터 A가

데이터의 주소, 명령어의 주소, 임시 데이터 보관 역활을 하는건가보다.

 

 

CPU

앞에서 메모리니, 어셈블리어니, 명령어니 계속 정리해왔는데, 이제 CPU를 다룰 차례가 되었다. 계속 공부해왔지만 구현했던 ALU, 레지스터, 제어기 등으로 구성되어 있다.

 

ALU : 이름 그대로 산술 논리 연산하는 장치이며, 구현되지 않은 기능은 하드웨어로 구현해도되고 소프트웨어로 구현해도되며 비용이나 성능, 효율성을 고려해서 설계된다.

 

레지스터 : 레지스터는 CPU 안에 있는 작지만 고속의 기억 장치인데 CPU가 아주 빠르게 동작하다보니, 기억 장치 CPU 밖에 있는 메모리라 한다면 메모리에 값을 잠깐 저장한다거나 원하는 값이든 명령어든 가져 오려고 하면 CPU의 작업 속도에 비해 오랜 시간이 걸려 일을 못하고 지연되는데 이를 stavation 기아 상태라고 한다. 이런 기아 상태를 방지하고, 계산 속도를 늘리기 위해 CPU 내부에 작지만 고속으로 읽고 쓰기가 가능한 기억 장치를 둔 걸 레지스터라고 한다. 일반 컴퓨터에서는 레지스터가 많이 존재하지만 우리가 만들 HACK에는 어드레스 레지스터, 데이터 레지스터 그리고 프로그램 카운터 3개 뿐이다.

 

제어 : 어셈블리어를 보면 알수 있지만 명령어들은 ALU에 입력으로 쓰거나, 메모리에서 가져오거나, 레지스터에 잠깐 저장하는 등 각 하드웨어 장치에 읽고/쓰기 등의 동작들 중에서 어떤 동작을 프로그램 실행중에 할지를 의미한다.

 

가져오기 및 실행 : fetch-execute를 가져오기 및 실행이라 적었는데, 대강 의미는 맞으니까 CPU의 과정은 명령어를 가져오고 실행하기의 반복이라 할수 있을거 같다. 에뮬레이터에서 봤지만 CPU는 프로그램이 실행되는 동안 각 사이클(클럭 마다) 명령어 메모리 ROM에서 실행할 명령어(에뮬레이터상에서는 어셈블리어지만 실제로는 이진 기계어)를 가져오게 되고 C 명령어의 c 비트에 따라 어떤 동작을 할지 해석(판단)하여 그 동작을 실행/수행 execute한다. 그래서 이 과정을 fetch-execute 사이클이라고 부르나보다.

 

 

 

 

입출력 장치

 지난 장에서 설명한거지만 컴퓨터 주변장치인 키보드와 화면을 memory mapped i/o 방식으로 ram 상에 화면과 키보드의 메모리맵에 접근해서 값을 읽거나 써왔다. 이런식으로 입출력 장치를 제어하는 이유는 실제 컴퓨터 주변 장치로 키보드, 화면 뿐만 아니라 마우스, 카메라도 있을 것이고, 프린트나 다른 센서 등 수 많은 장치들이 있다. 하지만 이런 장치들 각각을 어떻게 컴퓨터와 연결해서 사용할까 각 장치가 어떤지 다 알아야할까?

 

 그런 번거로움을 줄이기 위해서 각 장치들의 메모리 맵을 RAM의 영역에 배당하여 해당 매모리맵 영역에 접근함으로서 주변장치들을 사용가능하도록 약속한게 memory mapped i/o 방식이고 이덕분에 지난 과제에서 간편하게 스크린과 화면을 제어할수가 있었다.

 

 오늘 새벽에 그 과제를 하면서 너무 피곤하기도 하고 시간이 늦어서 제대로 설명하지는 않았지만, 클럭 사이클마다 각 주변장치의 매모리맵을 보고 (ex. 키보드 입력이 들어오면 화면에 검은칠을 하라)원하는 동작을 하도록 처리하다보니 사람이 보기에는 알아차릴수 없을 만큼 빠르게 반영된다.

 

 그리고 화면은 2차원 배열 형태로 되어있는데, 메모리는 1차원 주소로 접근 했었다. 그런데도 입출력 메모리 맵핑 방식으로 스크린에 접근할수 있었던건 스크린의 2차원 주소를 1차원으로 직렬화를 했기 때문이다. 일일히 적기는 번거로워서 안했지만 1차원으로 변형한 주소를 이용해서 스크린의 모든 픽셀에(정확히는 각 픽셀들을 담은 레지스터에) 접근할수 있었다.

 

 입출력 매모리 맵핑 방식을 사용하기로 약속/표준화 하여 컴퓨터든 주변장치든 서로 상관없이 만들더라도 이런 약속을 지킨 덕분에 주변 장치의 매모리 맵을 할당하고 사용할수 있게 되었다고 이해하면 될거같다.

 

 주변장치 인스톨러 : 그래서인가 예전에 카메라든 프린터든 새로사서 컴퓨터에서 쓰려면 그런 장치를 쓸수 있도록 설치 프로그램을 돌렸는데, 이런 설치 프로그램을 설치하면서 컴퓨터가 새 장치의  메모리 맵과 베이스 주소를 가져서 사용할수 있게 되는거고

 

 디바이스 드라이버 : 리눅스를 공부하면서 보게되는 디바이스 드라이버도 이것도 인스톨러와 해당 입출력 장치의 메모리맵을 설정하고 물리적인 주변장치에서 값을 어떻게 가져올지를 정리하는 프로그램이라고 한다.

 

 

 

 

 

이제 이번장 이론 마지막으로 HACK 컴퓨터의 구성 요소들을 간단하게 보고 과제를 좀 해야겠다.

그래도 이번 장은 생각보다 빨리 정리 끝낼거같네.. 과제가 얼마나 걸릴지는 모르겠지만 ㅋㅋㅋㅋㅋㅋㅋㅋ

 

 

HACK 컴퓨터

 HACK 컴퓨터는 확장자명을 hack으로 하는 기계어 프로그램을 동작시키는 16비트의 폰 노이만 구조의 컴퓨터다. 데이터 메모리인 RAM과 명령여 메모리인 ROM이 컴퓨터에 내장되며 같은 버스, 어드레스 레지스터 A로 접근해서 값 혹은 주소를 읽고 썻었다.

 

CPU

1) 입력

- inM은 이름 그대로 데이터 메모리에서 가져오는 값

- instruction인 A 명령어 혹인 C명령어로 A 명령어일때는 A=값,  C명령어일땐 명령을 수행 or A/D/M 레지스터 중 지정된 곳에 저장(C 명령어의 목적지가 M이면 writeM은 쓰기 명령을 위해 1이되고, 그렇지 않으면 0이된다. 결과는 outM)

- reset이 0이면 다음 명령을 하지만 1이되면 프로그램 카운터가 0을 가리킨다.

* 주의사항 : 출력 outM과 writeM은 조합 논리회로로 구현되서 명령어 실행 즉시 반영된다!

                출력 addressM과 pc은 순차 논리회로로 구현되어 다음 타임 스탭, 클럭에서 반영된다.

 

 

명령어 메모리

 ROM32K이기도 하며, 0000 0000 0000 0000 ~ 0111 1111 1111 1111 2^15(32K)만큼 접근할수 있고, 한 레지스터가 16비트로 이뤄지다보니 출력은 16비트 크기를 갖는다. ROM이다 보니 어드레스로 접근은 해도 쓰기 작업은 없어 in이나 load 단자는 없다.

 

입출력 장치

 화면과 키보드는 데이터 메모리 RAM에 매핑되어 사용되고, 클럭마다 반영되는데 이 장치들의 매모리 맵을 별도의 빌트인 칩인 Screen과 Keyboard으로 다룬다고 한다. 이것들이 따로 있다는건지 아니면, 램 상에 들어있는걸 칩이라고 부르는지는 잘 이해는 안되지만 일단은 좀 더보자

 

 화면 메모리 맵 : 쓰기 작업을 하다보니 전에 구현한 RAM과 비슷하게 address와 load 입력을 받는다. 차이라면 스크린 공간이 8K다보니 주소가 13비트 입력으로 되어있다.

 

 키보드 메모리 맵 : 키보드 베이스 어드레스의 위치에 있는 레지스터 하나의 값으로 키보드 입력을 나타내다보니, 입력 값이나 주소가 필요없고, 16비트 출력만 내보낸다.

 

데이터 메모리 RAM

 앞서서 스크린과 키보드를 칩으로 나타내고 있으니까 순간 혼동했는데 RAM4K, RAM16K를 구현할 떄 처럼 저만 스크린, 키보드 칩은 그냥 저 크기를 가지는 주변장치의 입출력을 저장하는 기억장치였다. 이 주변장치들이 RAM 안에 포함되어 있어서 16K와 합친다고 별도의 칩으로 표현해놓은 것이고, 결국에는 RAM = RAM16K + 스크린(RAM8K) + 키보드(레지스터, 읽기전용) 하여 데이터 메모리를 구현하나보다.

 

 

 

컴퓨터

드디여 난드 투 테트리스의 마지막 하드웨어인 컴퓨터를 구현할 차례다. 프로그램을 어떻게 집어 넣는지는 아직 잘은 모르겠지만 앞서 만든 CPU와 RAM, ROM을 잘 조합한게 컴퓨터이고, 이 컴퓨터는 reset 입력만 받는다. 0일때는 그대로 프로그램 카운터 진행되는데로 연산하지만 reset 1이 되었다가 0이되면 프로그램 카운터가 0이되어 다시 시작한다. 

 

 

와 벌써 하드웨어 마지막 과제라니 내가 이걸 주말부터만들기 시작해서 이제서야 5장까지 왔다.

책 페이지로는 1/3 조금 넘게 밖에 못온게 너무 충격이긴한데,

디지털 논리회로만 넘기면 나머지는 쉬울줄 알앗지만 장난아니었다.

 

2년전에 ALU하다가 포기하기도 했었고

이번에 다시하면서 처음에 논리회로 만들때만 해도 포기하고 싶었는데 계속 하다보니까 컴퓨터 구현까지

생각보다 많이 왔다.

 

근데 이속도로 어셈블러, 가상머신, 컴파일러, 고급언어, 운영체제까지 하려면 다음주까지는 걸릴것같네 ...

되게 유익하기는 한데 시간 엄청 잡아먹고 있긴하다.

 

아무튼 이론 글은 여기까지 하고

과제 하는거보고 다음 글을 써야겠다.

 

아아 진짜 오늘 너무 바보짓했다 ㅠㅜ

 

기계어/어셈블리어 파트 이론 마무리하고 과제하는데

 

이번 장의 과제가 어셈블리어로 곱셈 연산과

 

키보드, 화면 입출력 어셈블리어로 구현하는 예제인데

 

곱샘 연산 구현하는데만 4~5시간은 낭비해버리고 말았다.

 

// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/04/mult/Mult.tst

load Mult.asm,
output-file Mult.out,
compare-to Mult.cmp,
output-list RAM[0]%D2.6.2 RAM[1]%D2.6.2 RAM[2]%D2.6.2;

set RAM[0] 0,   // Set test arguments
set RAM[1] 0,
set RAM[2] -1;  // Test that program initialized product to 0
repeat 20 {
  ticktock;
}
set RAM[0] 0,   // Restore arguments in case program used them as loop counter
set RAM[1] 0,
output;

set PC 0,
set RAM[0] 1,   // Set test arguments
set RAM[1] 0,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 50 {
  ticktock;
}
set RAM[0] 1,   // Restore arguments in case program used them as loop counter
set RAM[1] 0,
output;

set PC 0,
set RAM[0] 0,   // Set test arguments
set RAM[1] 2,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 80 {
  ticktock;
}
set RAM[0] 0,   // Restore arguments in case program used them as loop counter
set RAM[1] 2,
output;

set PC 0,
set RAM[0] 3,   // Set test arguments
set RAM[1] 1,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 120 {
  ticktock;
}
set RAM[0] 3,   // Restore arguments in case program used them as loop counter
set RAM[1] 1,
output;

set PC 0,
set RAM[0] 2,   // Set test arguments
set RAM[1] 4,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 150 {
  ticktock;
}
set RAM[0] 2,   // Restore arguments in case program used them as loop counter
set RAM[1] 4,
output;

set PC 0,
set RAM[0] 6,   // Set test arguments
set RAM[1] 7,
set RAM[2] -1;  // Ensure that program initialized product to 0
repeat 210 {
  ticktock;
}
set RAM[0] 6,   // Restore arguments in case program used them as loop counter
set RAM[1] 7,
output;

사용한 테스트 스크립트는 이건데

 

 

ALU로 M=M+D 연산이 가능한걸 잊어버리고,

+1연산으로 이중루프를 만들어 곱셈 연산을 구현하느라

 

클럭 루프를 너무 많이 돌아버렸고, 

앞에 작은 수를 다루는 경우는 문제 없었지만

 

마지막 6 * 7 예제에서 210회 클럭안에 수행해야하는데

M=M+D 연산으로 하면 금방할걸

+1연산으로 구현해 놓으니 210회 클럭안에 연산을 마치지 못해서 자꾸 에러뜨길래

이게 라인 수가 너무 많아서 그런갑다 싶어 라인 수를 줄이느라 시간낭비했다.

 

그러다가 갑자기 +1 안해도 되는게 생각나서 했더니 20분도 안걸리고 해결했다 ㅜㅜ

아아아아아ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ

이거에서 해매지만 않았어도 진작에 입출력 제어 끝내고 다음 장으로 넘어갈텐데

 

곱셈 연산 해결한게 밤 11시 30분이라

입출력 헨들링까지 하고 글 정리하고 가면 좀 더 지나야 갈수있을거같다.

Mult_v1.asm Mult_v2.asm Mult_v3.asm
//210 클럭안에 6 * 7 곱연산을 마무리하지 못해 실패
@0 // init R2 as 0
D=A
@R2
M=D
@R0                  // if R0 or R1 == 0 -> end
D=M
@BIG_STOP
D;JEQ
@R1
D=M
@BIG_STOP 
D;JEQ
@j                    // init j(=R0)     -  j * i = result
M=1
(BIG_LOOP)        //--start of big loop--------------------------------
@j                    // make big loop end condition value
D=M
@R0
D=M-D
@BIG_STOP       // if (R0 - j < 0 )  goto BIG_STOP  r0 = 3, d = 4 => 3-4 < 0
D;JLT
@i
M=1
(SMALL_LOOP)   // -----------start of small loop--------------
@i                   //make small loop end condition value
D=M
@R1
D=M-D
@SMALL_STOP  // if (R1 - i < 0 )  goto SMALL_STOP r1 =5, i = 6    -> 5 - 6 < 0
D;JLT
@R2                 //small loop start
M=M+1
@i
M=M+1
@SMALL_LOOP // goto small loop
0;JMP
(SMALL_STOP)   // -----------end of small loop--------------
@j                   //start big loop
M=M+1
@BIG_LOOP
0;JMP               //goto big loop
(BIG_STOP)        //--end of big loop--------------------------------
@BIG_STOP
0;JMP
@R2 // init R2 as 0
M=0
@R1
D=M
@y // x * y = resuilt    <- R0 * R1 = R2, y = R1
M=D
@R0
D=M
@BIG_LOOP_END
D;JEQ
(BIG_LOOP_START)
@y
D=M
@R1
M=D
@R0
D=M
@BIG_LOOP_END
D;JEQ
(SMALL_LOOP_START)
@R1
M=M-1
@R1
D=M
@SMALL_LOOP_END
D;JLT
@R2
M=M+1
@SMALL_LOOP_START
0;JMP
(SMALL_LOOP_END)
@R0
M=M-1
@BIG_LOOP_START
0;JMP
(BIG_LOOP_END)
@BIG_LOOP_END
0;JMP
@R2
M=0
@R0
D=M
@END
D;JEQ
@R1
D=M
@END
D;JEQ
  (LOOP)
@R1
D=M
@R2
M=M+D
@R0
M=M-1
@R0
D=M
@END
D;JEQ
@LOOP
0;JMP
  (END)
@END
0;JMP

 

삽질하면서  HACK 어셈블리어 구현 연습한건 좋긴한데

이렇게 간단하게 할수있는걸 하루/4 시간을 낭비한게 너무 아깝긴하다 ㅜㅜ

마지막 Mult.asm이 마지막 구현 결과

 

 

gif 용량이 너무 커져서 0 곱셈 부분은 좀 자르고

활용한 CPU 에뮬레이터로 앞서 구현한 ALU와 메모리, 프로그램 카운터가 어떻게 동작하는지 알수 있다.

 

 

위에껀 너무 빠르니까

천천히 볼수있게 6 * 7부분만 캡처

 

 

두번째 과제로는

입출력 처리 어셈블리어 작성

1. 무한 루프를 돌며 키보드 입력을 받고, 입력되면 화면 전체를 꺼멓캐 한다.

2. 키를 놓으면 허옇게 하면된다.

 

 

이걸 어떻게 하냐

지난 글에 키보드, 스크린에 대해서 제대로 설명안했는데

각 입출력장치 메모리맵에다가 값을 읽고 써주면 될거같다.

 

키보드와 스크린은 어디있나 싶었는데

CPU 에뮬레이터에 상에 같이 있었다.

 

 

키보드 버튼을 누르면 키보드가 사용가능해져서

실제 키입력을 누르면 가상 머신에 반영되는거같다.

 

 

HACK 스크린

아무튼 HACK 컴퓨터의 스크린은

1. 256 x 512 = 2^8 * 2^9 = 2^17 = 131,072개의 픽셀을 가지고 있고

-> 레지스터가 16비트이고, 한비트가 한픽셀 값을 저장하니까 전체 픽셀을 16으로 나누면 8K의 RAM 공간이 사용된다.

ex) 131,072 / 16 = 2^17/2^4 = 2 ^ 13 = 8192 = 8K

 -> 전체 메모리 32K 중에 RAM이 16K니까, RAM 절반을 얘가 사용하네?

2. RAM의 16384번지에 있고, 이 베이스 주소는 SCREEN 이란 이름으로 미리 정의되어있다.

3. 보통 이미지 다룰때 처럼 베이스 번지가 0, 0을 나타낸다.

4. 이 화면은 그레이스케일이나 컬러가아닌 단순 흑백 화면으로 해당 픽셀에 1을 넣으면 검정, 0을 넣으면 흰색이된다.

5. RAM[SCREEN + row * 32 + col/16] : 1에서 말했듯이 레지스터에서 16개의 픽셀을 보관하고 있다보니, 픽셀 하나하나에 접근할수 없고, 접근하고자 하는 레지스터의 해당 픽셀 부분을 바꿔주면 될거같다.

 

 

 

스크린 예시1 ) 스크린 좌표 0,0 픽셀에다가 검정 표시하기

대강 스크린 베이스 레지스터에다가 1000 0000 0000 0000을 넣으면 될거같은데

A 명령어는 0부터 시작하고 값은 15비트밖에 못넣지 않았던가? 

 

아 다시 생각해보니까

A 명령어는 @SCREEN이니까 상관이 없네

 

그럼 1000 0000 0000 0000은 어떻게 만들어야 할지가 막막하다.

C명령어 테이블을 보면 C비트에다가 111010을 넣으면 -1이 나온다고 한다.

10진수 -1은   0000 0000 0000 0001에다가 2의 보수를 취해 구하면

-1 (10) = 1111 1111 1111 1111(2)이 된다.

 

우리가 구하고자 하는 값은 1000 0000 0000 0000이니

1. 어드레스 레지스터의 값은 2^15승이 최대이므로, A 명령어의 값에 2^16을 넣지 않고

2. 0111 1111 1111 1111를 컨트롤 비트로 -A든 -D든 해서

3. 목적지를 데이터 레지스터로 설정하여 담은 뒤

4. 스크린의 베이스 레지스터에다가 데이터 레지스터 값을 넣어보자

 

진짜 될진 모르겟네

0111 1111 1111 1111은

 

   1000 0000 0000 0000

-  0000 0000 0000 0001

2^16 - 1이므로 65,535가 된다.

 

@65535

D=-A

이걸 하려고 했지만 위 표를 보니 A 레지스터 최대값이 32767이라고 안돌아간다..

 

@32767
D=-A

으로 에뮬레이터에 돌려봤는데

 

데이터레지스터의 값을 2진수로 보니 내가 원했던 값이랑 거의 비슷하다?

그리고 왜 32767이 2^15 = 32768(10) = 0100 0000 0000 0000 이니까

32767의 1의보수 = 1011 1111 1111 1111이고

-32767 = 1100 0000 0000 0000 라 생각했지만

 

2진법 변환기에서 32767은 

0111 1111 1111 1111

32768이

1000 0000 0000 0000 이라고 한다.

 

 

오늘 곱샘연산에서 1/4나절 헛짓하고, 시간이 시간이라 그런가 잘 이해가 안된다

 

 

 

1 = 2^0

0000 0000 0000 0001

2 = 2^1

0000 0000 0000 0010

4 = 2^2

0000 0000 0000 0100

8 = 2^3

0000 0000 0000 1000

...

 

2^15

1000 0000 0000 000

 

하 이렇게 2진법을 쓰니까 이해가간다

2^0 = 1이 LSM이니 0에서 부터 시작해서 16비트의 MSB가 2^15 위치인데

 

2^15=32,768이고

A 명령어의 맨 앞비트만 사용 불가하니

최대 가능한 값은 0111 1111 1111 1111 = 32,768 -1 = 32,767이니까

위의 코드가 동작해서

 

-A 연산한 결과가

-32,767이 1000 0000 0000 0001이 나왔다!

 

내가 원하는 건 0, 0픽셀만 출력하면 되니

1000 0000 0000 0000으로 만들어주기 위해

-A 한 결과에다가 -1을 해주면 된다.

 

@32767
D=-A
@SCREEN
M=D-1

 

픽셀 하나만 찍다보니 뭐가 바뀌었나 싶긴한데 잘보면

화면 좌측 상단 쪽에 M=D-1 연산한 뒤에 아주 작은 검은점 하나 생겼다.

 

 

 

 

스크린 예시2 ) 첫째 행만 검은줄로 채우기

첫째 행만 검은줄을 채우기 위해 네 가지를 기억하면 될거같다.

1.  메모리 레지스터 하나당 16비트를 가지고 있어 16개의 픽셀 값을 표현한다.

2. 1111 1111 1111 1111은 -1이다.

3. 스크린의 열 길이가 512픽셀이니 512/16 = 32개의 픽셀에다가 -1을 채우면 된다.

4. @SCREEN 그러니까 스크린 베이스 어드레스는 16384이므로 16416번지까지 -1을 넣어주자

 

 

금방 할줄 알았는데 생각보다 머릴 좀 써야된다.

앞에서 어셈블리어 연습 많이 했다고 생각했는데 값을 넣었다 뺏다만 했지

값 + 1은 몰라도 주소 + 1은 안해서 어떻게 하나 고민했는데

 

 

ㅋㅋㅋ 진짜 이것도 결국에 검색해서 알았는데

꼭 A명령어 1번, C 명령어 1번 같은 식으로 할 필요가 없고,

A명령어 1번, C명령어 2번이든 여러번 하는게 가능했다.

 

A명령어

C명령어 각각 한번씩만 하면

 

도저히 RAM[A+1]에 접근할 방법이 생각이 안났는데

A=100일때, A=A+1을 시켜놔도

 

@3

M=D를 하려고하면 A의 값이 3으로 바뀌어져

아무리 생각해도 101번 번지에 담을수가 없었기 때문이다.

 

잠깐 찾아봤더니 이게 가능한거더라 .

@x
A=M+D
M=-1

 

이러면 나머지 문제도 금방 풀수 있을거같다.

 

 

 

@16416
D=A
@dest
M=D
@SCREEN
D=A
@cur_addr
M=D
    (LOOP)
@cur_addr
A=M
M=-1
@cur_addr
M=M+1
D=M
@dest
D=D-M
@LOOP
D;JLT
     (END)
@END

 

 

 

 

 

 

벌써 시간이 2시 넘었네 ;;

 

나머지는 얼른 대충 구현하고 가야겠다.

 

 

하도 앞에서 삽질을 많이 하는 바람에 

실제 과제는 금방 끝냈다.

 

 

두 번째 과제 : 어셈블리어로 키보드 스크린 입출력 제어 

1. 키보드 입력 받으면 화면 꺼멓게

2. 키보드 입력이 없으면 화면 허옇게

 

@INIT
0;JMP
     (INIT)
@SCREEN  //start screen addr
D=A
@cur_addr //cur screen addr
M=D
@8192      //total screen register num
D=A
@SCREEN
D=M+D
@end_addr // end screen addr
M=D
     (LOOP)
@KBD
D=M
@BLACK     // when key is pressed, val is greater than 0 
D;JGT
@WHITE
0;JMP
     (WHITE)
@cur_addr
D=M
@SCREEN
D=D-M
@LOOP
D;JEQ        // cur_addr ==screen  dont ram[cur_addr]=1 and  cur_addr -= -1
@cur_addr
A=M
M=0
@cur_addr
M=M-1
@LOOP
0;JMP
     (BLACK)
@cur_addr
D=M
@end_addr
D=D-M
@LOOP
D;JEQ         // cur_addr == end__addr dont ran[cur_addr]=-1 and cur_addr+1
@cur_addr
A=M
M=-1         // ram[cur_addr] = -1
@cur_addr
M=M+1      // cur_addr += 1
@LOOP
0;JMP

 

캡처한 gif는 너무 느려서 커지는 바람에 용량 줄인다고 반토막 냈다

동작은 잘되는데

하드웨어 시뮬레이션이라 그런가 클럭 속도 제한때문에 그런건지

아니면 내가 한번에 모든 픽셀을 바꿀줄 몰라서 그런걸까 

생각보다 반영되는게 좀 많이 느리다

 

 

 

 

새벽까지 하긴 해버렸지만 거의 이틀만에

난드투 테트리스 기계/어셈블리어 파트를 마무리했다 

대충 책보는거랑 정리하는거랑 걸리는 시간 차이가..

 

내일 5장 얼마나 진행할수 있을까 ㅋㅋㅋㅋㅋㅋ

어재 수업 빼고는 하루종일 hack 기계어와 어셈블리어를 정리했는데

내용은 절반? 정도 밖에 못했는데 

시간에 들인거에 비해서 분량도 얼마 되지는 않았다..

 

 

 

 

아 뭔가 보다가 조금 이상하다 싶었는데

어제 내가 C명령어를 정리하지 않았었다.

 

 

 

C 명령어

C명령어는 연산 비트(comp, 7비트), 목적지 비트(dest, 3비트), 점프 조건 비트(jmp, 3비트) 세 부분으로 111accccccdddjjj 같이 구성된다.

 

(1) acccccc : 7비트로 ALU가 수행할 연산을 결정하는 comp 비트

* 위 표에 나오다 시피 a가 0일때와, 1일때 수행하는 연산이 따로 나눠져있다.

(2) ddd : 3비트로 ALU 연산 결과를 담을 목적지를 나타내는데 저장하지 않을지(null), M에 담을지(RAM[A]), D 레지스터에 담을지, A레지스터 D레지스터 둘다에 연산 결과를 담을지 등 이 세 비트의 값으로 결정한다. 

(3) jjj : 3비트로 이뤄진 어떤 조건에 따라 점프를 할지를 의미하는 비트로 000인경우 점프 안함, 001 인경우 JGT로 연산 결과가 0보다 클때 점프, 111일때는 연산 결과에 상관없이 무조건 점프하는 JMP 명령이된다.

 

 

C 명령어의 연산 comp

 

 2장에서 ALU 구현한걸 다시 떠올리면, 두 16비트 입력을 받도록 되어있었다. 그러면 이 두 입력을 가지고 어떻게 기계어로 연산을 할수 있을까? 첫번째 입력 x는 데이터 레지스터 D에 있던 값을, 두 번째 입력 y는 어드레스 레지스터 A로 지정한 데이터 메모리 레지스터 M에 담아둔 값을 사용한다.

(이부분이 조금 햇갈리긴한데 그냥 진행한다)

 

 

 

 

 한번 comp의 입출력,  ALU 상태 비트의 입출력 테이블을 같이 보면

comp 명령에서는 a가 0이고, cccccc가 001100일때 D의 값을 가져오고

ALU 상태 입력이 comp의 c비트와 동일하게 001100일때 x를 출력으로 하는걸 볼수있다.

 

 그러니까 c가 001100 이더라도 a가 0일때는 데이터 레지스터 D, 1일때는 데이터 메모리 레지스터 M에있는 값을 가져온다.

 

 

 

ex) 심볼릭과 기계어로 D에 31, RAM[24]에 13이 들어있을때  D + M 연산해서 RAM[24]에 저장하자.

  =>  RAM[24] = 31 + 13

 

심볼릭 기계어(가독성을 위해 4비트씩 띄워쓰기함)
//1.  RAM[24]에 13넣기
@13
D=A
@24
M=D

//2. D = 31
@31
D=31

//3. D+M 후 RAM[24]에 쓰기
@24
M=D+M
0000 0000 0000 1101  // A명령어 13
1110 1100 0001 0000  // C 명령어 A를 로드해서(110000), D에다 저장(010), jmp안함(000)
0000 0000 0001 1000 // 24
1110 0011 0000 1000 // D의 값을 M에 쓰기     RAM[24]에 13넣기 끝

0000 0000 0001 1111 // 31
1110 1100 0001 0000 // A에 저장된 31을 D에다가 넣기

0000 0000 0001 1000 // 24
1111 0000 1000 1000 // RAM[24] = D + M

 

 

주석과 띄워쓰기를 지워 정리하면 hack 컴퓨터에서

RAM[24] = 31 +13 연산을 하는 기계어는 아래와 같다.

 

0000000000001101
1110110000010000
0000000000011000
1110001100001000
0000000000011111
1110110000010000
0000000000011000

1111000010001000

 

 

JMP 비트 사용하기

C 명령어의 JMP 비트는 연산 결과에 따라 어드레스 레지스터 A에 지정된 주소의 명령어를 가져오는(점프)하는 역활을 한다.

 

JMP 예시.  참고  c 명령어 : dest = comp;jmp

- D의 값이 0과 같으면 (LOOP)로 점프하라

@LOOP

D;JEQ

 

 

 

ex) 1에서 N까지의 합 계산하는 어셈블리 (RAM[1] = 1 + 2 + ... +RAM[0]

// 변수 i, sum 초기화
@i
M=1
@sum
M=0
(LOOP) // loop문 시작
@i
D=M
// D(i)-M(R0) = 0이면  i가 R0의 값과 같다 = i가 N까지 도달하였다.

@R0
D=D-M 
 // 모든 수를 더하였으므로 루프를 그만하고 (STOP) 문으로 넘어간다.

@STOP
D;JGT
// STOP으로 점프하지 않았다면 루프를 진행하면 되니 sum = sum + i 연산 하자
@sum
D=M
@i
D=D+M
@sum
D=M
// sum + i 연산을 한 뒤 i = i+1을 하자

@i
M=M+1
// 다시 루프 시작으로 가자
@LOOP
0;JMP
(STOP) // 루프 종료 조건 충족시 아래의 루틴 진행
@sum
D=M
@R1
M=D
(END) 
@END
0;JMP

주석을 지우면 아래와 같다.

@i
M=1
@sum
M=0
@i
D=M
@R0
D=D-M 
@STOP
D;JGT
@sum
D=M
@i
D=D+M
@sum
D=M

@i
M=M+1
@LOOP
0;JMP
(STOP)
@sum
D=M
@R1
M=D
(END)
@END
0;JMP

 

 

A 명령어의 값은 RAM 데이터 메모리의 주소인가? ROM 명령어 메모리의 주소인가?

진짜 A 명령어와 C명령어의 내용이 너무 햇갈린다. C 명령어의 dst와 jmp 둘다 쓸수 있지 않은지, 둘다 동시에 쓴다면 C 명령어 앞에 @R3이 있다면 R3의 값을 가져와서 1. dst 비트에 따라 M or A or D 에다가 C명령어의 연산 결과를 담고, 2. R3으로 JMP 간다고 생각하면 될까? 아니면 dst와 jmp를 동시에 쓰지 않고 dst만 하던가 jmp만 하는지. 지금까지 본 내용들을 보면 jmp와 dst 를 동시에 쓰는것 같지는 않다.

 

 또 A 명령어의 값이 RAM과 ROM의 주소를 동시에 나타대나보니  C 명령어와 함께 너무 햇갈리게 되는데, 뒤에 오는 C 명령어에 M이 존재하는지 여부에 따라 A가 RAM 혹은 ROM의 주소를 나타내는지 판단할 수 있다.

 

뒤에 오는 C 명령어가 M을 사용하는 경우 => A 명령어의 값을 RAM의 주소로 쓴다

C 명령어로 점프를 사용하는 경우 => A 명령어의 값을 ROM의 주소로 쓴다

 

위의 두가지 경우 정도로 이해하고 넘어가자

 

 

 

 

hack 어셈블리어의 심볼

지금까지 본 심볼들은 숫자, 상수와 달리 의미를 가진 알아볼수 있는 문자였다. 어느 위치에 접근할 때마다. 숫자를 주소로 하여 사용하기도 하고, 위의 예시에선 i나 sum 같이 변수를 이용해서 값에 접근하기도 하였다. 근데 지금까지 본 어셈블리어에서는 i, 4 같은 숫자와 문자 뿐만아니라 R1, R3, (LOOP), (END) 같은 다른 것들도 있었는데 이 심볼들은 무엇일까? hack 어셈블리어에는 우리가 만든 심볼 외에도 미리 정의된 심볼들이 있다.

 

미리 정의된 심볼들

1. R0, ..., R15(가상 레지스터)

 hack cpu는 16개의 레지스터를 가지고 있으며 R0은 RAM[0] ~ R15는 RAM[15]이 주소를 가지고 있다. A 레지스터가 주소 값으로 사용 될 때 @3의 경우 @R3가 동일한 동작을 한다고 할수 있긴 하지만, 미리 정의된 심볼을 사용하여 @3은 (확실하진 않지만) 데이터 레지스터로 사용되고 있구나, @R3은 데이터 메모리 주소로 사용되구나를 짐작할수 있는 장점이 있다.

 

2. SP, LCL, ARG, THIS 등

 가상 레지스터 이외에도 스텍 포인터를 나타내는 SP, LOCAL 등 다른 미리 정의된 심볼들이 있지만 가상 머신을 배울때 다루므로 일단 넘어가자.

 

3. SCREEN, KBD

 hack 컴퓨터는 IO장치로 스크린과 키보드를 사용하고 있다. 이 장치들을 사용하기 위해서 memory maped IO, 그러니까 스크린의 메모리 영역에 값을 쓰면 화면상에 표시되고, 키보드의 특정 키를 누르면 해당 키와 매핑된 메모리 주소의 값이 1이되는 식의 형태인데 이 미리 정의된 심볼들은 스크린 매모리맵, 키보드 매모리맵의 시작 주소 base address를 나타낸다. 이 내용은 5장 쯤에 다룬다.

 

4. 라벨 심볼

 이 외에도 앞서 루프문 예제에서 봤듯이 (LOOP), (STOP), (END)와 같이 (내용)과 같은 형태의 심볼을 봤는데 이러한 심볼들을 라벨 심볼이라 한다. 이 라벨 심볼은 바로 다음에 나오는 명령어의 주소를 가리키고 있어 점프 시 라벨 심볼 뒤의 명령어를 시작하게 된다. 그리고 이 라벨 심볼은 대문자로 써야한다.

 

일반 변수 심볼

 앞서 4가지의 미리 정의된 심볼에 대해서 정리하였고, 이런 심볼들 외  i, sum, x같은 것들을 변수 심볼이라 하며 @3을 사용했을때는 숫자 주소라 했지만 @i 일때는 어느 주소를 의미하는지 아직 알아보지는 않았는데 이는 어셈블리 파트에서 배울 수 있다.

 

 

 

스크린이랑 키보드에 내용은 그냥 넘어가고

간단하게 어셈블리어 작성 예시도 위에 했으니 기계어 장 이론은 여기까지 하고

다음 글에서는 이번 장 과제를 정리하자

 

글 하나 하나를 내가 아는 데로 정리하면서 쓰다보니 몇시간이 걸리는데

실제 글 내용은 생각보다 너무 적다.

차라리 교재든 강의 ppt든 번역하는게 더 빠르겠다 ㅠㅠ

3장에서 메모리 구현을 마무리하고,

이제 4장에서는 hack의 기계어와 어셈블리어에 관해서 설명한다.

 

일단 이책 7장 가상 머신 중간까지 완전히 이해했다고 하기는 힘들지만

어느정도 보기는 했는데

 

4장 부터는 어떻게 정리해야되나 싶다.

전처럼 아예 책을 통째로 번역하는짓은 하고 싶지는 않은데,

 

 

 

 

 

기계어와 고급언어

아무튼 이번 장에서는 우리가 구현할 컴퓨터 hack의 기계어와 어셈블리어가 어떻게 되는지

메모리 구조 등에서 간단하게 살펴본다.

 

일단 기계어에 대해서 설명하고 있는데,

보통 강의에서는 간단하게 컴퓨터가 알아들을 수 있는 2진수를 보고 기계어라고 하는 정도로만 설명한다.

 

하지만 우리가 할 내용들은 거기서 조금 더 나아가서

기계어란 CPU가 산술 논리 연산을 하고, 메모리에 읽고 쓰기를 할수 있도록 작성한 언어라고 이해하면 된다.

 

또 보통 강의에서는 기계어와 고급언어 차이를 설명한다면

고급언어는 사람이 보고 이해할수 있고, 기계어는 사람이 이해할수 없는 언어라는 정도로 이야기 하고 넘어가지만.

 

 

 

고급언어의 이식성? 저급언어는 하드웨어 의존적? 무슨소리인가.

기계어와 고급언어의 또 다른 차이점이라 하면

 

앞서 우리가 만들 컴퓨터 hack은 16비트 컴퓨터로 한번에 16비트 크기의 데이터를 처리하는 만큼

레지스터, 메모리, 프로그램 카운터, ALU 등 모두 16비트 입력을 받고 연산 할수있도록 만들었었다.

또, 메모리의 경우 16K 공간을 차지하도록 4K 메모리를 4개를 붙여서 만들었었는데,

 

그러면 hack 컴퓨터의 기계어를 다른 컴퓨터의 프로세서에다가 동일하게 쓸수 있을까?

당연히 안된다. 16비트 길이의 기계어 00000000 11001010를

32비트 컴퓨터에는 길이가 다르니 당연히 쓸수 없고

 

동일한 16비트 컴퓨터라 하더라도 이전에 만든 alu에서 상태 출력으로 zr과 ng가 있었는데,

상태 출력을 없애거나 다른 상태 결과도 출력하도록 혹은 어떻게든간에 수정한다면 기존의 논리 회로가 달라질것이고

(동일한 16비트 기계어에서 동작가능하도록 변경이야 할수는 있겠지만, 기존의 기계어 규칙으로 동작못하게 바꾸면)

 

우리가 이미 만든 alu로 and 연산을 하든 +1 연산을 하는든 뭔 연산을 할때의 기계어와

수정한 프로세서로하는 and 연산, +1 연산을 하도록 하는 기계어가 다를 것이다.

 

그래서 한 프로세서의 기계어는 다른 프로세서에서 사용할수 없으며

이를 하드웨어에 의존적이다. 호환되지 않는다는 식으로 표현한다.

 

x86이니 x64니 arm이니 코어텍스가 뭐니 하는 내용까지는 제대로 정리해본적이 없어서 설명할수는 없지만

이런 이유로 저급언어는 각 프로세스마다 다르기 때문에 하드웨어에 의존적이라고 한다.

 

 

하지만 고급언어는 어떨까?

처음 C언어나 Java, 파이썬 같은 언어를 공부할 때 이식성이 좋다,

하나의 코드로 여러 플랫폼에서 사용가능하다는 식으로 설명한다.

 

나도 처음에 수업 들을때 아무도 이거에 대해서 제대로 설명해주질 않아서

이식성이 좋다는게 무슨 의미인지는 잘 모르지만, 그냥 그러려니 하고 넘어갔었다.

 

이걸 공부하면서 조금 더 와닿게 이해할수 있는데,

여기서 이식성이 좋다는 말은 어느 환경에서 코드를 작성하던 간에

 

이 프로그램을 리눅스면 리눅스, 윈도우면 윈도우, x86, atmega이든지

각 프로세서가 뭔지 동작 할 환경을 선택해서 컴파일하기 때문에

 

하나의 코드로도 사용하는 컴파일러나 가상머신에 따라

해당 프로세서에 적합하게 기계어/바이너리를 만들어내어

똑같은 코드지만 안드로이드에서도 돌아가고 윈도우에서 돌아가고, 다른 MCU 등

다양한 환경에서 실행가능하게 된다는 의미로 이식성, 호환성이 좋다고 하는 거더라

 

하지만 이런 고급언어와 컴파일러같은걸 생각안하고

직접 어셈블리어든 기계어를 바로 작성 한다면 프로세서 마다 다르게 작성해줘야 할거다.

 

 

 

 

 

 

기계어와 구성 요소

그러면 기계어에는 어떤 것들이 있을까?

이미 알겠지만 이진수로 된 바이너리와 알아볼수 있는 문자로 된(=심볼릭한) 어셈블리어가 있다.

 

저자가 말하기를 기계어로 직접 코딩하는 사람은 많진 않지만 이런 저수준 프로그래밍을 할 수 있다면

우리가 일반적으로 쓰는 고급 언어의 동작을 더 잘 이해하고 효율적으로 작성할수 있게 된다고 한다.

 

이미 앞에서 말한거지만 기계어는 단순한 0과 1의 조합에서 조금 더 정확하게 이야기하면

프로세서가 레지스터와 메모리 사이에 값을 읽고 쓰도록 제어하도록 만든 동작 과정인데

 

메모리는 명령어와 데이터들을 저장하는 장치로, 지난 장에서 메모리를 만들때 봤다시피 한 레지스터마다 고유의 주소를 가지고 있어 우리는 이 주소값을 이용하여 해당 메모리에 접근해 값을 읽거나 덮어씌운다.

 

프로세서(CPU)의 경우 구성요소 중 하나인 ALU를 구현했을때 봤다시피 산술, 논리 연산뿐만이아니라 기억장치에 접근하고, 분기 연산을 수행가능하도록 만든 장치라 할수 있다. 분기 연산이 어떻게 되는지는 아직 보지는 않았지만 2장 과제를 하면서 이 게이트, 저게이트 합쳐서 만든 ALU가 실제로 산술논리 연산하는것을 시뮬레이션을 돌리면서 봤었다.

 

프로세서는 레지스터라고 하는 작은 기억장치를 가지고 있는데, 레지스터의 경우 프로세서가 메모리를 접근해서 값을 읽고 쓴다고 했었지만 프로세서와 메모리 사이의 거리는 사람 기준으로 가깝다 해도 컴퓨터의 동작 클럭을 생각하면 너무 먼 거리고 시간도 오래걸릴것이다. 그래서 다양한 이유로 CPU 내부에 사용할수 있도록 만든 작지만 고속의 기억장치를 레지스터라 한다.

 

잠깐 딴 소리지만 레지스터에 대해서 정리하다가 생각난게 그 동안 이전에 컴공 공부하거나 자격증 준비하면서 SRAM은 정적인 비활성메모리다, DRAM은 동적인 비활성메모리다. 정도로만 이해하고 있다가. 전기 자격증도 준비하고, 폴리텍에서 공부하면서 SRAM과 DRAM에 대해서 조금 더 자세히 알게 되었었는데,

 

SRAM의 경우 플립플롭을 이용해서 만든 저용량이지만 고속의 기억장치로 케시메모리로 사용되고,

DRAM의 경우 캐패시터를 이용해서 만든 기억장치로 케시메모리에 비해 느리지만 큰 용량이라 메인 메모리로 사용된다고한다.

 

 

전처럼 아예 책 번역하는게 아니라 생각 정리하는건데도 생각보다 오래걸린다..

 

 

아무튼 다시 레지스터로 돌아와서 다른 cpu도 비슷하겟지만 hack의 레지스터로는 값을 저장하는 데이터 레지스터와 데이터 값 또는 메모리 주소를 저장하는 어드레스 레지스터 두 종류가 있고, 어드레스 레지스터가 가진 주소값에 접근하여 값을 읽고 쓰게 된다. 지난 렘 시뮬레이션때는 어드레스 레지스터의 값은 아니지만 .cmp 파일에서 지정한 주소에 접근해서 값을 읽고 쓰는고 원하는 결과가 나왔는지 비교하는 시뮬레이션을 돌렸었다.

 

 

 

 

기계어 : 바이너리와 심볼릭 

아까 기계어는 2진수 바이너리와 사람이 알아볼 수 있는 심볼릭(=어셈블리어 같은거) 두 가지가 있다고 했다.

R1 + R2한 결과를 R1에다가 저장하라는 연산을 바이너리랑 심볼릭으로 적어본다고 하자.

 

기계어로 쓸때 덧셈 연산을 6비트로 101011, R1과 R2 레지스터는 각각 5비트로 00001, 00010이라 할때 이들을 합쳐 16비트 바이너리 코드로 만들면 1010110001000001 -> 101011(add)/00010(R2)/00001(R1)이 된다.

심볼릭 하게 쓴다면(어셈블리어는 아니지만, 뭔소린지 알수있게) set R1 = R1 + R2 같은 식으로 될거같다.

 

심볼릭을 보면 R, 1, 2, + 같은 심볼(상징, 부호, 기호라고 하는데 부호 정도로 해야지)이 여러 차례 나오게 될텐데 이들을 어셈블리어라고 하며, 이런 심볼들을 기계어로 바꾸는 번역기가 어셈블리어가 된다.

 

아까도 말한거지만 어떤 하드웨어에 상관없이 사용가능한 고급언어와는 다르게 저급 언어는 ALU나 메모리, 레지스터 등에 따라서 다르다지만 비슷한 CPU 제품군 끼리는 동일한 기계어를 쓸수 있도록 되어있다고 한다.

 

 

 

심볼릭 명령어의 예시

1. 산술 논리 연산

load R1, 17        //17을 R1에 넣어라

add R1, R1, R2    //R1, R2를 더해서 R1에 넣어라

load R1, true      //R1에다가 true를 넣어라

and R1, R1, R2    //R1에다가 R1, R2 and 연산 결과를 넣어라

 

2. 메모리 접근 방법

다른 컴퓨터는 어떤지는 모르지만 hack 컴퓨터의 경우 어드레스 레지스터 A를 이용해서 메모리 접근을 한다.

메모리의 15번지에다가 1을 넣고 싶다면,

 

(1) load A, 15 명령으로 어드레스 레지스터 A에다가 1을 넣고

(어드레스 레지스터 A는 값을 임시로 저장하거나, A에 저장된 값은 메모리 레지스터 M의 주소를 의미한다. A에다가 값을 저장한건지 M의 주소를 담는 건지는 기계어에 따라서 구분하니 뒤에 참고하면 된다.)

 

(2) load M, 1 명령으로 메모리 레지스터 M에다가 1을 대입하면 된다.

왜 M에 저장이 안되고 15번지에 저장이 되었는지는 어드레스 레지스터 A에 입력된 값이 곧 메모리 레지스터 M의 주소이기 때문이다. (꼭 A에 저장된 값이 M의 주소값으로만 사용되는 건 아니다.)

 

3. 흐름 제어와 심볼

우리가 C언어를 공부할 때 goto나 어떤 언어를 공부하던간에 반복문을 배웠을거고 정말 자주 사용한다.

goto나 반복문에서는 어떻게든 간에 goto를 만나거나 반복문의 끝에 도달하면 지정한 곳이나

반복문의 앞으로 돌아갔지만 그러면 어셈블리어에서는 어떻게 사용할까?

 

물리적인 주소를 사용하는 경우와 심볼릭 주소를 사용하는 두가지가 있다.

 

일단 물리적인 주소를 사용하는 경우는 돌아갈 장소의 줄을 직접 써서 가고

9: add R2, R2, 1

...

13: goto 9

 

심볼릭을 사용하는 경우는 알아볼 수 있는 심볼을 써서 해당 심볼로 돌아가게 된다.

add r3, r3, 1

...

(LOOP)

...

goto LOOP

 

그러면 물리적인 주소와 심볼릭한 주소를 사용한 경우의 차이점은 무엇일까? 우리가 프로그래밍 언어를 배우면서 상수보다는 변수 쓰는것과 동일하다고 생각할수 있을거같은데, 

 

간단한 출력 예시를 든다면

print(3)

print(a)

 

위의 경우는 상수 3을 쓴 만큼 동작 중에는 바뀔수가 없고, 3대신 2를 출력하고 싶을때 기존의 3을 모두 2를 고쳐야 하면서 코드 작성할때 이해하거나 나중에 디버깅하는데 불편하다.

 

하지만 아래의 심볼릭으로 a를 썻을때는 a 에 3이 들어있더라도 2를 출력하길 원하면 a에 2를 넣으면 전체 a에 반영이 되기도 하고, 프로그램 실행 중에 a를 변경 할 수 이를 재할당 가능하다고 한다. 그래서 이런 재할당 가능한 심볼릭을 쓰면 물리적인 상수 주소를 썻을때보다 조금더 동적으로 변화하는 상황에 알맞게 동작하며 다루기도 좋다.

 

 

 

 

 

 

 

 

HACK 컴퓨터 구조

앞에 기계어가 뭐고, 고급언어가 뭐고, 심볼릭이니 어셈블리어니 하느라 말이 많았지만 이번에는 HACK 컴퓨터의 기계어에 대해서 정리하려고 하는데 HACK 컴퓨터에 대해서 잠깐 말하자면 앞서 구현한대로 HACK은 16비트 단위로 처리하는 폰 노이만 구조의 16비트 컴퓨터이다. 렘 파트에서 마지막에 16K 구현하길래 HACK의 메모리가 16K인줄 알았지만 32K였다.. 16K까지만 구현한 이유가 있는 곧 나오니도 하고 컴퓨터 구조를 간단하게 복습하면

* 이책 어딘가에 봤었는데 어딘질 모르겟다.

 

대표적인 컴퓨터 구조로는 폰 노이만 구조와 하버드 구조가 있다.

두 구조의 가장 큰 차이점이라면 폰 노이만 구조는 데이터메모리와 명령어 메모리가 붙어있어 동일한 통로(버스)로 되어있고, 하버드 구조는 데이터 메모리와 명령어 메모리가 분리되 서로 다른 버스로 읽고 쓴다고 한다.

 

 

폰 노이만 구조에서는 이전에 하드웨어로 직접 프로그래밍을 했던 것과는 달리 메모리에 저장하여 사용하게 되었지만(내장 프로그래밍) 통로가 하나 뿐이라 명령어와 데이터를 한번에 가져 올수없어 상대적으로 느리며(병목현상),

하버드 구조에서는 명령어 메모리와 데이터 메모리를 분리하여 한번에 두개를 가지고 올수 있어 상대적으로 빠른 처리가 가능하나 더 많은 메모리와 버스로 많은 공간을 차지한다고 한다. 

 

 

 

 

 

 

 

 

 

 

HACK의 메모리와 레지스터 그리고 주소

 

 아무튼 HACK 컴퓨터는 32K의 메모리를 가지고 있는데 폰 노이만 구조로 데이터 메모리와 명령어 메모리가 각각 16K씩 공간을 차지하고 있다. 그래서 지난번에 16K까지만 구현했나 보다. 데이터 값들을 담는 데이터 메모리는 값을 읽고 쓰니 RAM, 명령어 메모리는 여기에 저장된 명령어들을 읽기만 하니 ROM이라고 한다. 근데 이거 RAM 16K로 구현하였으니 휘발성 이니 이것도 RAM이라 하는게 맞지 않나? 아니면 명령어를 읽기만 해서 ROM이라 한건지는 좀더 봐야알거같다.

 

메모리와 레지스터 : 위 그림을 보면 데이터 메모리와 명령어 메모리, 아까 레지스터를 설명할 때 이야기한 어드레스 레지스터 A와 데이터 레지스터 D가 있다. 다시 반복하면 어드레스 레지스터 A는 데이터 레지스터와 동일하게 어느 값을 저장하고 있거나 데이터 메모리와 명령어 메모리의 주소를 저장하는 역활을 하는데, A가 주소를 담고 있을때, M = 0을 하면 RAM의 해당 주소에다가 0을 대입하게 된다. 어드레스 레지스터로 지정된 RAM 상의 레지스터를 데이타 메모리 레지스터 M이라 한다. ROM은 이름 그대로 읽기 전용 기억 장치로 차후에 자세히 설명하고, ROM의 레지스터 값은 현재 명령어를 나타낸다.

 

 어드레스 레지스터 A에다가 18을 넣고자 한다면 어셈블리어로 @18(현재 A는 주소가 아닌 값을 저장하는 역할을한다)

이 값을 데이터 레지스터 D에 담고자 한다면 D = A를 하면 된다.

 

 잠깐 뒷 내용을 당겨와서 하면 hack의 심볼릭과 기계어에는 주소와 값을 지정하기 위한 A명령어, 제어 명령을 나타내는 C명령어 두 종류가 있고, 이 hack 컴퓨터는 폰 노이만 구조라 데이터와 명령어 메모리가 하나의 버스를 공유하고 있다보니 hack 컴퓨터의 기계어는 값을 지정하기위해 A 명령어 먼저, 지정한 값을 사용하기 위해 C명령어가 따라와 같이 사용된다.

 

* hack의 심볼릭 예시 - 데이터 레지스터에다가 18 넣기(A는 값 저장 역할)

@18    // A 명령어

D = A  // C 명령어

 

* 주소 처리 : 어드레스 레지스터를 이용하여 RAM 200번지에다가 값 18을 넣기 예시

@18     // A명령어 - 어드레스 레지스터에다가 데이터 레지스터에 임시로 담을 값을 지정  

D=A     // C명령어 - 어드레스 레지스터 값을 데이터 레지스터에 대입

@200   // A명령어 - 어드레스 레지스터에 200 대입 

M=D    // C명령어 - RAM 200번지에다가 데이터레지스터의 값(18) 대입

 

 위의 예시를 보면 A 명령어 다음에 C 명령어에 데이터 메모리(RAM) 레지스터M이 사용되는 경우 A 명령어는 어드레스 역활을 하고, M이 없는 경우 A는 값을 보관하는 역활을 하고 있다.

 

분기 : hack 심볼릭 언어에서 조건 혹은 무조건적으로 특정 위치로 넘어가기 위해 goto를 하는 JMP 명령이 있으며, 무조건적으로 분기/넘어가고자 한다면 C명렁어로 0;JMP를 사용한다. 만약 무조건 56번지에 간다면 A명령어와 같이사용하여 다음 처럼 쓰면 된다.

 

@56

0;JMP

 

하지만 조건에 따라(0과 같거나 JEQ, 크거나 JGT, 작거나JLT) 분기하고자 하는 경우도 가능하며, 조건이 주어질때의 분기는 다음과 같이 할수있다.

 

@56

D;JEQ //D가 0과 같은 경우 56번지로 점프한다.

 

 

변수 이용하기 : 앞서 A 명령어를 사용할때마다 @{숫자}를 넣어서 사용해왔는데, @뒤에는 상수 뿐만이 아니라 심볼도 올수 있다. 예를들어 심볼/변수 x를 26로 지정하여 사용하고자 한다면

 

@26

D=A

@x

M=D

 

이렇게 하면 심볼 x에다가 값으로 26을 넣게 된다. 그런데 이 변수 x의 주소가 어디인지는 좀 더 봐야 알수있을거같고 일단 변수에다가 이런 식으로 값을 넣을수있다 정도로만 알고 넘어가자.

 

빌트인 심볼 : 그리고 hack 어셈블리에는 기본적으로 미리 지정된 빌트인 심볼 16개가 있는데 R0, R1, ..., R15로 R0은 데이터 메모리 0번지, R1는 1번지 ..., R15는 15번지라 할수 있고, 다음 명령어를 수행하면

 

@14

D=A

@R6

M=D

 

RAM[6]에다가 14를 넣은것과 같다고 생각하면 된다. 이 빌트인 레지스터들을 가상 레지스터라 한다.

 

 

 

 

 

 

A명렁어와 C명령어

 조금 전에 hack 어셈블리어를 설명하기 위해 간단하게 A 명령어와 C 명령어 그리고 두 명령어의 심볼릭에 대해서 설명을 했었는데 어셈블리어는 기계어와 1:1 매칭되는 만큼 심볼릭을 아래와 같이 기계어 바이너리로 바꿀수 있다.

 

 A명령어와 C명령어 구분 : hack은 16비트 컴퓨터라 명령어 길이가 16비트인데, 앞에 시작하는 바이너리 수를 보고 A 명령어인지 C명령어인지 구분할 수 있다. 아 잠깐 놓친건데 기계어 바이너리는 2부분으로 나눌수 있는데 opcode와 오퍼랜드이다.  opcode는 이 명령어가 할 동작을 나타내고, 오퍼랜드는 이 명령어의 값 혹은 주소를 나타낸다.

 

 hack 기계어의 MSB, 즉 가장 좌측 비트가 0인 경우 A 명령어로 보고 나머지 15비트는 오퍼랜드가 되겠다. 만약 MSB가 1인 경우에는 C 명령어가 되는데, 뒤의 따라오는 11은 칸을 채워주기 위함이며 나머지 13개 비트가 오퍼랜드가 된다.

 

 

A 명령어 : A 명령어는 1비트를 이미 사용하고 있으므로 표현 가능한 숫자는 15비트, 부호없이 0 ~ 32767까지 표현이 가능하며 2^15만큼의 주소에 접근할수 있게 되겠다. RAM32K를 구현했지만 RAM과 ROM은 각각 16K이고, 16K = 2^10 * 2^4인데다가 실제로는 ROM 전체를 다 사용하지 않아 A 명령어를 통해 모든 메모리 주소에 접근 가능하다.

 

A 명령어의 역할은 다음 3가지로 정리 할 수 있겠다.

(1) 상수를 넣을 떄          ex) @15    D=A

(2) 주소를 지정할 때       ex) @15    D=A       @56      M=D

(3) 점프 목적지 지정할때  ex) @15 0;JMP

 

 

 

아아아 차라리 시뮬레이션 돌릴 때가 나았지 그냥 안짚고 넘어갈수도 없고 너무 많다ㅠㅠ

 

이번에는 3장을 할 차례

 

이 장에서는 다음의 순차 회로들을 만든다.

 

 

1. 1비트 레지스터(비트)

2. 16비트 레지스터(레지스터)

3. RAM8

4. RAM64

5. 프로그램 카운터

6. RAM512

7. RAM4K

8. RAM16K

 

 

여기서는 디지털 논리 회로에 대해서 제대로 설명하지는 않을거지만

전장에서 구현한 논리회로들을

조합논리회로

여기서 구현할 데이터를 저장한다던가, 시간에 따라 처리하는 논리회로를

순차논리회로라 한다.

 

논리회로의 출력에 시간의 영향이 있는지 없는지에 따라

조합논리회로와 순차논리회로가 구분되며

순차 논리회로는 = 조합논리회로 + 기억 논리회로 정도로 생각하면 되겠다.

 

클럭과 순차논리회로에 대해서는 디지털논리회로를 공부하면 잘 설명되어있으니

지연시간이니 기본적인 플립플롭이 뭔지에 대한 내용은 없이(설명 잘할 자신도, 그럴 여유도없고)

 

바로 구현시작한다.

 

이번에 순차논리회로 구현에 사용하는 기본 논리회로로는

데이타 플립플롭 DFF으로, 보통 공부하면 나오는 DS플립플롭에 바Q가 빠진거라 생각하면 될거같다.

 

책 내용 중에서 난드는 안쓰고 왜 DFF를 바로 쓰는지 내용을 설명하는 부분이 있었는데,

난드 게이트가지고 루프 만들고 최적화하기에는 너무 복잡해서 빌트인 DFF를 주니

이걸로 만들라고 한 내용이 있었던걸로 기억한다. 어딘진 잊었지만 대강 그런내용이었던거같다.

 

1. bit(1bit registor)

- 이 책에서는 1비트 레지스터를 비트라 부르기로 하고, DFF 하나를 이용해서 만들라고한다.

- DFF의 문제가 클럭을 받는동안 D 단자 입력을 Q로 내보내기는하는데, 새로운 값이 들어오면 새 값으로 저장할지 기존의 값을 유지할지 둘중 하나를 골라야하나 그럴수가없다.

- 그래서 비트는 (클럭은 당연하니 빼고) 입력으로 in, load, 출력으로 out을받는 형태인데 load의 값에 따라 in을 저장할지, 들어오는 in을 무시하고 기존의 값을 유지할지를 판단한다.

- 새로운 입력 저장 : load(t) =1이면 out(t+1) = in(t)

- 기존의 입력 유지 : load(t)=0, out(t+1) = out(t)

- 1비트 레지스터의 인터페이스가 어떻게 됫는지 찾아보다가 그냥 멀티비트 레지스터도 같이있는걸가져왔다.

그래서 load 값에 따라 저장하고 저장안하도록 어떻게 구현하나.

지난 글에 먹스 잘써먹었던것처럼 먹스써서 load = 1일때 새값이 들어와야하니 in이 넘어가게

load = 0일땐 기존 값을 유지해야하니 dff out이 들어오도록 해주면 된다.

CHIP Bit {
    IN in, load;
    OUT out;

    PARTS:
    Mux(a=loop1, b=in, sel=load, out=muxout);
    DFF(in=muxout, out=loop1, out=out);
}

mux쓸때마다 햇갈리는거지만

sel이 0일대 a, 1일때 b를 출력한다.

 

아무튼 1비트 레지스터 완성

 

 

2. 16비트 레지스터(레지스터)

16비트 레지스터의 경우 방금 구현한 비트를 16개 놓으면 된다.

CHIP Register {
    IN in[16], load;
    OUT out[16];

    PARTS:
    Bit(in=in[0], load=load, out=out[0]);
    Bit(in=in[1], load=load, out=out[1]);
    Bit(in=in[2], load=load, out=out[2]);
    Bit(in=in[3], load=load, out=out[3]);
    Bit(in=in[4], load=load, out=out[4]);
    Bit(in=in[5], load=load, out=out[5]);
    Bit(in=in[6], load=load, out=out[6]);
    Bit(in=in[7], load=load, out=out[7]);
    Bit(in=in[8], load=load, out=out[8]);
    Bit(in=in[9], load=load, out=out[9]);
    Bit(in=in[10], load=load, out=out[10]);
    Bit(in=in[11], load=load, out=out[11]);
    Bit(in=in[12], load=load, out=out[12]);
    Bit(in=in[13], load=load, out=out[13]);
    Bit(in=in[14], load=load, out=out[14]);
    Bit(in=in[15], load=load, out=out[15]);
}

 

 

3. RAM8

드디여! 이름은 자주 듣지만 어떻게 동작하는지는 몰랐던 램을 구현할 차례다.

책에서 설명하기를 RAM이 Random Access Memory인 이유는

 

RAM을 구성하고 있는 어떤 레지스터든 간에

이 레지스터의 크기가 2bit든, 8비트든 16비트든 

레지스터가 1024개든 4048개든 상관없이

모든 레지스터, 그중에 임의로 선택한 레지스터에 접근하는 시간이 동등하며 선택한 즉시 접근하여

Random 임의 Access 접근 Memory가 된다고 한다.

 

 

 

 

이제 8개의 레지스터로 구성된 RAM8을 구현해보자.

여기서 RAMn의 경우 입력으로 16비트 in 단자, 비트 수가 log_2 n인 address 단자,

write할지 read할지 판단하는 1비트인 load 단자가 있고,

 

출력은 16비트 크기인 out 하나가 나온다.

load가 0인경우 읽는 것이므로 현재 시간 t의 해당 주소의 16비트 값을 가져오고

load가 1인경우 읽는 것 자체는 load 0과 동일하게 현재 시간 t의 값을 가져오지만, t+1부터 입력한 값이 쓰여진게된다.

 

어드레스 단자의 경우

저장하는 레지스터의 개수가 8개 인경우,   3비트를 입력

저장하는 레지스터가 64개인경우, 6비트를 입력받는데

mux에서 입력 받는 개수가 늘어늘때 sel단자를 늘리던거랑 동일하게 생각하면 될거같다.

 

일단 RAM8부터 구현해보자.

그냥 머리로하려니까 안되서 그림으로 그리면

하나를 입력받아 여러개 중에 하나 선택하니 demux 쓰는거같은데

 

일단 1비트 4웨이 디먹스를 보면 이런식으로 되어있다.

하지만 우리는 출력이 8개니 8웨이 디먹스를 쓰면 되고,

하지만 입력이 16비트라.. 16개를 써야될거같은데?

 

1비트 8웨이 디먹스를 이렇게 놓는다면

* 8way 디먹스 인터페이스는 입력으로 in, sel[3]  출력으로 a, b, c, d, e, f, g, h다.

이결 16개해서 이런식이면 될거같다.

레지스터에 저장후 값을 출력해야하니

레지스터 8개에서 mux 8way 16bit로 하나를 sel는 그대로해서 출력하자

 

그림과 코드는 대강 이런식인데

CHIP RAM8 {
    IN in[16], load, address[3];
    OUT out[16];

    PARTS:
    DMux8Way(in=in[0], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r00, b=r01, c=r02, d=r03, e=r04, f=r05, g=r06, h=r07);
    DMux8Way(in=in[1], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r10, b=r11, c=r12, d=r13, e=r14, f=r15, g=r16, h=r17);
    DMux8Way(in=in[2], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r20, b=r21, c=r22, d=r23, e=r24, f=r25, g=r26, h=r27);
    DMux8Way(in=in[3], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r30, b=r31, c=r32, d=r33, e=r34, f=r35, g=r36, h=r37);
    DMux8Way(in=in[4], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r40, b=r41, c=r42, d=r43, e=r44, f=r45, g=r46, h=r47);
    DMux8Way(in=in[5], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r50, b=r51, c=r52, d=r53, e=r54, f=r55, g=r56, h=r57);
    DMux8Way(in=in[6], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r60, b=r61, c=r62, d=r63, e=r64, f=r65, g=r66, h=r67);
    DMux8Way(in=in[7], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r70, b=r71, c=r72, d=r73, e=r74, f=r75, g=r76, h=r77);

    DMux8Way(in=in[8], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r80, b=r81, c=r82, d=r83, e=r84, f=r85, g=r86, h=r87);
    DMux8Way(in=in[9], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r90, b=r91, c=r92, d=r93, e=r94, f=r95, g=r96, h=r97);
    DMux8Way(in=in[10], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r100, b=r101, c=r102, d=r103, e=r104, f=r105, g=r106, h=r107);
    DMux8Way(in=in[11], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r110, b=r111, c=r112, d=r113, e=r114, f=r115, g=r116, h=r117);
    DMux8Way(in=in[12], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r120, b=r121, c=r122, d=r123, e=r124, f=r125, g=r126, h=r127);
    DMux8Way(in=in[13], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r130, b=r131, c=r132, d=r133, e=r134, f=r135, g=r136, h=r137);
    DMux8Way(in=in[14], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r140, b=r141, c=r142, d=r143, e=r144, f=r145, g=r146, h=r147);
    DMux8Way(in=in[15], sel[2]=address[2], sel[1]=address[1], sel[0]=address[0]
    , a=r150, b=r151, c=r152, d=r153, e=r154, f=r155, g=r156, h=r157);


    Register(in[0]=r00, in[1]=r10, in[2]=r20, in[3]=r30, in[4]=r40, in[5]=r50,
    in[6]=r60, in[7]=r70, in[8]= r80, in[9] = r90, in[10] = r100, in[11] =r110,
    in[12] =r120, in[13] =r130, in[14]=r140, in[15]=r150,  load=load, out=r0);
    Register(in[0]=r01, in[1]=r11, in[2]=r21, in[3]=r31, in[4]=r41, in[5]=r51,
    in[6]=r61, in[7]=r71, in[8]= r81, in[9] = r91, in[10] = r101, in[11] =r111,
    in[12] =r121, in[13] =r131, in[14]=r141, in[15]=r151,  load=load, out=r1);
    Register(in[0]=r02, in[1]=r12, in[2]=r22, in[3]=r32, in[4]=r42, in[5]=r52,
    in[6]=r62, in[7]=r72, in[8]= r82, in[9] = r92, in[10] = r102, in[11] =r112,
    in[12] =r122, in[13] =r132, in[14]=r142, in[15]=r152,  load=load, out=r2);
    Register(in[0]=r03, in[1]=r13, in[2]=r23, in[3]=r33, in[4]=r43, in[5]=r53,
    in[6]=r63, in[7]=r73, in[8]= r83, in[9] = r93, in[10] = r103, in[11] =r113,
    in[12] =r123, in[13] =r130, in[14]=r143, in[15]=r153,  load=load, out=r3);


    Register(in[0]=r04, in[1]=r14, in[2]=r24, in[3]=r34, in[4]=r44, in[5]=r54,
    in[6]=r64, in[7]=r74, in[8]= r84, in[9] = r94, in[10] = r104, in[11] =r114,
    in[12] =r124, in[13] =r134, in[14]=r144, in[15]=r154,  load=load, out=r4);
    Register(in[0]=r05, in[1]=r15, in[2]=r25, in[3]=r35, in[4]=r45, in[5]=r55,
    in[6]=r65, in[7]=r75, in[8]= r85, in[9] = r95, in[10] = r105, in[11] =r115,
    in[12] =r125, in[13] =r135, in[14]=r145, in[15]=r155,  load=load, out=r5);
    Register(in[0]=r06, in[1]=r16, in[2]=r26, in[3]=r36, in[4]=r46, in[5]=r56,
    in[6]=r66, in[7]=r76, in[8]= r86, in[9] = r96, in[10] = r106, in[11] =r116,
    in[12] =r126, in[13] =r136, in[14]=r146, in[15]=r156,  load=load, out=r6);
    Register(in[0]=r07, in[1]=r17, in[2]=r27, in[3]=r37, in[4]=r47, in[5]=r57,
    in[6]=r67, in[7]=r73, in[8]= r87, in[9] = r97, in[10] = r107, in[11] =r117,
    in[12] =r127, in[13] =r137, in[14]=r147, in[15]=r157,  load=load, out=r7);

    Mux8Way16(a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7, sel=address, out=out);
}

 

코드는 보기도 안좋고 성능도 안좋겠지만 바로 생각나는게 이거라 이렇게 됬다.

동작도 거의 잘 되기는한데 문제가

모든 레지스터에 load를 동일하게 주는 탓에

다른 주소 연산중에 레지스터에 기존에 입력된 값이 사라져서인지

기존의 값을 읽어들이려할때마다 은근히 에러가 꽤 발생하고 있다.

 

 

마지막으로 할건 address 주소만 load를 주고 나머지 레지스터 load는 덮어쓰면 안되니 0을 주면 될거같은데..

디먹스가 이렇게 유용한건지 몰랐는데 만능 디먹스로 해결하자

    DMux8Way(in=load, sel=address, a=r0load, b=r1load, c=r2load, d=r3load,
    e=r4load, f=r5load, g=r6load, h=r7load);
    Register(in[0]=r00, in[1]=r10, in[2]=r20, in[3]=r30, in[4]=r40, in[5]=r50,
    in[6]=r60, in[7]=r70, in[8]= r80, in[9] = r90, in[10] = r100, in[11] =r110,
    in[12] =r120, in[13] =r130, in[14]=r140, in[15]=r150,  load=r0load, out=r0);
    Register(in[0]=r01, in[1]=r11, in[2]=r21, in[3]=r31, in[4]=r41, in[5]=r51,
    in[6]=r61, in[7]=r71, in[8]= r81, in[9] = r91, in[10] = r101, in[11] =r111,
    in[12] =r121, in[13] =r131, in[14]=r141, in[15]=r151,  load=r1load, out=r1);
    Register(in[0]=r02, in[1]=r12, in[2]=r22, in[3]=r32, in[4]=r42, in[5]=r52,
    in[6]=r62, in[7]=r72, in[8]= r82, in[9] = r92, in[10] = r102, in[11] =r112,
    in[12] =r122, in[13] =r132, in[14]=r142, in[15]=r152,  load=r2load, out=r2);
    Register(in[0]=r03, in[1]=r13, in[2]=r23, in[3]=r33, in[4]=r43, in[5]=r53,
    in[6]=r63, in[7]=r73, in[8]= r83, in[9] = r93, in[10] = r103, in[11] =r113,
    in[12] =r123, in[13] =r130, in[14]=r143, in[15]=r153,  load=r3load, out=r3);


    Register(in[0]=r04, in[1]=r14, in[2]=r24, in[3]=r34, in[4]=r44, in[5]=r54,
    in[6]=r64, in[7]=r74, in[8]= r84, in[9] = r94, in[10] = r104, in[11] =r114,
    in[12] =r124, in[13] =r134, in[14]=r144, in[15]=r154,  load=r4load, out=r4);
    Register(in[0]=r05, in[1]=r15, in[2]=r25, in[3]=r35, in[4]=r45, in[5]=r55,
    in[6]=r65, in[7]=r75, in[8]= r85, in[9] = r95, in[10] = r105, in[11] =r115,
    in[12] =r125, in[13] =r135, in[14]=r145, in[15]=r155,  load=r5load, out=r5);
    Register(in[0]=r06, in[1]=r16, in[2]=r26, in[3]=r36, in[4]=r46, in[5]=r56,
    in[6]=r66, in[7]=r76, in[8]= r86, in[9] = r96, in[10] = r106, in[11] =r116,
    in[12] =r126, in[13] =r136, in[14]=r146, in[15]=r156,  load=r6load, out=r6);
    Register(in[0]=r07, in[1]=r17, in[2]=r27, in[3]=r37, in[4]=r47, in[5]=r57,
    in[6]=r67, in[7]=r73, in[8]= r87, in[9] = r97, in[10] = r107, in[11] =r117,
    in[12] =r127, in[13] =r137, in[14]=r147, in[15]=r157,  load=r7load, out=r7);

 

이젠 저장도 잘되고, 로드도 잘되는데

오버플로우 때문인지 중간에 이상한 값이 떠서 자꾸 막힌다..

 

 

그냥 여기서 더 시간낭비하기싫어서

 

남이쓴거 써야겠다.

 

이렇게 단순하게 할수있는걸 내가 뭘한걸까 ㅋㅋㅋㅋ

 

CHIP RAM8 {
    IN in[16], load, address[3];
    OUT out[16];

    PARTS:
    DMux8Way(in=load, sel=address, a=a, b=b, c=c, d=d, e=e, f=f, g=g, h=h);
    Register(in=in, load=a, out=r0);
    Register(in=in, load=b, out=r1);
    Register(in=in, load=c, out=r2);
    Register(in=in, load=d, out=r3);
    Register(in=in, load=e, out=r4);
    Register(in=in, load=f, out=r5);
    Register(in=in, load=g, out=r6);
    Register(in=in, load=h, out=r7);
    Mux8Way16(a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7, sel=address, out=out);
}

 

 

 

 

 

 

 

 

4. RAM64

RAM8에서의 삽질을 멈추고 이번에는 RAM64를 만들어보자.

RAM64는 이름 그대로 레지스터 64개니까 RAM8 8개를 쓰면 된다.

어드레스의 경우 6비트가 되고.

 

 

아까의 삽질을 또 하지 않고 정리하면 이럴거같은데

CHIP RAM64 {
    IN in[16], load, address[6];
    OUT out[16];

    PARTS:
    DMux8Way(in=load, sel[2]=address[5], sel[1]=address[4], sel[0]=address[3],
    a=ram0load, b=ram1load, c=ram2load, d=ram3load, e=ram4load,
    f=ram5load, g=ram6load, h=ram7load);

    RAM8(in=in, load=ram0load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r0);
    RAM8(in=in, load=ram1load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r1);
    RAM8(in=in, load=ram2load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r2);
    RAM8(in=in, load=ram3load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r3);

    RAM8(in=in, load=ram4load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r4);
    RAM8(in=in, load=ram5load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r5);
    RAM8(in=in, load=ram6load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r6);
    RAM8(in=in, load=ram7load, address[2]=address[2], address[1]=address[1], address[0]=address[0], out=r7);

        
    Mux8Way16(a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7,
    sel[2]=address[5], sel[1]=address[4], sel[0]=address[3], out=out);
}

다행이 이번에는 잘된다!

* 시간이 느니까 용량도 너무 커져서 gif 를 반토막 냈다. 그래서 time이 2-3씩 뛴다.

 

 

 

5. 프로그램 카운터

PC는 조금 뒤에 하고 RAM부터 마무리하자

 

6. RAM512

RAM8가지고 RAM64도 만들었는데 안될게있나. 아까 그림 그린거처럼 해보면 될거같다.

CHIP RAM512 {
    IN in[16], load, address[9];
    OUT out[16];

    PARTS:
    DMux8Way(in=load, sel[2]=address[8],sel[1]=address[7],sel[0]=address[6],
    a=r0l, b=r1l, c=r2l, d=r3l, e=r4l, f=r5l, g=r6l, h=r7l);

    RAM64(in=in, load=r0l, address[0..5]=address[0..5], out=r0);
    RAM64(in=in, load=r1l, address[0..5]=address[0..5], out=r1);
    RAM64(in=in, load=r2l, address[0..5]=address[0..5], out=r2);
    RAM64(in=in, load=r3l, address[0..5]=address[0..5], out=r3);
    
    RAM64(in=in, load=r4l, address[0..5]=address[0..5], out=r4);
    RAM64(in=in, load=r5l, address[0..5]=address[0..5], out=r5);
    RAM64(in=in, load=r6l, address[0..5]=address[0..5], out=r6);
    RAM64(in=in, load=r7l, address[0..5]=address[0..5], out=r7);

    Mux8Way16(a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7,
    sel[2]=address[8], sel[1]=address[7], sel[0]=address[6], out=out);
}

 

7. RAM4K

 

남은 RAM4K, RAM16K도 RAM512 내용 복붙해서 조금 고치면 될거같다.

CHIP RAM4K {
    IN in[16], load, address[12];
    OUT out[16];

    PARTS:
    DMux8Way(in=load, sel[2]=address[11],sel[1]=address[10],sel[0]=address[9],
    a=r0l, b=r1l, c=r2l, d=r3l, e=r4l, f=r5l, g=r6l, h=r7l);

    RAM512(in=in, load=r0l, address[0..8]=address[0..8], out=r0);
    RAM512(in=in, load=r1l, address[0..8]=address[0..8], out=r1);
    RAM512(in=in, load=r2l, address[0..8]=address[0..8], out=r2);
    RAM512(in=in, load=r3l, address[0..8]=address[0..8], out=r3);
    
    RAM512(in=in, load=r4l, address[0..8]=address[0..8], out=r4);
    RAM512(in=in, load=r5l, address[0..8]=address[0..8], out=r5);
    RAM512(in=in, load=r6l, address[0..8]=address[0..8], out=r6);
    RAM512(in=in, load=r7l, address[0..8]=address[0..8], out=r7);

    Mux8Way16(a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7,
    sel[2]=address[11], sel[1]=address[10], sel[0]=address[9], out=out);
}

 

 

8. RAM16K

RAM16K는 4K를 4개만 쓰면되니

디먹스, 먹스 8way가 아니라 4way를 쓰자

CHIP RAM16K {
    IN in[16], load, address[14];
    OUT out[16];

    PARTS:
    DMux4Way(in=load, sel[1]=address[13],sel[0]=address[12],
    a=r0l, b=r1l, c=r2l, d=r3l);

    RAM4K(in=in, load=r0l, address[0..11]=address[0..11], out=r0);
    RAM4K(in=in, load=r1l, address[0..11]=address[0..11], out=r1);
    RAM4K(in=in, load=r2l, address[0..11]=address[0..11], out=r2);
    RAM4K(in=in, load=r3l, address[0..11]=address[0..11], out=r3);
    
    Mux4Way16(a=r0, b=r1, c=r2, d=r3,
    sel[1]=address[13], sel[0]=address[12], out=out);
}

 

 

 

으아아아 진짜 이렇게 RAM 16K를 하드웨어로 구현했다.

 

오늘 안에는 CPU까지 할수 있을줄 알았는데,

 

메모리 구현에서 이렇게 오래걸릴줄 몰랐다.

 

아직 PC도 못만들었는데,

 

마지막으로 프로그램 카운터를 만들고 마치자.

 

 

이번에 구현할 카운터는 16비트 카운터로

카운터야 기본적인 동작이 0에서 시작해서 한클럭마다 1씩 올라간다.

 

입력 : 16비트 입력, load, inc, reset

출력 : 16비트 출력

동작

if reset = 1 인경우 out(t+1) = 0

else if  load = 1일때 out(t+1) = in(t)

else if inc = 1 일때는 out(t+1) = out(t) + 1

else  out(t+1) = out(t)

 

내가 구현할 카운터는 16비트 레지스터에다가

reset 기능 load 기능 inc 기능이 필요하니

inc는 앞장에서 만든 증산기 쓰면되고

reset, load는 mux16가지고 다루면 될거같다.

 

 

일단 PC 완성시키고 정리한다고 봤는데

RAM보다 더 어려웠다.

RAM이야 8, 64, 512 .. 여러개다보니 오래걸렸는데

 

이건 뭐 지금까지 했던것들 다써서 만들어야하는 것도 모잘라서

 

프로그램 카운터가 reset을 최우선, 그다음 load를 우선, 그다음 inc, 이더저도 아니면 상태 유지다보니

위 조건에 맞게 겹치지 않게 세 연산을 하나의 레지스터에 저장되도록 회로 배치하는게 쉽지 않았다.

 

 

도저히 어떻게 해야할지 감이 안잡혀 우선 필요해보이는 논리 회로랑, reset 연산, load 연산, inc 연산 따로따로 어떻게 할수 있는지 올려봤다.

문제는 아래의 1, 2, 3 회로들을 단순히 먹스 디먹스로 연결시키면

레지스터를 3개를 쓰는게 되어서 할수가 없었고

 

하나의 레지스터만 쓰도록, 조건을 고려해서 동작하도록 한참 고민해서 완성시켰다!

 

일단 레지스터 로드 단자 부터 고민해봤는데

1. reset, load, inc 순서대로 셋중 하나의 값만을 반영해야 했기 때문에 먹스를 3개 연결한 형태로 만들었다.

2. 레지스터의 입력이 오기전에 reset, load, inc 연산하도록 가장 먼저되는 reset을 레지스터 옆에, 가장 나중에 되는 inc 연산은 멀리 그리고 셋다 아닌경우를 처리하기 위해 먹스로 in을 그대로 레지스터 까지 전달하도록 했다.

 

처음에는 in과 inc연산을 한 in 두개를 inc mux 넣었더니, 기존의 값을 +1하는게 아니라

새로운 입력을 저장해서 +1하는 바람에 레지스터 아웃을 피드백루프해서 in 대신 inc의 입력 으로 넣었다.

 

 

생각보다 어려워서 시간 걸리긴 했지만 완성했고 잘돌아간다.

CHIP PC {
    IN in[16],load,inc,reset;
    OUT out[16];

    PARTS:
    Inc16(in=loop1, out=incout);
    Mux16(a=in, b=incout, sel=inc, out=incmuxout);
    Mux16(a=incmuxout, b=in, sel=load, out=loadmuxout);
    Mux16(a=loadmuxout, b=false, sel=reset, out=resetmuxout);

    Mux(a=false, b=true, sel=inc, out=incmuxload);
    Mux(a=incmuxload, b=true, sel=load, out=loadmuxload);
    Mux(a=loadmuxload, b=true, sel=reset, out=resetmuxload);

    Register(in=resetmuxout, load=resetmuxload, out=loop1, out=out);
}

 

 

 

 

 

와 진짜 지난번에 nand2tetris할때는 alu부터 못넘기고 포기했었는데

드디여 CPU 만들기 앞에 까지 왔다.

 

이렇게 3장을 마무리했지만

이거 다음 장은 기계어에 대해서 소개하는데

 

기계어와 hack 컴퓨터에 쓰는 어샘블리어부터 파악해야

왜 이런 cpu 구조를 갖는지 알수있기 때문이고,

실제 cpu 만드는건 5장 가서야 한다.

 

오늘이면 cpu까지 다만들줄 알았는데,

여유 되는데로 마저 진행해야지

드디어 1장을 끝내고 2장으로 넘어왔다.

 

이번 장에서 구현해야할 논리 회로는

 

1. 반가산기

2. 전가산기

3. 16비트 전가산기

4. Incrementer 입력을 1씩 증가시키는건데 우리말로 뭐라해야할지 모르겠다. 그냥 증산기라 해야지

5. ALU

 

 

다음장(3장)에 레지스터, 메모리를 만들고 다다다음장(5장)에서 CPU 만들고,

 

io장치를 어째저째해주면 하드웨어 구현은 끝이다.

 

아 다음 장을 넘어가면 이전 장에서 구현한 논리회로 대신

 

빌트인 폴더 안에 있는 이미 만들어진 논리회로를 사용한다.

 

 

 

내용은 잘 생각 안나지만 1장에서 한거처럼 난드게이트로 만들수 있는걸 연습해봤고

 

최적화 문제로 이전 장에서 만든것보다는 빨리 동작가능한 빌트인 된걸 사용한다고 나왔던거같다.

 

 

1. 반가산기

반가산기의 진리표, 입출력, 구조는 아래와 같다.

 

 

그대로 hdl을 작성해서 돌리면

CHIP HalfAdder {
    IN a, b;    // 1-bit inputs
    OUT sum,    // Right bit of a + b 
        carry;  // Left bit of a + b

    PARTS:
    Xor(a=a, b=b, out=sum);
    And(a=a, b=b, out=carry);
}

 

잘 된다.

 

2. 전가산기

이번에는 전가산기

반가산기와 차이점이라면 캐리도 같이 입력받아서 가산연산을한다.

반가산기 2개와 or 게이트 1개로 만들 수있다.

진리표는 생략

CHIP FullAdder {
    IN a, b, c;  // 1-bit inputs
    OUT sum,     // Right bit of a + b + c
        carry;   // Left bit of a + b + c

    PARTS:
    HalfAdder(a=a, b=b, sum=ha1sum, carry=ha1carry);
    HalfAdder(a=ha1sum, b=c, sum=sum, carry=ha2carry);
    Or(a=ha2carry, b=ha1carry, out=carry);
}

 

 

캐리도 입력받지만 가산연산이 잘되고있다.

 

 

 

3. 16비트 전가산기

이번에는 16비트 전가산기. 처음에는 캐리가 없으니 반가산기 쓰고 뒤에서부터는 전가산기 15개 정도 쓰면될거같다.

4비트 전가산기의 경우 아래의 그림과 같으니 참고해서 작성해보자

 

CHIP Add16 {
    IN a[16], b[16];
    OUT out[16];

    PARTS:
    HalfAdder(a=a[0], b=b[0], sum=out[0], carry=carry1);
    FullAdder(a=a[1], b=b[1], c=carry1, sum=out[1], carry=carry2);
    FullAdder(a=a[2], b=b[2], c=carry2, sum=out[2], carry=carry3);
    FullAdder(a=a[3], b=b[3], c=carry3, sum=out[3], carry=carry4);
    FullAdder(a=a[4], b=b[4], c=carry4, sum=out[4], carry=carry5);
    FullAdder(a=a[5], b=b[5], c=carry5, sum=out[5], carry=carry6);
    FullAdder(a=a[6], b=b[6], c=carry6, sum=out[6], carry=carry7);
    FullAdder(a=a[7], b=b[7], c=carry7, sum=out[7], carry=carry8);
    FullAdder(a=a[8], b=b[8], c=carry8, sum=out[8], carry=carry9);
    FullAdder(a=a[9], b=b[9], c=carry9, sum=out[9], carry=carry10);
    FullAdder(a=a[10], b=b[10], c=carry10, sum=out[10], carry=carry11);
    FullAdder(a=a[11], b=b[11], c=carry11, sum=out[11], carry=carry12);
    FullAdder(a=a[12], b=b[12], c=carry12, sum=out[12], carry=carry13);
    FullAdder(a=a[13], b=b[13], c=carry13, sum=out[13], carry=carry14);
    FullAdder(a=a[14], b=b[14], c=carry14, sum=out[14], carry=carry15);
    FullAdder(a=a[15], b=b[15], c=carry15, sum=out[15]);
}

 

4. Incrementer 

이번에는 증산기 차례인데, 16비트 입력에다가 1만 더해주면 된다.

그러니까 전가산기에다가 뭘 더붙이면 될거같긴한데

아니다 16비트 전가산기에서 맨 앞에 반가산기 b에다가 한번 True를 주고

나머지 전가산기의 b에는 0 그러니까 False를 주면 될거같다.

CHIP Inc16 {
    IN in[16];
    OUT out[16];

    PARTS:
    HalfAdder(a=in[0], b=true, sum=out[0], carry=carry1);
    FullAdder(a=in[1], b=false, c=carry1, sum=out[1], carry=carry2);
    FullAdder(a=in[2], b=false, c=carry2, sum=out[2], carry=carry3);
    FullAdder(a=in[3], b=false, c=carry3, sum=out[3], carry=carry4);
    FullAdder(a=in[4], b=false, c=carry4, sum=out[4], carry=carry5);
    FullAdder(a=in[5], b=false, c=carry5, sum=out[5], carry=carry6);
    FullAdder(a=in[6], b=false, c=carry6, sum=out[6], carry=carry7);
    FullAdder(a=in[7], b=false, c=carry7, sum=out[7], carry=carry8);
    FullAdder(a=in[8], b=false, c=carry8, sum=out[8], carry=carry9);
    FullAdder(a=in[9], b=false, c=carry9, sum=out[9], carry=carry10);
    FullAdder(a=in[10], b=false, c=carry10, sum=out[10], carry=carry11);
    FullAdder(a=in[11], b=false, c=carry11, sum=out[11], carry=carry12);
    FullAdder(a=in[12], b=false, c=carry12, sum=out[12], carry=carry13);
    FullAdder(a=in[13], b=false, c=carry13, sum=out[13], carry=carry14);
    FullAdder(a=in[14], b=false, c=carry14, sum=out[14], carry=carry15);
    FullAdder(a=in[15], b=false, c=carry15, sum=out[15]);
}

 

 

 

5. ALU

앞서 본 회로들은 금방 했고,

이번에는 이 장에서 가장 중요한 ALU를 만들고자 한다.

hack의 ALU는 다음 구조와 입출력 관계를 갖는다.

 

생긴거만 봐도 참 골치아프게 생겼다.

입력으로 16비트짜리 x,y와 컨트롤비트 zx,nx,zy,ny, f, no가 있는데

zx,zy야 xy를 0으로 만들지 고르는거고, nx,ny는 부정연산을 한다.

f는 1일때 엔드연산, no는 출력을 부정하는 내용이다.

 

출력에는 16비트 out과 zr, ng가 있는데

zr의 경우 out이 0일때 1이 되고, ng의 경우 out이 음수인 경우 1이 되어 출력 결과의 상태를 나타낸다.

 

일단 zxy, nxy 논리연산부터 구현해보자.

z가 들어가면 0과 and16 연산을 하면되고

n이 들어가면 16비트 입력을 not16해주면 될거같다.

 

일단 zx값에 따라 0 or x가 나오도록 해봤다.

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute out = x + y (if 1) or x & y (if 0)
        no; // negate the out output?

    OUT 
        out[16], // 16-bit output
        zr, // 1 if (out == 0), 0 otherwise
        ng; // 1 if (out < 0),  0 otherwise

    PARTS:
    Mux16(a=x, b=false, sel=zx, out=out); //  zx == 0 ? x : 0000000..
}

제대로 구현한게 아니니까 진행 안되는게 맞기도하고 ,

생각한데로 x에 17이 들어갈때 zx=0이면 x가 나오고, 1이면 0이 뜬다.(mux16의 out을 바로 최종 출력으로보냇으니)

일단 이런 느낌으로 한번 그림으로 그리면

일단 nx, ny까지는 그렸다. 

 

 

이제 f와 no만 고려하면 되는데,

no야 앞에서 zxout한거 처럼 not 돌리면되니까 문제없고 f랑 zr, ng 부분만 더 생각해보자

 

if f == 1 ? x + y : x & y이고, mux는 sel이 1일때 아래거(b)를 내보내니

mux a = and(x,y), b=or(x,y), sel=f 대강 이런식이면 될거같다.

 

zr, ng 빼고는 완성!

일단 out만 생각한데로 되는지 보자

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute out = x + y (if 1) or x & y (if 0)
        no; // negate the out output?

    OUT 
        out[16], // 16-bit output
        zr, // 1 if (out == 0), 0 otherwise
        ng; // 1 if (out < 0),  0 otherwise

    PARTS:
    Mux16(a=x, b=false, sel=zx, out=zxout); 
    Mux16(a=y, b=false, sel=zy, out=zyout);

    Not16(in=zxout, out=notzxout);
    Not16(in=zyout, out=notzyout);
    Mux16(a=zxout, b=notzxout, sel=nx, out=nxout);
    Mux16(a=zyout, b=notzyout, sel=ny, out=nyout);

    And16(a=nxout, b=nyout, out=andout);
    Or16(a=nxout, b=nyout, out=orout);
    Mux16(a=andout, b=orout, sel=f, out=fout);

    Not16(in=fout, out=notfout);
    Mux16(a=fout, b=notfout, sel=no, out=out);
}

 

 

계산이 되기는한데 생각한거랑은 좀 다르게 나온다..

자꾸 에러때문에 막혀서 일단 zr, ng부터 구현하고 다시 보자

 

zr은 연산 결과가 0인 경우 1이 되는거니

기존의 out을 mux sel단자에 넣어서 쓰려고 했는데

mux16 sel은 1비트, out은 16비트다.

 

out 모든 비트를 or연산한 결과가 0이면 zr = 1, 아니면 zr=0이 되도록

우선 out을 or16 돌린뒤에 sel에 넣으면 될거같다.

다시보니 or 16은 16비트 두입력 or연산이었네

Or8way 두개, or 1개 쓰자.

 

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute out = x + y (if 1) or x & y (if 0)
        no; // negate the out output?

    OUT 
        out[16], // 16-bit output
        zr, // 1 if (out == 0), 0 otherwise
        ng; // 1 if (out < 0),  0 otherwise

    PARTS:
    Mux16(a=x, b=false, sel=zx, out=zxout); 
    Mux16(a=y, b=false, sel=zy, out=zyout);

    Not16(in=zxout, out=notzxout);
    Not16(in=zyout, out=notzyout);
    Mux16(a=zxout, b=notzxout, sel=nx, out=nxout);
    Mux16(a=zyout, b=notzyout, sel=ny, out=nyout);

    And16(a=nxout, b=nyout, out=andout);
    Or16(a=nxout, b=nyout, out=orout);
    Mux16(a=andout, b=orout, sel=f, out=fout);

    Not16(in=fout, out=notfout);
    Mux16(a=fout, b=notfout, sel=no, out[0..7]=out0to7, out[8..15]=out8to15, out=out);

    Or8Way(in=out0to7, out=or8way1out);
    Or8Way(in=out8to15, out=or8way2out);
    Or(a=or8way1out, b=or8way2out, out=zrsel);
    Mux(a=true, b=false, sel=zrsel, out=zr);
}

 

zr도 구현을 잘 했는데

컨트롤비트가 111111인경우 왜 1이 나오는지 잘 이해가 안된다.

 

계산도 해보니 ng는 그렇다쳐도 값이 좀 이상하게 나온다.

 

다시 진리 테이블을 보니

f=1 일때 or연산이 아니라 + 연산을 해야되더라. +라길래 or연산을 의미하는줄알았다.

 Or16을 Add16으로 바꿔줫더니 out 결과는 잘나온다.

 

이제마지막으로 ng만 처리해주면된다.

ng야 MSB가 1이면 음수로 보면 되니까 mux에 바로연결하자.

sel = 1- > ng = 1

sel = 0 -> ng = 0

 

 

 

으아아아 드디여 진리표만 보고 그림그려서 alu를 구현했다 

 

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit inputs        
        zx, // zero the x input?
        nx, // negate the x input?
        zy, // zero the y input?
        ny, // negate the y input?
        f,  // compute out = x + y (if 1) or x & y (if 0)
        no; // negate the out output?

    OUT 
        out[16], // 16-bit output
        zr, // 1 if (out == 0), 0 otherwise
        ng; // 1 if (out < 0),  0 otherwise

    PARTS:
    Mux16(a=x, b=false, sel=zx, out=zxout); 
    Mux16(a=y, b=false, sel=zy, out=zyout);

    Not16(in=zxout, out=notzxout);
    Not16(in=zyout, out=notzyout);
    Mux16(a=zxout, b=notzxout, sel=nx, out=nxout);
    Mux16(a=zyout, b=notzyout, sel=ny, out=nyout);

    And16(a=nxout, b=nyout, out=andout);
    Add16(a=nxout, b=nyout, out=addout);
    Mux16(a=andout, b=addout, sel=f, out=fout);

    Not16(in=fout, out=notfout);
    Mux16(a=fout, b=notfout, sel=no, out[0..7]=out0to7, out[8..15]=out8to15, out[15]=out15, out=out);

    Or8Way(in=out0to7, out=or8way1out);
    Or8Way(in=out8to15, out=or8way2out);
    Or(a=or8way1out, b=or8way2out, out=zrsel);
    Mux(a=true, b=false, sel=zrsel, out=zr);

    Mux(a=false, b=true, sel=out15, out=ng);    
}

 

 

 

 

2년 전쯤에 했을때는 ALU를 넘기지 못했는데

 

드디어 해냈다!

 

이제 기억장치가 포함된 순차논리회로를 만들고 cpu까지 가면 된다.

 

 

14. Multi-way/Multi-bit multiplexer(4way 16bit)

15. Multi-way/Multi-bit multiplexer(8way 16bit)

 
이제 마지막 기본 논리 회로인 4/8웨이 16비트 먹스만 남았다.
 
 

이름 그대로 16비트 입력 4/8개가 들어오는데 입력 중 하나를 선택해서 내보내는게 되겠다.

 

입력이 4개인 경우 앞에서 본 디먹스 처럼 셀렉터 단자가 2개필요하고,

 

8개일땐 셀렉터단자를 3개로 해주면된다.

 

 

4way 16bit mux의 경우 어떻게 해야할지 감이 전혀 안잡혀서 그냥 검색했다.

https://people.duke.edu/~nts9/logicgates/Mux4Way16.hdl

 

검색한 내용이랑 진리표, 이걸로 그림으로 그리면 대강 이런식이된다.

 

손으로 적긴했는데 글씨가 알아보기에는 좋진 않네

 

반대 순서로 설명하면 sel[1]의 값으로 위의 출력(about)을 쓸지 아래의 출력(cdout)을 쓸지 결정한다.

 

먹스가 2개 있는 곳으로 오면 sel[0]의 값으로 위(about 시 a, cdout시 c)를 쓸지 아래를 쓸지 결정한다.

 

 

 

위 그림 대로 이렇게 구현하면되고

CHIP Mux4Way16 {
    IN a[16], b[16], c[16], d[16], sel[2];
    OUT out[16];

    PARTS:
    Mux16(a=a, b=b, sel=sel[0], out=about);
    Mux16(a=c, b=d, sel=sel[0], out=cdout);
    Mux16(a=about, b=cdout, sel=sel[1], out =out);
}

4웨이 16비트 먹스도 잘 동작한다

 

 

4웨이를 했으니까 8웨이 16비트 먹스도 할수있을거같다.

진리표와 그림부터 그려보자

 

8way 16bit 먹스의 경우 4way 16bit mux 2개와 16bit mux 1개로 구현했다.

 

4way mux로 4개 입력 중에 고르게하고, a~d의것을 고를지 e~h의 것을 고를지를

 

뒤에 16bit mux로 선택하도록 하여 완성

 

CHIP Mux8Way16 {
    IN a[16], b[16], c[16], d[16],
       e[16], f[16], g[16], h[16],
       sel[3];
    OUT out[16];

    PARTS:
    Mux4Way16(a=a, b=b, c=c, d=d, sel[1]=sel[1], sel[0]=sel[0], out=a2dout);
    Mux4Way16(a=e, b=f, c=g, d=h, sel[1]=sel[1], sel[0]=sel[0], out=e2hout);
    Mux16(a=a2dout, b=e2hout, sel=sel[2], out=out);
}

 

 

wa 어떻게든 기본 논리 회로 구현을 마무리했다.

 

여기 나와있는 모든 회로들은 nand를 이용하여 만들었고

 

다음 장부터는 반가산기, 전가산기, ALU 등 부울 대수를 이용한 연산장치를 만들게 된다.

해야될 건 많은데 너무 오래걸리니까

 

좀 간단하게 정리해야지

 

 

4. Xor

Xor 게이트는 nand 4개로, nand+or+and로 만들수 있다.

https://ko.m.wikipedia.org/wiki/XOR_%EA%B2%8C%EC%9D%B4%ED%8A%B8

여기서는 nand + or + and로 해보자

CHIP Xor {
    IN a, b;
    OUT out;

    PARTS:
    Nand(a=a, b=b, out=nandOut);
    Or(a=a,b=b,out=orOut);
    And(a=nandOut, b=orOut, out=out);
}

 

 

 

5. Multiplexer

- (1비트) 멀티플렉서는 입력들 중에 하나를 선택하여 출력으로 내보낸다.

- 입력으로 a, b 그리고 입력 선택을 위한 sel 단자, 출력으로 out 단자가 있다.

- sel이 0이면 출력은 a, 아니면 b를 출력하도록 구현한다.

- 아래는 기본 논리게이트로 구현한 먹스

 

CHIP Mux {
    IN a, b, sel;
    OUT out;

    PARTS:
    // Put your code here:
    Not(in=sel, out=selNot);
    Nand(a=a, b=selNot, out=outNandA);
    Nand(a=sel, b= b, out=outNandB);
    Nand(a=outNandA, b=outNandB, out=out);
}

 실행 결과를 보면

- a = 1, b = 0, sel = 1일때, b의 값을 선택하여 출력이 0이 나오는걸 알수있다.

 

 

 

 

6. Demultiplexer

- 1비트 디멀티플렉서는 입력으로 in, sel, 출력은 a, b가 된다.

- 동작은 sel의 값에 따라 in의 값을 a(sel==0), b(sell==1)로 보낸다.

- 선택받지 못한 출력단자는 0을 출력한다.

CHIP DMux {
    IN in, sel;
    OUT a, b;

    PARTS:
    // Put your code here:
    Not(in =sel, out=selNot);
    And(a=in, b=selNot, out=a);
    And(a=in, b=sel, out=b);
}

 

dmux의 경우 in = 1, sel = 1일때 b 단자에서 1 출력하는걸 볼수있다.

 

 

 

 

 

* 멀티 비트 HDL

- 이제 멀티 비트를 다루는 논리 회로를 구현해야 한다.

- 책에 따르면 내부 버스 핀을 연결하는 경우와 참값 거짓값을 내보내는 경우가 나온다.

   내부 핀 입력 : chipPart1(..., x[i]= u, ...);   chipPart2(..., x[i..j]=v, ...);

   참/거짓값 입력 : chipPart(..., x[0..2] = true, ..., x[6..7] = true, ...); 

* 하지만 이 내용은 멀티 비트 논리 회로를 사용할때 필요하고, 멀티비트 논리회로를 만들때는 여러개를 해줘야될거같다..

 

 

** 16bit 논리회로 구현하는 이유

- 이 책의 3장인가? 4장쯤에 보면 hack 컴퓨터는 16비트씩 데이터를 처리하고, 레지스터 한개가 16비트를 차지한다.

 

 

7. Multi-bit Not(16bit)

이전에 이미 Not게이트를 구현한걸 위 내용 참고해서 16개를 사용해서 16비트 Not게이트를 만들었다.

CHIP Not16 {
    IN in[16];
    OUT out[16];

    PARTS:
    Not(in=in[0], out=out[0]);
    Not(in=in[1], out=out[1]);
    Not(in=in[2], out=out[2]);
    Not(in=in[3], out=out[3]);
    Not(in=in[4], out=out[4]);
    Not(in=in[5], out=out[5]);
    Not(in=in[6], out=out[6]);
    Not(in=in[7], out=out[7]);
    Not(in=in[8], out=out[8]);
    Not(in=in[9], out=out[9]);
    Not(in=in[10], out=out[10]);
    Not(in=in[11], out=out[11]);
    Not(in=in[12], out=out[12]);
    Not(in=in[13], out=out[13]);
    Not(in=in[14], out=out[14]);
    Not(in=in[15], out=out[15]);
}

 

* 좌측은 10진수 우측은 2진수, 음수는 2의 보수

 이전의 Not 게이트 연산때는 0아니면 1만 나왔는데, 이제는 16비트를 다루면서 십진수로 변환되어 나오고 있다.

아래의 분홍색으로 표시한 부분일때의 경우 %B11111111111111같은 식으로 입력이 됬는데,

좌측의 입력 핀에는 -1이 입력된 것으로 나오고 있다.

아마 2진수 1을 2의 보수를 취해 음수로 표현해서 그런것 같다.

2진수 1 = B00000000001

1의 보수로 음수 표현한다면 -1 =B111111111110 같은 식일것이고

2의 보수로 음수 표현한다면 -1 =B111111111111 이 된다. 

그래서 아래의 2진수 입력값이 10진수로 -1이 된다.  

 

8. Multi-bit And(16bit)

9. Multi-bit Or(16bit)

그냥 and, or는 and hdl 작성후 복사해서 파트 이름만  or로 바꿔서 만들었다.

 

CHIP And16 {
    IN a[16], b[16];
    OUT out[16];

    PARTS:
    And(a=a[0], b=b[0], out=out[0]);
    And(a=a[1], b=b[1], out=out[1]);
    And(a=a[2], b=b[2], out=out[2]);
    And(a=a[3], b=b[3], out=out[3]);
    And(a=a[4], b=b[4], out=out[4]);
    And(a=a[5], b=b[5], out=out[5]);
    And(a=a[6], b=b[6], out=out[6]);
    And(a=a[7], b=b[7], out=out[7]);
    And(a=a[8], b=b[8], out=out[8]);
    And(a=a[9], b=b[9], out=out[9]);
    And(a=a[10], b=b[10], out=out[10]);
    And(a=a[11], b=b[11], out=out[11]);
    And(a=a[12], b=b[12], out=out[12]);
    And(a=a[13], b=b[13], out=out[13]);
    And(a=a[14], b=b[14], out=out[14]);
    And(a=a[15], b=b[15], out=out[15]);
}

 

 

CHIP Or16 {
    IN a[16], b[16];
    OUT out[16];

    PARTS:
    Or(a=a[0], b=b[0], out=out[0]);
    Or(a=a[1], b=b[1], out=out[1]);
    Or(a=a[2], b=b[2], out=out[2]);
    Or(a=a[3], b=b[3], out=out[3]);
    Or(a=a[4], b=b[4], out=out[4]);
    Or(a=a[5], b=b[5], out=out[5]);
    Or(a=a[6], b=b[6], out=out[6]);
    Or(a=a[7], b=b[7], out=out[7]);
    Or(a=a[8], b=b[8], out=out[8]);
    Or(a=a[9], b=b[9], out=out[9]);
    Or(a=a[10], b=b[10], out=out[10]);
    Or(a=a[11], b=b[11], out=out[11]);
    Or(a=a[12], b=b[12], out=out[12]);
    Or(a=a[13], b=b[13], out=out[13]);
    Or(a=a[14], b=b[14], out=out[14]);
    Or(a=a[15], b=b[15], out=out[15]);
}

 

 

 

 

10. Multi-bit multiplexer(16bit)

- Mux도 동일하게 16줄짜리 파트 내용 복붙해서 이름만 바꾸려 했는데, sel이 있는걸 잊었다. sel도 추가하자.

CHIP Mux16 {
    IN a[16], b[16], sel;
    OUT out[16];

    PARTS:
    Mux(a=a[0], b=b[0], sel=sel, out=out[0]);
    Mux(a=a[1], b=b[1], sel=sel, out=out[1]);
    Mux(a=a[2], b=b[2], sel=sel, out=out[2]);
    Mux(a=a[3], b=b[3], sel=sel, out=out[3]);
    Mux(a=a[4], b=b[4], sel=sel, out=out[4]);
    Mux(a=a[5], b=b[5], sel=sel, out=out[5]);
    Mux(a=a[6], b=b[6], sel=sel, out=out[6]);
    Mux(a=a[7], b=b[7], sel=sel, out=out[7]);
    Mux(a=a[8], b=b[8], sel=sel, out=out[8]);
    Mux(a=a[9], b=b[9], sel=sel, out=out[9]);
    Mux(a=a[10], b=b[10], sel=sel, out=out[10]);
    Mux(a=a[11], b=b[11], sel=sel, out=out[11]);
    Mux(a=a[12], b=b[12], sel=sel, out=out[12]);
    Mux(a=a[13], b=b[13], sel=sel, out=out[13]);
    Mux(a=a[14], b=b[14], sel=sel, out=out[14]);
    Mux(a=a[15], b=b[15], sel=sel, out=out[15]);
}

 

 

 

11. Multi-way Or(8way)

- 이제 multi-way Or 게이트다! 멀티 웨이 뭐라고 적는게 좋을까 . 그냥 or는 입력을 2개받지만 이건 8way 8개 입력을 받는다는게 다르다.

- 8개의 입력다 or연산하면 되니까, or게이트 7개 가지고 다 or연산하면된다.

* 그림 찾다 보니까 nand2tetris.github.io도 있다. 여기에 설명 코드랑 그림 있으니 참고해도 괜찬을듯

https://nand2tetris-hdl.github.io/

 

HDL API & Gate Design

in[16], load, address[6] out[16]

nand2tetris-hdl.github.io

 

저기 나오는데로 해도되지만 난 그냥 쉽게 이런 식으로 해보려고한다.

비효율적인것 같긴한데 이해하기 쉬우면 됫지.

CHIP Or8Way {
    IN in[8];
    OUT out;

    PARTS:
    Or(a=in[0], b=in[1], out=or1);
    Or(a=or1, b=in[2], out=or2);
    Or(a=or2, b=in[3], out=or3);
    Or(a=or3, b=in[4], out=or4);
    Or(a=or4, b=in[5], out=or5);
    Or(a=or5, b=in[6], out=or6);
    Or(a=or6, b=in[7], out=out);
}

 

 

 

 

12. Multi-way Demultiplexer(4way)

- 이번에는 4way 디먹스다. 4way dmux의 경우 이름 그대로 4개의 길로 출력하는데 출력할 길을 선택하기 위해서 sel 단자 2개가 존재한다.

sel = 11 -> d

sel = 10 -> c

sel = 01 -> b

sel = 00 -> a

셀렉터 단자랑 출력 순서가 좀 햇갈리긴한데 일단 구현해보자.

여기서 and 게이트가 입력 3개를 받으니 고려해서 구현하면 되긴한데 되게 햇갈리네

CHIP DMux4Way {
    IN in, sel[2];
    OUT a, b, c, d;

    PARTS:
    Not(in=sel[1], out=not1);
    Not(in=sel[0], out=not0);
    And(a=in, b=not1, out=and1);
    And(a=and1, b=not0, out=a);

    And(a=in, b=not1, out=and2);
    And(a=and2, b=sel[0], out=b);

    And(a=in, b=sel[1], out=and3);
    And(a=and3, b=not0, out=c);

    And(a=in, b=sel[1], out=and4);
    And(a=and4, b=sel[0], out=d);
}

그래도 생각한데로 in 1일때, sel 값에 따라 생각대로 출력이 되긴한다.

 

 

 

 

 

13. Multi-way Demultiplexer(8way)

4출력 디멀티플렉서 만드는것도 머리햇갈리는데 8출력는 어떻게 하나 싶었지만

잠깐 찾아보니 일렉트로 허브란곳에서 4출력 디먹스 2개로 만드는 그림을 올려둿다.

https://www.electronicshub.org/demultiplexerdemux/

이거 참고해서 만들면 문제없겟지..?

구현하다가 문제가 위의 4출력 디먹스에는 E 단자가 존재한다.

E 단자를 고려하지 않고 hdl을 작성하면 똑같은 디먹스 두개를 사용하게 된다.

    PARTS:
    Not(in=sel[0], out=nota);
    DMux4Way(in=in, sel[1]=sel[1], sel[0]=sel[2]);
    DMux4Way(in=in, sel[1]=sel[1], sel[0]=sel[2]);

 

위 링크에서 잘보니까 E가 enable이더라

A가 0인경우 not으로 위의 디먹스가 사용되어 위 그림기준으로 Y3 ~ Y0중에서 선택

A가 1일때는 아래의 디먹스가 사용되서 Y7 ~ Y4중 하나를 선택한다.

는건 알겠는데 다시보니까

a, b, c, 출력 단자들 맞춰줘야되서 Dmux4way 두개 쓸게아니라 그냥 구현해야되겠다..

 

 

다행인지 불행인지 위의 일렉트로닉스허브에는 

어떻게 구현할지 그림도 있다. 이걸 hdl로 써보는게 ㅈ브ㅔㅂㅈ자체ㅐ

CHIP DMux8Way {
    IN in, sel[3];
    OUT a, b, c, d, e, f, g, h;

    PARTS:
    Not(in=sel[2], out=not2);
    Not(in=sel[1], out=not1);
    Not(in=sel[0], out=not0);

    And(a=in, b=not2, out=and12);
    And(a=and12, b=not1, out=and13);
    And(a=and13, b=not0, out=a);

    And(a=in, b=not2, out=and22);
    And(a=and22, b=not1, out=and23);
    And(a=and23, b=sel[0], out=b);

    And(a=in, b=not2, out=and32);
    And(a=and32, b=sel[1], out=and33);
    And(a=and33, b=not0, out=c);

    And(a=in, b=not2, out=and42);
    And(a=and42, b=sel[1], out=and43);
    And(a=and43, b=sel[0], out=d);


    And(a=in, b=sel[2], out=and52);
    And(a=and52, b=not1, out=and53);
    And(a=and53, b=not0, out=e);

    And(a=in, b=sel[2], out=and62);
    And(a=and62, b=not1, out=and63);
    And(a=and63, b=sel[0], out=f);

    And(a=in, b=sel[2], out=and72);
    And(a=and72, b=sel[1], out=and73);
    And(a=and73, b=not0, out=g);

    And(a=in, b=sel[2], out=and82);
    And(a=and82, b=sel[1], out=and83);
    And(a=and83, b=sel[0], out=h);
}

 

길어서 햇갈리긴 했지만 이상없이 잘동작한다.

 

 

이렇게 많이 구현했는데도

 

2개가 남았다. 시간도 늦었으니 내일해야지.

 

내일 해도 CPU 까지 할수있을지는 모르겠다..

 

 

14. Multi-way/Multi-bit multiplexer(4way 16bit)

15. Multi-way/Multi-bit multiplexer(8way 16bit)

최근 글을 아무것도 안올리고 있었는데,

 

수학 공부부터 해야할까, 알고리즘-자료구조를 해야할까, 파이썬 프로그래밍 하면서 경험을 쌓을까 등등..

 

뭘할지를 고민하다가 지금 하고 있는 것들이 조금씩 질리기 시작했고

(하나만 하면 질리니까 여러개를 느리지만 돌아가면서 하곤 있긴했다.)

 

결국에는 뭐든 만들기 위해서 컴퓨터 구조부터 다시 해보는게 필요하다고 생각이 들었다.

 

 

마침 생각난게 전에 하다가 만 nand2tetris 

 

우리나라에서는 아두이노나 라즈베리파이에 비하면 유명하지는 않다.

 

 

그렇지만 이 nand2tetris의 가장 큰 매력은 이름 그대로 nand 게이트를 이용해서 hack이라고

 

비교적 간단한? 구조의 컴퓨터(cpu, memory, registor 등을 하드웨어 시뮬레이션으로 직접 구현한다!)를 만들고 

 

jack이라고 하는 자바 비스무리한 객체지향 언어를 통해 테트리스를 구현하는 내용을 다루고 있다.

 

 

 

나는 일반적인 대학교 컴공과를 나오지는 않고 얼핏얼핏 디지털 논리회로 수업을 듣긴했었는데

 

처음 디지털 논리회로에 대해서 배울때는 카르노맵까지만 공부했었고, 정보처리기사 자격증 딸때도

 

이 논리회로가 뭐하는건지 몰랐다! 그냥 게이트에 입력이 어떻게 들어가면 어떻게 나오구나를 진리표대로외우기만 했다.

 

 

 

이 프로젝트에 대해서는 전에 밑바닥부터 시작하는 딥러닝 책에서 보고 알게되면서

 

 

우리가 알던 컴퓨터가 논리 게이트로 만들고, 난드게이트만가지고도 컴퓨터를 만들수 있다는것에 놀랐다.

 

처음 이걸 알았을 당시(4~5년 전쯤)에는 원서를 직접 구입해서 따라가려고는 했었지만 

 

 

우리나라에는 한글 자료도 없었고, 영어 원서 자료 뿐이다보니 책 한페이지 한페이지를 번역해가면서 보는데

 

처음에는 1페이지에 3~40분 걸릴만큼 오래걸렸고 힘들어서 포기 하고 말았다.

(2년 전쯤에  이전에 쓰던 블로그를 시작하면서 컴퓨터아나토미 책을 보고 다시 했었던것 같았는데 그때도 가산기인가? 거기까지밖에 가지 못했었다.)

 

 

 

지금이야 남인천 폴리텍 다니면서 그 동안 삽질하면서 영어 실력도 늘고,

 

전기전자에 대해서 공부하고나서 다시 디지털 논리회로를 보니 보는 눈이 달라지게 되었다.

 

and니 or 게이트란게 있는데 이전에는 진리표만 알고 있었다면,

 

지금은 다이오드나 트랜지스터에 대해서 조금이라도 알고나서 이러한 게이트를 어떻게 만드는지를 보니까

 

그동안 그놈의 게이트 게이트라는게 뭐하는 놈인지 덜 막연해지고 조금은 이해된다고 해야될까.

 

 

 

아무튼 블로그에 올려야지 생각만 하고 귀찬아서 안올렸지만 프로테우스라고 하는 전자 시뮬레이터를 이용해서

 

jk 플립플롭 가지고 (책을 참고했지만) 동기 카운터도 만들었고 영어 실력도 전보다는 늘었으니까

 

지금이라면 이 책을 할수 있겠다는 생각이 들었다.

 

그래서 한동안 이책을 보느라 블로그에 아무것도 올리지도 못하고 있었다.

 

전보다 영어 실력이 늘었다 해도 원서 보는건 힘들고 오래걸렸다.

 

 

 

 

기회가 되면 이 프로젝트가 코세라 강의에도 있고 책의 절반 정도(하드웨어  구현)까지는 공개되어있으니까

 

번역이라도 해볼까 싶다.

 

코딩 교육한답시고, 아두이노나 라즈베리파이 같은걸로 깔짝깔짝대는것 보다는 유익하다 생각하는데

 

생각해보니까 처음 코딩 공부할때 해보는게 괜찬은거같네

 

 

프로그래밍은 어느정도 땟는데 컴퓨터 구조를 제대로 느껴보고 싶은 사람에게 딱 맞다.

 

 

기본 게이트에서 테트리스 게임 만드는 것까지 목표로 하다보니,

 

한 개념이 나오면 구현에 필요한 정도로만 짧고 쉽게 이야기하고 넘어가는데

 

어떤 용어가 나오면 그걸 또 어렵게 설명하거나 번역을 엉망으로 해서 이해하기 힘든 책도 많은만큼

 

정말 영어 원서지만 엉터리 번역서보다 이해하기 훨씬 쉬웠다.

 

 

 

....

 

글을 적다가 알았는데 번역서가 나왔었네?

 

책 이름은 밑바닥부터 만드는 컴퓨팅 시스템이다.

 

아무튼 그건 그렇고 1/3 정도는 이미 그냥 읽었고 영어 공부한다 생각하고 계속 봐야겠다.

 

책 저자분이 만드신 홈페이지에 가면 이 HACK 컴퓨터를 FPGA로 실제로 구현했다던가

 

재밌어보이는 프로젝트가 꽤 존재한다.

 

 

 

 

아무튼 이 책은  두 파트로 나눠져있다.

 

1. 하드웨어

2. 소프트웨어

 

여기서 설명하기를 하드웨어 기본 소자인 게이트에서 부터 바닥에서 위로 바텀업식으로 진행하는데

 

1장에서 게이트

2장에서 조합 논리 회로(가산기, ALU)

3장에서 순차 논리 회로(플립플롭->레지스터->메모리)

4장에서는 기계어와 어셈블리어

5장에서는 CPU, IO장치

까지가 하드웨어 구현에 관한 내용이 되겠다.

 

이 이후로는 어셈블리어, 가상머신, 운영체제, 객체지향언어 등에 관한 내용이 있는데

 

아직은 6장 어셈블리어 조금까지만 봤지 뒷 내용은 보지않아서 잘 모른다.

 

 

자 그럼 이 프로젝트를 시작하기 위해서 필요한 툴 부터 다운로드 하자

 

아래의 링크에 들어가면 바로 보인다.

 

옆에 보면 papers나 demos에서 책/ppt 내용, 그리고 실제로 구현한 내용들을 확인할수 있다.

https://www.nand2tetris.org/software

 

 

 

 

굳이 여기서는 게이트가 뭔지 설명 하지도 않고, 설명 잘하는 분들도 많은데

 

진행할수 있는데까지는 대충 진행하는거 올리려고한다.

 

압축 풀고 tools에 들어오면

 

hardware simulator로 시작하면 되는데

 

자바 개발 키트 jdk 설치 안했다고 이런 경고창이 나온다.

 

 

 

jdk에는 오라클 jdk랑 open jdk가 있는데

 

오라클 jdk는 몇년 전부터 유료화 됬으니까 (개인으로 써도 괜찬은진 몰르겟다.)

 

그냥 공짜인 open jdk 다운받아서 쓰려고한다.

 

다운 설정하는 방법은 검색하면 금방 나오니 pass

 

 

------

 

하려했는데 나도 자바 설치하는법 잊어서 찾아봤다

 

이 글은 쓴지 좀오래되서 jdk 버전 13인데, 지금 openjdk 사이트 들어가면 18버전을 다운받을수 있다.

 

https://recipes4dev.tistory.com/173

 

예전에 자바 웹개발자 과정 수업 들으면서 자주 했었던건데 오랜만에 한다.

 

 

 

자바 설치랑 환경변수 잘 등록하고

 

다시 hadware simulator를 실행해보자

 

그러면 콘솔창이 갑자기 떳다 사라지면서 조용해진다.

 

잘못 된줄 알았는데 조금 기다렸더니 곧 시뮬레이터 창이 뜬다.

 

 

 

 

 

첫 번째 장에서 구현해야할 하드웨어 칩으로는

 

Nand, Not, And, Or/XOr, 멀티플렉서, 디멀티플렉서, 멀티비트/다중통로 게이트/먹스 들을 만들면 된다.

 

책 상에는 빌트인(이미 구현된) 칩과 이 빌트인 칩을 왜 쓰는지 어떻게 불러오는지 설명이 있긴한데

 

nand2tetris 사이트에 들어가면 책 나오니까 거기 내용 번역해서 보거나 번역서 보면 나와있다.

 

일단 바로 기본 논리게이트 구현부터 해보자

 

 

 

일단 1장에서 구현해야할 논리게이트는 난드를 제외한 15가지가 있는데,

 

* Nand - 난드 게이트의 경우, 이 프로젝트가 nand 게이트를 이용하여 쌓아올라가는것 만큼 기본 게이트로 제공하여 구현안해도 된다. 이걸로 다른 게이트들 만들고, nand로 만든 여러 게이트들로 조합 -> 순차논리회로->CPU까지 만들어간다.

 

아래의 15가지 기본 논리 게이트를 구현하면 된다! 

 

1. Not

2. And

3. Or

4. Xor

5. Multiplexer

6. Demultiplexer

7. Multi-bit Not(16bit)

8. Multi-bit And(16bit)

9. Multi-bit Or(16bit)

10. Multi-bit multiplexer(16bit)

11. Multi-way Or(8way)

12. Multi-way Demultiplexer(4way)

13. Multi-way Demultiplexer(8way)

14. Multi-way/Multi-bit multiplexer(4way 16bit)

15. Multi-way/Multi-bit multiplexer(8way 16bit)

 

 

 

nand2tetris에서는 HDL 하드웨어 기술 언어로 논리 회로를 작성하면 되고

(책 부록 참조)

 

논리 게이트를 구현하고 테스트 하는 방법은

 

아래의 projects/해당챕터/*.hdl 파일을 텍스트 에디터로 작성후에

 

하드웨어 시뮬레이터로 아래의 projects/해당챕터/*.hdl 파일과 *.tst를 로드해서 실행하면 된다.

 

 

- project 파일 유형 정리 - 

*.hdl : 논리 게이트 인터페이스(입출력이 뭔지), 동작 정의

*.tst : 테스트를 위한 스크립트. *.hdl을 실행한 결과가 *.cmp 파일과 동일한지 보고, 에러없이 일치하면 맞게 구현한것

*.cmp : hdl 파일과 tst 파일로 실행한 결과가 맞는지 비교하기 위한 파일

 

 

not.hdl과 not.hdl 테스트를 위한 not.tst을 로드하면 아래의 사진처럼 나오는데

 

텍스트 에디터로 클래스 정의하듯이 작성하자.

 

CHIP 논리회로명{

     입력단자 a, b, ...

     출력단자 out, ...

 

    PARTS:

       이미 구현된 게이트 or 빌트인 NAND 등으로 원하는 연산이 뜨도록 작성

}

 

 

 

 

이 책에서는 인터페이싱, abstract이라고 자주 표현이 자주 나오는데

 

여기서 말하는 인터페이스란 입력이 몇 개고 어떤 이름으로 들어오고, 출력은 무슨 이름으로 어떻게 나오는지를 말한다.

 

여기서 나오는 과제들은 *.hdl 파일에서 인터페이스 내용들은 이미 정의되어 나오니

 

해당 회로가 원하는 동작을 할수 있도록 PART에서 OUT이 원하는 데로 나오도록 회로들을 잘 연결해주면 된다.

 

일단 Not 게이트부터 시작해보자

 

 

 

 

1. Not

- 입력 : in

- 출력 : out

- 동작 : 입력을 반전시키면 된다

 막상 시작하려고 하니까 nand 게이트로 이걸 어떻게 만드나 싶다. 

not은 입력이 1개고, nand는 2개인데? 책에서 말하기를 난드 하나로 만들수 있다고한다.

난드 게이트 진리표를 보고 조금 생각해보니 알거같다.

x1과 x2에 똑같은 입력이 들어가면, 입력이 0일때 출력이 1, 입력이 둘다 1일때 0인게 보이니까.

Not의 in을 하나의 난드게이트 x1, x2에 바로 연결해주고

Not의 out과 난드게이트 F를 연결해주면 되겠다.

 

* 아까 빌트인 nand 게이트를 쓴다고 했는데 우리는 nand 게이트가 어떻게 구현됬는지 알필요 없이

입출력이 어떻게 되는지(=인터페이스)만 보면된다

빌트인 칩들은 tools/builtinChips에 존재한다.

 

구현 내용인 PARTS는 없고 입출력을 제외하면 BULTIN Nand;만 보이는데 Nand.class의 내용을 읽어서 처리하나보다.

 

이제 Nand 게이트가 어떻게 되어있는지 봤고, Nand로 Not게이트를 어떻게 만드는지도 알았다.

그럼 텍스트 에디터로 not.hdl을 열어서 수정하자.

나는 vs코드나 다른게 없어서 그냥 메모장에다가 했다.

주의 사항은 ';'을 빼먹은것처럼 문법을 틀린경우 칩이 로드되지 않는다.

 

Nand 게이트로 Not.hdl을 구현하고

CHIP Not {
    IN in;
    OUT out;

    PARTS:
    Nand(a=in, b=in, out=out);
}

 

테스트 스크립트를 돌리면 에러없이 입력핀이 0일때 출력핀은 1, 반대의 경우도 잘 나온다.

 

 

 

2. And

- And 게이트는 간단하다.

- 난드게이트에 A, B를 넣은 후 앞에 구현한 Not 게이트의 in에다가 넣어주면 끝.

* hdl 을 작성할때 언더스코어 _를 썻더니 로드가 안된다. 아무래도 매개변수 자리에는 특수문자를 넣으면안되나보다.

CHIP And {
    IN a, b;
    OUT out;

    PARTS:
    // Put your code here:
    Nand(a=a, b=b, out=notIn);
    Not(in=notIn, out=out);
}

 

 

3. Or

or 게이트에서 부터 어떻게 해야할지 머리가 아프기 시작한다.

고민해서 만들 시간 없으니 검색해서 만들자.

- 책에서는 and랑 not 게이트로 만들수 있다고하는데, nand만으로도 or를 만들수 있다. 근데 책대로 해야지

 

위 그림과 똑같이 hdl를 작성하면

CHIP Or {
    IN a, b;
    OUT out;

    PARTS:
    Not(in=a, out=aOut);
    Not(in=b, out=bOut);
    And(a=aOut, b=bOut, out=andOut);
    Not(in=andOut, out=out);
}

 

역시 or 게이트도 잘 동작된다.

그런데 하다보니까 이렇게 에러 안뜨고 잘 넘어가면 되는건지 아리까리하다

다시 책보면 되긴한데 몇일 내내 원서로 보다보니 눈에 잘 들어오지는 않아서

그냥 한번 마지막 Not을 빼고 and 출력을 out으로 해봤다.

 

역시나! 잘못 출력이 나온때부터 테스트는 멈춘다.

다음으로 넘어가기를 눌러줘야 테스트 스크립트가 아래로 가는데 원하는 결과랑은 다르게 나온다.

 

 

 

 

 

앞으로 남은 1장 게이트는 13개.. 이거 왜이렇게 많냐

다른 챕터는 복잡하긴 해도 구현할게 적은데

내용을 줄이던가 해야되겟다.

 

4. Xor

5. Multiplexer

6. Demultiplexer

7. Multi-bit Not(16bit)

8. Multi-bit And(16bit)

9. Multi-bit Or(16bit)

10. Multi-bit multiplexer(16bit)

11. Multi-way Or(8way)

12. Multi-way Demultiplexer(4way)

13. Multi-way Demultiplexer(8way)

14. Multi-way/Multi-bit multiplexer(4way 16bit)

15. Multi-way/Multi-bit multiplexer(8way 16bit)

+ Recent posts