Chủ Nhật, 28 tháng 7, 2019

[UVM] Bài 4 - Quá trình tạo transaction và lái DUT trong UVM

Bài viết này là một phần trong nội dung "mô tả ngắn gọn về UVM". Bài này sẽ giúp các bạn hiểu mối liên hệ giữa Transaction, Sequence, Sequencer và Driver trong môi trường UVM. Driver kết nối với UVM như thế nào? và Làm thế nào một transaction được chuyển đổi thành các mức logic để lái các tín hiệu của DUT. Đồng thời, các bạn cũng có thể hình dung cấu trúc cơ bản của một class mở rộng từ UVM class.

1) Cấu trúc cơ bản của một class mở rộng từ các UVM class
Một class được mở rộng từ các UVM class là một class sử dụng cấu trúc sau:
class <user_class_name> extends <uvm_class_name>#(<list_of_arguments>);
<body_code>

endclass
Trong đó:
  • user_class_name là tên class mà người viết cần tạo, sẽ được gọi là User class trong bài viết này.
  • uvm_class_name là tên class của thư viện UVM.
  • list_of_arguments là danh sách các đối số (nếu có) được gán cho UVM class để tạo thành User class.
  • body_code là System Verilog (SV) code được thêm vào User class.
Ví dụ về một User class mở rộng từ UVM class:
class myDriver extends uvm_driver #(myTransaction);
<body_code>

endclass
Trong ví dụ trên, một User class tên myDriver được tạo ra bằng cách mở rộng từ class uvm_driver của thư viện UVM sử dụng một đối số là myTransction. myDriver sẽ kế thừa toàn bộ đặc điểm về thành phần dữ liệu (các biến) và các method (task/function) của uvm_driver ứng với đối số myTransaction.
Cấu trúc cơ bản của một body_code của một User class mở rộng từ UVM class để dùng trong môi trường UVM như sau:
<data_member_declaration> 
<factory_registry>
<constructor>
<uvm_method>
<user_method>
Trong đó:
  • data_member_declaration là các khai báo biến, hằng số mới được sử dụng trong User class
  • factory_registry là đăng ký User class đến UVM Factory, tham khảo bài 3.
  • constructor là một function mặc định sẽ được thực thi để gán giá trị ban đầu cho các biến hoặc thực hiện một số tác vụ khi một đối tượng class được khởi tạo, tham khảo bài viết về function new.
  • uvm_method là mô tả SV code cho các method có sẵn của thư viện UVM. Các method này phải được hỗ trợ bởi UVM class, được sử dụng để mở rộng User class. Ví dụ, các method thực thi cho từng phase mô phỏng (build_phase, run_phase, ...) của một class có nguồn gốc (bắt nguồn) từ uvm_component như các class uvm_driver, uvm_sequencer, ..., tham khảo hình minh họa về UVM class trong bài 3.
  • user_method là các method riêng của User class không có trong UVM class.
Lưu ý rằng, các thành phần liệt kê trên đây như một tham khảo cơ bản để bạn có thể hình dung về các thành phần có thể có của User class. Tùy vào mục đích cụ thể User class có thể chứa đầy đủ hoặc một phần các thành phần trên. Tuy nhiên, factory_registry là một thành phần quan trọng cần phải có, tham khảo bài 3.
Ví dụ sau đây minh họa về các thành phần trong một User class.
class myDriver extends uvm_driver #(myTransaction);
  //1. data_member_declaration
  virtual myIf myIf_inst;
  myTransaction myPacket;
  //2. factory_registry
  `uvm_component_utils(myDriver)
  //3. constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction: new
  //4. uvm_method
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(virtual interface myIf)::get(.cntxt(this),
          .inst_name(""),
          .field_name("vmyIf"),
          .value(myIf_inst))) begin       `uvm_fatal("NON-APBIF", {"A virtual interface must be set for: ", get_full_name(), ".myIf_inst"})
     end     `uvm_info(get_full_name(), "Build phase completed.", UVM_LOW)
  endfunction  //
  virtual task run_phase (uvm_phase phase);
    fork      get_seq_and_drive ();
    join  endtask  //
  //5. User Methods
  //
  virtual task get_seq_and_drive();
    forever begin      #1
      seq_item_port.get_next_item(myPacket);
      convert_seq2signal(myPacket);
      seq_item_port.item_done();
    end  endtask: get_seq_and_drive
  //
  virtual task convert_seq2signal (myTransaction userTransaction);
    //For write access
    if (userTransaction.write_enable) begin      myIf_inst.read_en    = 1'b0;
      myIf_inst.write_en   = 1'b1;
      myIf_inst.wdata[31:0] = userTransaction.wdata[31:0];
    end    //For read access
    else begin      myIf_inst.read_en  = 1'b1;
      myIf_inst.write_en = 1'b0;
      userTransaction.rdata[31:0] = myIf_inst.rdata[31:0];
    end  endtask: convert_seq2signal
  //
endclass: myDriver
Các bạn chú ý các chú thích tô màu xanh để xác định các thành phần code của class myDriver. Ý nghĩa chi tiết của từng đoạn code trong class này sẽ được mô tả trong phần sau của bài viết này.

2) Mối liên kết giữa Transaction, Sequence, Sequencer, Driver và Agent trong môi trường UVM
Một transaction sẽ được tạo ra như thế nào? transaction được gửi đến Driver bằng cách nào? Driver sử dụng transaction như thế nào để lái các tín hiệu của DUT? Ví dụ cụ thể sau đây sẽ làm rõ các điều này.
Hình 1: Cấu trúc môi trường UVM của ví dụ minh họa
Hình trên là cấu trúc môi trường UVM dùng để minh họa và giải thích đường đi của transaction từ một Sequence đến DUT. Chú ý, đây là môi trường giản lược chỉ nhằm mục đích làm rõ đường đi của transaction đến DUT nên sẽ không có Monitor, Scoreboard hay các thành phần khác.
Tên ngoài ngoặc đơn là tên instance của module hoặc class hoặc interface. Tên trong ngoặc đơn là tên module hoặc class hoặc interface. Ví dụ, OmyEnv là tên instance của class myEnv trong cTest.

2.1) myDut
DUT là module myDut chỉ chứa một bộ đếm để tạo giá trị cho rdata[31:0]. Chú ý, đây không phải là một thiết kế có ứng dụng thực tế mà chỉ dùng để làm ví dụ minh họa. Mục đích của DUT là tạo một interface kết nối với Driver giúp minh họa hoạt động giữa Driver và DUT.
Các tín hiệu của myDut hoạt động như sau:
  • Nếu là một truy cập ghi (trong trường hợp này, DUT không làm gì)
    • write_en = 1
    • read_en = 0
    • wdata[31:0] sẽ là giá trị cần ghi đến DUT
  • Nếu là một truy cập đọc
    • write_en = 0
    • read_en = 1
    • rdata[31:0] sẽ là giá trị từ DUT gửi ra. Giá trị này tăng một đơn vị tại mỗi cạnh lên xung clock clk khi read_en=1.
SV code:
module myDut (
  input clk,
  input read_en,
  input write_en,
  input [31:0] wdata,
  output reg [31:0] rdata
  );
  initial begin    
    rdata[31:0] = 32'd0;
  end  
  always @ (posedge clk) begin  
    if (read_en) rdata[31:0] <= rdata[31:0] + 1;
  end 
endmodule
2.2) myIf
Một khai báo interface chứa tất cả các input và output của myDut. Driver sẽ kết nối với myDut thông quan interface này.
SV code:
interface myIf;
  logic clk;
  logic read_en;
  logic write_en;
  logic [31:0] wdata;
  logic [31:0] rdata;
endinterface: myIf
2.3) tlm_test
tlm_test là module TOP của môi trường. Module này làm các nhiệm vụ sau:
  • Khai báo một instance của interface để kết nối giữa Driver và DUT
myIf myIf_inst();
  • Tạo clock với chu kỳ 10ns. Clock này sẽ được gán đến DUT và interface
reg clk = 1'b0;
always #5ns clk = ~clk;
  • Khai báo một instance của myDut và kết nối với interface
myDut myDut_inst (
    .clk(clk),
    .read_en(myIf_inst.read_en),
    .write_en(myIf_inst.write_en),
    .wdata(myIf_inst.wdata[31:0]),
    .rdata(myIf_inst.rdata[31:0])
    );
  • Kết nối DUT instance đến các thành phần UVM thông qua instance của interface. Trong ví dụ này, DUT instance được kết nối đến Driver trong Agent với đường dẫn uvm_test_top.OmyEnv.OmyAgent
uvm_config_db#(virtual interface myIf)::set(null,"uvm_test_top.OmyEnv.OmyAgent*","vmyIf",myIf_inst);
  • Thực thi method run_test() để chạy mô phỏng với testcase mong muốn. Testcase này chính là class cTest.
Hình 2: Cấu trúc module tlm_test
SV code chi tiết có trong link tải cuối bài viết.

2.4) myTransaction
Đây là một class được mở rộng từ uvm_sequence_item. Nó sẽ chứa tất cả các thành phần dữ liệu và thông tin cần thiết để Driver có thể sử dụng và lái interface của DUT.
Cấu trúc của Transaction trong ví dụ này gồm các thông tin sau:
rand logic write_enable;
rand logic [31:0] wdata;
logic [31:0] rdata;
Trong đó:
  • write_enable dùng để xác định xem một transaction là write hay read. Nếu write_enable=1 thì transaction là loại write. write_enable=0 thì transaction là loại read.
  • wdata[31:0] là dữ liệu ghi sẽ gửi đến DUT nếu transaction là loại write.
  • rdata[31:0] là dữ liệu đọc từ DUT nếu transaction là loại read.
Ở đây, write_enable wdata[31:0] được khai báo rand để có thể tạo ra các giá trị ngẫu nhiên cho transaction.
myTransaction được khởi tạo và sử dụng trong mySequence.
SV code hoàn chỉnh có thể tải ở cuối bài viết.

2.5) mySequence
Đây là class được mở rộng từ uvm_sequence. Nó được dùng để tạo ra transaction và đưa transaction này đến Sequencer. Điều này được thực hiện bằng cách sau:
  • Tạo transaction bằng cách tạo một đối tượng OmyTransaction từ class myTransaction. Sv code liên quan:
myTransaction OmyTransaction;
OmyTransaction = myTransaction::type_id::create("OmyTransaction");
  • Khai báo biến p_sequencer, một biến định nghĩa sẵn của UVM, với loại dữ liệu là mySequencer
`uvm_declare_p_sequencer(mySequencer)
  • Đưa transaction đến Sequencer để gửi cho Driver
`uvm_do_on(OmyTransaction, p_sequencer)
Hình 3: Cấu trúc class mySequence
mySequence được khởi tạo và sử dụng trong cTest.
SV code hoàn chỉnh có thể tải ở cuối bài viết.

2.6) mySequencer
Đây là class được mở rộng từ uvm_sequencer sẽ nhận transaction từ một Sequence và gửi đến Driver thông qua một TLM export.
Trong ví dụ này, mySequencer không có bất cứ hoạt động nào thêm ngoài việc được định nghĩa và đăng ký đến Factory.
SV code hoàn chỉnh:
class mySequencer extends uvm_sequencer#(myTransaction);
  //Register to Factory
  `uvm_component_utils(mySequencer)
  //Constructor
function new (string name = "mySequencer", uvm_component parent = null);
super.new(name,parent);
endfunctionendclass: mySequencer
mySequencer được khởi tạo và sử dụng trong myAgent.
SV code hoàn chỉnh có thể tải ở cuối bài viết.

2.7) myDriver
Đây là class được mở rộng từ uvm_driver. Nó có các nhiệm vụ chính như sau:
  • Trong build_phase, kiểm tra xem Driver đã kết nối đến interface của DUT hay chưa?
if (!uvm_config_db#(virtual interface myIf)::get(.cntxt(this),
          .inst_name(""),
          .field_name("vmyIf"),
          .value(myIf_inst))) begin       `uvm_fatal("NON-myIF", {"A virtual interface must be set for: ", get_full_name(), ".myIf_inst"})
     end
  • Trong run_phase, method get_seq_and_drive được thực thi. Nó gồm một chuỗi các hoạt động sau:
    • Lấy transaction thông qua TLM port đước kết nối với TLM export của Sequencer trong Agent
seq_item_port.get_next_item(myPacket);
    • Dựa trên thông tin transaction để lái các tín hiệu trên interface, cái sẽ được kết nối với DUT, bằng cách thực thi method convert_seq2signal
convert_seq2signal(myPacket);
...
virtual task convert_seq2signal (myTransaction userTransaction);
    //For write access
    if (userTransaction.write_enable) begin  
      myIf_inst.read_en    = 1'b0;
      myIf_inst.write_en   = 1'b1;
      myIf_inst.wdata[31:0] = userTransaction.wdata[31:0];
    end    //For read access
    else begin     
      myIf_inst.read_en  = 1'b1;
      myIf_inst.write_en = 1'b0;
      userTransaction.rdata[31:0] = myIf_inst.rdata[31:0];
    end  endtask: convert_seq2signal
    • Báo quá trình thực thi đã hoàn thành sau khi lái các tín hiệu trên interface
seq_item_port.item_done();
myDriver được khởi tạo và sử dụng trong myAgent.
SV code hoàn chỉnh có thể tải ở cuối bài viết.

2.8) myAgent
Đây là class được mở rộng từ uvm_agent. Nó làm các nhiệm vụ chính như sau:
  • Khai báo Sequencer và Driver
myDriver    OmyDriver;
mySequencer OmySequencer;
  • Khởi tạo Sequencer và Driver trong build_phase
OmyDriver    = myDriver::type_id::create("OmyDriver",this);
OmySequencer = mySequencer::type_id::create("OmySequencer",this);
  • Kết nối Sequencer và Driver thông qua TLM port trong connect_phase
OmyDriver.seq_item_port.connect(OmySequencer.seq_item_export);
Hình 4: Cấu trúc của myAgent
myAgent được khởi tạo và sử dụng trong myEnv.
SV code hoàn chỉnh có thể tải ở cuối bài viết.

2.9) myEnv
Đây là class được mở rộng từ class uvm_env. Nó khởi tạo một đối tượng Agent.
myAgent OmyAgent;
...
OmyAgent = myAgent::type_id::create("OmyAgent",this);
Hình 5: Cấu trúc của myEnv
myEnv được khởi tạo và sử dụng trong cTest.
SV code hoàn chỉnh có thể tải ở cuối bài viết.

2.10) cTest
Đây là class được mở rộng từ uvm_test. Nó làm nhiệm vụ:
  • Khởi tạo Sequence và UVM environment ở build_phase.
OmyEnv = myEnv::type_id::create("OmyEnv",this);
OmySequence = mySequence::type_id::create("OmySequence",this);
  • Thực thi Sequence
OmySequence.start(OmyEnv.OmyAgent.OmySequencer);
Hình 6: Cấu trúc của cTest

3) Hướng dẫn chạy mô phỏng ví dụ trên QuestaSim
Bật QuestaSim, tạo project và thực thi lệnh sau trên cửa sổ transacript:
do run_sim.do
Hình 7: Thực thi script chạy mô phỏng
Giải thích về script run_sim.do:
  • Tổng hợp System Verilog code
vlog -work work \
  +define+UVM_CMDLINE_NO_DPI \
  +define+UVM_REGEX_NO_DPI \
  +define+UVM_NO_DPI \
  +define+INTERRUPT_COM \
  +incdir+C:/questasim64_10.2c/uvm-1.2/src \
  -sv \
  tlm_test.sv \
  -timescale 1ns/1ns \
  +cover
  • Chạy mô phỏng với testcase cTest
vsim -novopt work.tlm_test \
  +UVM_TESTNAME=cTest \
  +UVM_VERBOSITY=UVM_LOW
  • Add các tín hiệu cần xem waveform đến của sổ waveform (tham khảo file add_wave.do)
do add_wave.do
  • Chạy mô phỏng trong 100ns
run 100ns
Khi quá trình mô phỏng kết thúc, bạn chọn "No" khi cửa sổ sau xuất hiện:

Hình 8: Chọn "No" khi kết thúc quá trình chạy mô phỏng
Kết quả mô phỏng có thể kiểm chứng ở 2 vị trí:
  • Cửa sổ transcript, kiểm tra số lượng và giá trị transaction được tạo ra. Trong trường hợp này, testcase tạo ra 8 transaction.
# UVM_INFO tlm_test.sv(130) @ 10: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=1
#  wdata=97e4bcf6
#  rdata=xxxxxxxx
#
# UVM_INFO tlm_test.sv(130) @ 20: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=1
#  wdata=1be3fc7f
#  rdata=xxxxxxxx
#
# UVM_INFO tlm_test.sv(130) @ 30: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=0
#  wdata=8cebecf4
#  rdata=00000000
#
# UVM_INFO tlm_test.sv(130) @ 40: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=0
#  wdata=a80291bb
#  rdata=00000001
#
# UVM_INFO tlm_test.sv(130) @ 50: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=0
#  wdata=099b2e6e
#  rdata=00000002
#
# UVM_INFO tlm_test.sv(130) @ 60: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=0
#  wdata=0a55b418
#  rdata=00000003
#
# UVM_INFO tlm_test.sv(130) @ 70: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=0
#  wdata=f2aad6f8
#  rdata=00000004
#
# UVM_INFO tlm_test.sv(130) @ 80: uvm_test_top.OmyEnv.OmyAgent.OmySequencer@@OmySequence [--- myTransaction: ]
#  write_enable=1
#  wdata=34a0125b
#  rdata=xxxxxxxx
  • Cửa sổ waveform, kiểm tra giá trị các tín hiệu trên DUT và interface
Hình 9: Kiểm tra waveform và so sánh với transaction trên cửa sổ transcript
4) Kết luận
Bài viết này nhằm mục đích giới thiệu chi tiết đường đi của transaction từ lúc được khởi tạo đến DUT qua các thành phần khác nhau trong môi trường UVM.
Bài này sẽ giúp các bạn có thể tìm hiểu và thử nghiệm trước khi đến với môi trường thực tế của ví dụ UART-APB ở những bài sau.
Chú ý, khi sử dụng source code, các bạn hãy đọc thêm các comment (tiếng Anh) trong source code để tham khảo.

Dữ liệu có thể download:
Pass (nếu có): vlsi_technology

Tham khảo:
1/ Accellera; Universal Verification Methodology (UVM) 1.2 User’s Guide; October 8, 2015
2/ Accellera; Universal Verification Methodology (UVM) 1.2 Class Reference; June 2014
3/ IEEE; IEEE Std 1800™-2017; IEEE Standard for SystemVerilog -- Unified Hardware Design, Specification, and Verification Language; December 6, 2017

Lịch sử cập nhật:
1/ 2019.07.28 - Tạo lần đầu
2/ 2019.07.29 - Bổ sung các hình minh họa cho các class và mô tả về script run_sim.do
3/ 2019.08.17 - Xóa dấu "\" tại dòng "+UVM_VERBOSITY=UVM_LOW" trong lệnh vsim

Danh sách tác giả:
1. Phạm Thanh Trâm
2. Đoàn Đức Hoàng (email: hoangbk154@gmail.com)
3. Trương Công Hoàng Việt
4. Nguyễn Sinh Tơn
5. Nguyễn Hùng Quân

11 bình luận:

  1. Cám ơn nhóm tác giả đã có những bài viết chuyên sâu rất hay và hữu ích. Các bạn là những người thật sự đam mê và tâm huyết.
    Theo tôi để bài viết nhìn đẹp và dể đọc hơn chúng ta nên cải thiện những điểm sau:
    1) Code bị chạy hàng nhìn rất khó (Xem trên laptop và điện thoại điều bị).
    2) Nếu các bạn có thể sử dụng highlight syntax trong code thì very good luôn.
    3) Các ví dụ nên chạy trên EDA ground thì tiện hơn rất nhiều vì người đọc không cần cài tool (chạy mô phỏng trên web luôn).
    https://www.edaplayground.com/x/5ib

    Hy vọng các bạn sẽ tiếp tục có những bài viết chuyên môn hay nữa.

    Trả lờiXóa
    Trả lời
    1. Cảm ơn ý kiến đóng góp của bạn.
      Về highlight syntax thì trước đây bên mình cũng thử nhưng do còn kém về web nên phát sinh nhiều lỗi không mong muốn. Tương lai gần, nhóm sẽ cố gắng tìm một bộ highlight phù hợp.
      Về EDAground, trước đây mình cũng có thử nghĩ đến nhưng quyết định không dùng vì 2 lý do:
      1/ Nếu bạn đọc là một người mới, việc tìm hiểu cài đặt các tool là cần thiết
      2/ Nếu bạn đọc là người có kinh nghiệm thì hầu hết đều có tool của riêng mình.

      Xóa
    2. Nhận xét này đã bị tác giả xóa.

      Xóa
    3. Hi @CuongTran,
      Cảm ơn vì đã đọc và góp ý, trong bài viết tác giả chỉ chú thích một số đoạn code, code đầy đủ bạn có thể tải ở link dưới bài viết. Về các góp ý của bạn, xin phản hồi như sau:
      1a/ Tác giả có sử dụng sự kiện chờ cạnh lên clock rồi mới thực thi task convert_seq2signal. Tức nếu sự kiện cạnh lên clock chưa xuất hiện thì task này chưa thực thi nên nó sẽ đảm bảo sau cạnh clock (bạn có thể xem bản code full)
      1b/ Như đã trình bày trong bài viết, code của DUT chỉ là để gán giá trị cho prdata với ý nghĩa minh họa, chứ không theo spec dữ liệu đọc delay 1 chu kỳ
      2/ TB không kiểm tra dữ liệu đọc là đúng hay sai, mục tiêu bài viết này, ngay từ tiêu đề và lời giới thiệu là "hiểu quá trình tạo transaction và lái DUT"

      Mong bạn tiếp tục trao đổi, sẽ rất hữu ích cho các bạn khác cùng tham khảo.

      Xóa
  2. Hi,
    Just small comments:
    - Better use clocking block in the interface instead of adding specific delay in your sequence (it's fragile!).
    - If possible, follow some good coding guidelines.

    ~D

    Trả lờiXóa
    Trả lời
    1. Hi,
      Good opinion,
      Thank very much for your opinion,
      We will continue improving the next time.

      Xóa
  3. Hi,
    BTW, I think you guys have Questasim.
    Why not investigating UVM Framework and trying to do one real example (for instance you can get your own designs in this blog) ?
    There are 30+ small/medium companies have used UVMF.
    In Mentor U2U conference this year, a guy from Microsoft HW team also presented a paper about how to adopt UVMF in their environment.

    My 0.02,
    ~D

    Trả lờiXóa
    Trả lời
    1. Hi,
      Why? A weight question :)
      Actually, my group would like to share from basic to advance. Many opinions sent to us to ask different edges. Something maybe share early, something need to investigate more.
      But opinions as your comment are recorded, we consider it at posible time.
      If you have an experience person, can you cooperate with us to share it?
      Now, we are very need to cooperate with the experience people.

      Xóa
    2. Hi,
      Well, your mileage may vary.
      IMHO we should not invent the wheel.
      I'm not saying that your approach is not good since again YMMV and you have different targeted users.
      Never mind, that's just my 0.02.

      Keep moving ;-),
      ~D

      Xóa
    3. Hi,
      No-problem, I always listen from diferent opinions to improve day by day. Besides, I really would like to cooperate with the experience persons to share more and more useful knowledge. Sometimes, you can think about this when you are ready.

      Xóa
  4. Bài viết rất dễ hiểu và đầy đủ, thanks a Quân

    Trả lờiXóa