본문 바로가기

컴퓨터/컴퓨터구조

2.1강 - BSV 조합회로 (Combinational Circuit)

Combinational circuit 만들기 with BSV

Combinational circuit이란?

Combinational circuit은 입력값에 따라 출력값이 결정되는 회로를 의미한다. 즉, 현재의 입력값만으로 출력값이 결정되며, 이전의 입력값이나 출력값에 의해 영향을 받지 않는다. 반대되는 개념은 Sequential circuit으로 매 clock마다 상태가 변하는 회로이다.

따라서 combinational circuit을 value expression으로 표현할 수 있다. 이때 value expression은 상수(constant), 변수(variable), 연산자(operator) 로 이루어진다. 다만, 소프트웨어 프로그래밍에서의 변수와는 조금 다른 의미를 가진다. 변수는 계산 그래프 상에서의 간선이라고도 생각할 수 있는데, 존재하는 상수를 연산자로 옮기는 "wire"으로서 역할을 한다. 따라서 변하는 메모리 공간보다는 wire의 이름이라고 생각하는 것이 옳다. 예를 들어 a=a*a라는 식에서 왼쪽의 a와 오른쪽의 a는 서로 다른 wire이다. 즉, a2=a1*a1라고 생각하는 것이 더 정확하다.

BSV 언어 기초

본격적으로 combinational circuit을 만들기 전에 BSV 언어의 기초 를 알아보자. 이전 강의에서는 BSV의 구조적인 측면을 논했다면 이번 강의에서는 BSV의 문법적인 측면을 다룬다.

변수1

type var[=init]; 꼴로 선언한다. 예를 들어

int x,y = 23, z;  // int 선언
Bool b; // Bool 선언
z = x + y; // z에 x+y 대입
x = z*z;
b = (x > 23);

와 같다.
한편, 앞서 언급한 것과 같이 assign 연산자(=)는 완전히 새로운 wire를 만드는 것이라는 걸 주의하자.

if/else

논리설계에서 다룬 Multiplexer(MUX)와 같다. 예를 들어

int a=10;

if (b) a+3;
else a+5;

위처럼 작성할 수 있다.

for

for (init; cond; update) begin body end 꼴로 작성한다. 예를 들어

Bit#(8) a=10;
for (Integer k=20; k<24; k=k+1) begin
    a=a+k;
end

컴파일러마다 다르겠지만 BSV에서는 for문을 펼쳐서 적용한다. 즉, 하드웨어적인 기능으로써 for가 아닌 소프트웨어적인 기능으로써 for를 사용한다.

변수2

let을 이용하여 auto키워드처럼 사용할 수 있다. 아래 예시를 참고하자

let x = 24'h9BEEF;  // compiler deduces type Bit#(24)
let y = x + 3;     // similarly, y is Bit#(24)
let z = MyStruct{ memberA: exprA, memberB: exprB };  // z is MyStruct type.

함수

함수(function)은 그저 표현식의 추상화일 뿐이다. 호출될시 inline으로 대체된다. 예를 들어

function int add(int x, int y);
    return x + y;
endfunction

// 생략

let d = add(a,b);

let d = a + b;

가 되는 것이다.

타입

  • Integer: 1,2,3, ...
  • Bool: True, False
  • Bit#(n): n-bit integer
  • pair of integers: Tuple2#(int, int)
  • function

...

위와 같은 타입들이 있다. 그중 몇개는 개념적으로 정의된 타입들으로 물리적 실체가 없는 경우가 있다 (e.g. Integer). 그에 반해 Int#(32)와 같이 정의된 타입은 실제로 32비트의 정수를 나타낸다. 그리고 &, |, ^, <<, >>, {...}, +, -, *, ||, &&, ! 등의 연산자들을 사용할 수 있다.

그리고 많이 쓰이는 synonyms은

typedef bit [7:0] Byte;
typedef Bit#(8) Byte;  // same as above
typedef Bit#(64) Word;
typedef Tuple2(a,a) Pair#(type a);
typedef Int#(n) MyInt#(type n);
typedef Int#(n) MyInt#(numeric type n);  // same as above

Combinational circuit 만들기

1-bit Full Adder

위 1bit full adder을 무작정 만들어 보자

function fa(a,b,c_in);
    s = (a^b)^c_in;
    c_out = (a&b) | (c_in&(a^b));
    return {c_out, s};
endfunction

일단 작성해보긴 했는데 문제가 있다. 설계는 있는데 type가 없다. BSV는 type을 강제하기 때문에 type을 지정해주어야 한다.

cf) {}는 두 wire을 합성하는 연산자이다.

function Bit#(2) fa(Bit#(1) a, Bit#(1) b, Bit#(1)c_in);
    Bit#(1) s = (a^b)^c_in;
    Bit#(1) c_out = (a&b) | (c_in&(a^b));
    return {c_out, s};
endfunction

위 코드에서는 function이 Bit#(2)를 반환한다고 명시적으로 선언했고, 세개의 Bit#(1) 인자를 받음을 선언했다.
이렇게 명시적으로 type을 선언해주어야 한다.

한편 Bit#(2) result를 선언하고 이를 반환할 수도 있다. 이 경우 result가 아래와 같이 저장되므로

1 bit 0 bit
result[1] result[1]
function Bit# (2) fa(Bit#(1) a, Bit#(1) b, Bit#(1)c_in);
    Bit#(2) result;
    Bit#(1) s = (a^b)^c_in;
    Bit#(1) c_out = (a&b) | (c_in&(a^b));
    result[0] = s; result[1] = c_out;
    return result;
endfunction

위처럼도 작성할 수 있다.

2-bit Ripple Carry Adder

위 2-bit Ripple Carry Adder를 만들어보자.

function Bit#(3) add(Bit#(2) a, Bit#(2) b, Bit#(1) c_in);
    Bit#(2) s = 0; Bit#(3) c=0; c[=0] = c_in;
    let cs0 = fa(a[0], b[0], c[0]);
    c[1] = cs0[1]; s[0] = cs0[0];
    let cs1 = fa(a[1], b[1], cs0[1]);
    c[2] = cs1[1]; s[1] = cs1[0];
    return {c[2], s};
endfunction

좋다. 이것을 기반으로 n-bit adder를 만들어보자.

n-bit Ripple Carry Adder

function Bit#(TAdd#(w,1)) add(Bit#(w) a, Bit#(w) b, Bit#(1) c_in);
    Bit#(w) s = 0; Bit#(TAdd#(w,1)) c=0; c[0] = c_in;
    let valw = valueOf(w);
    for (Integer i=0; i<valw; i=i+1)
    begin
        let cs = fa(a[i], b[i], c[i]);
        c[i+1] = cs[1]; s[i] = cs[0];
    return {c[valw], s};
endfunction

위와 같이 n-bit adder를 만들 수 있다.

갑자기 valueOf가 나왔다. 이는 BSV에서 제공하는 함수로, type을 받아 value를 반환한다.
이 함수가 필요한 이유는 BSV에서 타입과 값은 완전히 다른 영역에 있기에 이를 변환해주기 위해서이다.
Bit#(w)라 쓸 때 w는 타입으로서 사용되기에 이를 value로 변환하는 과정이라 이해하면 된다.

반대로 TAdd#(w,1)w+1 타입의 숫자끼리의 연산을 위해서 만들어진 함수이다. '타입'을 유지하면서 합연산을 해준다.
cf) TMul은 비슷한 용도로 곱을 해주는 함수이다.

BSV 컴파일러는 위 코드에서 하드웨어 기술에 필요없는 부분들은 모두 제거하고(타입 등) for 문은 쭉 펼치게 된다.

이 부분의 부가 설명을 위해 잠깐 하드웨어기술언어(HDL)의 computation을 살펴보자.

대부분의 HDL은 computation을 두가지로 나눈다

  • static computation: compile-time에 연산되는 부분
  • dynamic computation: run-time에 연산되는 부분

예를 들어 위 코드에서 for문, Integer과 같은 타입들은 static elaboration 과정을 거치는 static computation이 진행된다. 그 결과 기존 코드는 하드웨어에 맞게 변환되고 변환된 코드를 실제 하드웨어에 접목시키거나 시뮬레이션을 돌리는 것이다.