Chủ Nhật, 16 tháng 6, 2019

[SystemC][TLM] Bài 1 - Tổng quan về TLM và simple socket

Bài viết này trình bày nhưng khái niệm và thuật ngữ cơ bản về "mô hình hóa mức transaction" Transaction Level Modeling (TLM) trước khi trình bày cách sử dụng TLM trong việc mô hình hóa phần cứng. Bên cạnh đó, bài này sẽ minh họa việc chuyển giao tiếp dạng tín hiệu (signal) giữa các module thành giao tiếp dựa trên socket TLM.
Chú ý, các bạn cần đọc các bài về High Level Design và SystemC mà nhóm tác giả đã trình bày để có các khái niệm cơ bản và hiểu mô hình minh họa trước khi đọc bài này.
1) Tổng quan về TLM
Các quy chuẩn về TLM được nghiên cứu và công bố bởi tổ chức "Open SystemC Initiative (OSCI)".
TLM (Transaction Level Modeling) là một phần của chuẩn SystemC. TLM được tích hợp với chuẩn SystemC vào tháng 7, 2012. Nó nằm thuộc các mục từ 9 đến 17 trong tài liệu IEEE Std. 1666™-2011 tên "IEEE Standard for Standard SystemC® Language Reference Manual".
Bạn đọc có thể biết thêm thông tin tại trang này.
Như tên gọi, TLM dùng để mô hình hóa mức transaction. TLM tập trung vào việc mô hình cách thông tin giữa các process thông qua việc gọi và sử dụng các chức năng có sẵn. Dưới góc nhìn phần cứng, TLM sẽ được sử dụng để mô hình cách trao đổi thông tin giữa các khối (block) hoặc module they vì dùng cách kết nối từng tín hiệu.
Hình 1: Sự khác nhau giữa kết nối theo tín hiệu và kết nối theo mô hình TLM
Đối với mô hình kết nối dạng tín hiệu, đây là dạng mô hình giống với phần cứng, mỗi tín hiệu là một kết nối vật lý giữa các chân của module. Giá trị trên các tín hiệu là tức thời và thay đổi theo xung clock hệ thống của module tạo ra output.
Đối với mô hình kết nối theo TLM, đây là dạng mô hình "cách truyền nhận các gói dữ liệu (transaction)". Module tạo ra output gọi là Initiator, bộ khởi tạo transaction. Module nhận transaction gọi là Target, bộ tiếp nhận transaction. Các input và output port được thay bằng socket. Output port tương ứng với một "Initiator socket". Input port tương ứng với một "Target socket".
Một module có thể vừa là Initiator và vừa là Target khi nó có cả Initiator socket (Output) để gửi dữ liệu đến module khác và có cả Target socket (Input) để nhận dữ liệu từ các module khác gửi đến.
Việc mô hình sử dụng TLM có những lợi điểm sau:
  1. Mô hình giao tiếp giữa các module đơn giản, dễ phát triển vì hai module chỉ cần quy định cấu trúc gói dữ liệu cần truyền và cách thức truyền rồi gọi method thực thi. Điều này giúp giảm thời gian mô tả kết nối các tín hiệu của hai module.
  2. Mô hình các module interconnection như BUS hệ thống sẽ đơn giản hơn. BUS trong một hệ thống SoC là thành phần chuyển dữ liệu từ master đến slave. BUS có thể được mô hình đơn giản bằng cách "nhận transaction từ master, tạo độ trễ như phần cứng thực tế, chuyển transaction đến slave theo địa chỉ được yêu cầu" mà không cần mô hình các chức năng phức tạp của decoder, hay arbiter hay tạo ra các tín hiệu điều khiển phức tạp như thực tế phần cứng.
Hình 2: Minh họa một kênh truyền dữ liệu từ master đến slave trong hệ thống sử dụng kết nối TLM
2) Làm thế nào để tạo TLM connection?
2.1) Cách thực hiện
Việc tạo TLM connection là tạo ra các socket kết nối các module và gọi method thực thi việc truyền nhận dữ liệu trên socket đã tạo. Để tạo một TLM connection, các hoạt động chính cần thực hiện là:
  • Tạo socket
  • Tạo method để truyền/nhận gói dữ liệu (transaction) qua socket
    • Initiator socket
      • Tạo gói dữ liệu
      • Gửi gói dữ liệu
      • Kiểm tra gói dữ liệu đã được nhận bởi Target
    • Target socket
      • Nhận và lưu gói dữ liệu
      • Kiểm tra và báo trạng thái nhận cho Initiator
Hình 3: Tạo kết nối TLM trong header file scpu_decoder.h
Hình 4: Một method dùng để tạo transaction và gửi transaction thông qua initiator socket tên dc2fetch_socket
2.2) Tạo socket
SystemC đã hỗ trợ các class và method cần thiết để dùng cho việc này. Bảng sau đây liệt kê các class có sẵn hỗ trợ việc tạo socket (tham khảo trong tài liệu chuẩn SystemC).
Hình 5: Các class có sẵn hỗ trợ tạo initiator và target socket
Trong bài viết này, nhóm tác giả sử dụng simple_initiator_socket và simple_target_socket (gọi chung là simple socket).
Để tạo một initiator socket, chúng ta cần thực hiện các bước cơ bản sau đây:
  • Include các file thư viện TLM trong file header ".h"
 #include "tlm.h"
 #include "tlm_utils/simple_initiator_socket.h"
  • Khai báo initiator socket với cú pháp:
tlm_utils::simple_initiator_socket<module_name> socket_name;
  • Khởi tạo initiator socket tại Constructor với cú pháp:
SC_CTOR(module_name) : socket_name_0("string_0"), socket_name_1("string_1"), ...{
Để tạo một target socket, chúng ta cần thực hiện các bước cơ bản sau đây:
  • Include các file thư viện TLM trong file header ".h"
 #include "tlm.h"
 #include "tlm_utils/simple_target_socket.h"
  • Khai báo target socket với cú pháp:
tlm_utils::simple_target_socket<module_namesocket_name;
  • Khởi tạo target socket tại Constructor với cú pháp:
SC_CTOR(module_name) : socket_name_0("string_0"), socket_name_1("string_1"), ...{
2.3) Method truyền/gửi transaction của Initiator socket
Các bước cơ bản để gửi một transaction hay một gói dữ liệu qua initiator socket:
  • Khai báo một method cho việc gửi dữ liệu:
void method_name();
  • Khởi tạo method trong Constructor dùng SC_THREAD. Chú ý, SC_METHOD không thể sử dụng vì SC_METHOD không chấp nhận các tác vụ lặp hay delay.
SC_THREAD (method_name);
  • Mô tả gói dữ liệu (transaction) và cách thức gửi gói dữ liệu trong method đã tạo
2.4) Method nhận transaction của Target socket
Các bước cơ bản để nhận một transaction hay một gói dữ liệu qua target socket:
  • Đăng ký callback cho method truyền nhận dữ liệu qua target socket trong Constructor:
socket_name.register_b_transport(this, &module_name::method_name);
    • Trong đó:
      • socket_name là tên target socket mà bạn sẽ lấy transaction từ nó.
      • register_b_transporst là method đăng ký callback cho hàm b_transport. b_transport là một loại hàm được sử dụng để truyền, nhận dữ liệu qua socket. Cái này sẽ được trình bày ngay trong phần sau đây.
      • module_name là tên module gắn target socket
      • method_name là tên method mà trong đó bạn sẽ viết code để lấy dữ liệu từ target socket.
  • Xây dựng method lấy dữ liệu từ target socket
2.5) Tạo và truyền/nhận gói dữ liệu (transaction)
Các phần trên đã trình bày "làm thế nào để tạo socket?". Vấn đề còn lại là "làm thế nào để truyền nhận dữ liệu qua socket?". Để giải đáp điều này, trước hết chúng ta cần hiểu về một gói dữ liệu (transaction) được gửi qua socket có cấu trúc như thế nào.
2.5.1) Tạo và gửi transaction tại initiator socket
Một gói dữ liệu (transaction) gửi qua socket được tạo bởi loại dữ liệu tlm_generic_payload. Đây là một cấu dữ liệu mặc định giúp mô hình hóa các transaction.
tlm::tlm_generic_payload* trans_name = new tlm::tlm_generic_payload;
Trong đó:
  • trans_name là tên transaction do người dung đặt
  • new là hàm tạo đối tượng trans_name
Một transaction được khai báo loại tlm_generic_payload sẽ được hỗ trợ sẵn các method cần thiết để người dùng có thể thiết lập các thuộc tính và dữ liệu mong muốn cho transaction ở phía initiator socket và đọc các thuộc tính, lấy dữ liệu của transaction tại target socket.
Để thiết lập thuộc tính và giá trị cho một transaction tại initiator socket, chúng ta dùng các hàm có tiền tố "set_".
  • set_command( const tlm_command );
    • Thiết lập loại transaction
    • Initiator sẽ thiết lập loại transaction để target đáp ứng phù hợp với từng loại
      • TLM_READ_COMMAND: Target sẽ lấy dữ liệu của nó gán đến vùng bộ nhớ xác định bởi con trỏ dữ liệu mà initiator đã gửi
      • TLM_WRITE_COMMAND: Target sẽ đọc dữ liệu từ vùng bộ nhớ xác định bởi con trỏ dữ liệu mà initiator đã gửi
      • TLM_IGNORE_COMMAND: Target không cần thực thi gì khi nhận được loại transaction này. 
  • set_address( const sc_dt::uint64 );
    • Thiết lập địa chỉ cho transaction
  • set_data_ptr( unsigned char* );
    • Thiết lập giá trị pointer trỏ đến vùng lưu dữ liệu cần truyền qua socket
  • set_data_length( const unsigned int );
    • Thiết lập độ dài dữ liệu theo byte
  • set_streaming_width( const unsigned int );
    • Thiết lập độ dài burst
  • set_byte_enable_ptr( unsigned char* );
    • Thiết lập giá trị pointer trỏ đến cùng lưu giá trị byte enable của transaction.
  • set_dmi_allowed( bool );
    • Thiết lập giá trị điều khiển cơ chế DMI
  • set_response_status( const tlm_response_status ); 
    • Thiết lập trạng thái của transaction. Target sẽ thiết lập một giá trị thích hợp để báo cho initiator biết trạng thái của transaction mà nó nhận được
      • TLM_OK_RESPONSE = 1 
      • TLM_INCOMPLETE_RESPONSE = 0
      • TLM_GENERIC_ERROR_RESPONSE = –1
      • TLM_ADDRESS_ERROR_RESPONSE = –2
      • TLM_COMMAND_ERROR_RESPONSE = –3
      • TLM_BURST_ERROR_RESPONSE = –4
      • TLM_BURST_ERROR_RESPONSE = –4 
Hình 6: Minh họa việc truyền transaction thông qua simple socket sử dụng b_transport
Trong phần này, nhóm tác giả sử dụng hàm b_transport để truyền transaction tại initiator socket.
socket_name->b_transport (trans_name, delay_value);
Trong đó:
  • socket_name là tên của initiator socket
  • trans_name là tên của transaction được khai báo bởi tlm_generic_payload.
  • delay_name là giá trị delay được khai báo loại sc_time.
2.5.2) Đăng ký callback và nhận transaction tại target socket
Việc đăng ký callback cho b_transport đã trình bày ở mục trên.
Để đọc thuộc tính và giá trị của một transaction tại target socket, chúng ta dùng các hàm có tiền tố "get_". Mỗi hàm "set_" của một tlm_generic_payload sẽ có một hàm "get_" tương ứng. Các hàm "get_" sẽ được sử dụng để lấy thông tin các thuộc tính của một transaction.
  • get_command();
  • get_address();
  • get_data_ptr();
  • get_data_length();
  • get_streaming_width();
  • get_byte_enable_ptr();
  • get_response_status();
2.6) Quá trình truyền nhận thông qua TLM socket
Quá trình truyền nhận sử dụng hàm b_transport thông qua TLM socket được tóm gọn như sau:
  • Initiator:
    • Tạo transaction tlm_generic_payload.
    • Gửi transaction bằng hàm b_transport
  • Target:
    • Nhận transaction
    • Báo lại trạng thái transaction cho Initiator bằng cách dùng hàm set_response_status.
3) Áp dụng simple TLM socket với thiết kế SCPU
Chú ý, ví dụ về SCPU thuộc loạt bài giới thiệu về HLD khả tổng hợp. Nhóm tác giả sẽ dùng ví dụ đã hoàn thiện này để chuyển đổi tất cả các kết nối tín hiệu giữa các module thành kết nối dùng simple TLM socket.
Chú ý, SCPU đã là một mô hình hoàn chỉnh chạy theo xung clock hệ thống. Việc chuyển đổi phải đảm bảo mô hình chức năng của SCPU vẫn chạy đúng. Ý tưởng chính của cách chuyển đổi là:
  • Chuyển đổi các port giao tiếp của module (sc_in, sc_out) thành các tín hiệu nội (sc_signal) và biến nội (sc_uint)
  • Tất cả các chân output (sc_out) của module sau khi chuyển thành tín hiệu hoặc biến nội sẽ được tập hợp thành một gói dữ liệu để gửi qua initiator socket
  • Tất cả các chân input (sc_in) của module sau khi được chuyển thành tín hiệu hoặc biến nội sẽ lấy giá trị từ gói dữ liệu nhận được trên một target socket.

Hình 7: Minh họa việc chuyển từ kết nối tín hiệu (signal) thành kết nối TLM socket
Xét lại hình minh họa 1, hình này thể hiện rõ kết nối kiểu tín hiệu giữa FETCH và DECODER. Nhóm tác giả sẽ giải thích rõ cách chuyển đổi trên ví dụ minh họa này. Các kết nối còn lại sẽ thực hiện tương tự.
FETCH là khối gửi 3 tín hiệu fetch_ir, fetch_dr fetch_mem_dout còn DECODER là khối nhận nên:
  • Tại module FETCH:
    • Các output fetch_ir, fetch_dr fetch_mem_dout sẽ chuyển từ khai báo sc_out thành biến nội với khai báo sc_uint.
    • Khai báo một initiator socket tên fetch2dc_socket
tlm_utils::simple_initiator_socket<scpu_fetch> fetch2dc_socket;
    • Khai báo một method để gửi transaction qua socket trên
 void FETCH2DC_SOCKET();
    • Tạo một biến nội để đóng gói tất cả các giá trị output mà FETCH cần gửi với khai báo sc_uint với độ rộng bit bằng độ rộng bit của 3 tín hiệu ngõ ra cộng lại.
 sc_uint<24> fetch_data;
    • Tạo một biến trung gian để nhận giá trị đóng gói từ biến sc_unit và có loại dữ liệu phù hợp kiểu dữ liệu sử dụng trong hàm set_data_ptr.
unsigned int fetch_data_tmp;
    • Tạo một transaction
tlm::tlm_generic_payload* fetch2dc_trans = new tlm::tlm_generic_payload;
    • Khai báo thông số delay cần dùng
sc_time wire_delay = sc_time(0, SC_PS);
sc_time trans_delay = sc_time(10, SC_PS);
    • Đóng gói dữ liệu cần truyền qua initiator socket
fetch_data = (fetch_ir, fetch_dr, fetch_mem_dout);
fetch_data_tmp = fetch_data;
    • Cấu hình transaction
      • Loại lệnh luôn là WRITE vì đây là output từ FETCH đến DECODER
tlm::tlm_command fetch_cmd = tlm::TLM_WRITE_COMMAND;
fetch2dc_trans->set_command( fetch_cmd );
      • Thiết lập con trỏ dữ liệu cần truyền đến biến tạm fetch_data_tmp
 fetch2dc_trans->set_data_ptr(reinterpret_cast<unsigned char*>(&fetch_data_tmp));
      • Thiết lập độ dài dữ liệu là 3 byte ứng với 24 bit. Nếu độ dài dữ liệu lớn hơn 24 nhưng nhỏ hơn 32 bit thì thiết lập là 4 byte.
 fetch2dc_trans->set_data_length( 3 );
      • Thiết lập độ dài burst bằng độ dài dữ liệu là 3. Điều này nghĩa là không sử dụng streaming
fetch2dc_trans->set_streaming_width( 3 );
      • Thiết lập byte enable là 0 nghĩa là không sử dụng cấu hình này
fetch2dc_trans->set_byte_enable_ptr( 0 );
      • Thiết lập DMI là false, đây là bắt buộc cho phía initiator socket
 fetch2dc_trans->set_dmi_allowed( false );
      • Thiết lập trạng thái response là TLM_INCOMPLETE_RESPONSE, đây là bắt buộc cho phía initiator socket. Sau khi nhận được transaction, target socket sẽ thiết lập lại trạng thái response đến 1 giá trị phù hợp.
fetch2dc_trans->set_response_status( tlm::TLM_INCOMPLETE_RESPONSE );
    • Gửi transaction với delay là 0
fetch2dc_socket->b_transport( *fetch2dc_trans, wire_delay );
    • Giám sát trạng thái response
if ( fetch2dc_trans->is_response_error() )
  SC_REPORT_ERROR("TLM-2", "Response error from b_transport");
    • Delay giữa hai lần truyền. Các module hoạt động theo cạnh lên xung clock nên dữ liệu truyền qua socket phải đảm bảo target nhận được dữ liệu đúng trong từng chu kỳ. Vì vậy, trans_delay được thiết lập rất nhỏ so với chu kỳ hoạt động cấp cho SCPU khi mô phỏng.
 wait(trans_delay);
  • Tại module DECODER:
    • Tất cả các input chuyển từ sc_in thành sc_signal.
 sc_signal<sc_uint<8> > fetch_ir;
 sc_signal<sc_uint<8> > fetch_dr;
 sc_signal<sc_uint<8> > fetch_mem_dout;
    • Tạo một biến có loại dữ liệu phù hợp với hàm get_data_ptr
 unsigned int fetch2dc_pkt_tmp; 
    • Tạo một biến sc_uint có độ rộng bit bằng với số bit mà khối FETCH truyền tới. Biến này sẽ được dùng để tách từng bit dữ liệu gán đến tín hiệu tương ứng
 sc_uint<24> fetch2dc_pkt;
    • Khai báo target socket
tlm_utils::simple_target_socket<scpu_decoder> fetch2dc_socket;
    • Đăng ký callback cho b_transport
fetch2dc_socket.register_b_transport(this, &scpu_decoder::fetch2dc_transport)
    • Đọc gói transaction gửi từ FETCH
tlm::tlm_command dc_cmd = fetch2dc_trans.get_command();
unsigned char*   dc_ptr = fetch2dc_trans.get_data_ptr();
unsigned int     dc_len = fetch2dc_trans.get_data_length();
unsigned char*   dc_byt = fetch2dc_trans.get_byte_enable_ptr();
unsigned int     dc_wid = fetch2dc_trans.get_streaming_width();
    • Cấp phát bộ nhớ để lưu lại dữ liệu từ transaction
memcpy (&fetch2dc_pkt_tmp, dc_ptr, dc_len);
fetch2dc_pkt = fetch2dc_pkt_tmp;
    • Tách các bit dữ liệu cần dùng gán đến tín hiệu mong muốn
fetch_ir   = fetch2dc_pkt.range(23,16);
fetch_dr   = fetch2dc_pkt.range(15,8);
fetch_mem_dout  = fetch2dc_pkt.range(7,0);
    • Trả response lại cho initiator socket
fetch2dc_trans.set_response_status( tlm::TLM_OK_RESPONSE );
Các bạn có thể tham khảo và chạy mô phỏng với source code đầy đủ được chia sẻ ở link dưới bài viết.

Dữ liệu có thể download:
pass (nếu có): vlsi_technology

Lịch sử cập nhật:
1. 2019.June.16 - Tạo lần đầu
2. 2019.July.06 - Thêm pass giải nén

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

3 bình luận: