Chapter 3 순차 논리 - [밑바닥부터 만드는 컴퓨팅 시스템]

SeungHeon Kim·2022년 4월 14일
0

3.1 배경

조합 논리의 결과 데이터는 연산이 종료된 후 소멸되기 때문에 더 복잡한 연산을 하기 위해서는 데이터를 기억할 필요가 있다. 이때, 데이터를 기억하고 기억한 데이터를 사용하기 위해서 '순차 논리'를 사용한다.

  • 클록

    마스터 클록(master-clock)이 연속적인 신호를 발생시킨다.
    보통 0과 1로 표시된 두 상태를 연속해서 오가는 것으로 표현한다.

  • 플립플롭

    가장 기본적인 순차 논리 소자이다. 1비트 데이터 입력과 클록 신호 입력에 따라 새로 입력된 데이터를 출력할지 잠금상태가 되어 이전 입력을 유지할지 결정 된다.

  • 레지스터

    DFF(데이터 플립플롭)만으로는 데이터를 원하는대로 저장하고 가져올 방법이 없기 때문에, 이전 시간의 출력 데이터를 다시 입력하는 방법을 사용한 것이 레지스터이다. 여기에서 새로운 데이터를 입력할지 이전 결과를 입력할지를 MUX(멀티플렉서)가 결정한다.

    이렇게 만든 1Bit 레지스터를 이어붙혀서 아래와 같은 w-bit 레지스터를 만들 수도 있다.
    레지스터가 저장할 수 있는 비트의 개수는 폭(width)이라 부르며, 16, 32, 64비트가 그 폭을 뜻한다. 레지스터에 저장되는 멀티비트 값은 보통 단어(word)라 부른다.

  • 메모리

    이제 레지스터를 여러개 쌓아 올려서 여러 저장 공간을 한번에 사용할 수 있는 임의 접근 메모리를 만들 수 있다.
    n개의 각 레지스터에는 접근할 때 사용되는 유일한 주소가 할당된다.
    주소는 어떤 RAM 레지스터에 접근할지를 가리키고, 읽기 연산(load=0)인 경우, RAM은 선택된 레지스터의 값을 바로 출력한다. 쓰기 연산(load=1) 때는 다음 사이클 때 선택된 메모리 레지스터에서 입력값을 받아, 해당 값을 출력하기 시작한다.

  • 계수기

    매 시간 단위마다 내부 상태 값을 증가시키는 순차 칩이다. 대표적인 예시로 프로그램 계수기(program counter)가 있다.

  • 시간 문제

    컴퓨터는 짧은 시간 안에 수 많은 명령을 처리하기 때문에 조합 칩으로만 구성되어 있을 경우, 데이터 경쟁이 발생하기 쉽다. 하지만 순차 칩의 출력은 클록 사이클이 넘어갈 때만 바뀔 수 있기 때문에 '데이터 경쟁'을 피하고 전체 컴퓨터 구조를 동기화 시킬 수 있다.

3.2 명세

3.2.1 데이터 플립플롭

DFF는 모든 메모리 소자의 기본 부품이 된다. 이것들은 모두 하나의 마스터 클록에 연결되어, 거대한 '합창단' 같이 행동한다. 클록 사이클이 시작할 때, 컴퓨터 내 모든 DFF 출력들은 전 사이클의 입력에 따라 맞춰진다. 그 외 시간에는 DFF가 '잠금' 상태가 되는데, 이 말은 입력이 변해도 출력이 곧바로 영향을 받지 않는다는 뜻이다.

3.2.2 레지스터

우리가 비트, 또는 2진 셀이라 부르는 1비트 레지스터는 하나의 정보 비트(0 or 1)을 저장하도록 설계된 소자다. 레지스터(w-bit)는 멀티비트 값을 처리할 수 있다는 점을 제외하면 기본적으로 2진 셀과 API가 동일하다.

3.2.3 메모리

RAM은 직접 접근 메모리 장치로, n개의 w-비트 레지스터를 배열하고 직접 접근 회로를 연결한 소자다. 메모리에 들어간 레지스터의 개수(n) 및 비트 수(w)는 각각 메모리의 크기와 폭이라 부른다.
이제 우리는 폭이 16비트고 크기는 다양한 메모리 계층을 구축할 것이다.

3.2.4 계수기

컴퓨터가 다음번에 실행할 명령의 주소를 기록하는 계수기 칩을 생각해 보자. 일반적인 생태에서 계수기는 매 클록 사이클마다 단순히 상태 값을 1 증가시켜 프로그램의 다음번 명령어를 불러올 수 있게 한다.
그리고 프로그램의 첫 명령 주소가 0이라고 한다면 계수기를 0으로 리셋해서 프로그램을 언제든지 재시작시킬 수 있어야 한다.

3.3 구현

1Bit 레지스터

위에서 설명했듯이 DFF의 출력값을 다시 DFF의 입력으로 넣는 것이 기본 구성이다. 하지만 새로운 입력과 이전 데이터 중에서 충돌이 일어나기 때문에 Mux를 활용해 이를 해결했다.

CHIP Bit {
    IN in, load;
    OUT out;

    PARTS:
    // Put your code here:
    Mux(a=dout, b=in, sel=load, out=selected);
    DFF(in=selected, out=out, out=dout);
}

레지스터

기본적인 16비트 데이터를 저장할 수 있는 레지스터다. 위에서 구현한 1bit 레지스터를 16개 이어 붙혀서 만들었다.

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

    PARTS:
    // Put your code here:
    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]);
}

RAM8

레지스터를 8개 가지고 있는 RAM이다. 여기서 부터는 주소값을 입력으로 받는데, 3비트 주소 데이터로 어디 레지스터에 접근할지 DMux가 결정하고 접근된 레지스터에만 load비트 입력이 주어진다. 그리고 모든 레지스터칩이 작동을 하지만, 결국 그중 RAM의 출력값으로 결정되는 레지스터는 주소값에 해당되는 레지스터다. 이것을 칩의 마지막에 Mux가 담당하게 된다.

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

    PARTS:
    // Put your code here:
    DMux8Way(in=load, sel=address, a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7);
    Register(in=in, load=r0, out=out0);
    Register(in=in, load=r1, out=out1);
    Register(in=in, load=r2, out=out2);
    Register(in=in, load=r3, out=out3);
    Register(in=in, load=r4, out=out4);
    Register(in=in, load=r5, out=out5);
    Register(in=in, load=r6, out=out6);
    Register(in=in, load=r7, out=out7);
    Mux8Way16(a=out0, b=out1, c=out2, d=out3, e=out4, f=out5, g=out6, h=out7, sel=address, out=out);
}

이후의 RAM들은 저장공간이 점점 커지는데, RAM8에서 레지스터를 여러개 사용한 것처럼 RAM을 여러개 사용하면 된다. 그리고 그만큼 주소 비트를 늘리면 충분히 설계할 수 있을 것이다.

PC(Program Counter)

PC의 레지스터에 어떤 명령어 주소가 입력되는지가 중요하다. 그것을 load, inc, reset 비트를 활용해서 최종적으로 레지스터에 어떤 값을 넘겨줄지를 설계하자. 자세한 내용은 주석에 달아 놓았다.

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

    PARTS:
    // Put your code here:
    Inc16(in=din, out=incd);    // 레지스터에서 선택된 입력값의 1증가값을 미리 계산해 놓는다. 
    Mux16(a=din, b=incd, sel=inc, out=out1);    // inc값에 따라서 증가값을 사용할지 결정한다. 
    Mux16(a=out1, b=in, sel=load, out=out2);    // load값에 따라서 새로운 입력을 사용할지 결정한다. 
    Mux16(a=out2, b[0..15]=false, sel=reset, out=reseted);  // reset값에 따라서 선택된 입력을 0으로 리셋할지 결정한다. 
    Register(in=reseted, load=true, out=din, out=out);  // 최종 결정된 입력을 레지스터에 넣어 저장한다. 
}

0개의 댓글