You are on page 1of 12

[Verification] Hướng dẫn tạo testbench tự kiểm tra thiết kế bằng

Verilog, System Verilog


Tác giả Nguyễn Quân at 08:52  Kiến Thức Cơ Bản, Questa Sim, simtool, simulation, System Verilog, verification  0
bình luận
Bài viết này hướng dẫn các bạn viết một testbench đơn giản tự động kiểm tra một thiết kế. Đồng thời qua
đó các bạn có thể hiểu cơ bản về việc mô phỏng và kiểm tra thiết kế. Nội dung của bài viết này dành cho
các bạn mới bắt đầu tìm hiểu cách viết một testbench Verilog.

1. Môi trường mô phỏng là gì?


Môi trường mô phỏng (simulation environment) hay môi trường kiểm tra (verification environment) của
thiết kế là một tổ hợp các thành phần (bao gồm cả thiết kế) cho phép công cụ mô phỏng có thể sử dụng
để thực thi tính toán các dữ liệu đầu ra của thiết kế dựa trên các dữ liệu đầu vào được môi trường cung
cấp. Dữ liệu đầu ra có thể hiện dưới nhiều dạng khác nhau như một file dữ liệu, hoặc hiển thị trên ngõ ra
chuẩn (màn hình).
Với cùng một thiết kế, môi trường mô phỏng được xây dựng bởi các kỹ sư khác nhau có thể khác nhau
nhưng đều thực thi hai nhiệm vụ cơ bản:
1. Cấp giá trị ngõ vào cho thiết kế hoạt động
2. Giám sát giá trị ngõ ra của thiết kế tương ứng với những giá trị ngõ vào đã cấp để đảm bảo
thiết kế hoạt động chính xác
Testbench là một thành phần của môi trường mô phỏng thực thi trực tiếp hai nhiệm vụ trên hoặc chỉ cấp
giá trị ngõ vào cho thiết kế hoạt động. Thuật ngữ testbench cũng có thể dùng để hàm ý nói về một môi
trường mô phỏng hoàn chỉnh.

Cấu trúc môi trường mô phỏng cơ bản gồm:


1. DUT (Design Under Test) còn được gọi là UUT (Unit Under Test) chính là thiết kế cần mô
phỏng và kiểm tra. DUT sẽ được gọi và kết nối với các thành phần kiểm tra ở bên trong môi trường
mô phỏng hoặc trực tiếp trong testbench.
2. Testbench
1. Stimulus hoặc Driver hoặc Test Vector Generator là thành phần tạo và cung cấp
dữ liệu đầu vào của DUT, lái các tín hiệu đầu vào của DUT
2. Monitor hoặc Observer là thành phần giám sát và kiểm tra các giá trị ngõ ra của
DUT
Hình 1. Cấu trúc cơ bản của môi trường mô phỏng

Xin lưu ý với các bạn, đây chỉ là cấu trúc cơ bản. Hiện nay, với sự phức tạp ngày càng tăng của các thiết
kế môi trường mô phỏng cũng được xây dựng dựa trên các giao thức và phương pháp được chuẩn hóa
ví dụ như OVM, VMM, UVM,... Các giao thức này phân chia môi trường thành nhiều thành phần với tên
gọi khác nhau nhưng vẫn không nằm ngoài 2 mục đích là tạo dữ liệu đầu vào và giám sát dữ liệu đầu ra
của thiết kế.
2. Mô tả DUT 
Phần này mô tả DUT sẽ sử dụng trong bài viết này. DUT là một bộ đếm Johnson được mô tả chi tiết ở
link sau:
http://nguyenquanicd.blogspot.com/2017/08/verilog-rtl-code-mo-ta-cac-loai-bo-em.html

RTL code của bộ đếm Johnson N bit:


module johnson_counter (clk, rst_n, js_count);
//
parameter N  = 4;
//
//Interface
//
input clk;
input rst_n;
output reg [N-1:0] js_count;
wire js_msb_inv;
assign js_msb_inv = ~js_count[N-1];
always @ (posedge clk) begin
 if (~rst_n)
   js_count[N-1:0] <= 0;
 else
   js_count[N-1:0] <= {js_count[N-2:0], js_msb_inv};
end
endmodule

Bộ đếm này đã được mô phỏng trong bài viết sau:

http://nguyenquanicd.blogspot.com/2017/08/questa-sim-huong-dan-cai-at-va-chay-mo.html

sử dụng testbench có nội dung như sau:


module tb_jc;
parameter N = 4;
//
//Interface
//
reg clk;
reg rst_n;
wire [N-1:0] js_count;
johnson_counter #(.N(N)) dut (.clk(clk), .rst_n(rst_n), .js_count(js_count));
initial begin
  clk = 0;
forever #10 clk = !clk;
end
initial begin
  rst_n = 0;
#20
rst_n = 1;
end
endmodule 

Testbench trên đơn giản chỉ làm nhiệm vụ:


1. Gọi thiết kế
2. Tạo giá trị ngõ vào cho xung clock clk và tín hiệu reset rst_n (Stimulus).
Monitor khi chạy mô phỏng testbench trên là cửa sổ waveform của trình mô phỏng. Qua cửa sổ này,
chúng ta quan sát bằng mắt để kiểm tra giá trị của tín hiệu ngx ra js_count theo hoạt động của xung
clock và reset.
Việc kiểm tra bằng mắt như trên sẽ gây mất thời gian, phụ thuộc vào "tình trạng" người quan sát nên dễ
nhầm lẫn. Nhất là với các thiết kế phức tạp thì việc làm như vậy là không đảm bảo tính đúng đắn của
thiết kế. Chính vì vậy, việc xây dựng các môi trường và testbench tự kiểm tra là cần thiết.
3. Mô tả cấu trúc testbench
Một file testbench có thể gồm các thành phần cơ bản như sau:
1. Định nghĩa chỉ dẫn mô phỏng là các khai báo về đơn vị thời
gian timescale, define, include các file liên quan
2. Tạo khối testbench bằng cặp từ khóa module/endmodule
3. Khai báo các tham số, hằng số như các parameter
4. Khai báo giao tiếp với DUT: các tín hiệu sẽ kết nối với DUT
5. Gọi DUT
6. Khai báo các tín hiệu, biến sử dụng nội bộ trong testbench
7. Tạo Stimulus
8. Tạo Monitor
9. Giới hạn thời gian chạy mô phỏng của testbench
Trong đó:
 Các thành phần bắt buộc phải có đối với testbench tối thiểu là 2, 4, 5, 7. Testbench loại này
như đã nói ở mục 2 phía trên.
 Các thành phần bắt buộc phải có đối với testbench tự động kiểm tra là 2, 4, 5, 7 và 8.
Sau đây, tôi xin mô tả chi tiết từng phần của một testbench dùng để tự động kiểm tra DUT bộ đếm
Johnson theo những thành phần đã trình bày ở trên.

3.1 Định nghĩa chỉ dẫn mô phỏng

Khai báo đơn vị thời gian mô phỏng


`timescale 1ns/1ns

3.2 Tạo khối testbench


module tb_auto;
   //testbench body
endmodule

3.3 Khai báo các tham số, hằng số


parameter N = 4;
parameter END_TIME = 100;
parameter VALUE_NUM = 2*N;

Trong đó:
 N là số bit của DUT bộ đếm Johnson
 VALUE_NUM là số giá trị cần phải kiểm tra của DUT
 END_TIME là thời gian giới hạn của testbench khi chạy mô phỏng tính theo số chu kỳ xung
clock clk. Ở đây, sau 100 xung clock clk thì mô phỏng sẽ kết thúc.
3.4 Khai báo giao tiếp với DUT
reg clk;
reg rst_n;
wire [N-1:0] js_count;
Đây chính là các ngõ vào và ngõ ra của DUT, Trong đó:
 Ngõ vào được khai báo kiểu biến (variable) để gán giá trị lái DUT
 Ngõ ra khai báo kiểu net chỉ để giám sát
3.5 Gọi DUT
johnson_counter #(.N(N)) dut (.clk(clk), .rst_n(rst_n), .js_count(js_count));

Dòng code trên gọi module johnson_counter và thiết lập số bit cho bộ đếm là N = 4 bit.

3.6 Khai báo các tín hiệu, biến sử dụng nội bộ

reg [N-1:0] test_value[2*N-1:0];
integer i,j;

Trong đó:
 test_value là một mảng dùng để lưu các giá trị cần kiểm tra, giá trị này sẽ được sử dụng để
so sánh với ngõ ra DUT để biết DUT có hoạt động đúng hay không.
 i,j là hai biến lặp dùng để xây dựng Stimulus và Monitor
3.7  Tạo Stimulus

Tạo clock với chu kỳ 20 đơn vị thời gian. Đơn vị thời gian ở đây là 1ns, được quy định bởi 3.1. Nếu
không có định nghĩa trước về đơn vị thời gian thì testbench sẽ chạy theo đơn vị thời gian mặc định của
trình mô phỏng:

initial begin
       clk = 0;
forever #10 clk = !clk;
end

Tạo reset tích cực trong 20ns ở hai điểm là thời điểm bắt đầu mô phỏng 0ns và sau khi
chạy 200ns. Điểm thứ 2 là để kiểm tra DUT có hoạt động chính xác khi bị reset trong lúc hoạt động hay
không.

initial begin
        rst_n = 0;
#20
rst_n = 1;
#200
rst_n = 0;
#20
rst_n = 1;
end

3.8 Tạo Monitor


Tạo giá trị kiểm tra ngõ ra DUT. Phương pháp tạo ở đây là sử dụng một mảng lưu lại tất cả các giá trị
mong muốn được kiểm tra theo thứ tự đúng như cách mà DUT hoạt động. Lưu ý, kỹ sư mô phỏng sẽ xây
dựng dựa trên mô tả kỹ thuật (specification) của DUT chứ không phải theo thiết kế DUT của người thiết
kế nên cách thực hiện có thể giống và khác với với code của người thiết kế.

initial begin
  j = VALUE_NUM;
  test_value[0] = 0;
  for (i = 1; i < VALUE_NUM; i=i+1) begin
  test_value[i] = {test_value[i-1][N-2:0], ~test_value[i-1][N-1]};
  end
end

Chú ý, số lượng giá trị cần kiểm tra là VALUE_NUM  sẽ được lưu theo thứ tự từ ô thứ 0 đến ô
thứ VALUE_NUM-1. Tuy nhiên, biến  j  được gán giá trị ban đầu ngoài tầm này, j = VALUE_NUM là để sử
dụng xác định thời điểm testbench bắt đầu kiểm tra sau này. Cụ thể, testbench chỉ kiểm tra bắt đầu tính
từ khi DUT được reset. Thời điểm bắt đầu mô phỏng cho đến trước khi reset, giá trị DUT là không xác
định nên không kiểm tra.

Tạo địa chỉ để lấy giá trị test từ mảng test_value:

always @ (posedge clk) begin
  if (~rst_n) j <= 0;
else if (j == VALUE_NUM-1) j <= 0;
else j <= j + 1;
end

So sánh và kiểm tra giá trị reset của DUT:

always @ (*) begin
  if (!rst_n && (j != VALUE_NUM) && (js_count[N-1:0] != test_value[j])) begin
   $display ("--------------- SIMULATION FAIL ---------------");
  $display ("[%t]The reset value of DUT: %b\tThe TEST value: %b", $time, js_count[N-1:0], test_value[j]);
   $stop;
   end
end

Ở đây, giá trị reset được test từ khi:


1. Tín hiệu reset tích cực mức thấp, tương ứng rst_n=0
2. Sau khi DUT đã reset, tương ứng, j phải khác giá trị khởi tạo ban đầu
Nếu giá trị ngõ ra DUT js_count khác giá trị kiểm tra test_value, testbench sẽ hiển thị báo sai; hiện vị trí
thời gian sai; hiện giá trị DUT và giá trị kiểm tra tại vị trí phát hiện sai. Cuối cùng, dừng mô phỏng
bởi $stop.

So sánh và kiểm tra ngõ ra DUT trong lúc hoạt động và không có reset:

always @ (posedge clk) begin
  if (rst_n && (js_count[N-1:0] != test_value[j])) begin
    $display ("--------------- SIMULATION FAIL ---------------");
   $display ("[%t]The DUT value: %b\tThe TEST value: %b", $time, js_count[N-1:0], test_value[j]);
    $stop;
end
end

Khối kiểm tra trên chỉ lấy mẫu kiểm tra tại các vị trí cạnh lên xung clock và báo lỗi bất cứ khi nào giá trị
ngõ ra DUT khác với giá trị kiểm tra. Mô phỏng cũng dừng ngay khi phát hiện lỗi.

3.9 Giới hạn thời gian chạy mô phỏng của testbench

initial begin
  repeat (END_TIME) begin
@ (posedge clk);
  end
$display ("[%t]--------------- SIMULATION PASS ---------------", $time);
$stop;
end

Kết thúc thời gian chạy mô phỏng, sau số cạnh lên xung clock clk  được quy định
bởi END_TIME, testbench sẽ báo mô phỏng thành công với thông điệp "SIMULATION PASS". Điều kiện
PASS ở đây không cần vì chỉ cần xuất hiện 1 lỗi thì testbench đã dừng và báo FAIL ngay lập tức.

3.10 Nội dung file testbench hoàn chỉnh


`timescale 1ns/1ns
module tb_auto;
//parameter
parameter N = 4;
parameter END_TIME = 100;
parameter VALUE_NUM = 2*N;
//
//Interface of DUT
//
reg clk;
reg rst_n;
wire [N-1:0] js_count;
//DUT instance
johnson_counter #(.N(N)) dut (.clk(clk), .rst_n(rst_n), .js_count(js_count));
//internal variables
reg [N-1:0] test_value[2*N-1:0];
integer i,j;
//Generate clock
initial begin
  clk = 0;
forever #10 clk = !clk;
end
//Generate reset
initial begin
  rst_n = 0;
#20
rst_n = 1;
#200
rst_n = 0;
#20
rst_n = 1;
end
//END SIMULATION
//After END_TIME cycles, if any error occurs, this testcase is ended and pased
initial begin
  repeat (END_TIME) begin
  @ (posedge clk);
end
$display ("[%t]--------------- SIMULATION PASS ---------------", $time);
$stop;
end
//
//Automatic TEST
//
//Generate the test value table
initial begin
  j = VALUE_NUM;
  test_value[0] = 0;
  for (i = 1; i < VALUE_NUM; i=i+1) begin
  test_value[i] = {test_value[i-1][N-2:0], ~test_value[i-1][N-1]};
  end
end
//Create the address to search the test value from the above table
always @ (posedge clk) begin
  if (~rst_n) j <= 0;
else if (j == VALUE_NUM-1) j <= 0;
else j <= j + 1;
end
//
//Compare and check
//
//Check the reset value
always @ (*) begin
  if (!rst_n && (j != VALUE_NUM) && (js_count[N-1:0] != test_value[j])) begin
  $display ("--------------- SIMULATION FAIL ---------------");
$display ("[%t]The reset value of DUT: %b\tThe TEST value: %b", $time, js_count[N-1:0], test_value[j]);
$stop;
end
end
//Check the output of the Johnson counter
always @ (posedge clk) begin
  if (rst_n && (js_count[N-1:0] != test_value[j])) begin
  $display ("--------------- SIMULATION FAIL ---------------");
$display ("[%t]The DUT value: %b\tThe TEST value: %b", $time, js_count[N-1:0], test_value[j]);
$stop;
end
end
endmodule 

4. Kết quả chạy trên Questa SIM


Việc biên dịch và chạy mô phỏng với Questa SIM các bạn hãy tham khảo hướng dẫn ở link:

http://nguyenquanicd.blogspot.com/2017/08/questa-sim-huong-dan-cai-at-va-chay-mo.html

Ở đây, tôi chỉ phân tích kết quả mô phỏng. Nếu mô phỏng PASS, các bạn sẽ quan sát trên cửa sổ
Transcript hiện thông báo "SIMULATION PASS" và thời điểm kết thúc mô phỏng ngay trước thông điệp.
Đồng thời, một dòng báo vị trị code trong testbench làm ngắt mô phỏng, chính là dòng $stop trong
mục 3.9.
Hình 2. Thông điệp báo PASS trên cửa sổ transcript
Nếu giá trị reset bị sai, tác giả  đã sửa giá trị reset trong RTL code của DUT thành 1 (lưu ý phải biên dịch
lại DUT), kết quả mô phỏng như sau:

Hình 3: Sai giá trị reset


Sau khi reset, nếu giá trị DUT sai trong khi hoạt động thì kết quả sẽ như sau. Lưu ý, tôi sửa RTL của
DUT thành bộ đếm binary tuần tự như sau:

always @ (posedge clk) begin
 if (~rst_n)
   js_count[N-1:0] <= 0;
 else
   js_count[N-1:0] <= js_count + 1;
end 

Chuỗi giá trị sẽ là:


1. Bộ đếm Binary 4 bit:    0000 -> 0001 -> 0010
2. Bộ đếm Johnson 4 bit:  0000 -> 0001 -> 0011
Vì vậy, testbench sẽ báo lỗi tại vị trí ngõ ra DUT bằng 0010 và giá trị cần test là 0011.
Hình 4. Thông điệp báo lỗi bộ đếm

Việc tạo một testbench kiểm tra cho phép kiểm tra nhiều trường hợp trong thời gian mô phỏng dài mà
không cần tốn công dò dạng sóng. Việc xem waveform chỉ thực hiện khi bạn mới xây dựng testbench để
kiểm tra tính đúng đắn của testbench và khi phát hiện lỗi để gỡ lỗi (debug).
Trên đây chỉ là một hướng dẫn cơ bản giúp bạn nhanh chóng viết một testbench. Để một DUT được
kiểm tra toàn diện và đảm bảo tính đúng đắn cao thì testbench và môi trường mô phỏng có thể cần thêm
nhiều thành phần khác.

You might also like