레이블이 반도체 설계 언어인 게시물을 표시합니다. 모든 게시물 표시
레이블이 반도체 설계 언어인 게시물을 표시합니다. 모든 게시물 표시

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


2024년 6월 2일 일요일

ETRI 0.5um CMOS DK 예제: counter8/16, 디지털 회로 칩 테스트 방법

ETRI 0.5um CMOS DK 예제: counter8/16, 디지털 회로 칩 테스트 방법

목차

    1. 개요
    2. Verilog: 적재가능한 8비트 2진 카운터
    3. SystemC: 테스트 벤치
    4. Arduino: 칩과 테스트벤치 인터페이스
    5. 칩 테스트
    6. 결론

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

1. 개요

한 객체(신호)를 다수의 비트로 표현하는 디지털 회로를 테스트 하기는 쉽지 않다. 로직 아날라이져(logic analyzer)라는 측정 장비를 사용하면 트리거를 설정한 후 출력을 병렬로 획득하고 이를 분석할 수 있으나 입력은 사정이 다르다. 병렬로 주어야 할 입력 신호의 각 비트선 마다 펑션 제네레이터(function generator)를 쓸 수도 있겠으나 동원해야할 신호 발생기의 댓수도 많아질 뿐만 아니라 정작 동시에 값을 주어야 하기 때문에 동기를 맞춰줘야 하는 어려움이 있다. 로직 아날라이져 측정기는 가격이 만만치 않긴 하지만 시장에서 구입 할 수 있다. 하지만 임의 병렬 신호 발생기는 시장에 나와있지 않다. 이는 설계마다 주어야할 입력신호 열이 다르기 때문에 범용 장비로 발매되기 곤란하다. 임의 신호열을 적절하게 발생 시키려면 장비를 프로그램 해주어야 한다. 통상 테스트 엔지니어가 이 부분을 담당한다. 디지털 테스트 신호 발생 장비는 주문형으로써 매우 고가다.

"내칩 MPW"에서 제작해 주는 칩의 핀 수는 28개다. 이중 전원과 접지를 빼면 26개의 핀이 신호선으로 사용할 수 있다. 입력 또는 출력으로 사용 할 핀의 갯수는 설계마다 다르다. 만일 디지털 설계로 칩을 제작했을 경우 이를 테스트 하려면 장비를 동원해야 하는 어려움에 봉착하게된다.

디지털 설계의 검증은 레지스터 전송 수준(RTL, Register Transfer Level) 시뮬레이션을 수행 한다. 이를 위해 검증 대상(DUT, Design Under Test)에 입력을 주고 출력을 검사하는 테스트 벤치(testbench)를 작성한다. 테스트 벤치는 DUT를 기술했던 HDL로 작성하거나 그보다 높은 추상화 수준의 C++ 언어를 사용한다. 어떤 경우든, Verilog, VHDL, C, C++ 등은 모두 컴퓨팅 언어다.

상당한 성능의 마이크로 컨트롤러를 채택한 응용 보드들이 매우 저렴하게 보급되고 있다. 교육 뿐만 아니라 취미가, 심지어 연구 및 산업 현장에서도 활용되고 있다. 그중 가장 널리 보급된 아두이노 보드를 활용하여 칩 테스트 방법을 설명한다. 사용할 아두이노 보드는 Arduino 2560 MEGA 다. 입출력 전압이 5V 이며 입출력 설정이 가능한 디지털 핀의 갯수가 32개 이상으로 "내칩 MPW"에서 제작되는 칩을 테스트 할 수 있는 요건에 부합한다. 가격 또한 2~3만원 대로 매우 저렵하다. 게다가 수많은 사용자들이 만들어 놓은 라이브러리들이 풍부하게 제공된다는 점 또한 큰 매력이라고 하겠다.

아듀이노 보드는 컴퓨터에 기본적으로 장착된 USB 주변장치와 직렬 통신 방식으로 연결 된다. 운영체제(윈도우즈, 리눅스 등)는 컴퓨터에 장착된 모든 주변장치를 컴퓨팅 언어로 제어 할 수 있도록 장치 구동기 소프트웨어를 제공한다. USB의 직렬 통신 포트도 예외는 아니다.  앞서 DUT의 시뮬레이션 검증을 위해 테스트 벤치는 컴퓨팅 언어로 제작해 두었다. 따라서 테스트 벤치에서 DUT와 주고받던 모든 입출력 신호는 모두 USB를 통해 아듀이노보드와 통신 할 수 있다는 뜻이다.

Verilog, VHDL 같은 하드웨어 기술 언어(HDL)도 C/C++ 언어의 함수를 호출 할 수 있는 방법(PLI/VPI, DPI, VHPI 등)이 마련되어 있으며 이를 통해 운영체제의 장치 구동기에 접근 할 수 있다. 하지만 매우 난해하다. 합성을 고려한 칩의 RTL 설계는 HDL을 이용하고 테스트 벤치는 SystemC를 활용하길 권한다. SystemC는 소프트웨어 및 하드웨어를 망라한 시스템 모델링을 위해 제공되는 C++ 라이브러리(libraries)다. GNU C++ 컴파일러로 컴퓨터로 할 수 있는 모든 수단을 칩 설계와 검증에 동원 할 수 있다는 뜻이다[참조]. 게다가 SystemC 는 오픈-소스다.

                        +---------------+
                        | SystemC TB    |
                        +-----+  in     |
                   +-------+  |  C++    |
                   |  DUT  |  |         |
                   |   in  +--->        |
                   |Verilog|  |         |
                   |       <----        |
                   |       |  |         |
                   +-------+  |         |
                         +----+         |
             Simulation  |              |
                         +--------------+

                         Re-Use Testbench

                         +--------------+
                         | SystemC TB   |
                         +----+   in    |
+--------+  +---------+       |   C++   |
|  DUT   |  | Arduino |       |         |
|   in   +---> MEGA   |       |         |
| MyChip |  |         <=[USB]=>         |
|  MPW   <----        |       |         |
|        |  |         |       |         |
+--------+  +---------+       |         |
                         +----+         |
        Chip Test        |              |
       by Emulation      +--------------+

Verilog 로 작성된 설계물의 검증을 위해 SystemC 로 작성된 테스트 벤치를 칩-테스트에 재활용(testbench re-use)하는 방법을 제시한다. 예제를 위한 환경구성 요건은 아래와 같다.

소프트웨어
    - 운영체제: WSL/Ubuntu 20 [설치법]
    -  개발도구: GNU C++/clang 17, SystemC, Verilator [설치법]

하드웨어
    - Arduino 2560 MEGA [설치법]

예제 깃-허브:
    https://github.com/GoodKook/ETRI-0.5u-CMOS-MPW-DK-Example--Counter8


2. Verilog: 적재가능한 8비트 2진 카운터

표준 LSI인 4비트 2진 카운터74163을 2개 엮어 8비트 2진 카운터를 설계 했다. 74163의 데이터 쉬트에 따르면 2진 카운터의 내부 논리 회로도는 아래와 같다.

논리회로로 그려진 이 회로도는 쉽게 눈에 들어오지 않는다. 인간의 언어와 가까운 Verilog로 작성하면 아래와 같다. 베릴로그 언어를 잘 알지 못한다 해도 한눈에 1씩 증가하는 카운터 임을 알 수 있다. 반도체 설계에 언어를 하려는 근본적인 이유다.

// Filename: LS163.v
// Loadable binary 4-bit counter

module LS163(nCLR, nLOAD, Din, CLK, ENP, ENT , Dout, RCO);
    input           CLK, nCLR, nLOAD;
    input           ENP, ENT;
    input  [3:0]    Din;
    output [3:0]    Dout;
    output          RCO;

    reg [3:0] cnt;
    always @(posedge CLK or negedge nCLR)
    begin
        if (!nCLR)
            cnt <= 0;
        else
        begin
            if (!nLOAD)
                cnt <= Din;
            else if (ENP && ENT)
                cnt <= cnt + 4'b0001;
        end
    end

    assign RCO = cnt[3] & cnt[2] & cnt[1] & cnt[0];
    assign Dout = cnt;
endmodule

다음은 2개의 4비트 카운터를 이어붙인 8비트 카운터다. 컴퓨팅 언어에 익숙한 현대인은 금방 뜻을 알아차릴 수 있다. 역시 언어로 하드웨어를 묘사하기 쉽다.

module counter8(CLK, nCLR_L, nCLR_H, nLOAD_L, nLOAD_H, ENP, ENT, Din, Dout, RCO);
    input           CLK;
    input           nCLR_L, nCLR_H;
    input           nLOAD_L, nLOAD_H;
    input           ENP, ENT;
    input  [7:0]    Din;
    output [7:0]    Dout;
    output          RCO;

    wire Full;    

    LS163 u_LS163xL(
            .nCLR(nCLR_L),
            .nLOAD(nLOAD_L),
            .Din(Din[3:0]),
            .CLK(CLK),
            .ENP(ENP),
            .ENT(ENT),
            .Dout(Dout[3:0]),
            .RCO(Full));

    LS163 u_LS163xH(
            .nCLR(nCLR_H),
            .nLOAD(nLOAD_H),
            .Din(Din[7:4]),
            .CLK(CLK),
            .ENP(ENP),
            .ENT(Full),
            .Dout(Dout[7:4]),
            .RCO(RCO));
endmodule

3. SystemC: 테스트 벤치

8비트 2진 카운터의 테스트 벤치는 SystemC로 작성 하였다. SystemC를 잘 모른다 하더라도 앞서 베릴로그를 읽을 수 있다면 그 뜻을 바로 알아차릴 수 있을 것이다. 컴퓨팅 언어 공부는 일단 예제 읽기로 시작하자.

/*****************************************************
Vendor: GoodKook, goodkook@gmail.com
Associated Filename: sc_counter8_TB.h
Purpose: Testbench for counter8
Revision History: Jun. 1, 2024
*******************************************************/

#ifndef _SC_COUNTER8_TB_H_
#define _SC_COUNTER8_TB_H_

#include <systemc.h>
#include "Vcounter8.h"
#ifdef CO_EMULATION
#include "counter8.h"
#endif

SC_MODULE(sc_counter8_TB)
{

    sc_clock            CLK;
    sc_signal<bool>     nCLR_L;
    sc_signal<bool>     nCLR_H;
    sc_signal<bool>     nLOAD_L;
    sc_signal<bool>     nLOAD_H;
    sc_signal<bool>     ENP;
    sc_signal<bool>     ENT;
    sc_signal<bool>     RCO;

    // Verilator treats all Verilog's vector as <uint32_t>
    sc_signal<uint32_t> Din_n32;
    sc_signal<uint32_t> Dout_n32;
    // Exact DUT ports' vector width
    sc_signal<sc_uint<8> > Din_n8;
    sc_signal<sc_uint<8> > Dout_n8;

    // Verilated DUT or Foreign Verilog
    Vcounter8*   u_Vcounter8;
#ifdef CO_EMULATION
    counter8*    u_counter8;
    sc_signal<sc_uint<8> >  Dout_emu;
    sc_signal<bool>         RCO_emu;
#endif

    // Convert Verilator's ports to DUT's ports
    void conv_method()
    {
        Din_n8.write((sc_uint<8>)Din_n32);
        Dout_n8.write((sc_uint<8>)Dout_n32);
    }

    void test_generator();

    sc_trace_file* fp;  // VCD file

    SC_CTOR(sc_counter8_TB) :   // Constructor
        CLK("CLK", 100, SC_NS, 0.5, 0.0, SC_NS, false)
    {
        // DUT Instantiation
        u_Vcounter8 = new Vcounter8("u_Vcounter8");
        // Binding
        u_Vcounter8->CLK(CLK);
        u_Vcounter8->nCLR_L(nCLR_L);
        u_Vcounter8->nCLR_H(nCLR_H);
        u_Vcounter8->nLOAD_L(nLOAD_L);
        u_Vcounter8->nLOAD_H(nLOAD_H);
        u_Vcounter8->ENP(ENP);
        u_Vcounter8->ENT(ENT);
        u_Vcounter8->Din(Din_n32);
        u_Vcounter8->Dout(Dout_n32);
        u_Vcounter8->RCO(RCO);

#ifdef CO_EMULATION

        u_counter8 = new counter8("u_counter8");
        u_counter8->CLK(CLK);
        u_counter8->nCLR_L(nCLR_L);
        u_counter8->nCLR_H(nCLR_H);
        u_counter8->nLOAD_L(nLOAD_L);
        u_counter8->nLOAD_H(nLOAD_H);
        u_counter8->ENP(ENP);
        u_counter8->ENT(ENT);
        u_counter8->Din(Din_n8);
        u_counter8->Dout(Dout_emu);
        u_counter8->RCO(RCO_emu);

#endif

        SC_THREAD(test_generator);
        sensitive << CLK;

        SC_METHOD(conv_method);
        sensitive << Din_n32 << Dout_n32;

        // VCD Trace
        fp = sc_create_vcd_trace_file("sc_counter8_TB");
        fp->set_time_unit(100, SC_PS);
        sc_trace(fp, CLK, "CLK");
        sc_trace(fp, nCLR_L, "nCLR_L");
        sc_trace(fp, nCLR_H, "nCLR_H");
        sc_trace(fp, nLOAD_L, "nLOAD_L");
        sc_trace(fp, nLOAD_H, "nLOAD_H");
        sc_trace(fp, ENP, "ENP");
        sc_trace(fp, ENT, "ENT");
        sc_trace(fp, Din_n8, "Din");
        sc_trace(fp, Dout_n8, "Dout");
        sc_trace(fp, RCO, "RCO");

#ifdef CO_EMULATION
        sc_trace(fp, Dout_emu, "Dout_emu");
        sc_trace(fp, RCO_emu, "RCO_emu");
#endif
    }

    // Destructor
    ~sc_counter8_TB() {}
};

#endif // _SC_CPU_6502_TOP_H_

RTL 시뮬레이션을 위해 Verilog 설계를 SystemC로 변환해주는 Verilator 도구를 사용했다. 베릴로그 상위 모듈 counter8 을 SystemC로 변환된 모듈명이 Vcounter8 이다. 이어 USB-UART 를 통해 아두이노 보드와 통신하는 에뮬레이션 모듈은 counter8 이다.

//
// Emulation Interface to "Counter8"/Cycle Accurate
// goodkook@gmail.com
//

#ifndef _COUNTER8_H_
#define _COUNTER8_H_

#include "systemc"

// Includes for accessing Arduino via serial port
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

SC_MODULE(counter8)
{
    // PORTS
    sc_in<bool>         CLK;
    sc_in<bool>         nCLR_L;
    sc_in<bool>         nCLR_H;
    sc_in<bool>         nLOAD_L;
    sc_in<bool>         nLOAD_H;
    sc_in<bool>         ENP;
    sc_in<bool>         ENT;
    sc_in<sc_uint<8> >  Din;
    sc_out<sc_uint<8> > Dout;
    sc_out<bool>        RCO;

    // Arduino Serial IF
    int fd;                 // Serial port file descriptor
    struct termios options; // Serial port setting

#define N_TX    2
#define N_RX    2

    void counter8_thread(void)
    {
        uint8_t     x, y, txPacket[N_TX], rxPacket[N_RX];
        while(true)
        {
            wait(CLK.posedge_event());
            txPacket[0] = (uint8_t)(CLK.read()?     0x40:0x00) |
                          (uint8_t)(nCLR_L.read()?  0x20:0x00) |
                          (uint8_t)(nCLR_H.read()?  0x10:0x00) |
                          (uint8_t)(nLOAD_L.read()? 0x08:0x00) |
                          (uint8_t)(nLOAD_H.read()? 0x04:0x00) |
                          (uint8_t)(ENP.read()?     0x02:0x00) |
                          (uint8_t)(ENT.read()?     0x01:0x00);
            txPacket[1] = (uint8_t)((sc_uint<8>)(Din.read()));

            // Send to Emulator
            for (int i=0; i<N_TX; i++)
            {
                x = txPacket[i];
                while(write(fd, &x, 1)<=0)  usleep(1);
            }

            // Receive from Emulator
            for (int i=0; i<N_RX; i++)
            {
                while(read(fd, &y, 1)<=0)   usleep(1);
                rxPacket[i] = y;
            }

            Dout.write(rxPacket[0]);
            RCO.write((rxPacket[1]&0x01)? true:false);
        }
    }

    SC_CTOR(counter8) :   // Constructor
        CLK("CLK")
    {
        SC_THREAD(counter8_thread);
        sensitive << CLK;

        // Arduino DUT
        fd = open("/dev/ttyACM0", O_RDWR | O_NOCTTY);
        if (fd < 0)
        {
            perror("Error opening serial port");
            return;
        }

        // Set up serial port
        options.c_cflag = B9600 | CS8 | CLOCAL | CREAD;
        options.c_iflag = IGNPAR;
        options.c_oflag = 0;
        options.c_lflag = 0;
        // Apply the settings
        tcflush(fd, TCIFLUSH);
        tcsetattr(fd, TCSANOW, &options);
        // Establish Contact
        int len = 0;
        char rx;
        while(!len)
            len = read(fd, &rx, 1);
        if (rx=='A')
            write(fd, &rx, 1);
        printf("Connection established...\n");
    }
};
#endif

줄 수는 많은듯 해도 내용은 별것 없다. 실제로 2진 카운터의 동작은 없으며 아두이노 보드와 직렬 통신을 열고 입출력 신호를 주고 받는 역활만 한다.

4. 아듀이노: 칩과 테스트벤치 인터페이스

아두이노 보드는 칩과 테스트 벤치 사이의 가교역활을 한다. 단순한 통신 중계를 수행한다. 먼저, 칩의 핀에 연결할 아두이노 보드의 디지털 핀 을 정의하고,

/*  counter8 with SystemC Co-Emulation */

#define PIN_CLK     22
#define PIN_nCLR_L  23
#define PIN_nCLR_H  24
#define PIN_nLOAD_L 25
#define PIN_nLOAD_H 26
#define PIN_ENP     27
#define PIN_ENT     28
#define PIN_Din_0   29
#define PIN_Din_1   30
#define PIN_Din_2   31
#define PIN_Din_3   32
#define PIN_Din_4   33
#define PIN_Din_5   34
#define PIN_Din_6   35
#define PIN_Din_7   36
#define PIN_Dout_0  37
#define PIN_Dout_1  38
#define PIN_Dout_2  39
#define PIN_Dout_3  40
#define PIN_Dout_4  41
#define PIN_Dout_5  42
#define PIN_Dout_6  43
#define PIN_Dout_7  44
#define PIN_RCO     45

아듀이노 보드와 PC 사이의 보오 레이트 지정 뿐인 UART 설정한다. 수많은 사용자들에 의해 직렬 통신에 관한 모든 준비가 이미 되어 있어 사용자로써 감사할 일이다. 이어 칩에 연결할 핀들의 입출력 방향 지정,

void setup()
{
  // start serial port at 9600 bps:
  Serial.begin(9600);
  while (!Serial)  {}

  establishContact();

  // Set digital pins to output connecting DUT's INPUT
  pinMode(PIN_CLK     , OUTPUT);  digitalWrite(PIN_CLK     , LOW);
  pinMode(PIN_nCLR_L  , OUTPUT);  digitalWrite(PIN_nCLR_L  , LOW);
  pinMode(PIN_nCLR_H  , OUTPUT);  digitalWrite(PIN_nCLR_H  , LOW);
  pinMode(PIN_nLOAD_L , OUTPUT);  digitalWrite(PIN_nLOAD_L , LOW);
  pinMode(PIN_nLOAD_H , OUTPUT);  digitalWrite(PIN_nLOAD_H , LOW);
  pinMode(PIN_ENP     , OUTPUT);  digitalWrite(PIN_ENP     , LOW);
  pinMode(PIN_ENT     , OUTPUT);  digitalWrite(PIN_ENT     , LOW);
  pinMode(PIN_Din_0   , OUTPUT);  digitalWrite(PIN_Din_0   , LOW);
    ......
  pinMode(PIN_Din_6   , OUTPUT);  digitalWrite(PIN_Din_6   , LOW);
  pinMode(PIN_Din_7   , OUTPUT);  digitalWrite(PIN_Din_7   , LOW);

  // Set digital pins to input connecting DUT's OUTPUT
  pinMode(PIN_Dout_0  , INPUT);
    ........
  pinMode(PIN_Dout_6  , INPUT);
  pinMode(PIN_Dout_7  , INPUT);
  pinMode(PIN_RCO     , INPUT);

  // Monitoring LED
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
}

테스트 벤치와 주고 받을 데이터들을 비트 단위로 분할(시신된 입력 벡터) 및 통합(송신할 출력 벡터)한다. 칩과는 비트 단위로 연결되어있지만 UART를 통해 한번에 전송되는 데이터 단위는 8비트다.

#define N_RX  2 // [0]={-|CLK|nCLR_L|nCLR_H|nLOAD_L|nLOAD_H|ENP|ENT}
                // [1]=Din;
#define N_TX  2 // [0]=Dout
                // [1]={-------|RCO}

uint8_t rxBuf[N_RX], txBuf[N_TX];

void RxPacket()
{
  int rxByte;
  while(true)
  {
    if (Serial.available() >= N_RX)
    {
      for(int i=0; i<N_RX; i++)
      {
        rxByte = Serial.read();
        rxBuf[i] = (uint8_t)rxByte;
      }
      //rxBuf[0]={-|CLK|nCLR_L|nCLR_H|nLOAD_L|nLOAD_H|ENP|ENT}
      digitalWrite(PIN_nCLR_L,  rxBuf[0] & 0x20);
      digitalWrite(PIN_nCLR_H,  rxBuf[0] & 0x10);
      digitalWrite(PIN_nLOAD_L, rxBuf[0] & 0x08);
      digitalWrite(PIN_nLOAD_H, rxBuf[0] & 0x04);
      digitalWrite(PIN_ENP,     rxBuf[0] & 0x02);
      digitalWrite(PIN_ENT,     rxBuf[0] & 0x01);
      // rxBuf[1] = Din
      digitalWrite(PIN_Din_0,   rxBuf[1] & 0x01);
        ........
      digitalWrite(PIN_Din_7,   rxBuf[1] & 0x80);

      // Clocking to DUT
      digitalWrite(PIN_CLK, HIGH);
      digitalWrite(PIN_CLK, LOW);

      return;
    }
  }
}

void TxPacket()
{
  int txByte;
  while(1)
  {
    if (Serial.availableForWrite() >= N_TX)
    {
      txBuf[0] = ((digitalRead(PIN_Dout_0)? 0x01:0x00) |
                  (digitalRead(PIN_Dout_1)? 0x02:0x00) |

                    .........

                  (digitalRead(PIN_Dout_7)? 0x80:0x00));  // Dout
      txBuf[1] = ((digitalRead(PIN_RCO)? 0x01:0x00)); // RCO

      for(int i=0; i<N_TX; i++)
      {
        txByte = (int)txBuf[i];
        Serial.write(txByte);
      }

      return;
    }
  }
}

PC쪽의 테스트벤치에서 테스트 벡터를 줄때마다 무한 반복한다. 무슨일이 벌어지는지 궁금 하니 가끔씩 LED를 깜박여 줘도 좋다.

uint8_t counter;

void loop()
{
  counter += 1;
  digitalWrite(LED_BUILTIN, (counter & 0x10)? HIGH:LOW);

  RxPacket();
  TxPacket();
}

5. 칩 테스트 실행

"내칩 MPW"를 통해 제작된 칩은 아래 사진의 오른쪽 처럼 28핀 SOP 패키지로 제공된다. 사진의 칩은 예제와 다른 칩이다. 예제에서 보인 8비트 카운터는 LS163 표준 LSI 2개와 동일하게 구현한 것이다. 실제 칩은 없지만 표준 LSI 칩을 이용하여 시험할 대상의 칩으로 간주하자. 브레드 보드에 회로를 꾸민 후 아두이노 보드에 연결 했다.

테스트 구성은 아래와 같다.

                                    +------------------------+
                                    |           SystemC TB   |
                                    |             in C++     |
                                    |           +---------+  |
                                    +-----+     |  Test   |  |
                                          |  +---Generator|  |
                              +-------+   |  |  |         |  |
                              |  DUT  |   |  |  +---------+  |
                              |   in  <------+               |
                              |Verilog|   |  |  +---------+  |
                              |       -------|-->[DUT_Out]|  |
                              |       |   |  |  |         |  |
                              +-------+   |  |  |         |  |
                                          |  |  |         |  |
+--------+  +---------+       +.......+   |  |  | Compare |  |
|  DUT   |  | Arduino |       :       :   |  |  |         |  |
|   in   +---> MEGA   |       : UART  <------+  |         |  |
| MyChip |  |         <=[USB]=> Driver:   |     |         |  |
|  MPW   <----        |       : S/W ............>[Emu_Out]|  |
|        |  |         |       :       :   |     +---------+  |
+--------+  +---------+       +.......+   |                  |
                                    +-----+                  |
                                    |                        |
                                    +------ -----------------+
        Chip Test by Co-Emulation

테스트 칩은 전원을 USB 포트로부터 공급받는다. 전원 소모를 확인하기 위해 전류 측정장치를 사용했다. 아울러 오실로 스코프로 클럭 신호를 모니터링 한다.

 

깃 허브에서 내려받은 예제를 실행한다. 메이크 스크립트가 준비되어 있으니 이를 실행 한다.

    % cd emulation

    % make -fcounter8.mak run

칩에 공급되는 클럭은 오실로 스코프 상으로 확인할 수 있다. 약 50Khz 의 속도로 측정됐다. 이는 모든 테스트 입력과 클럭 신호를 PC의 SystemC에서 주기 때문이다. 이를 싸이클 상세(Cycle Accurate) 데스트라 한다. 동작 속도를 포함한 물리적 특성을 시험하기 보다 동작을 검사하기 위한 목적이다. 테스트 벤치는 베릴로그 모델과 에뮬레이션 칩의 입출력을 모두 VCD로 저장한다.

베릴로그 모델의 출력 Dout[7:0]과 칩의 에뮬레이션 출력 Dout_emu[7:0]이 일치하는 것을 알 수 있다. 하지만 전체 256회의 클럭을 카운트 한후 RCO가 칩과 베릴로그 모델이 상이하다.

베릴로그 모델에 이상이 있음이 관찰 되었다. 베릴로그 모델의 LS163 모듈에서 RCO가 잘못 됐다.

    assign RCO = cnt[3] & cnt[2] & cnt[1] & cnt[0];

    assign RCO = cnt[3] & cnt[2] & cnt[1] & cnt[0] & ENT;

로 수정되어야 한다. 2개의 74163비트 카운터를 구성 했을 경우 문제 없으나 4개를 연결한 16비트 카운터에서는 문제를 일으킨다.

대규모 입출력 검사를 파형으로 관찰하는 것은 불가능하다. 위의 단순한 예만 보더라도 무려 65526개의 클럭이 주어진 후에야 비로서 RCO의 이상 유무를 관할 할 수 있다. 게다가 계수하는 중간에 일어날 오류를 파형으로 관찰하는 것은 무모한 짓이다. 테스트 벤치에서 표준 값과 테스트 출력을 자동 비교하는 루틴을 만들어 놓도록 하자. 

6. 결론

디지털 칩의 테스트는 다수의 입출력 신호선과 응용 별로 상이한 테스트 벡터를 사용하기 때문에 전문 장비를 요구한다. 이를 극복하는 방법으로 RTL 검증에 사용했던 SystemC 테스트 벤치와 아두이노 보드를 활용하여 저렴하게 테스트 하는 방법을 소개했다. 싸이클 상세로 시험 하기 때문에 동작 속도는 느리지만 칩의 기본동작을 검증하는데 충분하다.

[참고]

[1] SCE-MI(Standard Co-Emulation Modeling Interface), https://www.accellera.org/downloads/standards/sce-mi

[2] CPU 6502 emulation, https://github.com/GoodKook/ETRI-0.5um-CMOS-MPW-DK-Example--6502-CPU