Mở đầu: Bạn cần thử 5 kiến trúc bus khác nhau — mỗi lần mô phỏng RTL mất 3 tiếng

Toàn bộ 10 bài trước dạy bạn mô tả phần cứng ở mức chính xác từng chu kỳ, từng bit — đúng thứ cần khi thiết kế đã chốt. Nhưng trước khi chốt thiết kế, đội kiến trúc còn phải trả lời những câu hỏi lớn hơn nhiều: "Nên dùng 2 hay 4 kênh DMA?", "Bus AXI hay kênh riêng cho khối video?" — mỗi phương án thử ở RTL đầy đủ mất hàng giờ mô phỏng, mà có thể phải thử hàng chục phương án. Tệ hơn: đội phần mềm thường phải bắt đầu viết driver trước khi RTL xong (chiến lược shift-left) — họ không thể đợi hàng tháng chỉ để có thứ gì đó chạy thử.

Lời giải của ngành công nghiệp là SystemC — một thư viện C++ cho phép mô tả hệ thống ở nhiều mức trừu tượng, từ thuật toán thuần tới gần-RTL — và TLM (Transaction-Level Modeling), chuẩn giao tiếp giữa các khối bằng "giao dịch" thay vì tín hiệu từng bit. Bài này giải thích vì sao mô hình TLM chạy nhanh hơn RTL hàng trăm-nghìn lần, đối chiếu SystemC với chính engine VeriLite bạn đã dùng suốt series, và kết thúc bằng demo tự đếm số sự kiện mô phỏng thật giữa 2 tầng — con số chênh lệch sẽ cho bạn thấy tại sao ngành công nghiệp không bao giờ chỉ dùng RTL.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 7 (Bộ nhớ, FIFO & CDC) — demo cuối bài này mô hình lại đúng hệ producer-consumer đã xây ở đó; Series C++ Bài 5 (Class, Constructor)Series C++ Bài 10 (Templates) — SystemC là thư viện C++ dùng nặng cả 2 khái niệm này.
Tài nguyên ngoài: HDLBits, ChipVerify, EDA Playground.

1. Khoảng trống giữa kiến trúc và RTL

Một con chip thật có thể mất 6-18 tháng từ ý tưởng kiến trúc tới RTL hoàn chỉnh. Trong khoảng thời gian đó, đội kiến trúc cần trả lời hàng loạt câu hỏi mà RTL chưa tồn tại để trả lời: throughput bus đủ chưa, cache size bao nhiêu là đủ, đội phần mềm cần "phần cứng giả" nào để bắt đầu viết driver/OS. Mô phỏng RTL quá chậm và quá tốn công viết để dùng cho giai đoạn khám phá này — mỗi phương án thử nghiệm cần RTL đầy đủ, tổng hợp được, chạy hàng giờ chỉ để trả lời 1 câu hỏi kiến trúc. SystemC giải quyết đúng khoảng trống này: mô hình hoá hệ thống ở mức đủ trừu tượng để viết nhanh, chạy nhanh, nhưng vẫn đủ chi tiết để đưa ra quyết định kiến trúc đáng tin cậy.

2. SystemC là thư viện C++, không phải ngôn ngữ mới

Điểm khác biệt lớn nhất so với SystemVerilog: SystemC không phải HDL riêng — nó là 1 thư viện C++ (class + macro), biên dịch bằng compiler C++ thông thường. Các khối chính đối chiếu 1-1 với khái niệm bạn đã quen:

SystemC Vai trò Tương đương SystemVerilog (đã học)
SC_MODULE Khối thiết kế (thực chất là 1 class C++) module
SC_METHOD Hàm tổ hợp, chạy lại mỗi khi tín hiệu nhạy (sensitive) đổi always_comb (Bài 2)
SC_THREAD Tiến trình có thể "chờ" (wait()), giữ trạng thái qua nhiều lần gọi always_ff (Bài 3) — nhưng linh hoạt hơn, có thể chờ theo thời gian thật
sc_signal<T> Tín hiệu kết nối giữa các module, kiểu dữ liệu C++ bất kỳ logic/wire
producer_sc.h (SystemC — ngoài subset VeriLite)
// ⚠️ Ngoài subset VeriLite — engine trang này chỉ hiểu SystemVerilog, không
// biên dịch C++. Code dưới là SystemC THẬT, cần g++ + thư viện SystemC để
// chạy (xem link cài đặt cuối bài).
#include <systemc.h>

SC_MODULE(Producer) {
  sc_out<bool> push;
  sc_out<int>  data_out;

  void generate_item() {
    // SC_METHOD: chạy lại mỗi khi tín hiệu 'sensitive' đổi — giống hệt
    // always_comb tự động re-evaluate khi input đổi.
    push.write(true);
    data_out.write(rand() % 256);
  }

  SC_CTOR(Producer) {
    SC_METHOD(generate_item);
    sensitive << clk.pos();  // "nhạy" với cạnh lên clock — giống always_ff
  }
};

Khái niệm quan trọng nhất SystemC chia sẻ với VeriLite: delta-cycle. Khi 1 tín hiệu đổi giá trị, mọi process "nhạy" với nó không chạy ngay lập tức mà được xếp vào hàng đợi, chạy ở 1 "vi chu kỳ" (delta-cycle) riêng — đảm bảo mọi process đọc giá trị ổn định của tín hiệu trước khi tín hiệu đó cập nhật tiếp. Đây chính xác là lý do applyAlwaysFF() trong vlsi-verilite.js phải lấy 1 snapshot đóng băng DUY NHẤT cho toàn bộ cạnh clock thay vì để từng khối đọc trạng thái đang thay đổi giữa chừng — con bug đó (đã sửa ở Bài 6) chính là vi phạm nguyên tắc delta-cycle mà scheduler SystemC thật được thiết kế để tránh ngay từ đầu.

3. TLM-2.0: Giao dịch thay tín hiệu từng bit

Mô hình hoá từng tín hiệu push/pop/data riêng lẻ (như SystemC thuần ở Mục 2) vẫn còn quá chi tiết cho giai đoạn khám phá kiến trúc. TLM (TLM) đi xa hơn: gói toàn bộ 1 lần trao đổi dữ liệu thành 1 lời gọi hàm (transaction) — không còn khái niệm "từng bit đổi ở chu kỳ nào".

producer_tlm.h (TLM-2.0 — ngoài subset VeriLite)
// So sánh với producer_sc.h ở Mục 2: KHÔNG còn sc_signal cho từng bit —
// toàn bộ 1 lần "gửi dữ liệu" gói vào 1 LỜI GỌI HÀM duy nhất.
void Producer::run() {
  for (int i = 0; i < N_ITEMS; i++) {
    tlm::tlm_generic_payload trans;
    trans.set_data_ptr((unsigned char*)&data[i]);
    trans.set_data_length(4);

    // 1 dòng này = TOÀN BỘ giao dịch — không mô phỏng từng cạnh clock nào
    // để "đẩy" dữ liệu qua bus, chỉ có kết quả cuối cùng.
    socket->b_transport(trans, delay);
  }
}

Hai biến thể TLM phổ biến nhất: Loosely-Timed (LT) — chỉ ước lượng độ trễ tổng bằng 1 con số annotate thêm vào giao dịch, không mô phỏng chi tiết bus; và Approximately-Timed (AT) — chia giao dịch thành vài "pha" (bắt đầu, chấp nhận, hoàn tất...) để nắm bắt hiệu ứng nghẽn bus gần đúng hơn, đổi lại chậm hơn LT. Cả 2 đều là chuẩn công nghiệp dùng trong virtual platform — mô hình toàn hệ thống chạy được cả OS thật lên trên, trước khi có bất kỳ silicon nào tồn tại.

⚠️ Cạm bẫy: Coi kết quả TLM là bằng chứng đạt timing thật

Con số delay trong 1 giao dịch TLM-LT là số người viết model tự ước lượng (annotate thủ công), không phải kết quả đo từ STA thật (Bài 8). Một giao dịch "thành công" ở TLM chỉ chứng minh đúng chức năng (đúng dữ liệu, đúng thứ tự) — hoàn toàn không phải bằng chứng RTL tổng hợp thật sẽ đáp ứng đúng deadline tần số đó.

tlm_timing_pitfall.h (SystemC — ngoài subset VeriLite)
// ❌ SAI: suy ra thiết kế "đạt 100MHz" từ 1 con số ước lượng trong TLM
if (socket->b_transport(trans, delay) == TLM_OK) {
  // delay o day CHI LA so nguoi viet model tu danh gia so bo, khong phai
  // ket qua do tu STA that tren RTL da tong hop.
  assert(delay < target_period);  // <-- KHONG chung minh duoc gi ve timing that
}

// ✅ ĐÚNG: TLM chỉ xác minh CHỨC NĂNG — mọi kết luận về timing PHẢI verify
// lại bằng STA thật trên RTL đã tổng hợp (Bài 8), không suy diễn từ TLM.
if (socket->b_transport(trans, delay) == TLM_OK) {
  log_functional_pass();  // chi khang dinh: dung du lieu, dung thu tu
}
// timing_verified = run_sta_on_synthesized_rtl();  // buoc bat buoc rieng,
// KHONG the thay the bang ket qua TLM

4. Bảng so sánh các mức mô hình hoá

Toàn bộ phổ mô hình hoá, từ nhanh/trừu tượng nhất tới chậm/chi tiết nhất — series này chủ yếu dạy mức RTL (cột thứ 4), TLM là bước "lùi ra xa" để nhìn hệ thống trước khi lao vào chi tiết đó:

← Tốc độ mô phỏng nhanh hơn Độ chính xác chu kỳ cao hơn → Untimed / Algorithmic — hàm số thuần, không có khái niệm thời gian TLM Loosely-Timed (LT) — giao dịch + 1 con số ước lượng độ trễ TLM Approximately-Timed (AT) — giao dịch chia nhiều pha đồng bộ RTL — tín hiệu mỗi cạnh clock (toàn bộ series tới Bài 10) Gate-level — cổng logic thật + độ trễ vật lý (Bài 8-10)
Mức Đơn vị mô phỏng Tốc độ tương đối Dùng khi nào
Untimed Lời gọi hàm thuần ~10.000× RTL Khám phá thuật toán, chưa quan tâm phần cứng
TLM-LT Giao dịch (transaction) ~1.000× RTL Virtual platform, viết driver phần mềm sớm
TLM-AT Giao dịch + pha (phase) ~100× RTL Đánh giá hiệu năng bus/kiến trúc trước RTL
RTL Tín hiệu mỗi cạnh clock 1× (mốc đối chiếu) Thiết kế/verify chức năng thật
Gate-level Cổng logic + độ trễ vật lý thật Chậm nhất STA hậu-layout, kiểm tra cuối trước tape-out

Công thức tốc độ tương đối đơn giản: $$\text{Speedup} = \frac{T_{RTL}}{T_{TLM}}$$ — với $T_{RTL}$/$T_{TLM}$ là thời gian máy tính THẬT cần để mô phỏng cùng 1 khoảng thời gian hệ thống; con số càng lớn, mô hình càng "rẻ" để thử nhiều phương án kiến trúc.

5. Demo tương tác: Producer-Consumer — đếm sự kiện TLM vs RTL

Cùng 1 hệ thống — producer phát N item, mỗi item cách nhau G chu kỳ đồng hồ, đưa qua FIFO (đúng module fifo_ctrl từ Bài 7) tới consumer — chạy ở 2 tầng: RTL (VeriLite mô phỏng thật từng cạnh clock, kể cả hàng trăm chu kỳ "không có gì xảy ra" giữa 2 item) và TLM (chỉ đếm đúng N lời gọi giao dịch, bỏ qua hoàn toàn thời gian chờ). Đổi G để xem khoảng cách 2 con số doãng ra thế nào.

⚡ TLM vs RTL — đếm sự kiện mô phỏng
Khoảng cách G (chu kỳ giữa 2 item):
TLM: —
RTL: —

fifo_ctrl.sv
// Module ĐÚNG NGUYÊN VĂN từ Bài 7 — tầng RTL của demo dùng lại module này,
// mô phỏng thật từng cạnh clock bằng VeriLite (xem file tải về cuối bài).
module fifo_ctrl (
  input  logic clk, rst,
  input  logic push, pop,
  output logic full, empty,
  output logic [3:0] count
);
  always_ff @(posedge clk) begin
    if (rst)
      count <= 0;
    else
      count <= (push && !full && pop && !empty) ? count :
                (push && !full)                 ? count + 1 :
                (pop && !empty)                  ? count - 1 :
                count;
  end

  assign full  = (count == 8);
  assign empty = (count == 0);
endmodule
🔬 Đào sâu: "Sự kiện mô phỏng" trong demo tính thế nào?
Tầng RTL: mỗi lần gọi cycle() của VeriLite (1 cạnh clock thật) tính là 1 sự kiện — kể cả những chu kỳ producer/consumer hoàn toàn "im lặng", vì phần cứng thật vẫn phải toggle clock và giữ trạng thái mỗi chu kỳ dù không có gì mới xảy ra. Tầng TLM: mỗi lần gọi hàm giao dịch (tương đương b_transport() ở Mục 3) tính là 1 sự kiện, hoàn toàn bỏ qua khái niệm "chu kỳ chờ" — đây chính xác là lý do khoảng cách 2 con số tăng tuyến tính theo G: RTL phải "trả giá" cho mọi chu kỳ chờ, TLM thì không.

Tóm lược

Bài này mở ra góc nhìn "lùi xa" khỏi RTL, trước khi quay lại chi tiết vật lý ở các bài cuối series:

  • SystemC là thư viện C++ (không phải HDL riêng): SC_MODULE = module, SC_METHOD/SC_THREAD = always_comb/always_ff, sc_signal = logic.
  • Delta-cycle của SystemC là cùng nguyên tắc đã sửa trong chính engine VeriLite ở Bài 6 — mọi process phải đọc trạng thái ĐÓNG BĂNG trước khi cập nhật đồng loạt.
  • TLM gói giao dịch thành 1 lời gọi hàm, bỏ hẳn khái niệm tín hiệu từng bit — LT ước lượng độ trễ bằng 1 con số, AT chia nhiều pha để gần đúng hơn.
  • Phổ mô hình hoá Untimed → TLM-LT → TLM-AT → RTL → Gate-level: tốc độ mô phỏng và độ chính xác chu kỳ luôn đánh đổi ngược chiều nhau.
  • Chênh lệch sự kiện mô phỏng giữa TLM và RTL tăng tuyến tính theo số chu kỳ "chờ" giữa các giao dịch — đúng lý do ngành công nghiệp không bao giờ chỉ mô phỏng ở RTL.

Trắc nghiệm ôn tập

Câu 1

SystemC khác SystemVerilog ở điểm cơ bản nào?

Câu 2

Điểm khác biệt cốt lõi giữa mô hình TLM và RTL là gì?

Câu 3

Trong demo Producer-Consumer, vì sao khoảng cách số sự kiện TLM vs RTL tăng khi G (khoảng cách chu kỳ giữa 2 item) tăng?

Câu 4

Khi nào nên dùng TLM Loosely-Timed thay vì TLM Approximately-Timed?

Tải file code thực hành minh họa bài học

File SystemVerilog gồm fifo_ctrl (nguyên văn từ Bài 7, dùng cho tầng RTL của demo) kèm testbench đếm số chu kỳ cần thiết để truyền N item, cộng ví dụ SystemC/TLM thật dạng comment tham khảo và lệnh biên dịch:

Tải về systemc_tlm_example.sv

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 10: FPGA Flow: Synthesis, Place & Route, Bitstream Bài 12: ASIC Flow mã nguồn mở: Standard Cell → GDSII Quay lại Lộ trình Series Thiết Kế Vi Mạch Số & FPGA

Bình luận