2025년 7월 7일 월요일

기술노트4: C++ 템플릿 크래스와 SystemC의 최소한 이해

기술노트4:
C++ 템플릿 크래스와 SystemC의 최소한의 이해

목차:

1. 개요

2. C++의 크래스: 객체를 담는 그릇
    2-1. 크래스 예: "복소수 다루기"
    2-2. 템플릿 크래스 예: "비트 벡터"
    2-3. 템플릭 크래스 예제 실습

3. SystemC 병렬 시뮬레이션
    3-1. 레지스터 전송 수준
    3-2. 병렬 시뮬레이션 커널 기작
    3-3. 사건 구동 병렬 실행문 예제 실습
    3-4. 감응 지정이 논리회로의 행동에 미치는 영향
    3-5. 재귀적 사건 반복이 금지된 이유

4. 맺음말

[주] 깃-허브 저장소 내용이 매주 갱신되고 있습니다. 기존에 받은 내용이 있다면 갱신 하십시요.

    $ cd ~/ETRI050_DesignKit

    $ git pull

[주] 새로 받는 경우 앞서 발행한 설치 문서를 참고하십시요.

    기술노트1, 2: 가상머신 리눅스 설치[pdf], 오픈-소스 반도체 설계도구 설치[pdf] [바로가기]
    기술노트3: 하드웨어 기술 언어의 코딩 스타일[pdf][바로가기]

--------------------------------

1. 개요

컴퓨팅 환경을 접하며 생활하는 요즘 굳이 전공을 따지지 않더라도 컴퓨팅 언어에 친숙하다. 반도체(하드웨어) 설계 역시 컴퓨팅 언어를 사용하여 설계 생산성을 한층 높여왔다. 이에 약간의 프로그래밍 언어에 대한 이해를 가졌다면 반도체 설계를 시작할 수 있다. 넓은 추상성을 포용하는 C++는 가장 널리 사용되는 컴퓨팅 언어로서 알고리즘의 기술은 물론 하드웨어 시스템 모델링까지 활용 된다. SystemC는 하드웨어를 기술하고 병렬 시뮬레이션 커널을 갖춘 C++의 크래스 라이브러리다. ETRI 0.5um CMOS 공정 표준셀 디자인 킷(이하 '디자인 킷')에서 제공하는 각종 예제와 학습자료들의 테스트벤치는 SystemC 기반으로 작성 되었다. C++의 템플릿 크래스의 최소한 이해와 베릴로그와 비교 그리고 병렬 시뮬레이션 커널의 사건 구동 병렬 시뮬레이션이 작동 하는 원리를 정성적으로 설명한다.

SystemC를 처음 접하면 마치 새로운 언어처럼 보이나 실은 C++ 그 자체다. 다양한 템플릿 크래스를 매우 현명하게 사용 되었을 뿐이다. 본 문서는 SystemC를 활용하여 시스템 수준 모델링을 시작하기전에 C++ 템플릿 크래스의 최소한 이해를 돕기 위한 것이다.

본 문서에서 설명하는 예제의 파일들은 디자인 킷[바로가기]의 튜토리얼 중 아래 디렉토리에서  찾아볼 수 있다.

    $ cd ~/ETRI050_DesignKit/Tutorials/2-2_Verilog_SystemC_in_a_Day

    $ tree
    .
    ├── ex1_template_classC++ 템플릿 크래스의 최소한
    │   └── templated_class.cpp
    ├── ex2_delta_cycle: 사건 구동 시뮬레이션 커널(병렬실행의 원리)
    │   ├── Makefile
    │   ├── sc_delta_cycle.h
    │   ├── sc_delta_cycle_TB.gtkw
    │   ├── sc_delta_cycle_TB.h
    │   └── sc_main.cpp
    ├── ex3_dff_strange
    │   ├── dffD-플립플롭의 행위수준 Verilog 기술
    │   │   ├── dff.v
    │   │   ├── Makefile
    │   │   ├── sc_dff_TB.h
    │   │   └── sc_main.cpp
    │   ├── sc_dffD-플립플롭의 행위수준 SystemC 기술
    │   │   ├── Makefile
    │   │   └── Vdff.h
    │   └── sc_dff_strange: 감응 지정이 행위에 미치는 영향
    │       ├── Makefile
    │       └── Vdff.h
    └── ex4_rsff_recursive_event
        ├── rsff_gates: 게이트 수준 RS-래치 Verilog 기술
        │   ├── Makefile
        │   └── rsff.v
        ├── sc_rsff_gates: 게이트 수준 RS-래치 SystemC 기술
        │   ├── Makefile
        │   ├── sc_main.cpp
        │   ├── sc_rsff_TB.h
        │   └── Vrsff.h
        └── sc_rsff_gates_hier: SystemC 기술(계층적)
            ├── Makefile
            ├── sc_main.cpp
            ├── sc_nand.h
            ├── sc_rsff.h
            └── sc_rsff_TB.h


2. C++의 크래스: 객체를 담는 그릇

SystemC를 맞이하기 전에 먼저 C++의 객체 표현방식을 알아보자. C++를 객체 지향적 언어라고 한다. 객체 지향적 프로그래밍은 현대 컴퓨팅언어의 상식이 되었다. 대부분 컴퓨팅언어에서 객체를 표현하는 형식으로 크래스의 개념을 채용하고 있다. 크래스는 단적으로 복합 자료(변수)를 묶는 C 언어의 구조체(structure)의 확장이다. 자료에 더하여 이 자료를 취급하는 행동(함수)을 함께 묶어 담는다. 

2-1. 크래스의 예: "복소수 다루기"

복합자료를 묶는 가장 널리 활용되는 예가 아마도 복소수(complex number) 체계일 것이다. 복소수체계에서 한 객체는 두개의 값, 허수와 실수로 표현된다.

    Complex_Number = (Real) + (Image)i

이를 컴퓨팅을 위해 자료 구조체로 표현하면 다음과 같다.

    struct Complex_t
    {
        int Real;
        int Image;

    }

복소수 체계를 다루는 방법은 일반적인 수체계와 상이하다. 예를 들어 복소값의 크기(power)는 켤레(conjugate) 복소수를 취한 제곱근 구하기다. 앞서 정의한 복소수 구조체 Complex_t를 활용하여 표현하면 다음과 같다.

    struct Complex_t X;

    mult = (X.Re*X.Re) - (X.Im*X.Im);

    power =  sqrt(abs(mult));

이 계산법은 구조체 X가 복소수 일때 유효하다. 따라서 복소수를 취급하는 방법을 아예 자료 구조체 내에 넣어두는 것이 현명하다.

    class Conplex_t {
        int Re;
        int Im;

        int conjugate(){
            return  (Re*Re - Im*Im);
        }

        int power(){

            return (sqrt(abs(conjugate()));
        }
    };

자료(소속 자료, member data)와 그 자료를 취급하는 방법(소속함수, member function)을 한 그릇에 묷어 놓기 위한 구문 형식이 바로 크래스(class)다. 취급할수 있는 숫자가 정수(int)뿐만 아니라 부동 소숫점 실수(float)도 있으므로 위의 크래스에 더하여 실수형 자료 선언으로 또 만들어야 한다면 매우 불합리하다. 이럴때 아래와 같이 템플릿을 사용한다. 자료형에 맞춰 크래스를 재사용하는 아주 유용한 방법을 제공한다.

    template<typename T>
    class Complex_t {
        T Re;
        T Im;

        public:

        Complex_t(T re, T im) {    // Constructor
            Re = re; Im = im;
        }

        void put_Re(T x) { Re = x;}
        void put_Im(T x) { Im = x;}
        T get_Re() { return Re;}
        T get_Im() { return Im;}
        T conjugate(){ return((real*real)-(image*image));}
        T Power(){ return (sqrt(abs(conjugate())));}
    };

객체를 선언하여 사례화 할 때 자료형을 템플릿에 지정한다.

    Complex_t<int> IntX;

    Complex_t<float> FloatY;

복소수 표현에서 실수부와 허수부만으로 의미가 없으므로 크래스의 내부 자료 Re 와 Im은 외부에 노출되지 않도록 했다. 내부로 접근 하려면 외부 공개된 소속 함수들을 통한다. 이를 위해 공용 public 지정을 하고 있다.

    IntX.put_Image(5);

    float Re_of_Y = Float.Y.get_Real();

크래스와 동일한 이름을 갖는 특별한 함수가 있다. 크래스 객체를 선언 할 때 내부를 구축하는 역활을 한다. 이를 구성자(constructor)라 한다. 구성자 함수는 선언될 때 한번 호출될 뿐이다. 위의 예에서 구성자는 객체 선언과 함께 내부 자료를 초기화한다.

    Complex_t<int>      intX(2,3);

    Complex_t<float>    floatY(3.14, 4.5);

2-2. 템플릿 크래스의 예: "비트 벡터"

템플릿의 활용도는 매우 넓다. C++의 기본 자료형의 경우 비트 폭과 연산 방법이 정해져 있다. 하지만 임의 비트 폭(bit-width)을 갖는 RTL 수준 하드웨어를 묘사하려면 별도의 객체의 표현이 필요하다. 표현뿐만 아니라 이 자료를 다루는 연산자 또한 이에 맞춰져야 한다. 비트 폭이 N 디지털 데이터 자료형을 템플릿 크래스로 선언하면  다음과 같다.

template <u_int N>
class bit_vector_t
{
    bool m_next_val[N];
    bool m_curr_val[N];
    int  nLen;
    char m_sz_val[N+1];

    public:
    bit_vector_t():nLen((int)N)
    {
        for (int i=0; i<N; i++) m_next_val[i] = false;
        for (int i=0; i<N; i++) m_curr_val[i] = false;
    }

    bit_vector_t(const char* szVal):nLen((int)N)
    {
        write(szVal);
    }

    char* to_string();
    void write(const char* szVal);
    int length();

    // overload the | operator
    bit_vector_t operator | (const bit_vector_t& obj1, const bit_vector_t& obj2);

    // overload the & operator
    bit_vector_t operator & (const bit_vector_t& obj1, const bit_vector_t& obj2);

};

크래스 내부 자료형은 2진 디지털 데이터를 표현하기 위해 C++의 bool 형 배열로 선언되었다. 아울러 두가지 구성자를 두고 있는데, 하나는 모두 false 로 초기화 하는 경우와 다은 하나는 임의 값으로 초기화 한다. 고정된 초기값을 갖는 10비트짜리 디지털 객체의 선언은 다음과 같다.

    bit_vector_t<10> A;

    bit_vector<10> B;

임의 비트폭의 상수형 2진 자료를 표현하는 방법(리터럴, literal)은 객체를 정의할 때 약속한다. 위의 경우 문자열(char *)로 하기로 한다. 임의의 초기값을 갖는 10비트짜리 디지털 객체의 선언은 다음과 같다.

    bit_vector_t<10> X("0101010101");

    bit_vector_t<10> Y("1010101010");

객체의 소속 자료로 선언된 부울형 배열을 취급하는 방법을 소속 함수로 가지고 있다. 쓰기방법 write(), 읽어서 문자열로 표현하는 방법 to_string()등이 있다.

    void bit_vector_t::write(const char* szVal)

    {
        if ((int)strlen(szVal)!=nLen)
        {
            fprintf(stderr, "Bit Vector NOT match!\n");
            return;
        }

        for (int i=0; i<strlen(szVal); i++)
            if (szVal[i]=='1')  m_curr_val[i] = true;
            else                m_curr_val[i] = false;
    }

    char* bit_vector_t::to_string()

    {
        for (int i=0; i<(int)N; i++)
            if (m_curr_val[i])  m_sz_val[i] = '1';
            else                m_sz_val[i] = '0';

        m_sz_val[N] = '\0';
        return m_sz_val;
    }

만일 써넣으려는 리터럴 상수가 객체의 선언된 자료형과 일치하지 않을 경우 오류로 간주한다. C++언어 고유의 자료형 사이의 연산에 대하여 내부적으로 규칙을 가지고 있다(컴파일러는 자료형 변환으로 정밀도가 상실될 수 있다는 경고를 낸다). 하지만 예의 bit_vector<> 자료형은 비트 폭이 정확히 일치해야 하므로 연산 규칙을 만들어 주어야 한다. C++는 특별한 객체에 대하여 연산자와 규칙을 정의해 줄 수도 있다. 다음은 비트 단위 논리합(|)과 논리 곱(&) 연산자를 정의한 것이다. 다소 낮설기는 하지만 연산자에 |(), &()처럼 함수 표현이 가능하다. C/C++언어에서 소괄호 ()는 오직 함수의 의미라는 규칙에 매우 충실하게 따르고 있다.

    // overload the | operator
    bit_vector_t operator | (const bit_vector_t& obj1, const bit_vector_t& obj2)
    {
        bit_vector_t<N> Temp;

        for (int i=0; i<N; i++)
            Temp.m_curr_val[i] = obj1.m_curr_val[i] | obj2.m_curr_val[i];

        return Temp;
    }

    // overload the & operator
    bit_vector_t operator & (const bit_vector_t& obj1, const bit_vector_t& obj2)
    {
        bit_vector_t<N> Temp;
        for (int i=0; i<N; i++)
            Temp.m_curr_val[i] = obj1.m_curr_val[i] & obj2.m_curr_val[i];
        return Temp;
    }

크래스의 작은 부분만 살펴 봤다. 이정도 만으로도 C++는 하드웨어 모델링을 위해 만들어진 것은 아닐까라는 생각마져 든다. C++ 언어가 넓은 추상성을 가진 최고의 언어라는 점에 동의할 것이다.


[출처] UML for ESL Design - Basic Principles, Tools, and Applications

SystemC는 하드웨어를 묘사하기 위해 C++의 템틀릿 크래스를 매우 현명하게 활용한 라이브러리다. 2진 논리를 표현할 수 있는 비트 형과 비트 벡터형은 다음과 같다.

    sc_bit

        single bit value: '0', '1'

    sc_bv<N>

        Vector of sc_bit values, N is number of bits

        Methods:
            range(x,y), to_int(), to_uint(), length()
            set_bit(i, d), get_bit(i), to_string()
            and_reduce(), nand_reduce(), nor_reduce() or_reduce(),
            xor_reduce(), xnor_reduce()

2진 수 비트 형 은 물론 하드웨어의 베릴로그와 호환되는 다치 논리(multi-valued) 형도 있다. 

    sc_logic

        single bit value: '0', '1', 'X', 'Z'

    sc_lv<N>

        Vector of sc_logic values, N is number of bits

        Methods:
            range(x,y),to_int(),to_uint(),length(),
            set_bit(i, d), get_bit(i), to_string(),
            and_reduce(), nand_reduce(), nor_reduce(), or_reduce(),
            xor_reduce(), xnor_reduce()

C++의 템플릿 크래스의 기초만 이해하는 것 만으로도 SystemC를 반도체 설계에 활용하는데 충분하다. 최소한 첫만남의 낮설음을 덜 수 있다. 이제 자주 활용하여 익혀보자. 하드웨어 기술자로서 또다른 HDL을 다뤄야 한다는 부담을 갖는다면 피곤할 일이다. C++의 넓은 활용도를 생각해 보길 바란다. 하드웨어 기술자를 위한 SystemC를 개괄적으로 설명한 블로그 글이 있다. 일독을 권한다.

    "Tour of SystemC for Hardware Engineers"
    https://techne-atelier.com/digital-design/a-tour-of-systemc/

2-3. C++ 템플릿 크래스 예제

간단한 예제를 통해 C++ 템플릿 크래스를 익혀보자. '디자인 킷'의 튜토리얼 폴더에 크래스 템플릿의 예제 폴더로 이동,

    $ cd ~/ETRI050_DesignKit/Tutorials/2-2_Verilog_SystemC_in_a_Day/ex1_template_class

파일 목록을 보면 C++ 소스 파일이 한개 있다.

    $ ll

    total 12
    drwxrwxr-x 2 goodkook goodkook 4096 Jul  7 11:09 ./
    drwxrwxr-x 6 goodkook goodkook 4096 Jul  9 17:30 ../
    -rw-rw-r-- 1 goodkook goodkook 3311 Jul  7 11:05 templated_class.cpp

GNU C++ 컴파일러로 실행파일 만들기 한다. 소스 파일 ex_class.cpp를 읽어 컴파일 및 링크하여 얻은 실행화일의 이름은 기본적으로 a.out이다. 다른 이름으로 생성하려면 -o 옵션을 사용한다. 아래의 명령줄에서 실행 파일의 이름은 ex_class 다.

    $ g++ -o ex_class templated_class.cpp

예제를 컴파일 하여 생성된 바이너리 파일을 실행 시킨다. 리눅스 명령줄 환경('쉘' 이라고 한다)에서 현재 디렉토리가 실행 파일 탐색 경로 PATH 에 포함되어 있지 않다. 현재 디렉토리에서 실행파일 ex_class를 찾아 실행한다.

    $ ./templated_class

    Float: Re=2.000000 Im=1.000000
    Int. : Re=4 Im=3
[Conjugate] Float:3.000000 Int.:7
    [Power] Float:1.732051 Int.:2

    1010101010 | 0101010101 = 1111111111
    Bit Vector NOT match!
    Bit Vector NOT match!
    1111100000 & 0001100011 = 0001100000

예제의 C++ 소스 파일 ex_class.cpp를 열어 내용을 보면 위의 실행이 어떻게 출력되었는지 쉽게 이해될 것이다. 쉘 명령 less는 파일을 열어 화면에 페이지 단위로 보여준다.

    $ less templated_class.cpp

간단한 복소수 객체 Complex_t 와 비트 벡터 객체 bit_vector_t를 템플릿 크래스로 정의하고 사용예를 볼 수 있다.

[주]리눅스의 명령줄 환경(Command-Line Interface, CLI) 본-어게인 쉘(bash)의 환경변수 PATH를 출력해 보면 현재 디렉토리가 포함되어 있지 않은 것을 알 수 있다.
    $ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/snap/bin
쉘 명령으로 현재 디렉토리를 환경변수에 포함시킬 수 있다.
    $ PATH=.:$PATH
    $ echo $PATH
 .:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/snap/bin
환경변수 설정은 해당 명령줄 환경에서 만 유효하다. 터미널의 닫으면 무효화 된다. 터미널을 열때 마다 환경변수를 지정하려면 리눅스 본-어게인 쉘 환경 설정 파일 ~/.bashrc 을 수정한다.

3. SystemC 병렬 시뮬레이션 커널

컴퓨팅 언어로 알고리즘을 기술하는 목적을 생각해 보자. 디지털 전자회로의 작동은 매우 낮은 수준의 스위칭 동작으로 근사할 수 있다. 몇개의 트랜지스터를 사용하여 지연된 스위칭으로 디지털 논리회로(논리 게이트와 플립플롭)를 설명 할 수 있으며 수많은 논리소자를 동원하여 거대한 컴퓨팅 시스템을 구성 할 수 있다. 반도체 설계를 위해 트랜지스터를 동원하기 보다 좀더 높은 추상화된 수준에서 알고리즘을 기술 하므로써 높은 설계 생산성을 얻을 수 있다. 인간의 언어와 유사한 컴퓨팅 언어를 사용하여 알고리즘을 기술하고 이를 자동화된 도구(소프트웨어)를 써서 물리적 장치로 바꾸는 방법이 널리 채택되었다.

[주] 아래의 동영상은 트랜지스터 회로가 2진 부호를 받아 어떤 동작을 하는지 쉽게 설명한다.

    HOW TRANSISTORS RUN CODE?

    https://youtu.be/HjneAhCy2N4?si=PDoHQ4b3CiD_72eW

컴퓨팅 언어는 기본적으로 대수 표현을 차용한 문장과 기본 영어 문법을 원용한 제어구조로 구성된다. 전자회로를 대상으로 하므로 전기적 특성이 언어의 규칙에 반영되어야 한다. 대표적으로 할당문의 왼편과 오른편에 놓일수 있는 객체의 속성이 확실히 구분 한다. 이에 덧붙여 전자회로 하드웨어를 표현한 문장의 실행은 모두 동시에 수행되며 절차적 표현을 별도로 수용한다. 문법에 맞춰 작성된 다수의 문장이 의도한 대로 작동할 것이라는 보장은 없다. 큰 비용을 들여 제작된  실물의 동작에 오류가 발견되면 이미 늦다. 시뮬레이션은 실물이 제작되기 전에 그 동작을 미리 확인해 보는 행위다.

3-1. 레지스터 전송 수준

디지털 하드웨어를 레지스터 트랜스퍼 수준(Register Transfer Level, RTL)에서 기술하고 이를 자동화된 도구(합성기 또는 컴파일러)로 논리 회로 소자로 변환하는 방법론은 이미 정점에 이르럿다. RTL을 단적으로 표현하면 다음과 같다.

"비트 단위로 표현된 하드웨어 객체를 클럭의 동기에 맞춘 동작"

C++의 템플릿 크래스로 임의 비트폭을 갖는 하드웨어 객체를 표현하는 방법은 앞선 절에서 설명하였다. 컴퓨팅 언어의 한 문장은 결국 할당 연산자(=)의 오른편에 놓인 계산 결과를 왼편에 놓인 객체에 전송하는 것으로 완료된다.

[주] 컴퓨터에서 한 문장이 수행되는 절차를 따져보면 메모리에서 값을 읽어 CPU에서 계산을 수행 한 후 다시 메모리에 저장한다. 메모리와 CPU는 모두 디지털 전자회로다. 단순한 문장이지만 이 표현에 많은 것들이 생략되어 있다. 문장은 매우 높은 수준의 추상적 표현으로 단지 자료가 이동하는 경로만을 묘사했을 뿐이지 실제 전자회로가 작동하는 절차는 감춰져 있다.

높은 수준의 컴퓨팅 언어에서 할당이 일어나는 순간을 따지지 않는 반면 디지털 하드웨어 언어는 이를 구분 한다. 위 그림에서 (a)는 완결된 문장이 아니다. 연산의 결과가 표현 되어야 문장이다. (b)와 (c)의 문장에서 할당 연산 = 의 의미는 완전히 다르다. (d)와 (e)처럼 문장의 순서에 따라 다른 회로가 될 수 있다. 병렬실행 문장은 순서가 달라도 동일한 회로이어야 한다. 할당에 대한 심도있는 고려가 필요하다. 하드웨어를 묘사하는 언어에서 이 문제를 해결하고자 별도의 할당 연산자 <= 를 정의했지만 혼란만 더하고 있다. C++는 새로운 할당 연산자를 정의할 경우 변종의 언어가 탄생하는 결과를 초래하게 된다. 이 문제는 코딩 스타일[비로가기]로 해결하고 있다.

    [참고] "기술문서4: 하드웨어 언어의 코딩 스타일"[바로가기]

상태만을 표현하는 소프트웨어 언어에서 하드웨어의 사건(상승 또는 하강 엣지)을 검출하고 이를 동작에 반영하려면 별도의 실행 기작을 갖춰야 한다. 이를 위해 하드웨어 시뮬레이터는 문장의 실행이 문장이 놓인 순서에 따르는 절차적 운영 방식외에 사건에 따라 지정된 함수를 호출하는 별도의 장치를 두고 있다. 이를 병렬 실행 시뮬레이션 커널 이라고 한다. 하드웨어를 묘사한 문장을 실행하는 병렬 시뮬레이터는 사건구동과 시간을 운영한다는 점을 기억해 두자. "사건과 동기"는 RTL의 기본 개념중 하나이기도 하다.

3-2. 병렬 실행 시뮬레이터의 기작

컴퓨팅 언어로 묘사한 하드웨어의 병렬성을 모사하는 방법은 의외로 단순하다. 기본 개념은 이미 소프트웨어 개발 기법으로 널리 활용되고 있는 비 선점형 다중처리(Non-preemptive multi-processing)다. 이 기법은 전통적으로 마이크로 프로세서의 인터럽트 처리 기작과 흡사하다. 그래픽 사용자 인터페이스에 활용되는 사건에 대한 대응 함수의 호출(Event & Call-Back function)과 다를바 없다.

SystemC는 하드웨어 객체들을 묘사하기 위한 크래스들의 집합체이며 아울러 병렬실행 시뮬레이션 커널을 가지고 있다. 시뮬레이션 커널의 기작을 정성적으로 살펴보자. SystemC를 구성하는 가장 기본요소는 모듈과 채널이다.

a. 모듈

모듈은 설계의 기본 단위다. 외부와 통신을 위해 입출력 방향을 지정한 포트를 가지고 있으며 사건에 void 소속 함수를 감응되어 불려질 프로세스로 지정할 수 있다. 모듈은 계층적 구조를 취할 수 있다. 하위모듈을 사례화 하고 채널로 모듈 사이를 연결한다.

[그림출처] SystemC and Simulation Kernel

SystemC가 C++의 크래스 라이브러리라고 하지만 여전히 생소하다. 마치 새로운 언어체계처롬 보이는 것은 #defing 매크로를 적극적으로 활용했기 때문이다. 복잡한 크래스 계승을 단순화 할뿐만 아니라 하드웨어 언어와 비교하여 가독성을 높이기 위한 조치이기도 하다.

    #include <systemc.h>

    SC_MODULE(my_module) {

        // Module content (ports, signals, processes, etc.)

          SC_CTOR(my_module) {

        // Constructor code

      }

    };

SC_MODULE() 과 SC_CTOR() 등은 모두 systemc.h 에 정의되어 있는 매크로 들이다. 풀어보면 다음과 같다.

    struct my_module : ::sc_core::sc_module {

      // Module content

      my_module(sc_core::sc_module_name name) : sc_module(name) {

        // Constructor code

      }

    };

하드웨어 묘사에 필요한 기초 크래스들을 계승하고 있는 것을 볼 수 있다. 모듈 뿐만 아니라 채널을 포함해 매우 다양한 속성을 갖는 하드웨어 객체들이 C++의 크래스로 기술된다.

b. 채널

하드웨어의 동작을 기술한 두 프로세스(입출력과 동작이 포함된 함수 또는 모듈 등 뭐든 좋다.) 사이에 연결 통로가 있다고 하자. SystemC는 이 통로를 채널(channel)이라고 부른다. 이 채널은 템플릿 크래스를 활용하여 비트 단위 자료형을 담을수 있고 사건을 감지하는 매개로서 두 저장소를 운용한다. 자료형을 다루는 소속 함수 외에 사건 감지와 보고 그리고 갱신등의 방법들을 가지고 시뮬레이션 커널과 유기적으로 연결되어 있다.

[그림출처] SystemC and Simulation Kernel

설계할 시스템의 필요에 따라 사용자 정의 채널을 만들 수도 있다. SystemC가 기본적으로 제공하는 채널로는 sc_fifo<T>, sc_signal<T>, sc_signal_resolved<>, sc_mutex<T>, sc_semapore<T> 등이 있다. 하드웨어의 묘사에 가장 기본적으로 사용되는 채널은 sc_signal<T>다. 하드웨어 전용 언어인 베릴로그의 wire 와 같다.

c. sc_main()

C/C++ 언어로 작성한 프로그램의 main() 으로 시작 하듯 SystemC로 작성한 하드웨어 시스템 모델의 시작은 sc_main()이다. 매우 단순한 구성으로 시험 대상(Design Under Test, DUT) 모듈을 사례화 하고 시뮬레이션 커널을 시작한다. DUT가 사례화 되면서 모듈 크래스의 구성자가 실행 되어 내부를 구성하는 절차(elaboration)를 수행한다. 이때 사건을 감시할 채널과 이에 구동될 소속함수를 지정한다.

[주] 베릴로그 HDL은 병렬구문을 기본으로 순차구문을 always 영역에 기술 할 수 있도록 하였다. 병렬실행의 관점에서 always 영역은 병렬실행 구문 1개와 같다. 모든 구문이 순차실행인 C++에서 병렬실행의 단위는 함수다. 시뮬레이션 준비과정에서 모듈의 소속 함수를 사건에 구동될 함수로 지정한다. 베릴로그의 always 와 그에 딸린 순차구문 영역을 C++에서 모듈의 사건과 구동 함수 연결은 아래와 같이 설명될 수 있다.

시뮬레이션이 개시되기 전 준비를 마치면 프로그램 실행권은 시뮬레이션 커널이 갖는다. 시뮬레이션 커널은 등록된 사건구동 함수를 모두 한번씩 호출하여  채널에 최초 사건을 일으키도록 한다.

[출처] SystemC: From the Ground-Up 

d. 병렬 시뮬레이션 커널의 스케쥴러

SystemC는 하드웨어를 묘사할 수 있도록 자료형을 갖췄을 뿐만 아니라 병렬 시뮬레이션 커널을 내장하고 있다.

[그림출처] SystemC and Simulation Kernel

SystemC의 시뮬레이션 커널은 비 선점형 다중 프로세스 처리 방식으로 프로세스 사이에 연결된 사건에 의해 구동되는 콜백 함수를 운용한다. 병렬 실행 시뮬레이션 커널을 스케쥴러의 절차는 다음과 같다.

[출처] SystemC를 이용한 시스템 설계

병렬 실행 시뮬레이션 커널은 사건처리와 시뮬레이션 시간 진행을 관리한다. 커널은 시뮬레이션 시간을 멈추고 프로세스들을 호출하여 채널에 쓰기 접근을 허용함으로써 스케쥴러가 개시된다.

(1) 두 프로세스 사이에 연결된 채널을 통해 통신한다. 프로세스에서 채널로 읽기 및 쓰기 접근을 위해 크래스의 소속함수 read()와 write()를 사용한다. 채널 내부에 두개의 저장소를 가지며 이를 비교하여 사건을 탐지한다.

(2) 한 프로세스에서 채널의 쓰기 방법 write()으로 새값을 써넣음으로서 사건 구동 스케쥴러가 개시된다. 이때 써넣게 되는 값은 m_new_val에 저장된다.

(3) 채널 내에서 두 저장소 m_new_val 과 m_cur_val이 다를경우 시뮬레이터에 사건이 발생 했음을 공지한다.

사건 공지 후 시뮬레이터의 실행 제어는 커널로 옮겨간다.

(4) 시뮬레이터는 채널로부터 공지받은 사건들을 모두 수집하여 해당 사건에 감응이 지정된 프로세스를 호출한다.

(5) 프로세스를 호출하기 전에 채널의 새값을 현재 값으로 갱신한다.

(6) 사건에 감응되어 호출된 프로세스는 채널로 읽기 접근 접근한다. 이때 읽히는 값은 갱신된 값 m_cur_val 이다.

채널과 사건처리 커널은 실행을 선점하지 않고 서로 협조 관계에 있다. (1)~(3)의 과정은 채널에서 수행되고 (4)~(5)는 커널의 역활이며 (6)은 프로세스의 수행이다. 어느 프로세스에서 채널로 쓰기 접근함에 따라 사건이 발생하고 이에 감응이 지정된 함수를 호출하는 일련의 과정을 반복한다. 모든 사건이 완료되면 스케쥴러는 시뮬레이션 시간을 진행한다. 시뮬레이션 시간을 멈춘 상태에서 채널에서 발생한 사건을 모두 처리하는 동안을 "시뮬레이션 델타"라 한다. 시뮬레이션 델타가 완료된 후 다음 사건이 발생하지 않으면 시뮬레이션은 중지된다.

3-3. 병렬실행 예제

예제를 통해 SystemC의 병렬 실행 시뮬레이션 커널이 작동 메커니즘을 정성적으로 이해해 보기로 한다. 다음과 같은 회로를 3개의 할당문으로 표현하였다. 하드웨어(논리 게이트 회로)를 표현한 할당문은 각각 병렬 실행 되어야 한다.

예제 디렉토리로 이동,

    $ cd ~/ETRI050_DesignKit/devel/Tutorials/2-2_Verilog_SystemC_in_a_Day/ex2_delta_cycle

    $ ll

    total 28
    drwxrwxr-x 2 goodkook goodkook 4096 Jul 12 13:54 ./
    drwxrwxr-x 8 goodkook goodkook 4096 Jul 11 23:27 ../
    -rw-rw-r-- 1 goodkook goodkook 1274 Jul 12 13:15 Makefile
    -rw-rw-r-- 1 goodkook goodkook  505 Jul 11 23:10 sc_main.cpp
    -rw-rw-r-- 1 goodkook goodkook 1327 Jul 12 13:39 sc_delta_cycle.h
    -rw-rw-r-- 1 goodkook goodkook  723 Jul 11 23:33 sc_delta_cycle_TB.gtkw
    -rw-rw-r-- 1 goodkook goodkook 2535 Jul 11 23:24 sc_delta_cycle_TB.h

시험 대상 DUT을 기술한 파일 sc_delta_cycle.h을 보면 다음과 같다. 사건 구동 프로세스 behavior 내에 할당 연산자의 오른편에 놓인 채널 sc_signal 들을 모두 감응에 지정 하였다. 회로는 3개의 문장을 사용하여 기술되었다. C++의 문법으로 하드웨어를 기술 할 경우 할당 연산자 '='가 하드웨어의 추상성(조합회로 또는 순차회로)을 구분하지 못한다. C++는 절차적 언어로서 병렬실행 문장은 없으므로 하드웨어의 병렬성을 반영하지 못한다. 이를 극복할 방법으로 협력형 병렬실행 커널을 도입하였다. 병렬 실행은 사건 구동 프로세스(함수) 단위로 이뤄지며 할당의 구분은 코딩 스타일에 의해 결정된다.

    // Filename: sc_delta_cycle.h
    #include <systemc.h>

    SC_MODULE(sc_delta_cycle)
    {
        // IO Ports
        sc_in<bool>     clk, b, c, d;
        sc_out<bool>    q;

        sc_signal<bool> a, e;   // Local Channels

        SC_CTOR(sc_delta_cycle):    // constructor
            clk("clk"), d("d"), q("q")
        {
            SC_METHOD(behavior);
            sensitive << clk << a << b << c << d;    // exclude e ??
        }

        void behavior(void)
        {
            printf("\n[%03d] clk=%c b=%c c=%c a=%c d=%c e=%c q=%c",
                (int)(sc_time_stamp()).to_double()/1000,
                clk.read()? '1':'0',
                b.read()? '1':'0',
                c.read()? '1':'0',
                a.read()? '1':'0',
                d.read()? '1':'0',
                e.read()? '1':'0',
                q.read()? '1':'0'
            );

            a = !(b & c);
            e = !(a & d);
            if (clk)
                q = e;
        }
    };

예제 디렉토리에 준비된 Makefile로 시뮬레이터를 빌드하고 실행시켜보자.

    $ make run

    clang++ -I. -I../c_untimed -I/opt/systemc/include -g  -L/opt/systemc/lib \
    -o sc_test_d_TB -lsystemc sc_main.cpp

    ./sc_test_d_TB

        SystemC 3.0.2-Accellera --- Jun 13 2025 17:49:45
        Copyright (c) 1996-2025 by all Contributors,
        ALL RIGHTS RESERVED

    [000] clk=1 b=0 c=0 a=0 d=0 e=0 q=0
    [000] clk=0 b=0 c=0 a=1 d=0 e=1 q=0

    ......

    [300] clk=0 b=0 c=1 a=1 d=0 e=1 q=1
    [350] clk=1 b=0 c=1 a=1 d=0 e=1 q=1
    [350] clk=1 b=1 c=1 a=1 d=0 e=1 q=1
    [350] clk=1 b=1 c=1 a=0 d=0 e=1 q=1
    [400] clk=0 b=1 c=1 a=0 d=0 e=1 q=1
    [450] clk=1 b=1 c=1 a=0 d=0 e=1 q=1
    [450] clk=1 b=0 c=0 a=0 d=1 e=1 q=1
    [450] clk=1 b=0 c=0 a=1 d=1 e=1 q=1
    [500] clk=0 b=0 c=0 a=1 d=1 e=0 q=1
    [550] clk=1 b=0 c=0 a=1 d=1 e=0 q=1
    [550] clk=1 b=1 c=0 a=1 d=1 e=0 q=0

    ......

    Info: /OSCI/SystemC: Simulation stopped by user.

디지털 파형을 보면 다음과 같다.

    $ make wave

동일한 할당 연산이지만 행위 기술방식(코딩 스타일)에 따라 즉시할당 또는 지연할당이 된다. 그림에서 입출력 파형으로 즉시할당과 지연 할당이 일어나는 모습을 볼 수 있다.

(1) 논리게이트 입력측의 모든 채널 a, b, c, d 이 사건감응 되었다. 채널에 발생한 사건은 즉시 출력 채널에 반영된다.

(2) 클럭 clk 과 입력 e 역시 감응에 참여 한다. 다만 e 의 값이 q 로 전송되기 위한 행동에 조건이 clk가 상승 엣지일 때만 가능하다는 조건이 주어졌다.  이는 플립플롭의 행동을 묘사한 것이다. 채널 e 의 현재 값이 출력 q 의 다음 사건처리 델타에 반영 된다. 지연할당을 의미하는 전송은 즉시할당과 다르다.

프로세스 behavior() 내 할당문의 순서를 바꿔서 시뮬레이션 해보자. 결과는 달라지지 않는다. 이는 세 문장이 병렬실행 되고 있음을 보여준다. SystemC의 시뮬레이터의 기작으로 병렬실행을 설명해 보기로 한다.

아래 그림은 프로세스에서 채널로 접근과 채널 내에서 사건의 감지 그리고 커널에 의한 프로세스의 실행이 반복되는 과정(시뮬레이션 델타)을 설명한다.

시뮬레이션은 테스트벤치에서 clk의 상승 엣지를 기다려 채널 b, c, d에 새값 써넣기로 시작된다. 테스트 벤치의 프로세스에서 채널 b, c, d에 새 값을 써넣었다.

            wait(clk.posedge_event());
            b.write(0);
            c.write(0);
            d.write(1);

시뮬레이션 결과를 보여주는 위의 파형에서 시간 450ns 지점을 살펴본다. 채널 clk에 사건이 발생하였으므로 시뮬레이션 델타가 시작된다.

델타0:

    테스트 벤치에서 채널 clk 에 값을 씀으로써 사건 발생했다. 시뮬레이션 커널은 시간을 멈추고 시뮬레이션 델타 싸이클을 개시한다.

델타1:

    채널: b, c, d 채널의 현재값과 새값이 다르므로 사건발생 공지
    커널: b, c, d 채널 값 갱신 후 사건에 감응되도록 지정된 프로세스 호출
            (모두 동일한 프로세스에 감응 되어 있으므로 세 채널 모두 프로세스 호출 전 갱신)
    프로세스: 할당문 실행으로 내부 채널 a, e 에 새값 써넣기

델타2:

    채널: 채널 a 는 현재값과 새값이 다르므로 사건 발생 공지
            (채널 e 는 현재값과 새값이 동일 하므로 사건 없음)
    커널: 채널 a 의 사건에 감응되도록 지정된 프로세스 호출
    프로세스: 할당문 실행으로 채널 e 에 새값 써넣기

델타3:

    채널 e:  현재값과 새값이 다르므로 사건 발생 공지 후 갱신
            (채널 a는 현재값과 새값이 동일 하므로 사건 없음)
    커널: e 채널 갱신 후 사건에 감응되도록 지정된 프로세스 호출
    프로세스: 할당문 실행으로 채널 e 에 새값 써넣기
            (q 가 갱신 되려면 clk의 상승 엣지 사건의 조건에 맞지 않으므로 q 값은 갱신되지 않음)

시뮬레이션 델타 완료:

    시뮬레이션 델타 싸이클은 채널 a, b, c, d , e 에 더이상 사건이 탐지되지 않으면 종료 후 시간 진행

3-4. 감응 지정이 논리회로의 행동에 미치는 영향

프로세스가 구동되는 기작의 원인은 감응으로 지정된 채널에 발생한 사건이다. 이때 프로세스를 호출하기 전 갱신은 감응으로 지정된 채널에 한한다는 점에 유의해야한다. 위의 예에서 NAND 게이트의 출력은 현재 값이 할당되면 즉시 반영된다. 이는 조합 논리회로의 동작이다. 플립플롭의 경우 클럭의 상승 엣지에 맞춰 입력이 출력으로 전송 된다. 이는 순차 논리회로의 동작이다. 이 둘을 구분하는 방법은 감응의 지정 여부에 달렸다.

아래의 예를 보자. 프로세스 beh_dff()는 clk 채널에 감응이 지정되었지만 beh_dff_strange()는 clk와 d 채널 모두에 감응이 지정되었다. 시뮬레이션 결과 관찰을 VCD 파형보기 외에 프로세스가 구동될 때마다 채널의 현재값 변화를 읽어 출력하였다.

    // Filename: ex3_dff_strange/sc_dff_strange/Vdff.h

    #include <systemc.h>

    SC_MODULE(Vdff)
    {
        sc_in<bool>     clk, d;
        sc_out<bool>    q;

        sc_signal<bool> _q, _q_strange;

        sc_trace_file* fp;  // VCD file

        SC_CTOR(Vdff):    // constructor
            clk("clk"), d("d"), q("q")
        {
            SC_METHOD(beh_dff);
            sensitive << clk;

            SC_METHOD(beh_dff_strange);
            sensitive << clk << d;

            SC_METHOD(beh_output);
            sensitive << _q;

            // VCD Trace
            fp = sc_create_vcd_trace_file("Vdff");
            sc_trace(fp, clk, "clk");
            sc_trace(fp, d, "d");
            sc_trace(fp, q, "q");
            sc_trace(fp, _q, "_q");
            sc_trace(fp, _q_strange, "_q_strange");
        }

        void beh_dff()
        {
            printf("\n[%03d] beh_dff        : clk=%c d=%c",
                (int)(sc_time_stamp()).to_double()/1000,
                clk.read()? '1':'0', d.read()? '1':'0');

            if (clk.read())
                _q.write(d);
        }

        void beh_dff_strange()
        {
            printf("\n[%03d] beh_dff_strange: clk=%c d=%c",
                (int)(sc_time_stamp()).to_double()/1000,
                clk.read()? '1':'0', d.read()? '1':'0');

            if (clk.read())
                _q_strange.write(d);
        }

        void beh_output()
        {
            q.write(_q);
        }
    };

시뮬레이션을 수행해보면 다음과 같은 동작을 보여준다.

채널 clk와 d 에 사건이 동시에 발생했다.

(1) 프로세스 beh_dff()를 호출 할 때 감응에 지정되지 않은 채널 d는 갱신되지 않는다. 따라서 출력 _q는 이전 값을 갖는다. 이는 플립플롭의 행동(클럭 동기에 맞춘 전송)을 반영한다.

(2) 프로세스 beh_dff_strange()는 clk는 물론 d 에도 감응 되었다. 체널 clk와 d 모두 새 값으로 갱신된 후 프로세스가 호출 된다. 새 값이 즉시 출력 _q_strange 에 반영 된다. 이는 조합 논리회로의 행동을 반영한다.

감응 목록에 넣는 것 만으로도 하드웨어의 행동에 영향을 줄 수 있다. 하드웨어 기술 언어의 지침에 코딩 스타일에 주의를 기울여야 한다고 강조하고 있는 이유다. 디지털 조합 회로를 기술하는 경우 입력신호들을 모두 감응 목록에 포함시켜야 한다. 불완전한 감응 목록은 의도치 않은 결과를 낳을 수 있다. 다음은 멀티플렉서를 기술한 예다.

    always @ (a, b)
    begin
        if (sel)    // 'sel' must be added into sensitivity list
            y <= a;
        else
            y <= b;
    end

선택을 제어하는 sel 이 감응 목록에 빠져 있다. 이로 인한 행동의 오류를 사건 구동 시뮬레이터의 기작으로 설명해 보라.

불완전한 감응 목록의 실수를 방지하고자 와일드 문자 '*' 를 사용하기도 한다.

    always @ (*)
    begin

        ......

    end

불완전한 감응 목록은 베릴로그 언어에서 가장 흔히 저지르는 실수다. 베릴로그 언어가 SystemVerilog 로 개정되면서 이에대한 대비로 always 외에 always_comb 와 always_latch, always_ff 가 추가 되었다. 행동을 기술한 내용이 설계자의 의도와 맞지 않을 경우 빌드(컴파일) 오류 메시지를 내보낸다. 일례로, 다음과 같이 always_comb로 조합 회로를 기술 한다고 명시 했지만 기술된 내용은 else가 빠져 래치가 되었다.

    always_comb
    begin
        if (sel)
            y <= a;
    end

3-5. 재귀적 사건 반복이 금지된 이유

사건은 프로세스에서 체널에 새 값을 써넣어 발생한다. 만일 한 프로세스가 자신을 구동하는 채널에 새 값을 써넣을 경우 사건이 재귀적으로 반복되어 시뮬레이션 델타가 무한히 증가하게 만들 수 있다. 결국 시뮬레이터 시간 진행되지 못하게 된다. 디지털 정보를 저장하는 래치는 디지털 게이트를 재귀적으로 연결시킨 구조로 만든다. 전류가 게이트를 통과할 때 트랜지스터 내부의 저항과 컨덴서 성분이 작용하여 발생 하는 약간의 지연을 교묘히 이용한다. 전압의 유무로 정보를 취급하는 사건 구동 시뮬레이터는 이와 같은 전류-전압의 미세한 시간상 변화를 다룰 수 없다. 디지털 시뮬레이터는 전자회로를 매우 높은 추상화 수준에서 묘사한다는 점을 기억해 두자.

하드웨어 언어로 NAND 게이트를 상호 연결하여 RS-래치 회로를 어렵지 않게 묘사할 수 있다.

물론 SystemC 로도 묘사할 수 있다.

두 NAND 게이트의 입출력이 서로 연결 되어 있다. 두 프로세스 사이에 사건이 재귀적으로 반복되는 상황이 벌어진다. 디자인 킷의 예제를 이를 사건 구동 시뮬레이터의 기작으로 따져보자.

예제 디렉토리로 이동,

    $ cd ~/ETRI050_DesignKit/devel/Tutorials/2-2_Verilog_SystemC_in_a_Day/ex4_rsff_recursive_event/sc_rsff_gates

    $ ll

    total 260
    drwxrwxr-x 2 goodkook goodkook   4096 Jul 13 20:02 ./
    drwxrwxr-x 5 goodkook goodkook   4096 Jul  8 14:36 ../
    -rw-rw-r-- 1 goodkook goodkook   1258 Jul 13 19:21 Makefile
    -rw-rw-r-- 1 goodkook goodkook    572 Jul 13 19:36 sc_main.cpp
    -rw-rw-r-- 1 goodkook goodkook   1939 Jul 13 16:05 sc_rsff_TB.h
    -rw-rw-r-- 1 goodkook goodkook   1898 Jul 13 20:01 Vrsff.h

준비된 Makefile을 이용하여 시뮬레이터 빌드 및 실행,

    $ DELAY=ZERO_TIME make build

    $ make run

    ./sc_rsff_TB

        SystemC 3.0.2-Accellera --- Jun 13 2025 17:49:45
        Copyright (c) 1996-2025 by all Contributors,
        ALL RIGHTS RESERVED

Info: (I702) default timescale unit used for tracing: 1 ps (sc_rsff_TB.vcd)

[Time:010ns][Delta:003] NAND2(s=1, q    =0) -> q_bar=[new=1|curr=0]
[Time:010ns][Delta:005] NAND1(r=0, q_bar=1) -> q    =[new=1|curr=0]
[Time:010ns][Delta:007] NAND2(s=1, q    =1) -> q_bar=[new=0|curr=1]
[Time:010ns][Delta:009] NAND1(r=0, q_bar=0) -> q    =[new=1|curr=1]
[Time:020ns][Delta:012] NAND2(s=0, q    =1) -> q_bar=[new=1|curr=0]
[Time:020ns][Delta:012] NAND1(r=1, q_bar=0) -> q    =[new=1|curr=1]

......

[Time:050ns][Delta:029] NAND1(r=1, q_bar=0) -> q    =[new=1|curr=1]
[Time:060ns][Delta:032] NAND2(s=0, q    =1) -> q_bar=[new=1|curr=0]
[Time:060ns][Delta:032] NAND1(r=0, q_bar=0) -> q    =[new=1|curr=1]
[Time:060ns][Delta:034] NAND1(r=0, q_bar=1) -> q    =[new=1|curr=1]
[Time:070ns][Delta:037] NAND2(s=1, q    =1) -> q_bar=[new=0|curr=1]
[Time:070ns][Delta:037] NAND1(r=1, q_bar=1) -> q    =[new=0|curr=1]
[Time:070ns][Delta:039] NAND2(s=1, q    =0) -> q_bar=[new=1|curr=0]
[Time:070ns][Delta:039] NAND1(r=1, q_bar=0) -> q    =[new=1|curr=0]
[Time:070ns][Delta:041] NAND2(s=1, q    =1) -> q_bar=[new=0|curr=1]
[Time:070ns][Delta:041] NAND1(r=1, q_bar=1) -> q    =[new=0|curr=1]
[Time:070ns][Delta:043] NAND2(s=1, q    =0) -> q_bar=[new=1|curr=0]
[Time:070ns][Delta:043] NAND1(r=1, q_bar=0) -> q    =[new=1|curr=0]
[Time:070ns][Delta:045] NAND2(s=1, q    =1) -> q_bar=[new=0|curr=1]
[Time:070ns][Delta:045] NAND1(r=1, q_bar=1) -> q    =[new=0|curr=1]
[Time:070ns][Delta:047] NAND2(s=1, q    =0) -> q_bar=[new=1|curr=0]
[Time:070ns][Delta:047] NAND1(r=1, q_bar=0) -> q    =[new=1|curr=0]

......

시간 60ns 에서 각각 0 이었던 r 과 s 입력을 70n 에서 모두 1로 변경하자 시뮬레이션 시간을 중지하고 사건 처리 시뮬레이션 델타가 진행된다. 두 NAND 게이트의 출력 q 와 q_bar의 새값이 현재 값과 다르므로 사건이 다시 게이트 입력으로 들어간다. RS-래치의 입력 r 과 s 는 변함이 없지만 q 와 q_bar의 변화는 재귀적으로 반복되므로 시뮬레이션 델타는 무한히 늘어나고 있다.

4. 맺음말

컴퓨팅 언어를 활용할 수 있게 되면서 반도체 설계의 생산성이 극적으로 향상되었다. 특히 크래스 개념은 전자회로의 묘사를 최고의 추상화 수준에서 가능하게 한다. 그럼에도 설계 생산성은 여전히 반도체 제조 집적도 기술을 따라잡지 못하고 있고 검증은 그보다 훨씬 미치지 못하고 있다. 이 격차의 극복 방안 역시 높은 추상성 추구다. 디지털 반도체 설계의 검증 방법에서 RTL 시뮬레이션이 차지하는 비중이 여전히 높지만 SystemC/C++를 활용한 시스템 수준 설계 방법론이 활발하여 상당한 성과를 거두고 있다.

아쉽게도 하드웨어 설계자에게 C++ 언어는 높은 장벽이 되고 있다. 이 글은 C++ 언어의 근간인 크래스와 시뮬레이션 커널의 기초를 정성적으로 설명하였다. 높은 추상화 수준에서 반도체를 설계하고 검증을 위한 시뮬레이션 환경 구축의 입문에 도움이 되길 바란다.

 

[출처] OpenROAD: Toward a Self-Driving, Open-Source Digital Layout Implementation Tool Chain


2025년 6월 29일 일요일

기술노트3: 하드웨어 기술 언어의 코딩 스타일

"RTL Verilog-Verilator-SystemC TB 방법론 기초"
하드웨어 기술 언어의 코딩 스타일

목차:

1. 개요
2. 베릴로그로 작성하는 D-플립플롭
    2-1. 모듈 (module ~ endmodule)
    2-2. D-플립플롭의 행위 기술

3. SystemC 테스트벤치
    3-1. SystemC 모듈의 기본구성
    3-2. DUT의 사례화와 연결
    3-3. 테스트 벡터 생성(test vector generation)
        a. 클럭 객체 sc_clock
        b. 사건구동 함수 지정
        c. 하드웨어 객체 sc_signal<>
    3-4. VCD 파형
    3-5. int sc_main(int argc, char** argv)

4. 메이크(make) 유틸리티
    4-1. 목표와 의존 관계
    4-2. 내부 변수
    4-3. 다중 목표
        a. all
        b. lint & build
        c. run
        d. wave
        e. clean

5. 하드웨어의 코딩 스타일
    5-1. D-플립플롭 시뮬레이터의 빌드와 실행
    5-2. 레지스터 전송 수준
    5-3. 컴퓨팅 언어에 의한 하드웨어의 묘사
    5-4. 하드웨어 행위 묘사의 코딩 스타일
    5-5. 시뮬레이션 델타
    5-6. 추상성 수준: 행위 묘사 vs 회로 묘사
    5-7. 테스트벤치 재사용
    5-8. 설계 생산성을 높이는 팁

6. 맺음말

----------------------------------------------------

1. 개요

    Verilog로 설계하고 SystemC로 검증 하는 설계 방법론을 소개한다. 설계의 예는 디지털 회로의 가장 단순한 D-플립플롭이다. D-플립플롭은 이미 이전 강좌에서 트랜지스터 회로 수준에서 설계 했었다[바로가기]. 디지털 저장장치(메모리 또는 플립플롭)를 일일이 트랜지스터의 지연된 스위칭 동작으로 구현했던 것과 비교하면 높은 추상화 수준의 설계 방법론이 생산적일 수 있다는 점을 알게될 것이다. 하드웨어의 행동을 언어로 표현 하므로서 얻는 장점을 확장하여 Verilog 설계와 SystemC 검증을 통하여 “시스템 수준”이라는 새로운 관점에서 살펴본다.

    Verilog와 SystemC의 기본 구성요건과 시뮬레이터를 구성하는 과정을 소개하고 소프트웨어로 하드웨어의 병렬 실행을 모의하는 방법을 살펴볼 것이다. 설계자로서 도구의 사용자 이지만 도구가 작동하는 원리를 이해하면 학습진도를 가속화 할 수 있을 뿐만 아니라 높은 추상화 수준에서 시스템 모형화(system modeling)를 시작할 때 기초가 될 것이다.

    본 문서에서 설명하는 예제의 파일들은 디자인 킷[바로가기]의 튜토리얼 중 아래 디렉토리에서  찾아볼 수 있다.

    ~$ cd ~/ETRI050_DesignKit/Tutorials/2-3_Lab1_dff

    ~/ETRI050_DesignKit/devel/Tutorials/2-3_Lab1_dff$ tree
    .
    ├── dff : D-플립플롭의 행위기술 
    │   ├── dff_Config.txt
    │   ├── dff.v
    │   ├── Makefile
    │   ├── sc_dff_TB.gtkw
    │   ├── sc_dff_TB.h
    │   ├── sc_main.cpp
    │   └── Vdff.gtkw
    ├── dff_gate D-플립플롭의 게이트 기술 및 테스트벤치 재사용
    │   ├── dff.v
    │   ├── Makefile
    │   ├── sc_dff_TB.gtkw
    │   └── Vdff.gtkw
    ├── dffrs : 비동기 셋과 리셋을 가진 D-플립플롭
    │   ├── dffrs.v
    │   ├── Makefile
    │   ├── sc_dffrs_TB.gtkw
    │   ├── sc_dffrs_TB.h
    │   ├── sc_main.cpp
    │   └── Vdffrs.gtkw
    └── shifter : 코딩 생산성 향상 팁: FOR 반복문과 `define 매크로
        ├── Makefile
        ├── sc_main.cpp
        ├── sc_shifter_TB.gtkw
        ├── sc_shifter_TB.h
        ├── shifter.v
        └── Vshifter.gtkw


2. 베릴로그로 작성하는 D-플립플롭

    Verilog는 하드웨어의 행동을 묘사하기 위해 등장한 컴퓨팅 언어다. 하드웨어를 다루는 만큼 병렬실행(concurrency) 구문을 기본으로 행위의 기술을 위해 순차실행(procedural)을 수용한다. 베릴로그 시뮬레이터는 하드웨어의 병렬성을 흉내내기 위해 사건 구동(event-driven)과 지연 할당(deferred assign) 기법을 활용한 소프트웨어다. Verilog 언어에서 이 기법들이 어떻게 동원되는지 살펴본다.

2-1. 모듈 (module ~ endmodule)

    베릴로그의 설계(기술)단위는 모듈(module)이다. 이름이 "dff"인 모듈의 외형(boundary)을 기술하면 아래와 같다.

  Filename: dff.v

    - 모듈을 정의하는 베릴로그의 키워드는 module 이다. 모듈 정의는 세미콜론(;)으로 끝난다.
    - 모듈 기술의 끝은 endmodule 이다.

[주] 세미콜론(;)은 문장의 끝을 표시하는 마침 부호다. C 언어와 같다. 모듈의 끝을 표시하는 endmodule 은 문장의 끝이 아니므로 문장  마침표(;)가 없다. C 언어에서 제어 영역을  묶기 위해 중괄호 {...}를 사용 하듯 베릴로그는 예약어 begin ... end 를 사용한다.

    - 모듈 dff 는 입출력 포트로 clk, d, q를 가지고 있다.
    - 포트(port)는 입출력 방향(direction)을 명시해야 한다. 방향을 표시하는 예약어는 input, output, inout으로 3종류다.

[주] 하드웨어 언어는 전자회로를 기술하고 있다는 점을 항상 기억해 두자. 전류의 입력(current source)과 출력(current sink)을 반드시 구분해 주어야 한다. 입력 포트는 할당(=)의 오른편 rhs(right-hand side)에, 출력 포트는 왼편 lhs(left-hand side)에 만 놓일 수 있다.

    - 주석문(comments)은 C 언어의 것과 동일하다.

2-2. D-플립플롭의 행위기술

    D-플립플롭의 행동(behavior)는 클럭으로 지정된 신호의 에지 사건(edge event)에 의해서 만 반응한다. 아래의 예는 clk의 상승 엣지에 반응하는 플립플롭을 기술하였다.

  Filename: dff.v

    - 순차적 행동의 묘사(procedural behavior description)는 always 구역 내에서 기술된다.
    - 순차구문의 할당연산(<=) 왼편에 놓일 신호는 reg 속성을 가져야 한다. 출력 포드 q 는 네트(net 또는 wire)이지만 순차구문 구역 내에서 사용되기 위해 reg 로 속성이 부여되었다.

[주] 마치 중복 선언된 것처럼 보이지만 하드웨어 객체 속성을 지정한 것이다. 베릴로그는 선언(declare)과 속성부여(attribute)에 애매한 면을 가지고 있다.

[주] 레지스터 reg 속성을 가졌다고 해서 반드시 플립플롭을 의미하는 것은 아니다. 단지 언어적으로 순차할당(procedural assignment)의 왼편에 놓을 수 있다는 뜻이다.

    - 행위의 기술(behavior description)로부터 D-플립플롭으로 해석될 수 있다.

[주] 묘사(행위 기술)를 해석하여 전자회로를 유추(inference)해 내는 일은 합성기(synthesizer)의 중요한 역할 중 하나다. 합성기의 또 다른 역할로 최적화(optimization)가 있다.

    - 예약어 posedge 는 입력 신호 clk의 상태를 평가(evaluate)하여 상승 엣지 사건인지 판별해 주는 연산자다. 이 평가가 참이면 always 구역내의 순차구문이 실행된다.

[주] 사건에 의한 always 구역의 실행을 사건구동(event-driven)이라 한다. 소프트웨어 작성 기법에서 사건과 콜백(event & call-back function) 또는 마이크로프로세서의 인터럽트(interrupt) 메커니즘과 완벽히 같은 의미다.

3. SystemC 테스트벤치

    우리는 정보화 사회에 살면서 컴퓨팅 언어를 어느 정도 들어 알고 있다. 대부분 컴퓨팅 언어들은 기본적인 대수 표현법을 차용하고 기초 영문 단어와 문법를 채택하고 있어서 따로 배울 필요가 있나 할 정도다. 컴퓨팅 언어에 의한 묘사는 결국 컴파일러라고 하는 자동 변환 소프트웨어에 의해 기계가 이해할 수 있는 형식으로 바뀐다. 따라서 컴퓨팅 언어는 단순 명료할 수 밖에 없다. 컴퓨팅 언어의 문법 보다 특정 컴퓨팅 언어가 묘사하려는 대상의 특성을 이해하는 것이 중요하다. 베릴로그는 하드웨어를 기술하려는 목적으로 발명된 컴퓨팅 언어다. 하드웨어 중에서도 디지털 회로 요소들의 행동을 묘사할 목적을 가지고 있다. 연산자와 제어 구문의 의미는 다른 컴퓨팅 언어와 다를 바 없다. 다만 문장의 실행 방식이 하드웨어의 성격대로 동시성(문장의 순서에 무관한)을 가진다는 점이 가장 큰 차이라 할 것이다. 우리가 알고리즘을 묘사한 원시 코드를 보면서 어렵다고 느끼는 것은 내용을 모르는 것이지 컴퓨팅 언어의 문법 탓이 아니라는 점을 알아야 한다.

    역사적으로 묘사하려는 대상과 목적에 따라 다양한 컴퓨팅 언어가 발명 되었고, 목적이 분명히 나뉘는 소프트웨어와 하드웨어로 양분되어 발전해왔다. 하지만 설계의 규모가 기하급수적으로 증가함에 따라 생산성 문제의 해결 방안으로 설계 방법론에 높은 추상성이 요구 되었다. 이에 현존하는 컴퓨팅 언어 중 가장 광범위하게 사용되면서 높은 그리고 폭넓은 추상성을 가진 컴퓨팅 언어 C++로 합치려는 시도가 이뤄져 이에 탄생한 것이 SystemC 다. 자료구조와 알고리즘의 묘사를 보다 수월하고 재사용성을 높이기 위해 제정된 STL(Standard Template Library)이 있듯이 SystemC는 하드웨어의 동시성을 "시스템 수준"에서 묘사하기 위한 C++의 크래스 라이브러리로써 IEEE Std.1666 으로 제정되었다.

[주] "시스템 수준(System Level)"이라는 문구가 매우 다양하게 인용 되어 때로 남용을 넘어 오해를 낳기도 한다. "시스템 수준"을 한마디로 말하자면 알고리즘의 묘사를 크래스 객체로 두고 이를 소속 함수로 다루겠다는 의미다. 합성을 목적으로 하드웨어를 기술한 레지스터 트랜스퍼 수준(RTL)은 "시스템 수준"의 하위에 놓인다. 아주 단순히 말하면 여러 알고리즘들이 모여 한 집합체를 이루는 시스템에서 한 알고리즘을 수행할 때 해당 객체의 소속함수를 호출 하는 것이다. 해당 객체가 구현된 추상성의 수준(하드웨어 인지 소프트웨어 인지)은 객체 내에서 해결할 문제일 뿐 "시스템 수준"에서는 개의치 않는다. 시스템 수준에서는 각 알고리즘들이 어떻게 구현되었는지 알고 싶지도 않다. 하드웨어로 구현 되었더라도 소요 클럭 수와 동작 주파수 그리고 비트 폭은 관심없다. 시스템 수준에서 검증(verification)은 단지 적절한 때에 원하는 값을 얻고자 할 뿐이며, 구조 탐색(architecture exploration)은 입력에 대하여 출력을 얻기까지 어느 정도 자원(소요 클럭 수, 하드웨어 량)을 사용하는지 따져보기 위함이다.

    베릴로그로 작성된 D-플립플롭을 DUT(Design Under Test) 삼아 테스트벤치(testbench)를 SystemC로 작성한다. 비록 매우 단순한 DUT와 테스트벤치 이지만 앞으로 시스템 수준의 검증환경을 구축하는 첫걸음이다. 일반적인 테스트벤치의 구성은 다음과 같다.

    - DUT의 사례화(instantiate)
    - DUT의 입력에 줄 신호의 생성
    - DUT의 출력 검토

[주] '사례화'라는 용어가 생소하다. 조금 의미를 담아 표현하면 기술해 놓은 함수 또는 모듈 객체를 "존재하게 한다"고 하겠다.

    SystemC 가 C++의 크래스 라이브러리라고 하지만 처음 접하면 마치 또다른 언어처럼 생소하다. C++의 크래스에 대한 기초 지식을 동원하여 이해 해보자. 당장 이해가 않되는 부분은 일단 받아들이자.

[주] SystemC를 C++를 이용하여 베릴로그를 흉내냈다는 식으로 소개 되곤 한다. 이런 이유로 굳이 SystemC를 해야 하나 하는 생각이 일견 들것이다. 이는 SystemC를 시스템 수준 모델링 능력을 파악하지 못한 채 매우 일부분인 RTL로 만 접했기 때문이다. 시스템 수준 모델링은 천천히 알아가기로 하고 기왕 배워놓은 C/C++를 하드웨어 설계에 활용한다고 여기자. C++도 익히고 하드웨어 설계도 배우면 일석이조 아닌가.

[주] SystemC를 설명하는 아주 좋은 동영상이 있다.  6편의 짧은 동영상으로 구성 되어 약 2시간 분량이다. SystemC가 생소하다면 잠시 시간을 내서 들어보길 권한다. Learn SystemC [바로가기]

3-1. SystemC 모듈의 기본구성

    SystemC의 모듈의 구성은 아래와 같다. 외형적으로 보면 마치 또다른 언어체계 같지만 뜯어보면 C++의 크래스다.

  Filename: sc_dff_TB.h

    - SystemC 크래스 라이브러리를 사용하기 위해 systemc.h 헤더 파일 인클루드(include)
    - SC_MODULE 은 C++의 크래스를 재정의한 매크로다.

        #define SC_MODULE(name) class name: ......

    - 크래스 끝에 세미콜론(;) 이 있다.

[주] C++의 크래스는 C 의 구조체를 매우 확장한 것이다. 크래스는 자료의 정의뿐만 아니라 객체 자료형 정의 typedef 까지 수행한다.

    - SC_CTOR() 은 C++의 구성자(constructor) 지정 매크로다. 당연히 모듈 크래스와 동일한 이름을 가져야 한다.
    - 하드웨어 객체 sc_signal<>은 베릴로그의 wire 또는 reg 를 모사한 C++의 크래스다. 하드웨어의 비트 단위 선언을 위해 템플릿(template)을 사용하고 있다.
    - sc_clock 은 특별한 하드웨어 객체로 클럭 신호를 생성해 준다.

[주] SystemC의 구문을 보면서 매우 생소하다면 C++에 익숙치 못한 탓이다. 이참에 C++의 크래스를 좀더 이해해 보는 기회가 되기 바란다. 어쨌든 아래 구문은 모두 C++ 라는 점을 기억하자.

3-2. DUT의 사례화와 연결

    DUT의 사례화(instantiation)와 지역신호 연결(binding)은 두 언어의 문법적 차이만 있을 뿐이지 완전히 동일한 의미다. 아래와 같은 베릴로그의 신호 연결(binding)이 있다고 하자.

        dff u_dff (
            .clk(clk),
            .d(d),
            .q(q));

SystemC에서 연결을 수행하는 과정을 살펴보면 다음과 같다. C++의 포인터 매핑이다.

  Filename: sc_dff_TB.h

    - Verilog로 작성된 DUT를 C++에 직접 불러올 수 없으므로 언어 변환을 통하여 C++ 언어체계로 들여온다. 변환된 DUT의 헤더 파일이 Vdff.h 다.

[주] 매우 넓은 추상성을 가진 SystemC는 Verilog의 RTL 표현과 등가적 표현이 가능할 뿐만 아니라 사건구동 병렬 시뮬레이터 커널이 라이브러리에 내장되어 있다. Verilator는 베릴로그를 SystemC/C++ 로 변환해 주는 오픈-소스 도구다. 베릴로그 모듈 명에 대문자 V 를 붙여 SystemC 모듈 크래스를 생성한다.

    - 테스트벤치 모듈 크래스 내에 DUT를 선언하고 사례화 한다. DUT는 포인터로 선언되었고 C++의 객체 동적 할당 연산자 new로 사례화 한다.
    - 동적 할당된 DUT의 포트들을 SystemC 테스트벤치의 지역 하드웨어 객체 sc_clock, sc_signal<>와 연결한다.
    - DUT의 사례화와 연결은 모두 테스트벤치 크래스의 구성자 내에서 이뤄진다. 이 과정을 내부 구축(elaboration)이라고 한다. 베릴로그의 initial 에 해당하는 절차도 구성자 내에서 수행한다.

3-3. 테스트 벡터 생성(test vector generation)

    DUT에 입력을 넣고 시뮬레이션을 실시하여 그 동작을 확인한다. 예제 D-플립플롭의 입력은 클럭 clk와 d 다.

    a. 클럭 객체 sc_clock

    SystemC는 주기적으로 끝없이 반복되는 클럭을 생성하는 특별한 크래스 객체 sc_clock 를 가지고 있다. 주기를 가지고 파형을 자동 발생시키기 위해 초기화 해준다. SystemC 간단 참고문서[1]를 보면 sc_clock 객체의 초기화 방법은 다음과 같다.

        sc_clock("ID", period, duty_cycle, offset, first_edge_positive);

D-플립플롭 예제에서 클럭 객체는 모듈 크래스의 소속 데이터(member data)로 선언 되었다.

        sc_clock    clk;

이어 구성자가 실행될 때 클럭 객체를 초기화 한다.

    SC_CTOR(sc_dff_TB):    // constructor
        clk("clk", 100, SC_NS, 0.5, 0.0, SC_NS, false)
    { ...... }

D-플립플롭의 다른 입력 d 는 시험 절차에 따라 발생 시킨다. 모듈 크래스의 소속함수 (member function) test_generator() 에 시험 절차(순서)에 따른 d 의 생성을 기술 하였다.

    b. 사건구동 함수 지정

    SystemC는 하드웨어 묘사를 위한 다양한 객체들의 크래스와 사건 구동 병렬실행기(event-driven simulation kernel)를 내장하고 있다. 사건탐지에 쓸 신호를 지정하고 이에 구동될 함수(콜백, call-back)를 지정한다. 이 지정은 시뮬레이션이 가동되기 전에 미리 구축 되어야 하므로 모듈 크래스의 구성자에서 수행한다. 시뮬레이션 전에 준비하는 상세화(내부구축, elaboration)과정의 일부다.

  Filename: sc_dff_TB.h

    위의 SystemC예에서 보인 쓰레드 함수와 사건 감응신호 지정은 베릴로그의 always @(sensitivity_list) 에서 감응 리스트의 지정과 동일한 의미다.

  Filename: dff.v

[주] 하드웨어를 기술하는 언어(Verilog, VHDL 등)는 병렬 실행  구문을 기본으로 순차 실행  구문 구역을 지정하지만 C++ 는 병렬 실행의 개념을 가지고 있지 않다. SystemC는 병렬 실행을 모의하기 위해 사건 구동 시뮬레이션 커널을 내장하고 있다. 베릴로그에서 사건에 의하여 구동될 순차 구문 구역이 always 에 이어지지만 모두 순차구문인 C++로 사건 구동을 구현해야 하는 SystemC는 사정이 다르다. 감응(sensitive)과 이에 반응하여 호출될 함수(call-back function)를 따로 지정한다. 이는 마이크로 컨트롤러 프로그래밍에서 인터럽트 벡터 설정(인터럽트 번호에 서비스 루틴을 지정하는 절차)과 같다. 사건에 구동될 함수는 호출 인수와 되돌림 값이 모두 void 인 모듈 크래스의 소속함수(member function)다. 사건에 대하여 콜백 함수를 호출하는 주체는 사건구동 시뮬레이션 커널이다. 함수를 운영하는 방식은 메쏘드(method) 혹은 쓰레드(thread)가 있다. SC_METHOD() 로 지정된 함수는 감응 신호에 사건이 발생할 때마다 호출된다. SC_THREAD()로 지정된 함수는 준비과정(elaboration process)에서 한번 호출되므로 함수 내에 무한 반복 구간을 두고 사건 대기 구문을 가지고 있어야 한다. 소프트웨어에서 병렬 프로그래밍 기법으로 흔히 사용하는 멀티 쓰레딩(multi-threading)과 같다.

     테스트 입력 d 를 발생 시키는 쓰레드 함수 test_generator()의 무한 반복 구문 while(true) {...} 내에 사건을 대기하는 wait(....) 를 두고 있다. SystemC 의 wait(...) 는 감응으로 지정한 신호에 사건이 발생할 때까지 대기한다. 이는 실행 제어권을 시뮬레이션 커널에 양보(yield)하는 역할을 한다. SystemC 간단 참고문서[1]에 wait(...)의 사용 방법을 찾아보면 다음과 같다.

    wait(); // Wait for event as specified in static sensitivity list
    wait(event_expression); // Temporary overrides the static sensitivitiy list
    wait(time);
    wait(time, event_expression);

하드웨어 객체 clk 에 상승 엣지(또는 하강엣지) 사건이 발생할 때까지 시뮬레이션을 대기 시킬 수 있다.

    wait(clk.posedge_event());
    wait(clk.negedge_event());

    c. 하드웨어 객체 sc_signal<>

    예제에서 DUT의 입력에 연결한 d 는 sc_signal<> 로 선언된 하드웨어 객체다. 이 객체에 접근하는 방법은 읽기 .read() 와 쓰기 .write() 가 있다. 소속함수로 접근 할 수 있고 할당 연산자 = 가 중복정의(오버로드, overload) 되어 있다. 예를 들어,  d = 1; 과 d.write(1); 은 동일 하다. 하드웨어 언어에서 모든 할당은 하드웨어 객체를 상대로 하는 것이 기본 이지만 C++ 언어는 모두 변수 할당이다. 하드웨어 객체에 대한 접근과 변수 접근을 구분하기 위해서 라도 할당 연산자 대신 소속함수(메쏘드) .read().write()를 통해 접근하는 방식을 쓰도록 한다.

[주] 하드웨어에서 부품간 연결에 단순히 "전선"이라 부르는 "와이어"를 사용한다. 이 "와이어"를 컴퓨팅 언어로 표현하려면 고려할 사항이 많다. 소프트웨어에서 "전선"의 개념 없고 "변수"만 있을 뿐이다. 게다가 전류가 흐르는 전선에는 시간적 순서가 없다. 디지털 회로에서 조합회로와 순차회로를 구분 하는 이유는 "순서" 때문이다.  하드웨어 객체 sc_signal<>는 C++라는 소프트웨어 개발 언어로 하드웨어의 "와이어"를 모사하기 위한 템플릿-크래스로서 단순한 변수 이상의 의미를 가지고 있으며 다양한 접근 방법(method)을 갖추고 있다. 이 하드웨어 객체 sc_signal<>는 사건 구동 병렬실행 시뮬레이션 커널의 중요 매개이기도 하다. 프로그래밍 언어의 변수와 하드웨어 객체를  구분하는 근본적인 이유는 소프트웨어로 하드웨어의 행동을 모의하려고 하기 때문이다. 하드웨어의 행동을 소프트웨어로 흉내 내기 위해 사용된 기법이 사건구동(event-driven)과 지연 할당(deferred assignment)이다. 이에 대해서는 하드웨어 시뮬레이터의 내부(simulator kernel)를 다룰 때 살펴보기로 한다.

3-4. VCD 파형

    디지털 하드웨어 설계의 입출력을 관찰하는 고전적인 방법은 역시 파형보기(waveform view)다. VCD(Value Changed Dump)는 디지털 파형을 기록하는 베릴로그 표준 형식이다. SystemC는 하드웨어 객체의 변화를 VCD 로 기록하는 방법을 제공한다. 모듈 크래스 구성자에서 VCD를 기록하도록 지정할 수 있다. 베릴로그에서는 VCD 추적을 initial 구역에 지정 했었다.

  Filename: sc_dff_TB.h

- VCD 파일명은 "sc_dff_TB.vcd" 다.
- VCD 파일에 기록할 신호는 clk, d, q 다.

3-5. int sc_main(int argc, char** argv)

    C++ 프로그램의 시작은 main() 호출이다. SystemC의 시작은 sc_main()이다. 헤더 파일로 기술한 테스트 벤치 모듈 클래스를 들여와서 사례화 한 후 시뮬레이터를 개시한다.

  Filename: sc_main.cpp

    시뮬레이터는 아래 조건 중 하나를 충족할 때 중지된다.

    - 지정된 시뮬레이션 시간에 도달했을 때
    - 모든 채널(하드웨어 객체)에 사건이 발생하지 않을 때
    - 사용자가 sc_stop()을 호출 했을 때

4. 메이크(make) 유틸리티

    Verilog 설계에 SystemC 테스트벤치를 씌운 시뮬레이터를 만들기까지 Verilator 변환도구가 동원되었고 최종적으로 GNU C++ 컴파일러를 사용하여 실행 파일을 만들어낸다. 일반적인 소프트웨어 개발용 표준 라이브러리에 더하여 특수한 라이브러리들을 동원하면 명령줄 옵션이 매우 복잡해진다. 개발 중 반복되는 컴파일 명령을 매번 명령줄에 입력하기도 어렵고 개발자의 피로도가 쌓여 실수를 낳게 되므로 스크립트를 활용 하는 것이 좋다.

    그래픽 환경에서 통합된 개발도구의 활용이 늘고 있다. 통합 환경이 사용자 편의성을 높이긴 하지만 결국 스크립트의 실행이다. 필요할 때마다 외부 라이브러리를 추가하고 다양한 옵션을 자유롭게 활용 하려면 통합 환경의 사용법을 익혀야 하는데 결국 메이크 스크립트(Makefile)를 마주하게 된다. 메이크(make) 유틸리티는 명령줄에서 쓰이는 가장 강력한 스크립트 활용 방법이다. 긴 세월동안 수많은 개발자들에 의해 향상된 make 유틸리티의 내용은 매우 방대하지만 기본 사용법만 익혀도 활용도는 매우 높다.

[주] make의 사용법을 다룬 글들이 많지만 임대영(RAXIS)의 GNU Make강좌를 추천한다.

https://doc.kldp.org/KoreanDoc/html/GNU-Make/GNU-Make.html

4-1. 목표와 의존 관계

    명령줄에서 아무런 인수 없이 make 를 실행하면 현재 디렉토리에서 Makefile 을 기본으로 읽어 그 내용을 수행한다. Makefile 의 가장 단순한 사용법은 콜론(:) 을 사이에 둔 의존 관계(dependency)의 표현이다. 콜론의 왼편에 놓인 목표(target)를 얻기 위해 오른편에 놓인 파일들에 의존(dependent)한다는 의미다. 바로 이어 목표에 도달하는 방법을 기술 한다. 이때 들여쓰기(indent)는 반드시 공백없는 탭(tab) 문자가 선행되어야 한다. 의존성은 목표 파일의 존재 여부와 두 파일의 날짜와 시간을 살펴 정한다. 예를 들어 아래와 같은 간단한 의존 관계를 보자.

    # Simple 'Makefile'
    hello : hello.c
        gcc -o hello hello.c

    목표 파일 hello는 파일 hello.c 에 의존하는 관계에 있다. 목표인 hello 가 존재하지 않거나 hello.c 의 날짜가 목표보다 최신일 경우 바로 아래에 놓인 절차를 수행하라는 뜻이다. 위의 경우 hello.c 를 gcc 로 컴파일 하여 hello 를 만든다. 첫번째 칼럼에 # 은 주석이다. Makefile 내에 다수의 목표를 놓을 수 있다. 명령줄에서 make를 실행 할 때 명령줄 인수를 주어 여러 목표 중 하나를 선택 할 수 있다. 예를들어 명령줄에서 아래와 같이 인수를 주고 메이크 하였다.

    $ make lint

Makefile 에 lint를 목표로 하는 절차가 있었다면 이를 찾아 수행한다. 다음은 간단한 Makefile의 예다.

  Filename: Makefile

    의존 관계에서 콜론(:)의 왼편이 명령줄의 인수와 일치하는 목표를 찾아 이에 도달하는 절차를 수행한다. 위의 예에서 보듯이 목표 lint에 의존관계에 있는 파일들을 검사한다. 의존 관계에 있는 파일들의 목록은 변수 VERILOG_SRCS 에 지정 되었다. 목표에 도달하기 위한 절차는 VERILATOR 변수로 지정한 명령을 수행하는 것이다.

4-2. 내부 변수

    Makefile 스크립트는 내부적으로 변수를 사용할 수 있다. 변수를 아래와 같이 선언해 주었다면 위의 수행 절차가 이해 될 것이다.

    # Makefile variables:
    VERILOG_SRCS = dff.v
    SC_SRCS      = sc_main.cpp
    SC_TOP_H     = sc_dff_TB.h
    VERILATOR    = verilator
    TOP_MODULE   = dff
    TARGET       = V$(TOP_MODULE)
    TARGET_DIR   = obj_dir

4-3. 다중 목표

    위의 예에서 Makefile에 all, lint, run, wave, clean 등 5가지 명령줄 목표를 가지고 있다. 목표 all 은 make에 예약되었고 나머지는 사용자 지정 목표다.

    a. all

    목표 all 은 Makefile에서 예약되었다. 명령줄에서 인수없이 make 만 수행할 경우 자동으로 목표 all 을 찾아 수행한다. 위의 예에 따르면 목표 all 은 $(TARGET_DIR)/$(TARGET) 에 의존한다. 만들 방법이 제시되지 않고 있으므로 $(TARGET_DIR)/$(TARGET)가 목표인 의존성을 찾아 만들기(make)를 수행한다.

[주] Verilator 는 Verilog 를 SystemC/C++ 로 변환 한다. 옵션으로 --exe 와 --build 옵션을 주면 변환한 DUT와 테스트벤치 그리고 main() 이 포함된 C++ 원시 파일을 읽어 실행 파일(시뮬레이터)를 생성한다. 이때 GNU C++/clang++ 컴파일러를 사용한다.

    b. lint & build

    목표 lint 는 Verilator로 하여금 변환만 수행하도록 한다. Verilog의 무결성(문법적 오류는 물론 효과 없는 코드, 자료형 불일치 등)을 검사한다.

    c. run

    목표 run 은 Verilator와 C++ 컴파일러로 만들어진 실행 파일(시뮬레이터)를 실행한다. 리눅스 운영체제에서 실행되는 응용프로그램 이므로 실행 환경의 설정이 필요할 수 있다. DUT와 테스트벤치를 묶어 시뮬레이터를 빌드하는 과정에서 SystemC를 사용 하였으므로 크래스 헤더 파일을 들여오고 실행시 동적 라이브러리를 적재하기 위한 환경 변수를 설정해 주어야 한다.

    export SYSTEMC           = /opt/systemc
    export SYSTEMC_HOME      = $(SYSTEMC)
    export SYSTEMC_INCLUDE   = $(SYSTEMC_HOME)/include
    export SYSTEMC_LIBDIR    = $(SYSTEMC_HOME)/lib
    export LD_LIBRARY_PATH  :=$(LD_LIBRARY_PATH):$(SYSTEMC_LIBDIR)

SystemC를 설치하면 크래스 헤더와 라이브러리를 특정 위치에 놓는다. 따로 지정하지 않았다면 /opt/systemc 다. 시뮬레이터를 빌드하는 컴파일러에게 이 경로를 환경변수를 통해 알려준다. 아울러 SystemC의 시뮬레이션 엔진(simulation kernel)은 동적 라이브러리 libsystemc.so (shared object)로 설치되었다. 컴파일된 시뮬레이터(바이너리 파일)를 실행 할 때 시뮬레이션 커널과 함께 실행 되어야 하므로 동적 라이브러리가 놓인 경로 /opt/systemc/lib 를 리눅스 배쉬 쉘(bash-shell)의 환경변수 LD_LIBRARY_PATH에 추가 시켜야 한다. export 는 Makefile의 외부변수 지정을 의미한다.

    d. wave

    목표 wave 는 시뮬레이션을 수행한 후 기록된 VCD 파형을 보기 위한 것이다. VCD 파형 보기 소프트웨어로 gtkwave를 사용하였다.

    e. clean

    목표 clean 은 현재 디렉토리를 정리한다. 빌드하는 과정에서 여러 중간 파일들이 생성된다. 개발 중 파일명이 변경 되기도 하고 임시 저장된 파일도 있다. 보관을 위해 디렉토리 청소가 필요할 때 보존되어야 할 파일들과 지워도 좋을 파일을 분명히 해줄 필요가 있다.

5. 하드웨어 언어의 코딩 스타일

    “내 칩 서비스” 표준 셀 디자인 킷(이하 디자인 킷)과 함께 제공된 예제의 시뮬레이션을 수행해 보면서 코딩 스타일이 설계 자동화 도구(HDL 시뮬레이터, 합성기 등)에 미치는 영향을 살펴 보기로 한다. ‘디자인 킷’은 깃-허브를 통해 내려 받을 수 있다.

    https://github.com/GoodKook/ETRI-0.5um-CMOS-MPW-Std-Cell-DK

예제의 수행은 모두 오픈-소스 반도체 설계 도구를 활용한다. 오픈-소스 반도체 설계 도구와 디자인 킷의 설치 방법을 설명한 문서를 참조한다.

    “ETRI 0.5um CMOS Std-Cell DK: 오픈-소스 반도체 설계 도구 설치 “[내려받기]

5-1. D-플립플롭 시뮬레이터의 빌드와 실행

    레지스터 트랜스퍼 수준(RTL)에서 베릴로그로 기술한 D-플립플롭을 언어 변환 도구 Verilator를 사용하여  SystemC/C++ 모델로 변환하고 이를 SystemC 테스트벤치와 빌드(컴파일 및 링크)한다. 모두 C++ 이므로 GNU의 빌드 도구들(GNU GCC/G++)이 동원된다.

    예제의 폴더로 이동 후 Verilog RTL로 기술한  D-플립플롭 과 SystemC 테스트벤치를 묶어 시뮬레이터를 빌드한다. 먼저 예제 디렉토리로 이동하여 베릴로그 DUT와 SystemC 테스트 벤치를 살펴본다.

    $ cd ~/ETRI050_DesignKit/Tutorials/2-3_Lab1_dff/dff

    시험할 대상(DUT, Desugn Under Test)은 RTL에서 베릴로그로 기술된 모듈 dff 다.

    /**************************************
    Associated Filename: dff.v
    Purpose: D-FlipFlop
    ***************************************/

    module dff(clk, d, q);
    input clk, d;
    output q;
    reg q;

    always @(posedge clk) // edge trigger
    begin
        q <= d;
    end

    endmodule

    SystemC 테스트벤치는 다음과 같다. 시험할 DUT가 RTL 이므로  테스트를 수행에 필요한 시험입력 또한 이에 준하는 추상화 수준에서 작성 되어야 한다.

    /**************************************************************
    Associated Filename: sc_dff_TB.h
    Purpose: Testbench
    ***************************************************************/

    #ifndef _SC_DFF_TB_H_
    #define _SC_DFF_TB_H_
    #include <systemc.h>
    #include "Vdff.h" // Verilated DUT

    SC_MODULE(sc_dff_TB)
    {
        sc_clock        clk;
        sc_signal<bool> d, q;

        Vdff*   u_Vdff;

        sc_trace_file* fp;  // VCD file

        SC_CTOR(sc_dff_TB):    // constructor
            clk("clk", 100, SC_NS, 0.5, 0.0, SC_NS, false)
        {
            // instantiate DUT
            u_Vdff = new Vdff("u_Vdff");

            // Binding
            u_Vdff->clk(clk);
            u_Vdff->d(d);
            u_Vdff->q(q);

            SC_THREAD(test_generator);
            sensitive << clk;

            // VCD Trace
            fp = sc_create_vcd_trace_file("sc_dff_TB");
            sc_trace(fp, clk, "clk");
            sc_trace(fp, d, "d");
            sc_trace(fp, q, "q");
        }

        void test_generator()
        {
            int test_count =0;

            d.write(0);

            while(true)
            {
                wait(clk.posedge_event());
                d = 1;
                wait(clk.posedge_event());
                d = false;
                wait(clk.negedge_event());
                d = 1;
                wait(clk.posedge_event());
                d = 0;
                wait(clk.posedge_event());
                d = 1;
                wait(clk.posedge_event());
                wait(clk.posedge_event());

                sc_close_vcd_trace_file(fp);

                sc_stop();
            }
        }
    };
    #endif

    SystemC는 높은 시스템 수준에서 낮은 RTL 까지 폭넓은 추상화 수준을 지원한다. SystemC 모듈은 먼저 두개의 헤더 파일을 들여오고(include) 있는데, “systemc.h”는 SystemC의 하드웨어 묘사용 크래스들을 사용하기 위한 것이며, “Vdff.h”는 베릴로그에서 C++ 로 변환된 모델을 기술한 크래스 정의다. 위의 SystemC 테스트벤치는 가장 기본적인 구성을 갖추고 있다. DUT를 사례화 하고 지역 신호에 연결 하며 테스트 입력을 생성한다. 이번 예제는 워낙 단순한 DUT라 출력 검사 부분은 생략 되었다. 

    예제 디렉토리에 준비되어 있는 Makefile을 가지고 시뮬레이터를 빌드한다.

    $ make build
    verilator --sc -Wall --trace --top-module dff --exe --build \
        -CFLAGS -std=c++17 \
        dff.v sc_main.cpp

        ............

    - V e r i l a t i o n   R e p o r t: Verilator 5.027 ..…
    - Verilator: Built from 0.011 MB sources in 2 modules, ..…
    - Verilator: Walltime 0.770 s (elab=0.004, cvt=0.008, bld=0.744)...

    오류 없이 시뮬레이터(실행 파일)가 만들어 졌으면 실행시켜 보자.

    $ make run
    ./obj_dir/Vdff

        SystemC 3.0.0-Accellera --- Jun 18 2024 08:49:55
        Copyright (c) 1996-2024 by all Contributors,
            ALL RIGHTS RESERVED
    Info: (I702) default timescale unit used for tracing: 1 ps (sc_dff_TB.vcd)
    Info: /OSCI/SystemC: Simulation stopped by user.

    VCD 로 기록된 시뮬레이션의 결과를 살펴보자.

      $ make wave

    DUT가 단순하고 시뮬레이션도 아주 짧다. D 플립플롭의 RTL 묘사는 클럭 엣지의 사건에 감응되어 입력 d 를 출력 q 로 전송한다. 입력의 이전 값을 취하고 있는 점에 주목하며 시뮬레이션 결과 파형을 살펴보자.

    (1) 입력 d 와 clk 가 동시에 사건이 발생했다. 클럭의 상승 엣지 사건 (posedgge clk)에 구동된 순차 할당문은 입력 d 의 할당 이전 값을 취한다.
    (2) clk 에 하강 엣지 사건이 발생 했지만 이에 감응된 프로세스는 없다. D 플립플롭의 행동을 묘사한 always @() 는 상승 엣지에서만 반응한다.
    (3) 이번에도 clk의 상승 엣지 사건와 입력 d의 할당이 동시에 일어났다. 출력 q 를 앞서 (1)의 경우와 비교해 보면 동일하게 d 의 이전의 값을 취하고 있다.
    (4) 출력 q 는 clk의 하강 엣지 사건에 영향을 받지 않고 이전 값을 유지한다.
    (5) 앞서 (3)의 경우와 같다.
    (6) 앞서 (1)의 경우와 같다. clk의 상승 엣지 사건에 감응하여 실행된 할당문은 d 의 이전 값을 취한다.
    (7) 앞서 (3), (5)의 경우와 같다. 입력 d 의 이전 값을 취하여 출력 q에 전송되었다.

5-2. 레지스터 전송 수준

    앞서 D-플립플롭 시뮬레이션의 VCD 파형에서 (1), (3), (5), (6) 시점을 보면 clk의 상승 엣지 사건과 동시에 입력 d 의 사건이 발생했다. 이때 엣지 트리거 플립플롭의 출력 q는 클럭 사건이 일어난 이전 시점의 d 값을 취하고 있다. 이는 RTL 시뮬레이터가 순차 논리 회로의 동작 원리를 따르고 있음을 보여준다.

    흔히 RTL(레지스터 전송 수준)이라고 하는 추상화 수준은 디지털 하드웨어에 근접하여 "클럭 동기"에 맞춰 작동하는 디지털 회로를 묘사 했다는 의미가 포함되어 있다. 한 클럭으로 연동된 두 플립플롭 사이에 조합 회로들이 놓여 있다.

    플립플롭 U0의 출력 Q0은 클럭의 상승 엣지에서 시작된다. 이 출력 값은 조합회로를 통과하며 지연으로 인한 불안정을 보이기 시작한다. 플립플롭 U1은 클럭의 다음번 상승 엣지에서 조합회로를 통과하며 지연된 값을 취한다. RTL이란 연속적인 클럭의 엣지를 사이에 두고 플립플롭 사이에 이뤄지는 신호의 전송을 표현한 것이다.

    아래 그림은 클럭 엣지 트리거 D-플립플롭을 트랜지스터의 조합으로 구성한 후 SPICE 회로 시뮬레이터로 얻은 파형의 모습이다. 시뮬레이션 소프트웨어들이 하드웨어를 묘사하고 동작을 전자회로에 맞게 모의하고 있음을 단적으로 보여준다.

5-3. 컴퓨팅 언어에 의한 하드웨어의 묘사

   하드웨어 D-플립플롭을 Verilog로 RTL에서 기술하고 C++ 로 작성한 테스트벤치와 함께 시뮬레이터를 만들고 실행해 동작을 파형으로 관찰하였다. 그 결과 컴퓨팅 언어로 전자회로의 동작을 모의하고 있음을 알게 되었다. 하지만 묘사하려는 대상이 하드웨어의 행동이라는 점을 염두에 두어야 한다. 하드웨어를 언어적으로 표현하기 위해 사용하는 객체는 소프트웨어 언어의 변수와는 사뭇 다르다. 하드웨어 언어에서도 객체를 심볼로 표현하지만 행동의 묘사방법(코딩 스타일)에 따라 단순한 전선(또는 조합회로)이 될 수도 있고 저장소(또는 순차회로)가 되기도 한다. 하드웨어를 표현한 문장은 전선 혹은 저장소 모두 병렬 실행이 원칙이다. 다음은 베릴로그의 순차구문 구역 내에 연속적인 할당을 보여주는 예다.

입력  d가 q1을 거쳐 연속적으로 q 까지 할당 되었다. 이때 d 와 q1의 연속적인 할당은 하나의 전선으로 합쳐진다. 만일 연속 할당 구문의 순서를 바꿀 경우 다음과 같은 결과를 낳는다.

할당 문의 순서가 역전되므로 그로 인하여 저장소가 늘어났다. 이는 always 구역이 소프트웨어 처럼 순차 실행을 하고 있기 때문이다. 위의 예에서 두가지 할당 연산자가 사용되었는데, '='은 즉시 할당, <= 은 지연 할당이라 한다. 즉시 할당은 저장이 없는 조합회로(전선)를 표현할 때, 지연할당 연산자는 저장소(플립플롭)를 묘사할 때 적용된다. 하지만 순차구문 구역 내에서 할당 구문의 순서에 따라 즉시 할당문도 저장소를 만들 수 있다는 점에 주의해야한다. 할당 구문 순서에 상관없이 모두 연속적인 플립플롭을 묘사하고자 한다면 모두 지연 할당 연산자를 적용한다.

5-4. 하드웨어 행위 묘사의 코딩 스타일

    컴퓨팅 언어로 알고리즘 또는 하드웨어를 묘사 할 때 대상에 따라 주의가 필요하다. 하드웨어를 대상으로 할 경우 이에 제시되는 언어의 문장구성과 문장의 실행방식에 주의해야 한다. 문법 오류없이 기술 되었다고 해서 의미를 제대로 담았다고 보장 할 수는 없기 때문이다. 의미를 명확히 해주기 위한 지침이 필요한데 이를 코딩 스타일(coding style)이라 한다. 앞서 할당 연산자의 사용 만으로도 다른 의미가 될 수 있다는 점을 알게 되었다. 하드웨어 언어의 또 다른 주의점으로 문장의 실행이다. 하드웨어는 모든 문장이 병렬 실행을 원칙으로 하며 always 구역은 한개의 병렬 구문(여러개 순차 구문을 담은 절)과 같다. 언어로서 Verilog의 시뮬레이션 동작은 사건 감응과 구동이라는 원칙하에 일관성을 가지고 설명될 수 있다.

[주] 감응 목록에 넣고 빼기에 의해 할당문의 실행 결과가 다를 수 있다[4]. 코딩 스타일은 컴퓨팅 언어로서 문법적 규칙과 문장의 실행 방식(병렬실행과 순차실행) 외에 합성(synthesis)을 위한 작성 양식이다. 사건구동과 감응 목록은 하드웨어의 행동을 컴퓨팅 언어로 기술하기 위한 소프트웨어적 기법이며 실제 디지털 회로를 정확히 반영하지 않는다는 점을 염두에 두어야 한다. 심지어 합성기 마다 다른 회로를 생성해서 시뮬레이션과 일치하지 않을 수 있다. 이런 이유로 EDA 업계를 중심으로 합성용 RTL 코딩 스타일의 표준이 제정되었다[5].

    순차구문 영역 always의 감응 리스트에 posedge clkd 도  함께 주어 졌다면 문제가 발생한다. 할당문은 지정된 감응에 충실히 반응하여 아래와 같은 결과를 보여줄 것이다. 이는 우리가 원하던 플립플롭의 동작이 아니다.

    감응목록에 따라 다른 결과를 보여주기도 하지만 시뮬레이션의 실행 속도에 영향을 주기도 한다. 아래의 경우 D-플립플롭의 행동을 적절하게 기술하고 있다. 하지만 always 구역이 clk의 상승과 하강 엣지 사건에 모두 반응한다. 하강 엣지에서 불필요하게 always 구역을 수행하므로써 시뮬레이터의 부담을 증가시킨다.

    아래의 예는 마치 레벨 트리거 래치(level trigger latch)처럼 보인다. 감응신호 clk와 d 의 모든 사건에 always 가 반응 하지만 할당은 if 문의 조건을 따른다.

    비동기 셋과 리셋을 가진 D 플립플롭을 기술하면 아래와 같다. clk의 상승 엣지 사건과 무관하게 q 가 셋 또는 리셋되고 있다. 플립플롭 동작은 clk의 상승 엣지 사건에 의해 작동한다.

감응 목록에서 r 과 s 를 빼면 모두 clk에 동기 시킬 수 있다. 클럭 동기 셋과 리셋을 가진 플립플롭은 권장하지 않는다.

    동기 셋과 리셋을 가진 플립플롭을 표준셀로 가진 경우는 흔치 않다. 설계에 동기가 필요할 경우 대부분 입력 d 에 멀티 플렉서를 달아  구현한다.

5-5. 시뮬레이션 델타

    컴퓨팅 언어는 문법 뿐만 아니라 문장의 실행 방식을 정하고 있다.  문장이 실행된 후 그 결과의 일관성은 매우 중요하다. 특히 병렬 실행문과 순차실행문을 모두 수용하는 하드웨어 기술 언어의 경우 코딩 스타일에 따라 다른 결과를 가져올 수 있고 그 차이는 실행방식의 규정으로 설명 될 수 있어야 한다.

    감응 목록에 지정된 다수의 신호가 동시에 시건을 일으킨 경우를 상정해 보자. 각 사건에 대응하여 순차구역이 실행된다. 시뮬레이션 델타 시간 내에서 사건의 순서에 무관한 결과를 얻을 수 있다. 아래의 예에서 clk의 상승 엣지와 r 과 s의 하강 엣지 사건이  동시에 일어 났다. always 구역은 각 사건에 대해 모두 반응할 것이다. 시뮬레이터가 어느 사건을 먼저 처리하든 그 결과는 동일해야 한다. 사건 구동 방식에서 할당과 사건의 수집 그리고 콜-백 그리고 갱신의 과정을 시뮬레이션 델타(simulation delta)라한다. 모든 사건의 처리가 완료되기까지 시뮬레이션 시간은 멈춰있다. 아래의 예에서 감응으로 지정된 신호에 사건이 발생하면 순차구문 if~else 의 논리에 따라 우선순위가 결정(priority encoder)되어 있을 뿐 시뮬레이션은 일관성을 유지한다.

    감응 목록이 아래와 같은 경우 시뮬레이션 결과를 설명해 보자. 실제 디지털 회로가 되기 어렵지만 사건구동 시뮬레이션으로 일관성 있는 설명을 해보기 바란다.

5-6. 추상성 수준: 행위 묘사 vs 회로 묘사

    앞서 D-플립플롭을 클럭의 엣지 사건에 반응하는 행위로 기술 했다면 이번에는 논리 연산자를 사용하여 기술해본다. 기초적인 디지털 정보 저장 장치 D-래치를 베릴로그 논리 연산자로 표현하면 아래와 같다. assign은 병렬 할당 구문을 표현한다. 두 병렬 문장에서 할당 왼편과 오른편의 와이어가 서로 교차 결선되어 있지만 하드웨어를 표현한 병렬 실행 구문의 동작은 순서에 무관하다.

두개의 D-레치를 연속 사용하면 엣지 트리거 방식의 플립플롭을 구현하면 아래와 같다.

    D-플립플롭을 클럭 신호의 상승 엣지 사건에 반응하여 정보를 저장한다는 행위의 표현보다 좀더 실제 전자회로에 가까운 논리 게이트 소자로 기술 하였다. 디지털 정보의 저장장치는 서로다른 두 NAND 게이트의 입출력 포트를 교차 연결하므로서 전류의 지연된 궤환으로 전압을 유지시킨 수 있다는 물리적 현상에 기초한다. 논리소자(게이트)는 다시 트랜지스터의 회로로 표현하면 아래와 같다. 트랜지스터의 조합으로 표현되었지만 여전히 개념적인 표현이다.

    실제 반도체 제조에 사용될 도면(레이아웃)은 아래와 같다. 수십개의 트랜지스터를 사용해야 하는 D-플립플롭을 단 한줄의 할당문으로 기술하므로서 직관적며 높은 설계 생산성을 가질 수 있다. 전자부품의 동작을 행위 수준에서 기술 되었을 때 추상화 수준이 높다고 한다. 실제 제조가능한 도면으로 표현 되었을 때 추상화 수준이 낮다고 한다. 이 구체적인 전자회로는 전류의 흐름을 제어하여 동작을 일으키고 유지되는 전압으로 정보를 담는다.

    반도체 설계의 과정은 추상화 수준을 낮추는 과정이다. 추상화 수준의 변환에 반도체 설계 자동화 도구들이 동원된다. 합성기, 자동 배치배선기 등은 컴퓨팅 언어로 기술된 전자회로의 행위를 낮은 추상화 수준으로 변환(또는 합성)해주는 도구들이다. 추상화 수준이 변경되어 얻은 회로도의 등가성 확인은 필수다.  서로다른 추상화 수준으로 표현된 회로의 등가성을 확인하는 절차를 검증이라 한다. 검증에는 HDL 시뮬레이터, SPICE 회로 시뮬레이터 등이 동원된다. 

5-7. 테스트벤치 재사용

    목적에 따라 동일한 기능(알고리즘)을 상이한 언어 또는 다른 추상화 수준에서 표현할 수 있다. 자동화 도구에 의해 추상화 수준이 변경될 수도 있고 수동으로 변경 될 수도 있다. 어떤 경우든 그 동작의 결과는 동일해야 한다. 검증은 서로다른 두 표현이 낳는 동작의 등가성을 보장하는 절차다. 구문 형식이 다를 뿐 동일한 실행방식을 취하는 소프트웨어 언어에 비해 실행 방식조차 완전히 바뀌는 하드웨어언어를 감안 한다면 변환(또는 합성)의 과정에서 오류가 끼어들 여지는 더욱  다분하다. 이점에서 변환(translation)과 합성(synthesis)의 차이를 감지할 수 있다. 자동화 도구는 설계 생산성을 높여주고 인간의 오류를 줄여주는데 획기적으로 기여하지만 완벽하다고 장담할 수 없으므로 반드시 검증의 과정이 필요하다. 검증의 가장 확실한 방법은 설계를 시험하기 위해 마련된 테스트벤치를 활용한 시뮬레이션이다. 추상화 수준이 변경 되었더라도 동일한 테스트벤치를 사용하는 것이 원칙이다. 다양한 이유로 추상화 수준이 달라진 경우 다른 테스트벤치를 요구하기도 한다. 별도의 테스트벤치를 작성하는 과정은 매우 비생산적일 뿐만 아니라 유연성이 낮아 불완전한 검증으로 이어질 가능성이 매우 높다. 앞서 살펴본 대로 하드웨어를 표현할 수 있는 다양한 크래스 라이브러리를 갖추고 있는 SystemC는 높은 시스템 수준의 알고리즘에서 RTL 까지 매우 폭넓은 추상화 수준을 포용한다.

    행위를 묘사한 D-플립플롭을 시험하기 위해 작성한 테스트벤치를 게이트 수준의 표현에서도 변경 없이 동일하게 적용할 수 있다. 추상화 수준을 넘나들며 테스트벤치를 재사용하므로서 검증의 품질을 한층 높일 수 있음은 자명하다. 테스트벤치 재사용을 D-플립플롭 예제를 통해 살펴보자.

디자인 킷 예제에서 게이트 수준으로 묘사한 D-플립플롭 디렉토리로 이동한 후 파일 목록을 보면 별도의 테스트 벤치는 없다.

    $ cd ~/ETRI050_DesignKit/devel/Tutorials/2-3_Lab1_dff/dff_gate
    $ ll
    total 24
    drwxr-xr-x 2 goodkook goodkook 4096 Jul  4 16:50 ./
    drwxr-xr-x 6 goodkook goodkook 4096 Jul  3 22:08 ../
    -rw-r--r-- 1 goodkook goodkook  894 Jul  3 22:13 dff.v
    -rw-r--r-- 1 goodkook goodkook 1636 Jul  3 22:09 Makefile
    -rw-rw-r-- 1 goodkook goodkook  622 Jun 21 19:02 sc_dff_TB.gtkw
    -rw-rw-r-- 1 goodkook goodkook 1131 Jul  3 22:49 Vdff.gtkw

    다음은 Makefile의 일부다. 행위 묘사 D-플롭플롭을 시험하기 위해 사용했던 SystemC 테스트 벤치 ../dff/sc_dff_TB.h ../dff/sc_main.cpp 를 재사용하고 있다.

    VERILOG_SRCS = dff.v
    SC_SRCS      = ../dff/sc_main.cpp
    SC_TOP_H     = ../dff/sc_dff_TB.h
    VERILATOR    = verilator
    CFLAGS       = -g

    ......

    build : $(TARGET_DIR)/$(TARGET)

    $(TARGET_DIR)/$(TARGET) : $(VERILOG_SRCS) $(SC_SRCS) $(SC_TOP_H)
    $(VERILATOR) --sc -Wall --trace \
                --top-module $(TOP_MODULE) --exe --build \
    -CFLAGS $(CFLAGS) \
        $(VERILOG_SRCS) $(SC_SRCS)

게이트 수준 D-플립플롭 시뮬레이터를 빌드하고 실행하여 VCD 파형을 보면 행위 모델과 동일한 결과를 보여줄 것이다. 추상화 수준이 다른 두 모델의 등가성을 한 테스트벤치를 통해 확인 할 수 있다.

    $ make build

    verilator --sc -Wall --trace --top-module dff --exe --build \
-CFLAGS -g \
dff.v ../dff/sc_main.cpp

        ......

    make[1]: Leaving directory......
    - V e r i l a t i o n   R e p o r t: Verilator 5.037 devel rev v5.036-140-g47f5a6a52
    - Verilator: Built from 0.022 MB sources in 3 modules, into 0.047 MB in 10 C++ files needing 0.000 MB
    - Verilator: Walltime 0.347 s (elab=0.000, cvt=0.006, bld=0.337); cpu 0.010 s on 1 threads; alloced 20.816 MB

    make run

    ./obj_dir/Vdff
        SystemC 3.0.2-Accellera --- Jun 13 2025 17:49:45
        Copyright (c) 1996-2025 by all Contributors,
        ALL RIGHTS RESERVED
    Info: (I702) default timescale unit used for tracing: 1 ps (sc_dff_TB.vcd)
    Info: /OSCI/SystemC: Simulation stopped by user.

    $make wave

5-8. 설계 생산성을 높이는 팁: FOR 반복문과 `define 매크로

    컴퓨팅 언어를 사용하여 알고리즘을 기술할 때 생산성 향상을 위해 여러가지 기법이 동원 된다. 대표적으로 반복적인 구문을 위해 사용하는 for-반복문이 있다. 그외 재사용성을 높이기 위해 define 매크로를 활용하고 조건부 컴파일 기법을 동원한다. 베릴로그 하드웨어 기술 언어도 그와 동일한 기법을 적용 할 수 있다. 다음은 쉬프트 레지스터를 기술한 예다. 다단 쉬프트 레지스터를 묘사하기 위해 for-반복문을 사용했다. 쉬프트 단수를 `define 매크로에 정의해 둠으로써 필요에 따라 쉽게 조정할 수 있는 유연성을 갖추고 있다.

    // filename: shifter.v
    `define NUM_REG 4
    `define BIT_WIDTH 8

    module shifter(clk, rst, din, qout);
    input clk, rst;
    input [`BIT_WIDTH-1:0] din;
    output [`BIT_WIDTH-1:0] qout;

    reg qout;
    reg [`BIT_WIDTH-1:0] x[`NUM_REG];

    always @(posedge clk or negedge rst) // edge trigger, Async rst
    begin
        if (!rst) begin // Reset
            for (integer i = 0; i < `NUM_REG; i++)
                x[i] <= 0;
        end else begin
            for (integer i = 1; i<`NUM_REG; i++)
                x[i+1] <= x[i];
            
x[0] <= din;
            qout <= x[3];
        end
    end

    endmodule


6. 맺음말

    Verilog로 설계하고 SystemC로 검증 하는 설계 방법론을 간단한 D-플립플롭의 예를 들어 소개했다. 트랜지스터의 회로에 비하여 추상화 수준을 비교해 볼 수 있을 것이다. 설계자의 안목 또한 중요한 요소가 된다. 반도체 설계에 컴퓨팅 언어를 사용하므로써 얻는 장점이 많다. 컴퓨팅 언어 기반의 설계 방법론이 성숙되어 알고리즘 개발자도 높은 추상화 수준에서 반도체(하드웨어)를  쉽게 시작할 수 있다. 하지만 최종 목표가 전자회로라는 점을 인식하고 있어야 한다. Verilog 도 컴퓨팅 언어다. 컴퓨팅 언어로는 고도의(혹은 기이한 트릭) 행위의 표현이 가능 하지만 실제로 트랜지스터 회로로 전환 될 수 없을 수도 있다.

    하드웨어의 행위를 묘사하는 설계와 더블어 검증 또한 매우 중요하다. 제대로 검증되지 않은 설계가 하드웨어로 구현 되어 일으킬 손실은 막대하다는 점은 굳이 반도체 뿐만은 아니다. 검증은 설계보다 높고 넓은 추상성을 갖춰야 한다는 점도 이해했을 것이다. 시뮬레이션 소프트웨어에서 병렬성을 구현하는 방법을 간략히 살펴봤다. 코딩 스타일에 따라 시뮬레이션의 결과가 달라질 수 있는 이유를 설명 할 수 있을 것이다. 시뮬레이션 소프트웨어가 병렬성을 일관성있게 처리하는 방식을 알면 시스템 모델링에 큰 도움이 된다.

----------------------------------------------------------------

참고:

[1] SystemC Quick Reference card, http://www.eis.cs.tu-bs.de/klingauf/systemc/systemc_quickreference.pdf

[2] Verilog Quick Reference,  https://web.stanford.edu/class/ee183/handouts_win2003/VerilogQuickRef.pdf

[3] C++ Quick Reference, https://www.hoomanb.com/cs/quickref/CppQuickRef.pdf

[4] RTL Coding Styles That Yield Simulation and Synthesis Mismatches, http://www.sunburst-design.com/papers/CummingsSNUG1999SJ_SynthMismatch.pdf

[5] IEEE 1364.1-2002 - IEEE Standard for Verilog Register Transfer Level Synthesis, https://ieeexplore.ieee.org/document/1146718

[6] IEEE Standard for Verilog Hardware Description Language, https://www.eg.bucknell.edu/~csci320/2016-fall/wp-content/uploads/2015/08/verilog-std-1364-2005.pdf

[7] "VLSI 레이아웃 설계 기초" [8] Std-Cell 제작 실습: DFF-SR, https://fun-teaching-goodkook.blogspot.com/2024/07/vlsi-8-std-cell-dff-sr.html

[8] GNU Make강좌, https://doc.kldp.org/KoreanDoc/html/GNU-Make/GNU-Make.html

[9] FORTE Design, Learn SystemC, https://www.youtube.com/playlist?list=PLcvQHr8v8MQLj9tCYyOw44X1PLisEsX-J