A Tour of SystemC : for Hardware Engineers
- 이 글의 목적은 HLS를 활용해 하드웨어를 설계하려는 분들에게 SystemC이 무엇인지 알려드리는 것입니다.
- 이 글을 읽으시는 분은 C++ 문법과 Verilog를 활용한 하드웨어 설계 방법을 알고 있다고 가정합니다.
- 유튜브 영상 링크
Why Do We Need SystemC?
많은 HLS 툴들이 C++ 언어를 지원합니다. 그렇다면 C++ 만으로 모든 종류의 하드웨어를 모델링 할 수 있을까요? 그렇지 않습니다. C++와 HDL 언어는 근본적으로 다른 부분이 있기 때문이죠. C++는 명령형 언어이고, HDL은 선언형 언어입니다. C++은 명령어들을 순차적으로 실행하는 방식에 기반한 언어이고, HDL은 하드웨어를 기술하고 이벤트를 기반으로 동작하는 방식입니다.
예를 들어 아래와 같은 모듈을 만든다고 생각해 봅시다. 모듈 A는 입력 FIFO를 계속 읽어서 무언가를 처리한 뒤 FIFO로 출력합니다. 모듈 B도 모듈 A의 출력을 계속 읽어서 무언가를 처리하고 FIFO로 출력합니다. 그리고 이것을 C++로 모델링 해봅시다.
void moduleA(Fifo<T> &chan_input, Fifo<T> &chan_output) {
while(true) {
T input = chan_input.read();
// Do some processing
chan_output.write(output);
}
}
void moduleB(Fifo<T> &chan_input, Fifo<T> &chan_output) {
while(true) {
T input = chan_input.read();
// Do some processing
chan_output.write(output);
}
}
void Top(Fifo<T> &chan_input, Fifo<T> &chan_output) {
Fifo<T> fifo;
moduleA(chan_input, fifo);
moduleB(fifo, chan_output);
}
모델링이 원하는 대로 구현됐을까요? 그렇지 않습니다. 무한 루프가 있기 때문에 모듈 B는 실행되지 않고 모듈 A만 실행됩니다. HDL로 기술한다면 쉽게 구현될 텐데요. 그렇다고 C++로 위와 같은 하드웨어를 기술할 수 없는 건 아닙니다. 각 모듈의 동작을 적절하게 스케줄링 하면 됩니다. 모듈 A를 실행하다가 fifo가 가득 차면 모듈 B를 실행하고, fifo가 비었으면 다시 모듈 A를 실행하는 과정을 반복하면 되죠. 이렇게 FIFO empty, full 이벤트에 따라 스케줄링 하면서 각 모듈을 적절하게 번갈아가며 실행하면 우리가 원하는 동작을 모델링 할 수 있습니다.
이외에도 fifo empty, full 이벤트뿐만 아니라 다양한 이벤트들이 있을 수 있습니다. 클럭의 엣지, 신호의 변화 등 다양한 경우가 있을 수 있죠. 이렇게 다양한 이벤트에 따라 각 모듈의 동작을 스케줄링 하는 코드를 처음부터 작성하려면 해야 할 일이 많겠지만, 다행히 SystemC 라이브러리를 사용한다면 손쉽게 구현할 수 있습니다.
What is SystemC?
SystemC는 이벤트 기반 시뮬레이션 기능을 제공하는 C++ 라이브러리입니다. C++ 언어를 사용하기 때문에 C++ 언어의 장점들을 사용해서 하드웨어를 설계할 수 있습니다. Catapult HLS나 Stratus HLS 같은 상용 툴에서도 사용할 수 있습니다. IEEE에 표준으로 등재되어 있습니다.
Event-driven Simulation
앞서 살펴본 하드웨어를 모델링 하기 위해서는 이벤트 기반 시뮬레이션을 해야 합니다. 그렇다면 SystemC는 어떻게 이벤트 기반 시뮬레이션을 진행할까요? 아직 SystemC 코드를 배우지 않았으니 verilog 코드를 예시로 사용합니다.
// process 0, P0
always @ (posedge clk) begin
b <= a;
end
// process 1, P1
always @ (posedge clk) begin
a <= b;
end
// process 2, P2
always @ (b) begin
c = b * 3;
end
// process 3, P3
always @ (b) begin
d = b * 5;
end
각각의 always 문을 하나의 프로세스라고 생각하고 시뮬레이션 과정을 순서대로 적어보면 아래와 같습니다.
-
클럭 이벤트가 발생합니다. 클럭 이벤트를 기다리고 있는 P0과 P1이 runnable 프로세스가 됩니다.
-
Evaluation Phase
- P0를 실행합니다. 이때 b를 바로 a 값으로 업데이트하지 않고 b_temp와 같은 임시 저장소에 저장합니다.
- P1을 실행합니다. 마찬가지로 a를 바로 b 값으로 업데이트하지 않고 a_temp와 같은 임시 저장소에 저장합니다.
-
Update Phase
- 임시 저장소(a_temp, b_temp)에 저장된 값이 a와 b에 각각 대입됩니다.
-
Update 하면서 b의 값이 변경되었습니다. b 신호에 sensitive 한 프로세스 P2과 P3를 runnable 프로세스로 등록합니다.
-
Evaluation Phase
-
P2를 실행합니다. c에 b*3의 결과를 임시 저장합니다.
-
P3를 실행합니다. d에 b*5의 결과를 임시 저장합니다.
-
-
Update Phase
- c와 d에 임시 저장한 값을 반영합니다.
-
Update 하면서 c와 d 값을 변경하였습니다. c와 d에 sensitive 한 프로세스가 없으므로, 새로 runnable로 등록되는 프로세스는 없습니다. 모든 이벤트를 반영하였으므로 시뮬레이션 커널은 시간을 진행시키고 다음 클럭 사이클에 대해 시뮬레이션을 진행합니다.
Evaluation과 update를 번갈아가며 시뮬레이션을 진행하는 것이 핵심입니다. 이렇게 하면 프로세스를 하나씩 실행하더라도 동시성을 유지하며 시뮬레이션을 진행할 수 있습니다. 이때 하나의 evaluation과 update가 반복되는 주기를 delta cycle이라고 합니다.
SystemC vs Verilog
본격적으로 SystemC를 보기 전에 SystemC 코드와 Verilog 코드를 비교해 봅시다. 어딘가 닮아있지 않나요? 모듈 선언하고, 포트 선언하고, combinational logic이나 레지스터 기술하는 게 비슷해 보입니다.
Combinational Logic
module ModuleName (
a, b, c
);
input [7:0] a;
input [7:0] b;
output [15:0] c;
assign c = a * b;
endmodule
SC_MODULE(ModuleName) {
sc_in<uint8_t> a;
sc_in<uint8_t> b;
sc_out<uint16_t> c;
SC_CTOR(ModuleName) {
SC_METHOD(comb);
sensitive << a << b;
}
void comb() {
c->write( a->read() * b->read() );
}
};
Register
module ModuleName (
clk, reset_n, a, b, c
);
input clk;
input reset_n;
input [7:0] a;
input [7:0] b;
output reg [15:0] c;
always @(posedge clk or negedge reset_n) begin
if ( ~ reset_n ) begin
c <= 16'd0;
end else begin
c <= a * b;
end
end
endmodule
SC_MODULE(ModuleName) {
sc_in<bool> clk;
sc_in<bool> reset_n;
sc_in<uint8_t> a;
sc_in<uint8_t> b;
sc_out<uint16_t> c;
SC_CTOR(ModuleName) {
SC_METHOD(reg_0);
sensitive << clk.pos();
async_reset_signal_is(reset_n, false);
}
void reg_0() {
if(!reset_n) {
c->write(0);
} else {
c->write( a->read() * b->read() );
}
}
};
한 가지 당부드리고 싶은 것은, 이걸 보고 SystemC를 Verilog나 VHDL 같은 또 다른 종류의 RTL 언어라고 생각하지 않으셨으면 좋겠습니다. SystemC는 이보다 더 상위 레벨, Transaction Level에서 시스템을 모델링 할 수 있습니다. 그리고 SystemC의 진정한 가치는 이런 RTL 모델링 아니라 Transaction-Level 모델링에서 나오는데요, 자세한 내용은 뒤에서 더 설명드리겠습니다.
SystemC Code Structure
SystemC 코드 구조 예제입니다. 전체 구조를 파악하는 게 목적이기 때문에 생략된 부분이 있을 수 있습니다.
#include <systemc>
// In C++, classes are declared using 'class ModuleName', but when declaring a module using the SystemC library, the SC_MODULE() macro is used as shown below.
SC_MODULE(ModuleName) {
// In/Out Ports
// Channels
// ...
// Constructor
SC_CTOR(ModuleName) {
SC_METHOD(comb_0);
sensitive << signal_1 << signal_2;
SC_METHOD(reg_0);
sensitive << clk.pos();
async_reset_signal_is(reset_n, false);
}
void comb_0() {
// ...
}
void reg_0() {
// ...
}
}
// main function
int sc_main(int argc, char* argv[]) {
ModuleName module{"module"}; // Use the same name as the constructor parameter for easier debugging.
// ModuleName SC_NAMED(module);
sc_start();
return 0;
}
위 코드에서 필요한 내용을 하나씩 살펴봅시다.
sc_main
C/C++에서 프로그램의 시작은 main 함수입니다. 그러나 SystemC에서는 sc_main
을 사용합니다. 여기서 필요한 모듈들을 선언하고 sc_start
함수를 호출함으로써 시뮬레이션을 진행합니다.
Module
C++에서는 class
키워드를 사용해서 클래스를 선언하는데 SystemC에서는 SC_MODULE
매크로를 사용해서 클래스를 선언합니다. 생성자도 SC_CTOR
매크로를 사용해서 선언합니다.
// SystemC
SC_MODULE(ModuleName) {
SC_CTOR() {
}
};
모듈을 객체화할 때는 인수로 문자열을 주는데요, 문자열은 객체 이름과 동일하게 주는 것이 좋습니다. 또는 SC_NAMED 매크로를 사용해서 객체 이름과 동일한 문자열을 인수로 전달할 수도 있습니다. 모듈 안에서 name()
함수를 통해 주어진 이름에 접근할 수 있습니다.
ModuleName module("module"); // Use the same name as the constructor parameter for easier debugging.
ModuleName SC_NAMED(module); // Alternatively, use the SC_NAMED macro.
Port
// In/Out Ports
sc_port<T> clk;
sc_port<T> reset_n;
외부 모듈과 데이터를 주고받을 때는 포트를 통해서 데이터를 전달해야 합니다. 필요한 포트는 위와 같이 선언할 수 있습니다. 또는 아래에서 설명할 specialized port를 사용할 수도 있습니다.
Process
SC_METHOD
로 등록하는 프로세스는 Verilog에서의 always문이랑 비슷합니다. 아래 코드에서 always문의 느낌이 느껴졌으면 좋겠네요.
SC_METHOD(function_name);
sensitive << signal_1 << signal_2; // always @ (signal_1, signal_2)
SC_METHOD(function_name);
sensitive << clk.pos(); // always @ (posedge clk or negedge reset_n)
async_reset_signal_is(reset_n, false);
SystemC 커널의 핵심은 여러 프로세스들을 동시에 동작하는 것처럼 보이게 스케줄링하는 것이죠. 그러기 위해서는 커널에게 어떤 프로세스들을 스케줄링 해야 하는지 등록해야 합니다. 생성자에서 SC_METHOD
매크로를 사용해서 등록할 수 있습니다. 이렇게 등록한 프로세스를 메서드 프로세스라고 합니다.
이때 프로세스로 등록하는 함수는 입력 파라미터와 리턴 값이 없는 함수여야 합니다. 프로세스의 입력이나 출력이 필요한 경우는 추후 설명할 sc_signal
나 sc_fifo
같은 채널을 사용해야 합니다. 이렇게 해야 신호의 변화나 fifo full, empty와 같은 이벤트에 따라 커널이 프로세스를 스케줄링할 수 있기 때문입니다.
여기까지만 보면 이럴 거면 Verilog를 쓰지 뭐하러 SystemC를 쓸까 싶지만, SystemC는 메서드 프로세스 이외에도 다른 프로세스들도 있습니다. 관련된 내용은 뒤에서 더 설명드리겠습니다.
Summary
Verilog도 사실 모듈을 선언하고, 포트를 선언하고, always문으로 combinational logic이나 레지스터를 기술하는 게 거의 대부분입니다. SystemC에서도 모듈을 선언하고, 포트를 선언하고, always문 대신에 프로세스를 등록한다는 점에서 비슷하죠. 다만 이벤트 기반 시뮬레이션이 가능해야 하기 때문에 정해진 방식으로 모듈을 선언하고, 포트를 선언하고, 프로세스를 등록할 뿐입니다.
A Tour of SystemC
본격적으로 SystemC에 대해 알아볼 내용은 아래와 같습니다.
- Data Types : SystemcC에서 지원하는 자료형들을 살펴봅니다.
- Module : 모듈을 선언하고, 모듈의 생성자에서 해야 하는 일을 알아봅니다.
- Process : 프로세스의 종류와 커널에 프로세스를 등록하는 방법을 알아봅니다.
- Channel : 프로세스와 프로세스가 데이터를 주고받는 방법을 알아봅니다.
- Port : 모듈과 모듈이 데이터를 주고받는 방법을 알아봅니다.
- Clock : 클럭을 선언하고 모듈과 연결하는 방법을 알아봅니다.
Data Types
SystemC에서도 물론 C++에서 지원하는 자료형들을 사용 가능합니다. int, unsigned int, char, bool 등이 있겠죠. 그러나 C++에서 제공하는 자료형들은 데이터의 bitwidth가 8bit, 16bit, 32bit 등으로 정해져있습니다. 그러나 하드웨어를 설계하다 보면 6bit, 11bit 등 다양한 bitwidth의 데이터들을 다뤄야 하는 경우가 많죠. 이를 위해 SystemC에서는 다양한 형태의 자료형을 제공합니다. 아래와 같은 자료형들이 있습니다.
- sc_int<BITWIDTH>
- sc_uint<BITWIDTH>
- sc_fixed<W, WI, Q, O, S> (전체 bitwidth, 정수 부분의 bitwidth, Quantization 방식, 오버플로우 방식, saturation bit)
- …
Module
SystemC에서 시스템을 구성하는 기본 요소입니다.
모듈은 아래와 같은 것들을 포함할 수 있습니다.
- 외부 모듈과 데이터를 주고받기 위한 포트
- 하위 모듈
- 하위 모듈들을 연결하는 채널 (wire 또는 FIFO 등)
- 프로세스
이때 모듈의 생성자에서 하위 모듈들을 객체화하고, 연결하고, 커널에 프로세스를 등록하는 등의 역할을 수행합니다. 생성자는 SC_CTOR
매크로를 사용해서 선언할 수 있습니다.
SC_CTOR
SC_MODULE(ModuleName) {
// Declare necessary ports (e.g., clock, reset, input/output, ..)
// Declare channels to connect sub_1 and sub_2
SubModule_1 SC_NAMED(sub_1);
SubModule_2 SC_NAMED(sub_2);
SC_CTOR(ModuleName) {
// Connect necessary signals to sub_1 and sub_2 (e.g., clock, reset, input/output, ..)
SC_METHOD(process); // Register the process
sensitive << clk.pos();
async_reset_signal_is(reset_n, false);
}
void process(); // Declare the function to be registered in the process
}
아래와 같이 다양한 형식으로 생성자를 사용할 수 있습니다.
SC_MODULE(M1) {
SC_CTOR(M1) : i(0) {} // Constructor declaration
int i;
// ...
};
SC_MODULE(M2) {
SC_CTOR(M2); // Constructor declaration
int i;
// ...
};
M2::M2(sc_core::sc_module_name) : i(0) {}
SC_MODULE(M1) {
SC_CTOR(M1, int a, int b) // Additional constructor parameters
{}
// ...
};
위 내용은 SystemC 버전에 따라 다를 수 있습니다. SystemC v2.0.1에서는 생성자의 모듈 이름 이외의 인수를 전달하기 위해서는 SC_HAS_PROCESS 매크로를 사용해야 했습니다. 그러나 SC_HAS_PROCESS 매크로는 SystemC 3.0.1에서 deprecated 되었습니다.
Channel
이벤트 기반 시뮬레이션에서는 여러 프로세스들이 임의의 순서대로 실행될 수 있습니다. 따라서 프로세스들이 공유하는 데이터들을 주의해서 사용해야 합니다. sc_signal
이나 sc_fifo
와 같이 SystemC에서 제공하는 채널을 통해 데이터를 주고받으면 동시성을 유지하면서 안전하게 데이터를 주고받을 수 있습니다.
sc_signal
Verilog의 wire
와 같은 역할을 합니다. sc_signal<TYPENAME> NAME
과 같은 형태로 선언합니다.
sc_signal<int> signal;
signal.write(3); // write
int data = signal.read(); // read
sc_signal
에 쓴 값은 현재 delta cycle에서는 유지되고, 다음 delta cycle에서는 업데이트된 값이 사용됩니다. 위와 같이 write 직후에 read 하면 바로 위에서 쓴 값이 아니라 원래 있었던 값이 읽힙니다.
sensitivity list에 signal
이 등록된 프로세스가 있다면 signal의 값이 변할 때 해당 프로세스가 호출되거나 재개됩니다. 이때, 특별히 sc_signal<bool>
채널에 대해서는 posedge_event()
와 negedge_event()
이벤트를 사용할 수 있습니다.
SC_MODULE(ModuleName) {
sc_signal<int> channel;
SC_CTOR(ModuleName) {
// ...
}
void process_1() {
// ...
channel.write( data );
}
void process_2() {
int data = channel.read();
// ...
}
};
sc_fifo
FIFO를 모델링 하기 위한 채널입니다. sc_fifo<TYPENAME> NAME(SIZE)
형식으로 선언합니다. 아래와 같이 선언하고 Read/Write 할 수 있습니다.
sc_fifo<int> fifo(4);
int write_data = 4;
fifo.write(write_data);
int read_data = fifo.read();
만약 FIFO가 full인 상태에서 write을 하거나, empty인 상태에서 read 하게 되면 내부적으로 wait()
함수가 호출되면서 컨텍스트 스위칭이 일어납니다.
SC_MODULE(ModuleName) {
sc_fifo<int> channel;
SC_CTOR(ModuleName) {
// ...
}
void process_1() {
while(true) {
// ...
channel.write( data );
}
}
void process_2() {
while(true) {
int data = channel.read();
// ...
}
}
};
Port
마찬가지로 모듈과 모듈 사이의 통신에서도 모듈들이 공유하는 데이터는 포트를 통해 주고받는 것이 안전합니다. 모듈의 출력 포트는 채널과 연결되며, 채널은 다시 다른 모듈의 입력 포트와 연결됩니다. 이때 포트를 읽거나 쓸 때는 .read()
, .write()
가 아니라 ->read()
, ->write()
와 같은 포인터 접근 연산자를 사용해야 합니다.
채널을 읽고 쓸 때는
.read()
,.write()
를 사용하고 포트를 읽고 쓸 때는->read()
,->write()
를 사용한다는 것 기억해 주세요.
Specialized port class for use with signals
sc_signal의 경우 아래와 같은 특화된 포트를 사용할 수 있습니다.
- sc_in<T>
- sc_out<T>
- sc_inout<T>
SC_MODULE(Module1) {
sc_out<int> out_port;
SC_CTOR(Module1) {
// ...
}
void process() {
// ...
out_port->write(data);
}
};
SC_MODULE(Module2) {
sc_in<int> in_port;
SC_CTOR(Module2) {
// ...
}
void process() {
int data = in_port->read();
// ...
}
};
SC_MODULE(Top) {
sc_signal<int> signal;
Module1 SC_NAMED(module1);
Module2 SC_NAMED(module2);
SC_CTOR(Top) {
module1.out_port(signal);
module2.in_port(signal);
}
};
Specialized port class for use with fifo
- sc_fifo_in<T>
- sc_fifo_out<T>
SC_MODULE(Module1) {
sc_fifo_out<int> out_port;
SC_CTOR(Module1) {
// ...
}
void thread() {
while(true) {
// ...
out_port->write(data);
}
}
};
SC_MODULE(Module2) {
sc_fifo_in<int> in_port;
SC_CTOR(Module2) {
// ...
}
void thread() {
while(true) {
int data = in_port->read();
// ...
}
}
};
SC_MODULE(Top) {
sc_fifo<int> fifo;
Module1 SC_NAMED(module1);
Module2 SC_NAMED(module2);
SC_CTOR(Top) {
module1.out_port(fifo);
module2.in_port(fifo);
}
};
Process
대부분의 시스템에서는 여러 가지 일들이 병렬로 일어나고 있습니다. 이를 모델링 하기 위해서 SystemC 커널 역시 여러 가지 일들이 동시에 일어나는 것처럼 모델링 해야 합니다. 이때 SystemC 커널이 여러 일들을 스케줄링하는 단위가 프로세스입니다. 커널은 적절하게 프로세스를 호출하고 어떤 프로세스들이 실행되어야 하는지 등을 관리합니다. SystemC는 비선점형 스케줄링을 사용합니다. 따라서 다른 프로세스가 실행되기 위해서는 프로세스가 끝나거나 프로세스가 스스로 중단되어야 합니다. 프로세스 전환 조건에 따라 프로세스를 메서드 프로세스, 스레드 프로세스, 클럭 스레드 프로세스로 구분합니다.
Method Process
Verilog의 always문과 비슷한 역할을 합니다. 생성자에서 SC_METHOD
매크로를 사용해 모듈 내에 선언된 함수를 프로세스로 등록할 수 있습니다.
메서드 프로세스는 sensitivity list에 등록된 이벤트가 발생할 때마다 반복해서 호출되며, 완전히 함수가 종료되어 return 될 때까지 다른 프로세스로의 컨텍스트 스위칭은 발생하지 않습니다.
Combinational Logic
SC_MODULE(ModuleName) {
sc_in<uint8_t> a;
sc_in<uint8_t> b;
sc_out<uint16_t> c;
SC_CTOR(ModuleName) {
SC_METHOD(comb);
sensitive << a << b;
}
void comb() {
c->write( a->read() * b->read() );
}
};
Register
SC_MODULE(ModuleName) {
sc_in<bool> clk;
sc_in<bool> reset_n;
sc_in<uint8_t> a;
sc_in<uint8_t> b;
sc_out<uint16_t> c;
SC_CTOR(ModuleName) {
SC_METHOD(reg_0);
sensitive << clk.pos();
async_reset_signal_is(reset_n, false);
}
void reg_0() {
if(!reset_n) {
c->write(0);
} else {
c->write( a->read() * b->read() );
}
}
};
Thread Process
생성자에서 SC_THREAD
매크로를 사용해 모듈 내에 선언된 함수를 프로세스로 등록할 수 있습니다.
SC_MODULE(ModuleName) {
sc_fifo_in<int> chan_input;
sc_fifo_out<int> chan_output;
SC_CTOR(ModuleName) {
SC_THREAD(thread);
}
void thread() {
while(true) {
int input = chan_input->read();
int output = input + 7;
chan_output->write(output);
}
}
};
스레드 프로세스는 리셋되는 경우를 제외하면 커널에서 단 한 번만 호출됩니다. 따라서 대부분의 경우 프로세스가 끝나지 않도록 내부에 무한 루프를 가지고 있습니다. 코드 내에서 wait
함수를 호출함으로써 작업이 중단되고 다른 프로세스가 실행될 수 있습니다. 명시적으로 코드에서 wait
함수를 쓰지 않더라도 sc_fifo
같은 채널을 쓰는 경우 FIFO가 full이거나 empty 일 때 FIFO를 쓰거나 읽으려고 하면 내부적으로 선언된 wait
이 호출되면서 다른 프로세스로 컨텍스트 스위칭이 발생할 수도 있습니다. 다시 프로세스가 시작할 때는 마지막 wait
함수가 호출된 시점부터 실행됩니다.
sensitive << clk.pos();
와 같은 방식으로 생성자에서 등록한 sensitivity를 static sensitivity라고 합니다. 이와 달리 스레드 프로세스는 코드 안에서wait
함수를 통해 특정 이벤트를 기다리도록 dynamic sensitivity를 만들 수도 있습니다.
- 스레드는 한 번만 호출되므로, 일반적으로 내부에 무한 루프를 가지고 있는 것이 일반적입니다.
- 프로세스가 중단되었다가 다시 실행될 때 내부에 선언된 로컬 변수들의 값은 유지됩니다.
- 스레드 프로세스가
wait
함수를 사용하지 않으면 프로세스는 중단되지 않습니다.
Method Process vs Thread Process
메서드 프로세스는 wait
함수를 사용할 수 없습니다. 이것은 메서드 프로세스가 실행 중에 시뮬레이션 시간을 진행할 수 없고, 이벤트에 따라 트리거 된 후 즉시 실행되고 종료되어야 한다는 것을 의미합니다. Verilog의 always문을 떠올리시면 됩니다.
반면에 스레드 프로세스는 한 사이클 안에 작업이 끝나도 되고, 여러 사이클에 걸쳐 작업을 진행할 수도 있습니다. 예를 들어 FIFO를 읽어서 여러 사이클에 걸쳐 계산한 다음에 FIFO로 출력하는 시스템을 모델링 할 수 있습니다. 올바른 데이터가 정해진 순서대로 FIFO에 써지기만 한다면 시뮬레이션의 계산 결과가 같을 테니까요. 또는 모듈 사이에 버퍼(SRAM)를 두고 한 모듈은 버퍼에 데이터를 쓰고, 다른 모듈은 버퍼에서 데이터를 읽어가는데 모듈의 동작 순서는 handshake로 제어하는 시스템도 모델링 할 수 있죠. 핵심은 매 클럭 사이클마다의 동작을 기술하는 것이 아니라, 전송되는 데이터와 그 데이터들의 순서를 모델링 하는 것입니다. 이때 HLS를 사용한다면 출력이 나오기까지 몇 사이클이 걸릴지는 pragma
나 directive
를 통해 사용자가 설정한 하드웨어 구현 세부 사항, 목표 클럭 주파수, 플랫폼 – FPGA 인지 ASIC 인지, ASIC이라면 어떤 파운드리 회사의 어느 공정을 사용할지 등에 따라 결정됩니다.
이렇게 스레드 프로세스를 사용하면 RTL(Register-Transfer Level)이 아닌 TLM(Transaction-Level Modeling)이 가능합니다. 시스템을 클럭 사이클마다의 동작이 아니라 모듈 간의 데이터 전송을 중심으로 높은 추상화 수준에서 설명할 수 있습니다. SystemC의 진정한 가치는 이러한 TLM에 있다고 볼 수 있습니다.
물론 모듈은 메서드 프로세스와 스레드 프로세스를 같이 사용하는 것도 가능합니다. 이를 통해 한 설계 내에서 RTL 모델과 Transaction-Level 모델을 같이 사용할 수 있습니다. 예를 들면 각 서브 모듈들은 Transaction-Level에서 모델링하고 이렇게 모델링 된 각 서브 모듈들을 엮어서 컨트롤하는 Top 모듈은 RTL 수준에서 cycle-accurate 하게 모델링 하는 것이 SystemC 하나로 가능합니다.
Clocked Thread Process
클럭 스레드 프로세스는 하나의 클럭에 대해서만 sensitive 한 프로세스입니다. SC_CTHREAD
매크로를 사용해 클럭 스레드 프로세스를 등록할 수 있습니다.
SC_CTHREAD(process, clk.pos());
Reset
프로세스는 리셋을 사용할 수도 있습니다. sync_reset_signal_is()
또는 async_reset_signal_is
함수를 사용해서 리셋을 사용할 수 있습니다. 비동기 리셋의 경우 아래와 같이 사용합니다.
SC_MODULE(Module) {
SC_CTOR(Module) {
SC_METHOD(methodProcess);
sensitive << clk.pos();
async_reset_signal_is(reset_n, false);
SC_THREAD(threadProcess);
sensitive << clk.pos();
async_reset_signal_is(reset_n, false);
}
void methodProcess() {
if(!reset_n) {
... // Asynchronous reset behavior, executed whenever the reset is active
} else {
... // Synchronous behavior, only executed on a positive clock edge
}
}
void threadProcess() {
... // Reset actions
while(true) {
wait(); // wait for 1 clock cycle
...
}
}
};
dont_initialize()
SC_METHOD와 SC_THREAD로 등록된 프로세스는 SystemC 시뮬레이션 커널이 초기화될 때 모두 한 번씩 호출됩니다. 다시 말하면, sensitivity list에 등록된 이벤트가 발생하지 않아도 커널이 시작할 때 한 번 호출된다는 뜻입니다. 프로세스를 등록할 때 dont_initialize()
함수를 사용하면 커널이 시작할 때 프로세스가 실행되는 것을 막을 수 있습니다. 이때 dont_initialize()
은 바로 위에 등록된 프로세스에만 적용됩니다.
SC_METHOD(M);
sensitive << signal_1 << signal_2;
dont_initialize(); // METHOD process M is not made runnable during initialization.
SC_THREAD(T);
sensitive << signal_3 << signal_4;
dont_initialize(); // THREAD process T is not made runnable during initialization.
Clock
SystemC에서 제공하는 sc_clock
을 사용하면 쉽게 클럭을 사용할 수 있습니다. sc_clock
의 생성자는 아래와 같습니다.
sc_clock( const char* name_,
const sc_time& period_,
double duty_cycle_ = 0.5,
const sc_time& start_time_ = SC_ZERO_TIME,
bool posedge_first_ = true );
sc_clock( const char* name_,
double period_v_,
sc_time_unit period_tu_,
double duty_cycle_ = 0.5 );
sc_clock( const char* name_,
double period_v_,
sc_time_unit period_tu_,
double duty_cycle_,
double start_time_v_,
sc_time_unit start_time_tu_,
bool posedge_first_ = true );
아래는 10ns 주기의 클럭을 사용하는 예시입니다.
SC_MODULE(Module) {
sc_in<bool> clk;
SC_CTOR(Module) {
SC_METHOD(method);
sensitive << clk.pos();
}
void method() {
std::cout << "clock edge detected" << std::endl;
}
};
int sc_main(int argc, char* argv[]) {
sc_clock clk{"clk", 10, SC_NS}; // Alternatively, use SC_NAMED(clk, 10, SC_NS);
Module SC_NAMED(module);
module.clk(clk);
sc_start();
return 0;
}
Example Code
#include <systemc>
#include <iostream>
using namespace sc_core;
SC_MODULE(ModuleA) {
sc_fifo_in<int> chan_input;
sc_fifo_out<int> chan_output;
SC_CTOR(ModuleA) {
SC_THREAD(process);
}
void process() {
while(true) {
int input = chan_input->read();
chan_output->write(input+7);
}
}
};
SC_MODULE(ModuleB) {
sc_fifo_in<int> chan_input;
sc_fifo_out<int> chan_output;
SC_CTOR(ModuleB) {
SC_THREAD(process);
}
void process() {
while(true) {
int input = chan_input->read();
chan_output->write(input*3);
}
}
};
SC_MODULE(Top) {
sc_fifo_in<int> chan_input;
sc_fifo<int> moduleA_to_moduleB_fifo{1};
sc_fifo_out<int> chan_output;
ModuleA SC_NAMED(moduleA);
ModuleB SC_NAMED(moduleB);
SC_CTOR(Top) {
moduleA.chan_input(chan_input);
moduleA.chan_output(moduleA_to_moduleB_fifo);
moduleB.chan_input(moduleA_to_moduleB_fifo);
moduleB.chan_output(chan_output);
}
};
SC_MODULE(Testbench) {
sc_fifo<int> input_fifo;
sc_fifo<int> output_fifo;
Top SC_NAMED(top);
SC_CTOR(Testbench) {
top.chan_input(input_fifo);
top.chan_output(output_fifo);
SC_THREAD(produceTestInput);
SC_THREAD(verifyOutput);
}
void produceTestInput() {
for(int i = 0; i < 8; i++) {
input_fifo.write(i);
}
}
void verifyOutput() {
for(int i = 0; i < 8; i++) {
int ref = (i+7)*3;
int dut = output_fifo.read();
if(ref != dut) {
std::cout << "Error!" << std::endl;
std::cout << "Ref: " << ref << " DUT: " << dut << std::endl;
sc_stop();
}
}
std::cout << "Passed!" << std::endl;
sc_stop();
}
};
int sc_main(int, char*[]) {
Testbench SC_NAMED(testbench);
sc_start();
return 0;
}
Leave a comment