Chủ Nhật, 17 tháng 11, 2019

[SystemC][SC_(C)THREAD] Bài 1 - Mô hình mạch tuần tự tổng hợp được bằng SC_THREAD và SC_CTHREAD

Việc mô hình mạch tổ hợp và tuần tự bằng SC_METHOD đã được giới thiệu trong một số bài viết trước đây. Bài viết này trình bày về cách mô hình mạch tuần tự tổng hợp được, mạch có Flip-Flop (FF), bằng SC_THREAD và SC_CTHREAD. Bên cạnh đó, bài viết cũng sẽ giải thích sự tương đồng giữa hành vi của SystemC code và hoạt động phần cứng.
Tham khảo bài viết về mô hình phần cứng dùng SC_METHOD ở các bài viết sau:
1) Gới hạn của SC_THREAD và SC_CTHREAD trong mô tả mạch tuần tự
SC_THREAD SC_CTHREAD được phép dùng để mô tả mạch tuần tự (sequential circuit) khả tổng hợp nhưng phải tuân thủ một số yêu cầu nhất định để phù hợp với mô hình phần cứng thực tế.
  • Danh sách độ nhạy của một process phải được mô tả trong constructor của SC_MODULE chứa process đó.
  • Một process phải hoạt động theo một cạnh clock.
  • Một process phải có ít nhất một mô tả reset. Reset này là loại reset đồng bộ hoặc bất đồng bộ.
  • Reset đồng bộ được khai báo bằng:
reset_signal_is(<reset_name>, <active_level>)
  • Reset bất đồng bộ được khai báo bằng:
async_reset_signal_is(<reset_name>, <active_level>)
Trong đó:
  • reset_name là tên tín hiệu reset
  • active_level là mức tích cực của tín hiệu reset, nó là true, tích cực mức cao, hoặc false, tích cực mức thấp.
Trong SC_THREAD SC_CTHREAD, để tạo trễ (tạm dừng) trong process, chỉ hai chức năng sau được hỗ trợ:
  • wait()
  • wait(<number>) – number là một số nguyên dương
Chỉ các vòng lặp sau được hỗ trợ:
  • while( 1 ) { }
  • while( true ) { }
  • do { } while ( 1 );
  • do { } while ( true );
  • for ( ; ; ) { }
2) Cấu trúc của SC_THREAD và SC_CTHREAD khi mô tả mạch tuần tự
Phần này sẽ trình bày cụ thể cách làm thế nào để sử dụng các thành phần đã liệt kê trên đây trong SC_THREAD SC_CTHREAD để mô hình mạch tuần tự tổng hợp được.
Một mạch tuần tự dùng FF có hai hoạt động chính:
  • Hoạt động reset (có thể có hoặc không)
  • Hoạt động chức năng thông thường (phải có)
Một mạch tuần tự dùng SC_(C)THREAD cũng sẽ được mô hình hóa theo hai hành vi trên. Cấu trúc mô hình gồm:
  • Reset behavior: Gán giá trị reset cho các FF. Dùng cấu trúc sau để gán reset cho một thanh ghi (FF)
<signal_name>.write(<reset_value>)
Trong đó:
  • signal_name: tên thanh ghi (FF)
  • reset_value: giá trị reset muốn gán cho thanh ghi
  • wait(): Chờ đến chu kỳ clock kế tiếp sau khi kết thúc reset
  • Operational behavior: Một vòng lặp vô tận thực thi chức năng khi không có reset
    • Dùng while(1), while(true), do...while(1), do...while(true) hoặc vòng lặp for với điều kiện lặp vô tận để thực hiện ví dụ như for (int i=0; i == 0, i=0). Trong ví dụ của for, điều kiện thực hiện vòng lặp for i bằng 0 i luôn được gán bằng 0 nên vòng lặp for này là vòng lặp vô tận.
    • Read input: đọc các giá trị sử dụng làm đầu vào điều khiển mạch tuần tự gán vào một biến tạm sử dụng method read(). Mục đích của bước này là tránh phải dùng method read() nhiều lần trong khi mô tả hành vi mạch tuần tự.
    • Assign output: Dùng các biến tạm của bước "Read input" để mô tả hành vi mạch tuần tự. Bước này thực chất là gán giá trị cho các thanh ghi (FF) dựa trên các điều kiện ngõ vào lấy từ bước "Read input".
Hình 1: Cấu trúc của một SC_(C)THREAD dùng để mô hình hóa mạch tuần tự
Hình sau đây giải thích vì sao cấu trúc trên có thể mô hình hóa một mạch tuần tự. Hành vi reset (reset behavior) được thực thi khi reset tích cực, rst_n=0. Giá trị reset được duy trì đến cạnh lên clk đầu tiên bằng hàm wait() đầu tiên, nằm ngoài vòng lặp vô tận. Sau đó, ngõ ra Q của FF sẽ bắt đầu cập nhật giá trị từ D theo từng cạnh lên xung clock clk, nghĩa là cập nhật theo mỗi chu kỳ xung clock clk. Điều này được mô hình bởi một vòng lặp vô tận với một hàm wait() ở cuối vòng lặp.
Hình 2: Sự tương đồng giữa mô hình mạch tuần tự bằng SC_(C)THREAD với hành vi của một FF
3) Ví dụ về mô hình mạch tuần tự dùng SC_(C)THREAD
3.1) Yêu cầu
Giả sử, chúng ta cần mô hình hóa bộ đếm có mạch nguyên lý như sau:
  • rst_n là tín hiệu reset đồng bộ theo xung clock clk
  • plus_en là tín hiệu điều khiển việc bộ đếm sẽ tăng hay giảm 1 đơn vị theo xung clock clk
Hình 3: Mạch nguyên lý bộ đếm lên và xuống 1 đơn vị
Bộ đếm trên sẽ nằm trong một module tên counter để có thể tổng hợp và kiểm tra trên Vivado HDL.
Hình 4: Module counter
Một module trong SystemC code được mô tả trong 2 file là:
  • file header (.h)
  • file mô tả chức năng các process (.cpp)
3.2) Khai báo process và các biến (file .h)
Trong file này chúng ta cần khai báo:
  • Một process dùng để gán giá trị reset cho mạch tuần tự, initReset(). Đây chính là thành phần mô hình hóa hành vi reset (Reset behavior) như đã mô tả ở mục trên
  • Một process dùng để mô hình hóa mạch tuần tự, seqLogic(). Process này sẽ thực thi đầy đủ cấu trúc mô hình hóa mạch tuần tự như đã trình bày ở mục trên gồm:
    • initReset()
    • wait()
    • Vòng lặp vô tận dành cho "Operational behavior"
Ngoài ra, vì counter_value là port ngõ ra của module counter nên không thể dùng trực tiếp biến này trong process seqLogic() vì một port ngõ ra không được phép đọc lại giá trị để mô tả chức năng bộ đếm. Ví dụ, đoạn code sau đây là không hợp lệ:
counter_value.write(counter_value.read() + 1);
counter_value sc_out nên counter_value.read() không được phép sử dụng.
Vì vậy, một biến trung gian, counter_value_o, được sử dụng để mô hình hóa bộ đếm. Sau đó, biến này sẽ được gán đến ngõ ra counter_value thông qua một mạch tổ hợp được mô hình bằng một SC_METHOD. Mạch tổ hợp này là combLogic().
Hình 5: Mạch tuần tự của bộ đếm trong module counter được mô hình bằng hai process là seqLogic và combLogic
Các biến và process của bộ đếm trong module counter được viết trong file .h như sau:
Ví dụ 1 - Nội dung file counter.h
#include "systemc.h"
SC_MODULE (counter) {
  //
  //(1) Input/Output
  //
  sc_in< bool > clk;
  sc_in< bool > rst_n;
  sc_in< bool > plus_en;
  sc_out< sc_uint<4> > counter_value;
  //
  //(2) Intenal variables
  //
  sc_signal< sc_uint<4> > counter_value_o;
  //(3) Reset process
  void initReset();
  //(4) Function processes
  void seqLogic();
  void combLogic();
  //(5) Constructor
  SC_CTOR(counter) {
    //Sequential model
    SC_CTHREAD(seqLogic, clk.pos());
      reset_signal_is(rst_n, false);
    //Output assignment
    SC_METHOD(combLogic);
      sensitive << counter_value_o;
  }
};
Trong đoạn code trên, SC_CTHREAD được dùng để mô hình mạch tuần tự, là bộ đếm. Các khai báo của SC_CTHREAD trong constructor như sau:
  • seqLogic là tên process
  • clk.pos() là cạnh lên clk
  • reset_signal_is khai báo một reset tên rst_n với mức tích cực thấp (false)
Nếu sử dụng reset bất đồng bộ thì khai báo sẽ là:
async_reset_signal_is(rst_n, false);
Nếu sử dụng SC_THREAD thì khai báo sẽ là:
  • Reset đồng bộ
SC_THREAD(seqLogic);
sensitive << clk.pos();
reset_signal_is( rst_n, false);
  • Reset bất đồng bộ
SC_THREAD(seqLogic);
sensitive << clk.pos();
async_reset_signal_is( rst_n, false);
3.3) Mô hình hóa mạch tuần tự (file .cpp)
Hành vi của process được mô tả trong file .cpp. File này sẽ gọi file header (.h) trước khi mô tả các process.
Ví dụ 2 - Nội dung file counter.cpp
#include "counter.h"
//(1) Reset process - assign the initial value to registers in reset
void counter::initReset() {
  counter_value_o.write(0);
} //initReset
//(2) Function process
void counter::seqLogic() {
  //Call initReset executed when reseting
  initReset();
  //Delay 1 cycle
  wait();
  //Function description
  //It is executed during normal operation when reset is inactive
  while (1) {
    //Read input
    bool plus_en_i = plus_en.read();
    sc_uint<4> counter_value_o_i=counter_value_o.read();
    //Assign output
    if (plus_en_i == 1) {
      counter_value_o.write(counter_value_o_i + 1);
    }
    else {
      counter_value_o.write(counter_value_o_i - 1);
    }
    //
    //Delay 1 cycle before executing next step
    wait();
  }
} //seqLogic
void counter::combLogic() {
  counter_value.write(counter_value_o.read());
} //combLogic
Trong đoạn code trên, process initReset() thực hiện việc gán một giá trị reset là 0 cho bộ đếm counter_value_o.
counter_value_o.write(0);
process này sẽ được gọi và thực thi đầu tiên trong process seqLogic(). Lưu ý, việc gán giá trị reset có thể được mô tả trực tiếp ngay đầu process seqLogic() mà không cần tạo process initReset(). Tuy nhiên, việc tạo một process riêng giúp bạn có thể gán giá trị reset cho nhiều biến trong một thiết kế. Điều này giúp code dễ đọc hiểu hơn.
Ví dụ 3 - Gán giá trị reset trực tiếp bên trong process seqLogic()
void counter::seqLogic() {
  //Call initReset executed when reseting

  counter_value_o.write(0);

  //Delay 1 cycle
  wait();
  //Function description
  //It is executed during normal operation when reset is inactive
  while (1) {
    ...
  }
} //seqLogic
Sau khi gán giá trị reset, một hàm wait() được gọi trước khi mô tả vòng lặp vô tận cho "operational behavior".
Trong vòng lặp vô tận, đoạn code đầu tiên sẽ đọc tất cả các giá trị cần sử dụng trong vòng lặp. Trong ví dụ này là:
bool plus_en_i = plus_en.read();
sc_uint<4> counter_value_o_i = counter_value_o.read();
Điều này giúp tránh phải mô tả <signal_name>.read() nhiều lần trong code khi một tín hiệu được sử dụng nhiều lần.
Tiếp theo code hành vi của bộ đếm khi không có reset được mô tả:
   if (plus_en_i == 1) {
      counter_value_o.write(counter_value_o_i + 1);
    }
    else {
      counter_value_o.write(counter_value_o_i - 1);
    }
Cuối vòng lặp, một wait() được khai báo.
Như đã trình bày, trong ví dụ này, chúng ta cần một process combLogic() mô hình mạch tổ hợp để gán giá trị bộ đếm counter_value_o đến ngõ ra counter_value.
4) Những quy định về việc sử dụng SC_(C)THREAD được mô tả ở đâu?
Hiện tại, các thành phần SystemC khả tổng hợp được mô tả trong một tài liệu tên SystemC Synthesizable Subset của tổ chức Accellera.
Tuy nhiên, nhiều phần mềm tổng hợp SystemC hiện tại không hỗ trợ đầy đủ như mô tả trong tài liệu này. Mỗi phần mềm tổng hợp SystemC sẽ kèm theo những quy định về việc mô tả SystemC code để có thể tổng hợp được tốt nhất với phần mềm đó.
Vì vậy, để đảm bảo một code SystemC có thể tổng hợp được. Người thiết kế cần tuân thủ thêm các yêu cầu mà nhà cung cấp phần mềm quy định. Một số phần mềm hỗ trợ tổng hợp SystemC như Stratus HLS (Cadence), CoCentric™ SystemC Compiler (Synopsys), LegUp HLS (LegUp Computing Inc.), CyberWorkBench (DEC), Vivado HLS (Xilinx), ...
5) Lưu ý khi dùng SC_(C)THREAD
Note 1: Một số trình tổng hợp có thể không hỗ trợ tổng hợp SC_(C)THREAD, ví dụ như SystemC Compiler của Synopsys, phiên bản từ năm 2001 trở về trước, tham khảo tài liệu #3.

Note 2Theo tài liệu tham khảo #2, mục 4.2.1 Clock and Reset, tín hiệu reset phải được khai báo khi sử dụng SC_(C)THREAD cho dù các FF trong mạch tuần tự cần thiết kế có reset hay không.

Note 3: Theo tài liệu tham khảo #2, việc dùng SC_METHOD để mô tả mạch tuần tự cũng buộc phải khai báo reset cho dù các FF trong mạch tuần tự cần thiết kế có reset hay không, xem mục 4.1.2 Sequential SC_METHOD trong #2.

Note 4: Trong mô hình mạch tuần tự, hàm wait() đầu tiên có ý nghĩa phân tách giữa "reset process" và "operational process". Ngoài cách mô tả dùng 2 hàm wait() như đã trình bày trên đây, chúng ta có thể mô tả một mạch tổ hợp chỉ dùng một hàm wait() đặt ở đầu vòng lặp.
Hình 6: Mô hình mạch tuần tự chỉ dùng một hàm wait()
Note 5: Dùng wait(1) có tương đương với việc dùng wait() trong tổng hợp.

Note 6: Việc dùng nhiều hàm wait() được phân bổ khác với hai mô hình đã trình bày trên đây (hình 1 và hình 6) hoặc dùng wait(<number>) với number>1 có thể tổng hợp được. Tuy nhiên, cách mô tả này không nên sử dụng vì RTL code (Verilog, SV hoặc VHDL) được tạo ra ứng với hành vi wait() có thể khó kiểm soát hoặc không tối ưu như mong muốn người thiết kế. Điều này hoàn toàn phụ thuộc nào phần mềm. Vì vậy, nếu bạn sử dụng cách mô tả này, bạn cần biết chính xác RTL code sẽ được tạo ra bởi trình tổng hợp đang dùng. Để mô tả nhiều wait() hoặc wait(<number>) với number>1, bạn hãy thiết kế mạch logic (tư duy phần cứng) để tạo ra được các delay mong muốn. Nghĩa là chuyển tất cả về dạng mô tả mạch tổ hợp hoặc mạch tuần tự cơ bản như đã trình bày trên đây.
Ví dụ 4 - Mô hình mạch tuần tự với wait(<number>)
void counter::seqLogic() {
  //Call initReset executed when reseting
  initReset();
  //Function description
  while (1) {
    wait(10);
    //Read input
    bool plus_en_i = plus_en.read();
    sc_uint<4> counter_value_o_i=counter_value_o.read();
    //Assign output
    if (plus_en_i == 1) {
      counter_value_o.write(counter_value_o_i + 1);
    }
    else {
      counter_value_o.write(counter_value_o_i - 1);
    }
  }
} //seqLogic
Trong ví dụ trên, wait(10) được sử dụng để mô hình một mạch tuần tự. Với mô hình này, mạch hoạt động như sau:
  1. Reset counter_value_o
  2. Chờ 10 xung clock
  3. counter_value_o bắt đầu đếm lên hoặc xuống 1 đơn vị tùy vào giá trị của plus_en
  4. Chờ 10 xung clock
  5. Lặp lại từ bước 3
Mạch logic để tạo độ trễ 10 chu kỳ xung clock có thể được thực hiện bằng cách dùng một thanh ghi dịch 10 bit. Giá trị reset của thah ghi dịch là 10'b0000000001. Sau khi reset thanh ghi dịch sẽ thực thi dịch trái và xoay vòng. Bit 9 của thanh ghi dịch sẽ được dùng làm tín hiệu cho phép bộ đếm cập nhật giá trị. Mạch nguyên lý thể hiện trong hình vẽ sau đây.
Hình 7: Mạch nguyên lý của một RTL code được tổng hợp từ ví dụ 4
Vivado HLS 2017.4 đang tạo ra RTL code theo một cách gần tương tự như mô tả trên. Cách làm này tốn nhiều tài nguyên vì số lượng FF dùng cho chức năng tạo độ trên wait(10) lớn. Trong khi đó, nó có thể được thực hiện bằng một cách khác là dùng một bộ đếm 4 bit. Bộ đếm này sẽ được reset là 0 và đếm lên 1 đơn vị liên tục sau khi thôi reset. Khi bộ đếm bằng 9 thì nó sẽ được khởi tạo lại giá trị 0 và đếm lại. Giá trị bộ đếm khi bằng 9 sẽ là tín hiệu cho phép cập nhật counter_value.
Hình 8: Dùng bộ đếm 4 bit để tạo độ trễ 10 chu kỳ thay cho wait(10)
Theo cách thiết kế này, SystemC dùng wait(10) sẽ được chuyển về dạng cơ bản chỉ dùng dùng một wait() như sau.
Ví dụ 5 - SystemC code cho sơ đồ nguyên lý hình 8
//(1) Reset process - assign the initial value to registers in reset
void counter::initReset() {
  counter_value_o.write(0);
  delay_counter.write(0);
} //initReset
//(2) Function process
void counter::seqLogic() {
  //Call initReset executed when reseting
  initReset();
  //Function description
  while (1) {
    wait();
    //Read input
    bool plus_en_i = plus_en.read();
    sc_uint<4> counter_value_o_i=counter_value_o.read();
    sc_uint<4> delay_counter_i = delay_counter.read();
    //Assign output
    if (delay_counter_i == 9) {
      delay_counter.write(0);
    }

    else {
      delay_counter.write(delay_counter_i + 1);
    }
    //
    if (delay_counter_i == 9) {
      if (plus_en_i == 1) {
        counter_value_o.write(counter_value_o_i + 1);
      }
      else {
        counter_value_o.write(counter_value_o_i - 1);
      }
    }
  }
} //seqLogic
Note 8: Hành vi reset là hành vi được thực hiện khi tín hiệu reset tích cực. Một giá trị mặc định được gán cho biến trong constructor không phải là giá trị reset. Ví dụ, loại dữ liệu sc_uint có giá trị mặc định là 0 nhưng đây không được xem là giá trị reset. Giá trị reset là giá trị phải được mô tả khi xảy ra hành vi reset, giá trị gán trong initReset() ở các ví dụ đã trình bày.

Tài liệu tham khảo:
1) IEEE; IEEE Standard for Standard SystemC® Language Reference Manual; Jannuary, 2012
2) Accellera; SystemC Synthesizable Subset Version 1.4.7; March, 2016
3) Synopsys; CoCentric SystemC Compiler - RTL User and Modeling Guide; August, 2001

Lịch sử cập nhật:
1) 2019.11.17 - Tạo lần đầu

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

1 bình luận:

  1. công thức ứng dụng công nghệ điện tử giao thức IP VR MFD cũng thấy biểu thức ứng dụng phục tạp

    Trả lờiXóa