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.
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 |
// ⚠️ 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".
// 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.
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ố đó.
// ❌ 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 đó:
| 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.
// 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
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:
Bình luận