2024년 8월 1일 목요일

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

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

[알림] 아래 내용중 질문, 지적, 보강 등 어떤 사항도 환영 합니다.

"Verilog-Verilator-SystemC 방법론 기초"는 "내 칩(My Chip) MPW 서비스": 오픈-소스 도구 활용 반도체 설계 특별과정 중 두번째 강좌로서 베릴로그(Verilog)와 오픈-소스 시뮬레이션 도구 Verilator 그리고 시스템 수준 검증 방법론 SystemC의 입문 과정이다. 컴퓨팅 언어를 활용한 디지털 회로의 설계와 검증을 다룬다(Quantative approach to digital circuit design using computing language and Open-Source EDA tools).

강의 내용은 아래와 같다.

[1] 도구 설치 [링크]
[2] 설계 언어 Verilog 와 검증 언어 SystemC/C++[링크]
[3] 하드웨어 기술 언어의 코딩 스타일[링크]
[4] 실습: 쉬프트 레지스터 [링크]
[5] 실습: FIR 필터 [링크]
[6] "내 칩 MPW" 요건에 맞춘 FIR 필터의 PE 설계[링크]

[부록] FIR 필터 PE의 "내 칩MPW" 제출용 GDS 생성[링크]


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

목차

I. 개요

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

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

IV. 메이크(make) 유틸리티
    IV-1. 목표와 의존관계
    IV-2. 내부 변수
    IV-3. 다중 목표

V. 병렬 시뮬레이터의 작동 원리
    V-1. 시뮬레이터 빌드와 실행
    V-2. 병렬 시뮬레이션의 일관성(consistency) 문제
    V-3. 컴퓨팅 언어의 일반성
    V-4. 하드웨어 객체 크래스
    V-5. 시뮬레이션 델타

VI. 코딩 스타일
    VI-1. 감응 목록이 D-플립플롭에 미치는 영향
    VI-2. 동기 혹은 비동기 셋과 리셋을 가진 D 플립플롭

VII. 맺음말

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


I. 개요

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

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

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

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

먼저, 단순한 예를 통하여 이 언어의 가장 기본적인 요건을 알아본다.

II-1. 모듈 (module ~ endmodule)

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

- 모듈을 정의하는 키워드는 module 이다. 모듈 정의는 세미콜론 ; 으로 끝난다.

- 모듈 기술의 끝은 endmodule 이다.

[주] 세미콜론 ; 은 문장의 끝을 표시하는 마침부호다. C 언어와 같다. 모듈의 끝을 표시하는 endmodule 은 문장의 끝이 아니므로 ; 가 없다!

- 예제의 모듈 dff 는 입출력 포트로 clk, d, q를 가지고 있다.

- 포트(port)는 입출력 방향(direction)을 명시해야 한다. 방향을 표시하는 예약어(keyword)는 input, output, inout으로 3종류다.

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

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


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

D-플립플롭의 행동(behavior)은 클럭으로 지정된 신호의 상승 에지 사건(rising edge event)에 의해서 만 반응한다.

- 순차적 행위(procedural behavior description)은 always 구역 내에서 기술된다.

- 순차구문의 왼쪽 신호 lhs signal는 reg 속성을 가져야 한다. 출력 포드 q 는 네트(net, 또는 wire)다. 순차구문에 사용되기 위해 reg 로 속성을 부여한다.

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

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

- 행위의 기술(behavior description)로부터 논리회로 요소중 D-플립플롭으로 해석(inferencing D-FlipFlop)될 수 있다.

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

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


III. SystemC 테스트벤치

베릴로그로 작성된 D-플립플롭을 DUT(Design Under Test) 삼아 시뮬레이션 환경(테스트벤치, testbench)를 작성한다. 테스트벤치의 구성은 다음과 같다.

- DUT를 사례화(instantiate) 한다.

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

- DUT의 입력에 줄 신호를 생성한다.

- DUT의 출력을 검토한다.

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

SystemC는 C++를 이용하여 베릴로그를 흉내냈다. 굳이 C++로 해야 하나 하는 생각이 일견 들것이다. 기왕 배워놓은 C/C++를 하드웨어 설계에 활용한다고 여기자. C++도 익히고 하드웨어 설계도 배우면 일석이조 아닌가!

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

SystemC의 모듈의 구성은 아래와 같다. C++의 크래스다.

- 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++ 라는 점을 기억하자.


III-2. DUT의 사례화와 연결(instantiate DUT & binding to local signal)

DUT의 사례화와 지역신호 연결은 두 언어의 문법적 차이만 있을 뿐이지 완전히 동일한 의미다.  아래와 같은 베릴로그 구문을 C++에서 수행하는 과정을 살펴보자.

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

- Verilog로 작성한 DUT를 변환을 통하여 C++ 언어체계로 들여온다. Verilog로 작성된 DUT를 C++에 직접 불러올 수 없다. Verilator 를 사용하여 변환된 DUT의 헤더 파일 Vdff.h을 들여온다(include).

[주] Verilator는 베릴로그를 SystemC/C++ 로 변환해준다. 베릴로그 모듈 명에 대문자 V 를 붙여 SystemC 모듈 크래스를 생성한다.

- 테스트벤치 모듈 크래스 내에 DUT를 선언하고 사례화 한다. DUT는 포인터로 선언되었고 C++의 객체의 동적 할당 연산자 new로 사례화 한다.

- 동적 할당된 DUT의 포트들을 테스트벤치의 지역 하드웨어 객체 sc_clock, sc_signal<>와 연결한다(binding).

- DUT의 사례화와 연결은 모두 테스트벤치 크래스의 구성자(constructor) 내에서 이뤄진다. 이 과정을 내부구축(elaboration)이라고 한다. 베릴로그의 initial 에 해당하는 절차도 구성자 내에서 수행한다.


III-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는 사건 구동 시뮬레이터다. 사건탐지에 쓸 신호를 지정하고 이에 구동될 함수(콜백, call-back)를 지정한다. 이 지정은 시뮬레이션이 가동되기 전에 미리 구축 되어야 하므로 모듈 크래스의 구성자에서 수행한다. 시뮬레이션 전에 준비하는 상세화(내부구축)(elaboration)과정의 일부다.

쓰레드 함수와 사건 감응신호 지정은 베릴로그의 always @(...) 와 동일한 의미다.

병렬구문과 순차구문을 함께 기술하는 베릴로그는 사건에 의하여 구동될 순차 구문 구역이 always 에 곧이어 나온다. 하지만 모두 순차구문인 C++로 사건 구동을 구현해야 하는 SystemC는 사정이 다르다. 감응(sensitive)과 이에 반응하여 호출될 함수(call-back)를 지정한다. 구동될 함수는 호출 인수와 되돌림 값이 모두 void 인 모듈 크래스의 소속함수(member function)다. 사건에 대하여 콜백 함수를 호출하는 주체는 시뮬레이터다.

함수를 운영하는 방식은 메쏘드(method) 혹은 쓰레드(thread)가 있다. SC_METHOD() 로 지정된 함수는 감응 신호에 사건이 발생할 때마다 호출된다. SC_THREAD()로 지정된 함수는 한번호출되므로 함수 내에 무한 반복 구간을 두고 사건 대기 구문을 가지고 있어야 한다. 소프트웨어에서 병렬 프로그래밍 기법으로 흔히 사용하는 멀티 쓰레딩(multi-threading)과 같다.

위의 예에서 테스트용 d 를 발생 시키는 쓰레드 함수 test_generator()는 준비과정(elaboration process)에서 시뮬레이터에 의해 한번 호출되고 재호출 되지 않기 때문에 무한 반복 while(true) {...} 구문 내에 양보(yield)를 위해 wait(....) 를 두고 있다. SystemC 의 wait(...) 는 감응으로 지정한 신호에 사건이 발생할 때까지 대기한다. 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<>

하드웨어 객체는 템플릿-크래스로서 단순한 변수를 넘어 다양한 사용 방법(method)을 갖추고 있다. 예제에서 DUT의 입력에 연결한 d 는 sc_signal<> 로 선언된 하드웨어 객체다. 이 객체에 접근하는 방법은 읽기 .read() 와 쓰기 .write() 가 있다. 소속함수로 접근 할 수 있고 할당 연산자 = 가 중복정의(오버로드, overload) 되어 있다.

    d = 1;

    d.write(1);

은 동일 하다. 하드웨어 언어에서 모든 할당은 하드웨어 객체를 상대로 하는 것이 기본 이지만 C++ 언어는 모두 변수 할당이다. 하드웨어 객체에 대한 접근과 변수 접근을 구분하기 위해서라도 할당 연산자 대신 소속함수(메쏘드)를 통해 접근하는 방식을 쓰도록 한다.

하드웨어 객체를 프로그래밍 언어의 변수와 구분하는 근본적인 이유는 소프트웨어로 하드웨어의 행동을 모의하려고 하기 때문이다. 하드웨어의 행동을 소프트웨어로 흉내내기 위해 사용된 기법이 사건구동(event-driven)과 지연 할당(delayed assignment)이다. 이에 대해서는 하드웨어 시뮬레이터의 내부(simulator kernel)를 다룰 때 살펴보기로 한다.


III-4. VCD 파형

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

- VCD 파일명은 "sc_dff_TB.vcd" 다.

- VCD 파일에 기록할 신호는 clk, d, q 다.


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

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

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

- 지정된 시뮬레이션 시간에 도달했을 때

- 사건이 발생하지 않을 때

- 사용자가 sc_stop() 을 호출 했을 때


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

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

그래픽 환경에서 통합된 개발도구의 활용이 늘고 있다. 통합 환경이 사용자 편의성을 높이긴 하지만 결국 스크립트의 실행이다. 필요할 때마다 외부 라이브러리를 추가하고 다양한 옵션을 자유롭게 활용 하려면 통합 환경의 사용법을 익혀야 하는데 결국 메이크 스크립트를 마주하게 된다.

[주] 문서 편집 도구는 vi, vim 등이 있으나 사용법이 복잡하다. 윈도우즈의 응용프로그램에 익숙하다면 마이크로소프트의 코드(Microsoft Code)를 추천한다. 리눅스 판도 있다.

메이크(make) 유틸리티는 명령줄에서 쓰이는 가장 강력한 스크립트 활용 방법이다. 긴 세월동안 수많은 사용자들에 의해 향상된 make 유틸리티의 내용은 매우 방대하지만 기본 사용법만 익혀도 활용도는 매우 높다.

[주] make의 사용법을 다룬 글들이 많지만 임대영(RAXIS)의 GNU Make강좌를 추천한다. https://doc.kldp.org/KoreanDoc/html/GNU-Make/GNU-Make.html

IV-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의 날짜가 최신일 경우 gcc 로 컴파일 하라는 뜻이다. 첫번째 칼럼에 # 은 주석이다.

예제의 Makefile 내에 다수의 목표를 놓을 수 있다. 명령줄에서 make를 실행 할 때 명령줄 인수를 주어 여러 목표 중 하나를 선택 할 수 있다. 예를들어,

    $ make lint

Makefile의 lint 라고 명시된 목표를 찾아 수행한다. 예제의 Makefile의 내용이 아래와 같다고 하자.

의존 관계에서 콜론의 왼편이 명령줄의 인수와 일치하는지 먼저 확인 후 파일을 검사한다. 앞서 make 유틸리티를 실행할때 lint라는 인수를 주었으므로 Makefile 에서 목표가 lint인 의존성을 먼저 찾는다. 위의 Makefile 예에서 목표 lint는 $(VERILOG_SRCS)에 의존한다.

IV-2. 내부 변수

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

    # Makefile variables:
    VERILOG_SRCS = dff.v
    SC_SRCS      = sc_main.cpp
    SC_TOP_H     = sc_dff_TB.h
    VERILATOR    = verilator
    CFLAGS       = -std=c++17

    TOP_MODULE   = dff
    TARGET       = V$(TOP_MODULE)
    TARGET_DIR   = obj_dir

IV-3. 다중 목표

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

a. all

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

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

b. lint

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

c. run

목표 run 은 Verilator와 C++ 컴파일러로 만들어진 실행 파일(시뮬레이터)를 실행한다. 리눅스 운영체제에서 실행되는 응용프로그램 이므로 실행 환경의 설정이 필요할 수 있다. SystemC는 시뮬레이터를 빌드(build, 컴파일과 링크)하는 과정에서 크래스 헤더 파일을 들여오고 라이브러리를 참조한다.

SystemC를 설치하면 크래스 헤더와 라이브러리를 특정 위치에 놓는다. 따로 지정하지 않았다면 /usr/local/systemc-3.0.0 다. 시뮬레이터를 빌드하는 컴파일러에게 이 경로를 변수를 통해 알려준다.

SystemC의 시뮬레이터 커널(simulation kernel)은 동적 라이브러리 libsystemc.so (shared object)로 설치된다. 컴파일된 시뮬레이터(바이너리 파일)를 실행 할 때 시뮬레이션 커널과 함께 실행 되어야 하므로 동적 라이브러리가 놓인 경로 /usr/local/systemc-3.0.0/lib-linux64 를 리눅스 배쉬 쉘(bash-shell)의 환경변수 LD_LIBRARY_PATH에 추가 시켰다. export 는 Makefile의 외부변수 지정을 의미한다.

d. wave

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

e. clean

목표 clean 은 현재 디렉토리를 정리한다.


V. 병렬 시뮬레이터의 작동 원리

V-1. 시뮬레이터 빌드와 실행

Verilog D-플립플롭 과 SystemC 테스트벤치를 묶어 시뮬레이터를 빌드한다.

    $ 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도 단순하고 시뮬레이션도 아주 짧다. 컴퓨팅 언어 Verilog와 C++로 기술한 하드웨어 D 플립플롭의 시뮬레이션이 예상대로 작동했는지 사건을 따라가며 살펴보자.

(1) 입력 d 와 clk 가 동시에 사건이 발생했다. 클럭의 상승 엣지 사건 @(posedgge clk)에 구동된 순차 할당문은 입력 d 의 할당 이전 혹은 이후 중 어느 값을 취해야 할까?

[주] 트랜지스터회로의 D-플립플롭을 회로 시뮬레이션을 하면 아래와 같다. 위의 이전값 할당(또는 지연할당)의 논리가 타당하다.

(2) clk 에 하강 엣지 사건이 발생 했지만 이에 감응된 프로세스는 없다.

(3) clk의 상승 엣지 사건와 입력 d의 할당이 동시에 일어났다. 출력 q 를 앞서 (1)의 경우와 비교해 보면 d 의 할당 이전의 값을 취하고 있다.

(4) 출력 q 는 clk의 하강 엣지 사건에 영향을 받지 않고 이전 값을 유지한다.

(5) 앞서 (3)의 경우와 같다.

(6) 앞서 (1)의 경우와 같다. clk의 상승 엣지 사건에 감응하여 실행된 할당문은 d 의 이전 값을 취한다.

(7) 앞서 (3), (5)의 경우와 같다. 입력 d 의 이전 값을 취하여 출력 q에 전송되었다.

위에서 살펴본 대로 클럭 엣지의 사건에 감응되어 입력 d 를 출력 q 로 전송은 입력의 이전 값을 취한다. 나름 일관성이 있어 보인다. 하지만 소프트웨어로 이 일관성을 어떻게 구현할까? 순차실행 만 가능한 소프트웨어 언어로 작성된 병렬 시뮬레이터의 작동 원리를 간략하게 살펴보자.

V-2. 병렬 시뮬레이션의 일관성(consistency) 문제

입력 d에 새값이 할당되기 전에 clk 에 먼저 1이 할당 되었다면 q 에 전송되는 값은 0이다. 순서를 바꿔 입력 d 에 먼저 1이 할당 되었다면 q 에 전송될 값은 1이다. 할당이 일어나는 순서에 따라 결과가 달라지는 셈이다. 하드웨어의 병렬성을 가상으로 처리하는 시뮬레이션에 일관성(consistency) 문제가 발생한다.

아래와 같이 C++로 기술된 D 플립플롭이 있다고 하자.

    SC_MODULE(dff)
    {
        void event_callback() { q = d; }
        SC_CTOR(dff)
        {
            SC_METHOD(event_callback);
            sensitive << clk.posedge_event();

            clk = 0;
            d = 0;
        }
    }

C++ 테스트벤치에서 d 가 먼저 할당되고 이어 clk 가 할당되는 경우,

    d = 1;
    clk = 1;

또는 clk 가 먼저 할당되고 이어 d 가 할당되는 경우,

    clk = 1;
    d = 1;

clk에 사건이 발생되는 시점에 따라 출력 q 의 값은 다르다. 병렬 구문이 따로 존재하지 않는 소프트웨어 언어에서 병렬성을 일관성 있게 구현하기 위한 방안이 모색되어야 한다.

하드웨어 언어에서도 다수의 할당문장에 차례로 등장하지만 실제 동작은 순서와 무관한 병렬 문장이라는 점을 항상 염두에 두어야 한다.

V-3. 컴퓨팅 언어의 일반성

일관성 문제를 해결하기 위해 특별한 객체를 지정하고 이 객체에 사건이 발생하면 항상 이전 값을 가져오는 실행 규칙을 언어수준에서 정할 수도 있다. 아주 특별한 언어가 탄생하게 된다. C++와 같은 범용 언어체계로 시스템을 설계하겠다는 취지에 어긋난다.

개발 도구는 일반성을 가져야 한다. Verilog는 목적이 하드웨어의 묘사일 뿐 일반적인 컴퓨팅 언어다. 컴퓨팅 언어에서 특정 객체를 선정하여 특정 용도로 지정하면 일반성이 없다. 객체에 붙이는 이름은 단지 읽기 수월할 뿐이다. 아래의 두 구문을 비교해 보자. 단지 사람이 읽기에 불편할 뿐 하드웨어 D-플립플롭을 기술 했다는 점은 동일하다.

V-4. 하드웨어 객체 크래스

C++의 대표적 특징이자 가장 큰 장점은 클래스(class)다. 복합적인 특성을 가진 객체를 활용 방법(소속함수)과 함께 정의할 수 있다. 범용 컴퓨팅 언어 C++로 하드웨어를 묘사하기 위해 하드웨어 객체를 크래스로 정의한다.

Verilog의 wire 또는 reg 같은 하드웨어 객체(hardware object)는 소프트웨어 언어의 변수(variable)와 근본적으로 다르다. 아래의 C++의 크래스는 하드웨어 객체를 정의한 예이다.

    template <typename T>
    class hw_object
    {
    private:
        T cur_val, new_val;

    public:
        write(T val) {
            new_val = val;
            notify_event();
        }

        T operator=(const T rhs)
        {
            this.new_val = rhs;
            notify_event();
            return this.new_val;
        }

        read(T val) { return cur_val;}
        update() { cur_val = new_val;}
        notify_event() { return (new_val != cur_val) true : false;}
        posedge_event() {
            return (new_val==1 && cur_val==0) true : false;
        }
        negedge_event() {
            return 
(new_val==0 && cur_val==1) true : false;
        }
    }

위의 예에서 하드웨어 객체 크래스 hw_object 는 두개의 저장소 new_val, cur_val 를 갖추고 있다. 아울러 이 객체에 속한 소속 데이터(member data)를 취급하는 방법을 소속함수(member function)로 두고 있다.

V-5. 시뮬레이션 델타

하드웨어 객체에 새 값이 할단 되면서 시뮬레이터가 작동한다.

    hw_object<bool> sig;

    sig.write(true);

하드웨어 객체에 값의 할당은 단순한 저장에 그치지 않는다. 실제로는 new_val 에 저장 한 후 객체의 new_val 과 cur_val이 다르면 사건이 있음을 시뮬레이터 커널에 고지(notify event)한다. 새 값이 즉시 현재 값을 표시하는 것이 아니므로 할당을 수행하는 순서와 상관 없다. 객체에 할당이 완료되면 실행권은 시뮬레이터 커널로 넘어간다.

시뮬레이터 커널은 하드웨어 객체를 검사하여 고지된 사건들을 처리한다. 사건이 발생한 객체에 감응되도록 지정된 순차구문(또는 콜-백 함수)들을 실행 한다. 이때 호출된 순차구문이 읽게되는 값은 cur_val 이다.

고지된 모든 사건에 대하여 감응된 할당이 완료되면 객체들의 값을 갱신한다(update).

사건 감지와 사건대응 순차구문(또는 콜백 함수)의 호출 그리고 갱신을 반복하는데 이를 델타 타임(simulation delta time)이라 한다.

더이상 사건이 감지되지 않으면 시뮬레이션 시간을 진행한다. 만일 시뮬레이션 시간을 진행 시켜도 새로운 사건 발생이 없을 경우 시뮬레이션을 종료한다.


VI. 코딩 스타일

Verilog의 always 구역 감응 리스트에 clk 와 d 의 상승엣지 사건을 모두 넣어놓고 시뮬레이션을 해보면 아래와 같다. 출력 q 는 시뮬레이터의 시건구동 원리에 따라 설명될 수 있다.

언어로서 Verilog의 시뮬레이션 동작은 일관성을 가지고 설명될 수 있다. 감응 목록에 넣고 빼기에 의해 할당문의 실행 결과가 다를 수 있음을 보여준다.

코딩 스타일(coding style)은 컴퓨팅 언어로서 문법적 규약과 문장의 실행 방식(병렬실행과 순차실행) 외에 합성(synthesis)을 위한 작성 양식이다. 컴퓨팅 언어로 묘사한 내용을 하드웨어 생성을 목표하는 특별함 때문에 대두된 것이다[].

앞서 보인 예에서 감응 목록에 넣는 신호에 따라 수행 결과가 달라지는 것을 볼 수 있다. 사건구동과 감응 목록은 하드웨어의 행동을 컴퓨팅 언어로 기술하기 위한 소프트웨어적 기법이며 실제 디지털 회로를 정확히 반영하지 않는다는 점을 염두에 두어야 한다. 심지어 합성기 마다 다른 회로를 생성해서 시뮬레이션과 일치하지 않을 수 있다. 이런 이유로 EDA 업계를 중심으로 합성용 RTL 코딩 스타일의 표준이 제정되었다[].

VI-1. 감응 목록이 D-플립플롭에 미치는 영향

감응목록에 따라 다른 결과를 보여주는 예를 살펴보기로 한다. 아래의 경우 D 플립플롭을 제대로 기술하고 있다. 하지만 always 구역이 clk의 상승과 하강 엣지 사건에 모두 반응한다. 불필요하게 always 구역을 수행하므로써 시뮬레이터의 부담을 증가시킨다.

아래의 예는 마치 레벨 트리거 래치(level trigger latch)처럼 보인다. 감응신호 clk와 d 의 모든 사건에 always 가 반응 하지만 if 문에 의해 d 가 차단되고 있다.

VI-2. 동기 혹은 비동기 셋과 리셋을 가진 D 플립플롭

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

감응 목록에 지정된 다수의 신호가 동시에 시건을 일으킨 경우를 상정해 보자. 각 사건에 대응하여 순차구역이 실행된다. 시뮬레이션 델타 시간 내에서 사건의 순서에 무관한 결과를 얻을 수 있다. 순차구문 if~else 의 논리에 따라 우선순위가 결정(priority encoder)되어 있을 뿐 시뮬레이션은 일관성을 유지한다.

감응 목록에서 r 과 s 를 빼면 모두 clk에 동기 시킬 수 있다.

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


VII. 맺음말

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

시뮬레이션 소프트웨어에서 병렬성을 구현하는 방법을 간략히 살펴봤다. 코딩 스타일에 따라 시뮬레이션의 결과가 달라질 수 있는 이유를 설명 할 수 있을 것이다. 시뮬레이션 소프트웨어가 병렬성을 일관성있게 처리하는 방식을 알면 시스템 모델링에 큰 도움이 된다.

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

RTL Coding Styles That Yield Simulation and Synthesis Mismatches
[4] IEEE 1364.1-2002 - IEEE Standard for Verilog Register Transfer Level Synthesis
[5] IEEE Standard for Verilog Hardware Description Language
[6] "VLSI 레이아웃 설계 기초" [8] Std-Cell 제작 실습: DFF-SR, https://fun-teaching-goodkook.blogspot.com/2024/07/vlsi-8-std-cell-dff-sr.html



댓글 없음:

댓글 쓰기