Testbench thực ra là gì
Testbench chỉ là một module Verilog. Khác biệt so với một module tổng hợp được nằm ở bên trong:
- Nó không có port: nó là đỉnh của cây phân cấp mô phỏng.
- Nó khởi tạo thiết kế cần kiểm tra.
- Nó điều khiển các input của DUT từ các khối
initialvàalways. - Nó quan sát các output của DUT bằng
$display,$monitorhoặc bằng cách xuất tệp VCD. - Nó kết thúc mô phỏng bằng
$finish.
Toàn bộ công việc chỉ có vậy. Không có từ khóa "testbench" đặc biệt nào: bạn nhận ra một module là testbench bằng cách đọc xem nó làm gì.
Bộ khung chuẩn
Đó là toàn bộ mẫu. Năm phần, dễ sao chép, dễ điều chỉnh.
Với DUT tổ hợp (như bộ cộng này), bạn không cần xung clock. Với DUT tuần tự thì cần, và đó là lớp tiếp theo.
Tạo xung clock cho DUT tuần tự
Khi DUT có input clk, testbench phải tạo ra nó. Câu lệnh một dòng chuẩn:
reg clk = 0;
always #5 clk = ~clk;
Dòng này đảo clk mỗi 5 đơn vị thời gian. Mỗi lần đảo là nửa chu kỳ clock, nên một chu kỳ đầy đủ là 10 đơn vị (một chu kỳ = 5 ở mức cao + 5 ở mức thấp). Nếu timescale là 1ns / 1ps (mặc định ở phần lớn trình mô phỏng), đó là xung clock 100 MHz.
Chọn nửa chu kỳ theo thứ bạn muốn mô hình hóa. Với xung clock 10 MHz ở cùng timescale, dùng #50. Với xung clock 200 MHz, dùng #2.5 (hoặc đổi timescale).
Chuỗi reset
Thiết kế đồng bộ cần một tín hiệu reset được giữ ở mức cao trong vài chu kỳ sau thời điểm 0, rồi mới nhả ra. Dạng quen thuộc:
reg reset = 1; // start asserted
initial begin
// Hold reset for a few clocks.
#20 reset = 0;
end
Đoạn này giữ reset ở mức cao đến thời điểm 20, rồi hạ xuống. Đến lúc reset hạ xuống, xung clock đã chạy được vài chu kỳ, mọi flip-flop đã ghi nhận giá trị reset, và thiết kế ở một trạng thái đã biết.
Với reset tích cực mức thấp (reset_n thay cho reset), đảo giá trị ban đầu:
reg reset_n = 0; // active-low: 0 means asserted
initial begin
#20 reset_n = 1;
end
Một testbench tuần tự đầy đủ
Kết hợp xung clock, reset và tín hiệu kích thích:
Đó là một testbench hoàn chỉnh cho bộ đếm 4 bit. Nhấn "Chạy" và bạn sẽ thấy bộ đếm thoát khỏi reset, đếm lên khi được cho phép, tạm dừng khi enable hạ xuống, tiếp tục khi nó lên lại, rồi dừng.
Ba điều cần để ý về cấu trúc:
- Ba điểm hoạt động riêng biệt: bộ tạo xung clock (một
always), tín hiệu kích thích (mộtinitial) và bộ theo dõi (mộtalwayskhác). Mỗi phần làm một việc. - Các độ trễ
#trong khối kích thích được tính bằng đơn vị thời gian, không phải chu kỳ clock. Nếu chu kỳ clock là 10 đơn vị thì#10đúng bằng một chu kỳ. Một số testbench dùng@(posedge clk)thay vào đó, cách này luôn tiến đúng một chu kỳ bất kể chu kỳ dài bao nhiêu. - Bộ theo dõi dùng
$displaytrong một khốialways @(posedge clk). Nó in ra ở mỗi chu kỳ. Để có đầu ra tinh vi hơn, hãy chuyển sang$monitor(sẽ nói ở bài sau).
Tín hiệu kích thích theo chu kỳ
Đôi khi "chờ N chu kỳ" dễ đọc hơn "chờ N đơn vị thời gian":
initial begin
@(posedge clk); // wait for next clock edge
reset = 0;
repeat (5) @(posedge clk); // wait 5 more cycles
enable = 1;
repeat (8) @(posedge clk);
enable = 0;
repeat (10) @(posedge clk);
$finish;
end
@(posedge clk) chặn lại cho đến cạnh lên tiếp theo của xung clock. repeat (N) @(posedge clk); chờ N chu kỳ. Cách viết này không phụ thuộc vào chu kỳ clock: nếu bạn đổi tần số clock, tín hiệu kích thích vẫn làm đúng việc đó tính theo chu kỳ.
Với những testbench đầu tiên, độ trễ # đơn giản hơn. Với testbench thực tế có thể chạy ở nhiều tốc độ clock, cách theo chu kỳ an toàn hơn.
Testbench tự kiểm tra
Testbench từ đầu đến giờ in ra những gì đã xảy ra và để bạn tự đọc đầu ra. Một testbench tự kiểm tra thì kiểm tra đầu ra và báo đạt hay không đạt:
initial begin
#1;
a = 10; b = 20;
#1;
if (sum !== 30) begin
$display("FAIL: 10 + 20 = %0d (expected 30)", sum);
$finish;
end
a = 250; b = 5;
#1;
if (sum !== 255) begin
$display("FAIL: 250 + 5 = %0d (expected 255)", sum);
$finish;
end
$display("PASS");
$finish;
end
Dùng !== (toán tử so sánh khác theo kiểu case) cho an toàn: nó không trả về x khi một toán hạng có bit không xác định. Mẫu chung: đưa input vào, chờ mọi thứ ổn định, so sánh với giá trị mong đợi, báo đạt hoặc không đạt.
Tự kiểm tra cực kỳ có giá trị trong các bộ kiểm thử hồi quy: hàng nghìn bài kiểm tra có thể chạy mà không cần ai trông, và chỉ những lần thất bại mới cần bạn để ý.
Tiếp theo là gì
Giờ bạn đã có bộ khung testbench đủ dùng cho mọi module. Các bài tiếp theo nói về những công cụ dùng bên trong testbench: Display và Monitor cho đầu ra văn bản phong phú hơn, Dumpfile và VCD để gỡ lỗi bằng dạng sóng đồ họa, và Timescale và độ trễ để kiểm soát chính xác mối liên hệ giữa thời gian mô phỏng và thời gian thực.
Câu hỏi thường gặp
Testbench trong Verilog là gì?
Testbench là một module Verilog, thường không có port, chỉ có một mục đích là chạy thử thiết kế cần kiểm tra (DUT). Nó khởi tạo DUT, tạo xung clock, đưa tín hiệu kích thích vào qua các khối initial và always, quan sát output bằng $display/$monitor, có thể xuất dạng sóng VCD, và kết thúc mô phỏng bằng $finish.
Làm sao để tạo xung clock trong testbench Verilog?
Mẫu chuẩn chỉ có một dòng: always #5 clk = ~clk; (với reg clk = 0; được khai báo trước đó). Dòng này đảo clk mỗi 5 đơn vị thời gian mô phỏng, cho chu kỳ 10 đơn vị (xung clock 100 MHz nếu timescale tính bằng nano giây). #5 là nửa chu kỳ: nửa thời gian clk ở mức cao, nửa còn lại ở mức thấp.
DUT trong Verilog là gì?
DUT là viết tắt của Design Under Test, tức module mà testbench đang kiểm tra. Theo quy ước, instance DUT trong testbench được đặt tên là dut hoặc u_dut: my_module dut(.clk(clk), .reset(reset), .in(in), .out(out));. Tên chỉ là nhãn; điều quan trọng là nó được khởi tạo, các port được nối với tín hiệu của testbench, và testbench điều khiển các tín hiệu đó.
Mô phỏng Verilog nên chạy trong bao lâu?
Đủ lâu để chạy thử mọi thứ bạn muốn kiểm tra, rồi gọi $finish. Phần lớn testbench đặt giới hạn thời gian rõ ràng (#1000 $finish) để mô phỏng không bị treo khi chờ một sự kiện không bao giờ đến. Trong khoảng thời gian đó, đưa tín hiệu kích thích vào, để DUT ổn định, và tốt nhất là thêm vài câu lệnh if tự kiểm tra, in ra FAIL nếu output không khớp với mong đợi.