Thứ Năm, 18 tháng 4, 2019

[SystemC][High Level Design]Bài 3 - Mô tả model code cho sub-module bằng SystemC

Bài viết này hướng dẫn mô hình hóa các mạch tổ hợp và mạch tuần tự bằng SystemC. Cách xây dựng sub-module với SystemC. Bên cạnh đó, một số lưu ý khi sử dụng SystemC để mô hình hóa mạch logic tổng hợp được. Trong loạt bài viết này, phần mềm được sử dụng là phần mềm Xilinx Design Tool - Vivado HL Design, một phần mềm sử dụng cho FPGA.
Các bạn cần đọc bài 1 bài 2 trước khi đọc bài này. Đặc biệt, model code mình họa trong bài này được viết dựa trên model specification ở bài 2.
1) SystemC là gì?
SystemC là một thư viện C++. Nó hỗ trợ rất nhiều các class khác nhau để mô hình hóa các hoạt động phần cứng. Tuy nhiên, tài liệu này chỉ tập trung vào việc mô tả model code khả tổng hợp. Đồng thời, nhóm tác giả không có ý định phân tích tất cả các khả năng có thể sử dụng của SystemC mà chỉ giới thiệu một cách mô hình hóa đơn giản giúp bạn đọc nhanh chóng nắm bắt và thực hành.
Bên cạnh đó, chúng tôi sẽ so sánh sự tương đương giữa SystemC code và RTL code trong mô tả mạch logic.
2) Mô hình hóa sub-module
Một sub-module được mô hình hóa bằng các từ khóa SystemC sau đây:
  • SC_MODULE: khai báo module
  • sc_in: khai báo port input
  • sc_out: khai báo port output
  • sc_inout: khai báo port hai chiều vừa là input, vừa là output
  • sc_signal: khai báo tín hiệu giao tiếp nội bộ giữa các process.
  • SC_METHOD: khai báo các process để mô hình hóa mạch tổ hợp và mạch tuần tự
Hình 1: Mô hình model code của sub-module
Một sub-module được mô tả trong hai file:
  • file header (.h) chứa các khai báo
  • file body (.cpp) chứa code của các process để mô hình hóa mạch logic.
3) Mô tả file header (.h)
3.1) Cấu trúc file header như sau
Cú pháp:
#include "systemc.h"
SC_MODULE (module_name) {
  //port_declaration
  //signal_declaration
  //variable_declaration
  //process_declaration
  //module_constructor
};
Cấu trúc file header như sau:
  • [Bắt buộc] Gọi thư viện SytemC #include "systemc.h"
  • [Bắt buộc] Khai báo module với từ khóa SC_MODULE
    • module_name: tên module do người thiết kế đặt
  • [Bắt buộc] port_declaration: Khai báo port input/output/inout
  • [Tùy chọn] signal_declaration: Khai báo tín hiệu nội bộ nếu có
  • [Tùy chọn] variable_declaration: Khai báo biến sử dụng nội bộ nếu có
  • [Bắt buộc] process_declaration: Khai báo các process
  • [Bắt buộc] module_constructor: Khai báo constructor
    • [Bắt buộc] Đăng ký các process
    • [Bắt buộc] Khai báo danh sách độ nhạy cho các process
3.2) Khai báo port
Cú pháp:
port_type<data_type> port_name;
Trong đó:
  • port_type là loại port:
    • sc_in là port input
    • sc_out là port output
    • sc_inout là port hai chiều
  • data_type là loại dữ liệu
    • bool: kiểu dữ liệu chỉ có hai giá trị 1 hoặc 0
    • sc_uint<n>: kiểu dữ liệu không dấu có số bit là n ≤ 64 bit
    • sc_biguint<n>: kiểu dữ liệu không dấu có số bit là n > 64 bit
  • port_name là tên port do người thiết kế đặt
Một số ví dụ về khai báo port:
  • sc_in<bool>: khai báo port input độ rộng 1 bit
  • sc_out<bool>: khai báo port output độ rộng 1 bit
  • sc_inout<bool>: khai báo port hai chiều độ rộng 1 bit
  • sc_in<sc_uint<n> >: khai báo port input độ rộng n bit với n ≤ 64 bit
  • sc_out< sc_uint<n> >: khai báo port output độ rộng n bit với n ≤ 64 bit
  • sc_inout< sc_uint<n> >: khai báo port hai chiều độ rộng n bit với n ≤ 64 bit
  • sc_in<sc_biguint<n> >: khai báo port input độ rộng n bit với n > 64 bit
  • sc_out<sc_biguint<n> >: khai báo port output độ rộng n bit với n > 64 bit
  • sc_inout<sc_biguint<n> >: khai báo port hai chiều độ rộng n bit với n > 64 bit
Chú ý, khi khai báo một port nhiều bit, một khoảng trắng bắt buộc phải có nằm giữa dấu “>” cuối cùng và dấu “>” ngay trước nó.
3.3) Khai báo tín hiệu
Cú pháp:
sc_signal<data_type> signal_name;
Trong đó: 
  • data_type: giống như mô tả trong phần khai báo port
  • signal_name: là tên tín hiệu do người thiết kế đặt
3.4) Khai báo biến nội
Cú pháp:
<data_type> variable_name;
Trong đó: 
  • data_type: giống như mô tả trong phần khai báo port
  • variable_name: là tên biến do người thiết kế đặt
3.5) Khai báo process
Cú pháp:
void process_name();
Trong đó:
  • process_name là tên process do người thiết kế đặt
3.6) Constructor
Cú pháp constructor:
SC_CTOR (module_name) {
//process_register
//sensitivity_list

}
Trong đó:
  • module_name là tên module do người thiết kế đặt. module_name của constrcutor phải trùng với tên module đã khai báo tại SC_MODULE
  • process_register là khai báo đăng ký process sử dụng SC_METHOD. Ngoài SC_METHOD, chúng ta còn có thể dùng SC_THREAD SC_CTHREAD để mô tả process nhưng để đảm bảo tổng hợp tốt trên nhiều phần mềm, hai loại này sẽ không được sử dụng. 
  • sensitivity_list là danh sách độ nhạy của process đã đăng ký. 
Cú pháp đăng ký process:
SC_METHOD (process_name);
Trong đó:
  • process_name là tên process do người thiết kế đặt và trùng với khai báo process. 
Cú pháp khai báo danh sách độ nhạy của mô hình mạch tổ hợp:
sensitive << signal_name_0 << signal_name_1 << …; 
Cú pháp khai báo danh sách độ nhạy của mô hình mạch tuần tự dùng reset đồng bộ hoặc không có reset:
sensitive << clock_name.pos(); //positive edge
hoặc:
sensitive << clock_name.neg(); //negative edge
Cú pháp khai báo danh sách độ nhạy của mô hình mạch tuần tự dùng reset bất đồng bộ:
sensitive << clock_name.pos(); //positive edge
sensitive << reset_name.pos(); //positive edge 
hoặc:
sensitive << clock_name.pos(); //positive edge
sensitive << reset_name.neg(); //negative edge 
hoặc:
sensitive << clock_name.neg(); //negative edge
sensitive << reset_name.pos(); //positive edge 
hoặc:
sensitive << clock_name.neg(); //negative edge
sensitive << reset_name.neg(); //negative edge
3.7) Ví dụ về file header của khối FETCH

#include "systemc.h"
//--------------------------------------
//Project: Simple CPU
//Module: FETCH - header file
//Function:Read instruction and data from memory
//Author: Nguyen Hung Quan
//Page: VLSI Technology
//--------------------------------------

SC_MODULE (scpu_fetch) {
//
//Port declaration
//
//Input
sc_in<bool> clk;
sc_in<bool> rst_n;
sc_in<bool> dc_load_pc;
sc_in<bool> dc_imm;
sc_in<sc_uint<2> > dc_addr_sel;
sc_in<sc_uint<8> > dc_rs;
sc_in<sc_uint<8> > dc_rd;
sc_in<bool> dc_mem_wr;
sc_in<bool> dc_load_dr;
sc_in<bool> dc_load_ir;
//Ouptut
sc_out<sc_uint<8> > fetch_ir;
sc_out<sc_uint<8> > fetch_dr;
sc_out<sc_uint<8> > fetch_mem_dout;
//
//Signal declaration
//
sc_signal<sc_uint<8> > mem_dout;
sc_signal<sc_uint<8> > pc;
sc_signal<sc_uint<8> > mem_addr;
sc_signal<sc_uint<8> > fetch_dr_in;
//
//Variable declaration
//
sc_uint<8> mem_array [256];
//
//Process declaration
//
//memory
void IR_DR_INPUT();
void MEM_OUTPUT();
void MEM_WRITE();
//fetch
void INSTRUCTION_REG();
void DATA_REG_IN();
void DATA_OUTPUT();
void PROGRAM_COUNTER();
void MEMORY_ADDR();
//
//Module Constructor
//

SC_CTOR (scpu_fetch) {
  SC_METHOD (IR_DR_INPUT);
    sensitive << fetch_dr;
    sensitive << pc;
    sensitive << dc_imm;
  //
  SC_METHOD (MEM_OUTPUT);
    sensitive << mem_addr;
  //
  SC_METHOD (MEM_WRITE);
    sensitive << clk.pos();
  //
  SC_METHOD (INSTRUCTION_REG);
    sensitive << clk.pos();
  //
  SC_METHOD (DATA_REG_IN);
    sensitive << clk.pos();
  //
  SC_METHOD (DATA_OUTPUT);
    sensitive << fetch_dr_in;
   //
  SC_METHOD (PROGRAM_COUNTER);
    sensitive << clk.pos();
  //
  SC_METHOD (MEMORY_ADDR);
    sensitive << dc_addr_sel;
    sensitive << pc;
    sensitive << fetch_dr;
    sensitive << dc_rs;
  }
};
4) Mô tả file body (.cpp)
4.1) Cách mô tả file body
Cú pháp: 
#include "header_file"
//process_body
Trong đó:
  • header_file là file “.h” của sub-module
  • process_body là mô hình mạch logic, mạch tổ hợp hoặc mạch tuần tự, của tất cả các process trong sub-module
Cú pháp mô tả phần code của process:
void module_name::process_name () {
//variable_declaration
//model_code
}
Trong đó: 
  • module_name là tên module do người thiết kế đặt. Tên sử dụng ở đây phải trùng với tên khai báo bởi SC_MODULE
  • process_name là tên process do người thiết kế đặt. Tên sử dụng ở đây phải trùng với tên khai báo process bởi void. 
  • variable_declaration là khai báo biến trung gian sử dụng trong process nếu cần model_code là code SystemC mô hình mạch logic.
4.2) Cách thức mô hình hóa mạch tổ hợp
Trong mô hình hóa mạch tổ hợp, tất cả các tín hiệu đầu vào ảnh hưởng đến hoạt động của mạch phải được liệt kê đầy đủ trong danh sách độ nhạy. Việc khai báo thiếu tín hiệu trong danh sách độ nhạy sẽ làm việc mô phỏng model code không chính xác.
Method read() được sử dụng để đọc giá trị các tín hiệu đầu vào của một process.
Hình 2: Một ví dụ về mô tả mạch tổ hợp
4.3) Cách thức mô hình hóa mạch tuần tự
Khi mô hình hóa mạch tuần tự, danh sách độ nhạy chỉ chứa tín hiệu clock. Nếu mạch tuần tự sử dụng reset bất đồng bộ thì danh sách độ nhạy có thêm tín hiệu reset. 
Để khai báo cạnh tích cực cả tín hiệu clock và reset, hai method sau đây được sử dụng: 
  • pos() mô tả cạnh lên 
  • neg() mô tả cạnh xuống 
Chú ý, tuy còn có cách khác để mô tả cạnh tích cực của clock và reset như: 
  • sensitive_pos mô tả cạnh lên 
  • sensitive_neg mô tả cạnh xuống Nhưng để dảm bảo tương thích với phần mềm tổng hợp Vivado sử dụng trong loạt bài này, chúng ta nên chỉ dùng method pos() và neg().
Hình 3: Một ví dụ về mô tả mạch tuần tự dùng reset đồng bộ

4.4) Ví dụ về file body của khối FETCH
#include "scpu_fetch.h"
//
//MEMORY
//
void scpu_fetch::IR_DR_INPUT () {
//
sc_uint<8> mem_sel;
sc_uint<8> dr_tmp;
//
dr_tmp = fetch_dr_in.read();
//
mem_sel = (dc_imm.read() == 1)? dr_tmp: pc.read();
//
mem_dout = mem_array[mem_sel];
}
//
void scpu_fetch::MEM_OUTPUT () {
fetch_mem_dout.write(mem_array[mem_addr.read()]);
}
//
void scpu_fetch::MEM_WRITE () {
sc_uint<1> mem_wr;
sc_uint<8> mem_din;
//
mem_wr  = dc_mem_wr.read();
mem_din = dc_rd.read();
//
if (mem_wr == 1) {
mem_array[mem_addr.read()] = mem_din;
}
}
//
//FETCH
//
void scpu_fetch::INSTRUCTION_REG () {
if (rst_n.read() == 0) {
fetch_ir.write(0x00);
}
else if (dc_load_ir.read() == 1) {
fetch_ir.write(mem_dout.read());
}
}
//
void scpu_fetch::DATA_REG_IN () {
if (dc_load_dr.read() == 1) {
fetch_dr_in = mem_dout.read();
}
}
void scpu_fetch::DATA_OUTPUT () {
fetch_dr.write(fetch_dr_in.read());
}
//
void scpu_fetch::PROGRAM_COUNTER () {
sc_uint<8> pc_dr_tmp;
//
pc_dr_tmp = fetch_dr_in.read();
//
if (rst_n.read() == 0) {
pc = 0x00;
}
else if (dc_load_pc.read() == 1) {
if (dc_imm.read() == 1) {
pc = pc_dr_tmp;
}
else {
pc = pc.read() + 0x1;
}
}
}
//
void scpu_fetch::MEMORY_ADDR () {
sc_uint<8> mem_addr_dr_tmp;
sc_uint<2> mem_addr_sel;
//
mem_addr_dr_tmp = fetch_dr_in.read();
mem_addr_sel    = dc_addr_sel.read();
//
switch (mem_addr_sel) {
case 0x1:
mem_addr = dc_rs.read();
    break;
case 0x2:
mem_addr = mem_addr_dr_tmp;
    break;
default:
mem_addr = pc.read();
    break;
}
}
Chú ý:
  • Để lấy giá trị từ một port input hoặc một tín hiệu thì sử dụng cú pháp sau:
port_input_name/signal_name.read()
  • Để gán giá trị cho port output thì sử dụng cú pháp sau:
port_output_name.write(assigned_value)
  • Để gán giá trị cho một tín hiệu hoặc biến nội thì sử dụng cú pháp sau:
signal_name/variable_name = assigned_value;
assigned_value có thể là một hằng số hoặc một biểu thức.

5) So sánh giữa model code và RTL code
5.1) Mô tả module
Hình 4 thể hiện sự tương đồng giữa mô tả module trong model code với SystemC và RTL code với Verilog HDL. Một số điểm khác biệt cần lưu ý là: 
  • Khi mô tả module trong SystemC cần gọi thư viện SystemC và phải khai báo constructor vì các từ khóa của SystemC là tên các class được xây dựng từ C++. 
  • Trong SystemC, tên module xuất hiện tại hai vị trí là khi khai báo module với từ khóa SC_MODULE và khi khai báo constructor với từ khóa SC_CTOR. Trong Verilog HDL, tên module chỉ xuất hiện khi khai báo module với từ khóa module
  • Khi mô tả module trong SystemC không cần khai báo danh sách port như trong Verilog HDL. Việc khai báo tín hiệu nội và biến nội của SystemC có sự khác biệt. Tín hiệu nội cần khai báo với từ khóa sc_signal còn biến nội chỉ cần khai báo với loại dữ liệu. Trong khi đó, Verilog HDL không phân biệt điều này, tất cả các thành phần sử dụng nội bộ chỉ cần khai báo với kiểu dữ liệu như wire, reg, …
Hình 4: So sánh việc mô tả module của SystemC và Verilog HDL
5.2) Mô tả mạch logic
Hình 5 minh họa sự tương đồng giữa model code và RTL code khi mô tả một mạch logic. Trong minh họa này, mạch logic là một mạch tuần tự có reset đồng bộ. Một số điểm khác biệt cần lưu ý như sau: 
  • Mỗi mô hình mạch logic trong SystemC cần một định danh gọi là tên process, ví dụ như INSTRUCTION_REG. Trong Verilog HDL, điều này là không cần thiết. 
  • Trong SystemC, để mô hình một mạch logic, chúng ta cần “khai báo process” dùng từ khóa void và "đăng ký process” với từ khóa SC_METHOD. Trong Verilog HDL, chỉ cần dùng một từ khóa để mô tả như always, assign, …
Hình 5: So sánh việc mô tả mạch logic của SystemC và Verilog HDL

Lịch sử cập nhật:
1. 2019.Apr.18 - Tạo lần đầu

Danh sách tác giả:
1. Lê Hoàng Vân
2. Trương Công Hoàng Việt
3. Nguyễn Hùng Quân

0 bình luận:

Đăng nhận xét