• Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Vi mạch và Ứng dụng

  • Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Vi mạch và Ứng dụng

  • Integrated Circuit Design - Chia sẻ kiến thức về vi mạch

    Vi mạch và Ứng dụng

Hiển thị các bài đăng có nhãn verification. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn verification. Hiển thị tất cả bài đăng

Thứ Sáu, 11 tháng 10, 2019

[Sim][Race condition] Bài 1 - Tầm quan trọng của việc hiểu mô hình mô phỏng sự kiện

Chuỗi bài viết này trình bày về vấn đề chạy đua (race condition) và cách viết code tránh chạy đua trong mô hình mô phỏng sự kiện (event simulation) của System Verilog (SV) và Verilog HDL. Bài thứ nhất sẽ so sánh mô hình mô phỏng sự kiện của hai ngôn ngữ SV và Verilog. Từ đó, phân tích các vấn đề của mô hình mô phỏng này để làm rõ tại sao việc hiểu rõ mô hình mô phỏng sự kiện lại có ý nghĩa quan trọng đối với người sử dụng SV và Verilog.

1) Lời mở đầu
Trước đây, tác giả có một số bài viết trình bày về mô hình mô phỏng theo sự kiện. Bạn đọc có thể tham khảo ở các link sau:

Những bài viết này được viết ra trong giai đoạn đầu, khi tác giả bắt đầu tìm hiểu về cách xây dựng môi trường mô phỏng. Nội dung của các bài viết này ở mức cơ bản (mức dịch tài liệu). Bên cạnh đó, tác giả đưa ra các ví dụ và tự giải thích với mong muốn được bạn đọc có kinh nghiệm nhận xét, góp ý và chỉ ra những sai sót. Tuy nhiên, nội dung có vẻ như "lý thuyết" này rất ít được quan tâm trong khi việc hiểu nó là "RẤT QUAN TRỌNG". Quan trọng bởi vì:
  • Nó giúp giải thích hành vi thực thi của những dòng code
  • Nó giúp hiểu và sử dụng đúng các thành phần của ngôn ngữ
  • Nó giúp tránh những cách viết code sai. Ở đây, "sai" không phải là "sai cú pháp" mà là sai kết quả mô phỏng hoặc sai kết quả tổng hợp
  • Nó giúp hiểu một phần hoạt động của trình mô phỏng (simulator)
  • Với người kiểm tra thiết kế (verifier), nó giúp "điều khiển" và xác định đúng thời điểm thực thi các dòng code.
Dạo quanh một vài diễn đàn về công nghệ trên thế giới, các kỹ sư tốn rất nhiều công sức để nghiên cứu và tìm hiểu về vấn đề này. Một trong các trung tâm đào tạo hàng đầu thế giới Sunburst Design (http://www.sunburst-design.com/) có rất nhiều bài báo kỹ thuật phân tích về vấn đề này. Trong đó, nhiều bài là "Best paper", bài viết được bình chọn là tốt nhất, hay nhất trong các hội thảo khoa học.
  • Clifford E. Cummings, Clifford E. Cummings; Correct Methods For Adding Delays To Verilog Behavioral ModelsHDLCON, 1999
Bài viết mô tả về cách mô hình độ trễ trong Verilog.
  • Clifford E. Cummings, Sunburst Design; IncNonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!SNUG-2000 (This is the best paper)
Bài viết giải thích chi tiết hành vi của phép gán blocking và non-blocking trên mô hình mô phỏng sự kiện. Từ đó, các cách viết code tốt và xấu được đưa ra phân tích, so sánh.
  • Clifford E. Cummings, Sunburst Design;Verilog Nonblocking Assignments With Delays, Myths & MysteriesSNUG-2002 (This is the best paper)
Bài viết trình bày về cách sử dụng khai báo độ trễ và những ảnh hưởng có nó đến hiệu suất, thời gian mô phỏng với số liệu đo đạc thực tế.
  • Clifford E. Cummings, Sunburst Design; Arturo Salz, Synopsys; SystemVerilog Event Regions, Race Avoidance & GuidelinesSNUG Boston 2006
Bài viết trình bày về mô hình mô phỏng sự kiện của Verilog và SV trên ví dụ cụ thể cùng với cách tránh điều kiện chạy đua.
Các bài viết trên ra đời từ rất lâu, bài mới nhất cách đây 13 năm, bài lâu nhất cách đây 20 năm. Tuy vậy, giá trị của chúng là rất lớn và sẽ còn hữu ích với cộng đồng cho đến khi SV và Verilog không còn được dùng nữa. Có lẽ vậy!
Việc hiểu thấu đáo và tường tận các nội dung trình bày trong các bài viết này là không dễ, ngay cả đối với người có kinh nghiệm vì đôi khi chúng ta chỉ dùng một cách "thuần thục như một cái máy" nhưng lại không rõ vì sao lại như vậy. Tác giả thử kiểm nghiệm bằng cách hỏi một vài người bạn có kinh nghiệm lâu năm về sử dụng SV nhưng hầu hết không thể trả lời rõ ràng hoặc chưa nghiên cứu sâu hoặc không biết về mô hình mô hình mô phỏng sự kiện trong khi ảnh hưởng của nó đến việc viết code là không hề nhỏ.
Vì vậy, trong quá trình thành lập các nhóm tự học, đối với các bạn theo hướng kiểm tra thiết kế (verification), tác giả đã trao đổi và đề xuất cùng tìm hiểu về vấn đề này trước khi bắt tay sử dụng SV và xây dựng môi trường mô phỏng.
Trong chuỗi bài viết này, tác giả một phần sẽ chọn lọc những nội dung phù hợp từ các bài viết của Sunburst Design kèm với luận giải và ví dụ cá nhân để cố gắng làm chúng dễ hiểu hơn. Hy vọng là như vậy!
Trước khi trình bày về nội dung kỹ thuật, xin lưu ý với bạn đọc rằng, mọi chia sẻ được viết ra đều dựa trên việc tự đọc và kiểm chứng của tác giả. Nếu bạn đọc có nghi vấn hoặc bạn là người có nhiều kinh nghiệm hãy góp ý với tác giả để hiệu chỉnh nội dung và mang đến kiên thức đúng nhất cho cộng đồng.
2) Hiểu về mô hình mô phỏng sự kiện
Mô phỏng sự kiện (event simulation) là mô hình mô phỏng dự được thực thi dựa trên các sự kiện rời rạc. Quá trình thực thi mô phỏng sẽ lặp lại liên tục việc sinh ra (initialize), sắp xếp (schedule), thực thi (execute) và xóa bỏ (remove) các sự kiện.
Các trình mô phỏng (simulator) như ModelSim,QuestaSim, VCS, Incisive, Xcelium,... sẽ phải “lập thời khóa biểu” (event scheduler) để thực thi các sự kiện một cách tuần tự, nghĩa là có thứ tự trước sau, chứ không phải song song như mô hình phần cứng thực tế. Để đảm bảo sự tương thích giữa các trình mô phỏng được xây dựng bởi các nhà cung cấp phần mềm khác nhau, SV và Verilog định nghĩa rõ về mô hình mô phỏng sự kiện. Các trình mô phỏng có thể thiết kế với các thuật toán khác nhau nhưng không được phép vi phạm mô hình này.
Trong mô hình mô phỏng sự kiện, thời gian mô phỏng (simulation time) được chia thành các khe thời gian (time slot). Mỗi khe thời gian được chia nhỏ hơn thành các vùng/miền sự kiện (event region). Trong mỗi miền sự kiện, các thành phần code SV như các phát biểu, các task xây dựng sẵn ($display, $strobe, …),...  sẽ được sắp xếp và thực thi.

Hình 1: Thời gian mô phỏng (simulation time) trong SV
Thứ tự thực thi các thành phần code SV trong các miền sự kiện phải tuân theo chuẩn SV. Nếu chuẩn không quy định thì trình mô phỏng có thể sắp xếp theo bất kỳ thự tự nào.
Trong mỗi miền sự kiện, trình mô phỏng sẽ xác định thứ tự các sự kiện và sắp xếp chúng vào “hàng đợi sự kiện” (event queue). Hàng đợi sự kiện là một khái niệm được sử dụng để dễ hình dung hoạt động của mô hình mô phỏng dựa trên các sự kiện rời rạc. Hàng đợi sự kiện hoạt động như một FIFO, sự kiện nào được xếp vào trước sẽ được thực thi trước. Sau khi thực thi xong, sự kiện đó sẽ bị xóa khỏi hàng đợi.
Hình 2: Hàng đợi sự kiện (event queue)
Quá trình mô phỏng là sự lặp đi lặp lại của hai hoạt động sau:
  • Một biến (net hoặc variable) thay đổi trạng thái sẽ khởi động các process, cái “nhạy” với sự thay đổi này, thực thi.
  • Các process thực thi sẽ làm thay đổi giá trị các biến.
Trong đó:

  • Việc một biến thay đổi trạng thái gọi là sự kiện cập nhật (update event)
  • Việc ước lượng thực thi một process gọi là sự kiện ước lượng (evaluation event)
Việc thay đổi trạng thái của biến, sự kiện cập nhật, là khi giá trị của biến thay đổi đến một giá trị khác với giá trị hiện tại, ví dụ chuyển từ 0 sang 1, từ x sang 0, từ 1 sang z, …
Process là một trong các loại phát biểu như initial, always, final, assign, …
Hình 3: Hoạt động mô phỏng là sự lặp đi lặp lại của việc thực thi các sự kiện cập nhật và sự kiện ước lượng
Sự kiện ước lượng được hiểu là việc xác định các giá trị, trạng thái ngõ vào (input) của một process trước khi nó được thực thi (execute). Ví dụ, chúng ta có một process như sau:
always @ (posedge clk) y <= a;
Cạnh lên clock xuất hiện, nghĩa là một sự kiện cập nhật làm thay đổi giá trị của biến clk từ 0 sang 1 vừa được thực thi, thì một sự kiện ước lượng sinh ra trên process để xác định giá trị của a (nằm bền phải phép gán). Khi sự kiện ước lượng được thực thi, a đã được xác định một giá trị thì một sự kiện cập nhật giá trị từ a đến y được sinh ra nếu giá trị a khác với giá trị y hiện tại, ví dụ như a bằng 1 nhưng y đang bằng 0. Khi sự kiện cập nhật y được thực thi, y đã được gán giá trị mới từ a, thì một hoặc nhiều sự kiện ước lượng sẽ được sinh ra trên các process nhạy với sự thay đổi của y.
Việc hiểu mô hình mô phỏng này chỉ dành cho những kỹ sư kiểm tra (verifier), các kỹ sư thiết kế (designer) không cần biết điều này? SAI, việc không hiểu mô hình mô phỏng sự kiện sẽ gây ra các vấn đề nghiêm trọng cho cả kỹ sư thiết kế khi mô tả RTL code và kỹ sư kiểm tra khi mô tả testbench. 
  • Kỹ sư thiết kế có thể tạo ra các RTL code không thể tổng hợp được kết quả đúng như mong muốn.
  • Kỹ sư kiểm tra có thể tạo ra một môi trường mô phỏng với kết quả kiểm tra không ổn định và cho kết quả khác nhau khi chạy trên các trình mô phỏng khác nhau.
3) So sánh mô hình mô phỏng sự kiện của Verilog và System Verilog
Trước hết, SV là một ngôn ngữ hợp nhất dành cho cả thiết kế và kiểm tra. SV được xây dựng trên System Verilog3.1a của tổ chức Accellera, SV3.1a được mở rộng từ Verilog HDL. Như vậy, Verilog HDL là một phần của SV và được nhúng trong SV. SV đảm bảo sự tương thích giữa code viết theo Verilog HDL và code viết theo SV. 
Đối với mô hình mô phỏng sự kiện cũng không ngoại lệ, tuy số lượng miền sự kiện trong mô hình mô phỏng sự kiện SV nhiều hơn so với số miền sự kiện trong mô hình mô phỏng sự kiện Verilog nhưng SV chỉ mở rộng thêm các miền mới và vẫn giữ nguyên thứ tự, ý nghĩa và chức năng các miền sự kiện cũ từ Verilog.
4 miền sự kiện của Verilog được mang sang SV gồm:
  • Active 
  • Inactive 
  • NBA 
  • Postponed 
Hình 4: Các miền sự kiện của mô hình mô phỏng sự kiện trong Verilog
13 miền sự kiện mới được thêm vào trong SV gồm: 
  • Preponed 
  • Pre-Active 
  • Pre-NBA 
  • Post-NBA 
  • Pre-Observed 
  • Observed 
  • Post-Observed 
  • Reactive 
  • Re-Inactive 
  • Pre-Re-NBA 
  • Re-NBA 
  • Post-Re-NBA 
  • Pre-Postponed
Hình 5: Các miền sự kiện trong SV [5]
Các vùng mới được sinh ra để hỗ trợ việc thực thi các thành phần mới được thêm vào trong SV so với Verilog. Bên cạnh đó, các miền sự kiện mới còn hỗ trợ thực thi các thành phần code giúp tránh điều kiện chạy đua khi sử dụng SV.
Ví dụ, code trong khai báo module/endmodule được sắp xếp thực thi trong tập vùng active (active region set) còn code trong khai báo program/endprogram được sắp xếp thực thi trong tập vùng reactive (reactive region set) với mong muốn tách biệt quá trình thực thi của design (mô tả trong module/endmodule) và quá trình thực thi của testbench (được mô tả trong program/endprogram). Từ đó tránh chạy đua giữa việc thực thi design và testbench. Chú ý, đây chỉ là một ví dụ minh họa sự khác biệt giữa mô hình mô phỏng sự kiện SV và Verilog còn khả năng ứng dụng thực tế cần được nghiên cứu và xem xét kỹ hơn trong từng trường hợp cụ thể. Tác giả nói điều này là vì program/endprogram đang ít được dùng trong các môi trường mô phỏng hiện tại. Việc tránh race condition được áp dụng bằng nhiều biện pháp khác.
4) Vấn đề của mô hình mô phỏng sự kiện 
Vần đề chính của mô hình mô phỏng sự kiện chính là do đặc điểm các process có thể được ước lượng theo thứ tự bất kỳ. Đây là nguồn sinh ra hành vi nondeterminism, tạm dịch là hành vi không thể xác định. Hành vi không thể xác định là hành vi làm cho kết quả mô phỏng (simulation) hoặc tổng hợp (synthesis) không cố định. 
  • Đối với phần mềm tổng hợp, một RTL code rơi vào trường hợp nondeterminism sẽ làm cho chức năng của mạch logic được tổng hợp ra không thể xác định.
  • Đối với phần mềm mô phỏng, khi rơi vào trường hợp nondeterminism, cùng một code sẽ cho kết quả mô phỏng khác nhau sau mỗi lần chạy hoặc khác nhau khi thực thi trên các trình mô phỏng khác nhau.
Chú ý, trường hợp này các tool không hoàn toàn tương thích với chuẩn và hoạt động chính xác nhưng kết quả sinh ra sai là do người thiết kế hoặc người kiểm tra viết code không đúng. Xét ví dụ sau:
always @ (posedge clock) begin
  a = 0;
  a = 1;
end
always @ (posedge clock)
  b = a; 
Ví dụ trên lấy từ tài liệu tham khảo [6], trong đoạn code trên, tại cạnh lên clock, giá trị của b có thể là 0 hoặc 1. Nếu tổng hợp (synthesis), phần mềm tổng hợp có thể tạo logic gán giá trị 0 hoặc 1 cho b. Rõ ràng, chức năng mạch logic sinh ra là không thể xác định, hoàn toàn tùy phần mềm quyết định.
Nếu mô phỏng, kết quả mô phỏng của giá trị b là không cố định, có thể là 1 hoặc 0 hoặc x, hoàn toàn tùy thuộc vào trình mô phỏng. 
Chúng ta cùng phần tích nguồn gốc nguyên nhân của vấn đề này dựa trên mô hình mô phỏng sự kiện. Tại cạnh lên clock, hai process always có thể được thực thi theo bất kỳ thứ tự nào. Ở đây, trong cả hai process always, các phép gán blocking được sử dụng nên tất cả chúng sẽ được xếp vào vùng Active trong cùng một time slot. Theo quy định của chuẩn SV:
  • a=0 chắc chắn được thực hiện trước a=1 vì cùng trong một process sử dụng begin-end
  • b=a có thể thực hiện trước a=0, sau a=1 hoặc giữa a=0a=1 
Các tổ hợp khả năng có thể được thực thi bởi trình mô phỏng, tổng hợp như sau: 
  • a=0 -> a=1 -> b=a
    • Kết quả mô phỏng b=1
    • Kết quả tổng hợp b=1
  • a=0 -> b=a -> a=1
    • Kết quả mô phỏng b=0
    • Kết quả tổng hợp b=0
  • b=a -> a=0 -> a=1
    • Kết quả mô phỏng
      • b=x nếu a là kiểu dữ liệu 4 trạng thái
      • b=0 nếu a là kiểu dữ liệu 2 trạng thái
    • Kết quả tổng hợp b=0 hoặc b=1
Ở đây, phần mềm xử lý theo thứ tự nào cũng đều tương thích với chuẩn nhưng kết quả nhận được thì khác nhau. Hiện tượng trong ví dụ này còn được gọi bằng một tên khác là điều kiện chạy đua (race condition). 
Một điều kiện chạy đua xuất hiện khi 2 hoặc nhiều phát biểu (statement) được xắp xếp thực thi trong cùng một khe thời gian nhưng cho kết quả khác nhau khi thứ tự thực thi của chúng khác nhau [4]. Trong ví dụ trên đây, các phát biểu (statement) là các phép gán blocking trong các process always. Như vậy, việc tránh điều kiện chạy đua là cực kỳ quan trọng đối với cả kỹ sư thiết kế và kỹ sư kiểm tra khi sử dụng ngôn ngữ lập trình được xử lý theo mô hình mô phỏng sự kiện như SV và Verilog.
Tóm lại, bạn đọc có thể thấy việc hiểu mô hình mô phỏng sự kiện sẽ giúp phát hiện và giải thích hiện tượng nondeterminism và điều kiện chạy đua để áp dụng các phương pháp phòng tránh hiệu quả.

Tài liệu tham khảo:
1) Clifford E. Cummings, Clifford E. Cummings; Correct Methods For Adding Delays To Verilog Behavioral ModelsHDLCON, 1999
2) Clifford E. Cummings, Sunburst Design; IncNonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!SNUG-2000 (This is the best paper)
3) Clifford E. Cummings, Sunburst Design;Verilog Nonblocking Assignments With Delays, Myths & MysteriesSNUG-2002 (This is the best paper)
4) Clifford E. Cummings, Sunburst Design; Arturo Salz, Synopsys; SystemVerilog Event Regions, Race Avoidance & GuidelinesSNUG Boston 2006
5) IEEE Standards Association; IEEE Standard for SystemVerilog, Unified Hardware Design, Specification, and Verification Language; 2017
6) IEEE Standards AssociationIEEE Standard for Verilog® Register Transfer Level Synthesis; 2002

Lịch sử cập nhật:
1) 2019.10.11 - Tạo lần đầu

Thứ Bảy, 14 tháng 9, 2019

[UVM] Bài 6 - Mô tả hoạt động của Monitor và Scoreboard

Bài viết này mô tả giao tiếp và chức năng của Monitor và Scoreboard trong môi trường UVM đã được build ở bài 5. Bài viết tập trung vào việc giải thích làm thế nào một transaction được truyền từ Monitor đến Scoreboard và làm thế nào Scoreboard kiểm tra dữ liệu truyền nhận giữa hai UART trong môi trường. Bên cạnh đó, chức năng chi tiết của Monitor cũng sẽ được trình bày.
Tham khảo các bài viết trước ở đây.

1) Kết nối của Monitor và Scoreboard
1.1) Các giao tiếp và ports
Trước khi đi vào chức năng chi tiết của Monitor và Scoreboard. Nội dung phần này mô tả chi tiết kết nối của Monitor và Scoreboard trong môi trường.
Hình 1: Sơ đồ khối của môi trường UVM
Trong môi trường này, uart_0, còn gọi là UART-TX, kết nối với Agent coApbMasterAgentTx còn uart_1, còn gọi là UART-RX, kết nối với Agent coApbMasterAgentRx. Chú ý, việc đặt tên -TX và -RX không có nghĩa là uart_0 chỉ truyền còn uart_1 chỉ nhận. Hai UART này có chức năng truyền và nhận như nhau.
Quan sát hình minh họa trên đây, bạn sẽ thấy Monitor có các kết nối sau:
  • APB interface có tên instance là vifApbMaster để giám sát tất cả các transaction đọc/ghi
  • Interrupt interface có tên instance là vifInterrupt để giám sát tất cả các tín hiệu interrupt
  • Analysis port có tên instance là ap_toScoreboard để gửi các transaction có kiểu dữ liệu là cApbTransaction trên APB interface đến Scoreboard
Bên cạnh đó, giữa Monitor và Scoreboard còn một analysis port khác tên preset_toScoreboard được dùng để gửi thông tin reset cho Scoreboard. Kiểu dữ liệu gửi trên port này là logic. Cái này không thể hiện trên hình minh họa nên các bạn chú ý khi đọc code.
Trong MonitorcApbMasterMonitor.sv, các thành phần kết nối trên được khai báo như sau:
//Declare analysis ports
uvm_analysis_port #(logic) preset_toScoreboard;
uvm_analysis_port #(cApbTransaction) ap_toScoreboard;
//Declare the monitored interfaces
virtual interface ifApbMaster vifApbMaster;
virtual interface ifInterrupt vifInterrupt;
Hai analysis port sẽ được tạo trong build_phase
ap_toScoreboard = new("ap_toScoreboard", this); preset_toScoreboard = new("preset_toScoreboard", this);
Scoreboard có các kết nối như sau:
  • Port tên aimp_frmMonitorTX để nối với analysis port ap_toScoreboard của Monitor trên Agent coApbMasterAgentTx.
  • Port tên aimp_frmMonitorRX để nối với analysis port ap_toScoreboard của Monitor trên Agent coApbMasterAgentRx.
  • Port tên aimp_resetfrmTX để nối với analysis port preset_toScoreboard của Monitor trên Agent coApbMasterAgentTx.
Scoreboard không có port kết nối với analysis port preset_toScoreboard của Monitor trên Agent coApbMasterAgentRx vì trong môi trường này, chân preset_n của hai UART được sử dụng chung một nguồn. Khi reset xảy ra, toàn bộ DUT sẽ được reset.
Trong ScoreboardcScoreboard.sv, các thành phần kết nối được khai báo như sau:
uvm_analysis_imp_frmMonitorTX #(cApbTransaction, cScoreboard) aimp_frmMonitorTX;
uvm_analysis_imp_frmMonitorRX #(cApbTransaction, cScoreboard) aimp_frmMonitorRX;
uvm_analysis_imp_resetfrmTX #(logic, cScoreboard) aimp_resetfrmTX;
Các port này sẽ được tạo trong build_phase của Scoreboard như sau:
imp_frmMonitorTX = new("aimp_frmMonitorTX", this);
aimp_frmMonitorRX = new("aimp_frmMonitorRX", this);
aimp_resetfrmTX = new("aimp_resetfrmTX", this);
Chú ý, phần hậu tố khi khai báo port (tô màu vàng) phải được đăng ký để sử dụng như sau:
`uvm_analysis_imp_decl(_frmMonitorTX)
`uvm_analysis_imp_decl(_frmMonitorRX)
`uvm_analysis_imp_decl(_resetfrmTX)
1.2) Kết nối
Monitor kết nối với đến APB interface và interrupt interface thông qua khai báo uvm_config_db testTop.sv.
//Connect APB interface
uvm_config_db#(virtual interface ifApbMaster)::set(null,"uvm_test_top.coEnv.coApbMasterAgentTx*","vifApbMaster",vifApbMaster_Tx);
uvm_config_db#(virtual interface ifApbMaster)::set(null,"uvm_test_top.coEnv.coApbMasterAgentRx*","vifApbMaster",vifApbMaster_Rx);
//Connect Interrupt interface
uvm_config_db#(virtual interface ifInterrupt)::set(null,"uvm_test_top.coEnv.coApbMasterAgentTx*","vifInterrupt",vifInterrupt_Tx);
uvm_config_db#(virtual interface ifInterrupt)::set(null,"uvm_test_top.coEnv.coApbMasterAgentRx*","vifInterrupt",vifInterrupt_Rx);
Các khai báo uvm_config_db giúp kết nối các instance của interface khai báo ở testTop.sv kết nối đến các thành phần UVM trong các Agent coApbMasterAgentTx và coApbMasterAgentRx.
Trong build_phase của Monitor, các kết nối sẽ được kiểm tra lại để đảm bảo các kết nối này đã tồn tại.
//Check the APB connection
if(!uvm_config_db#(virtual interface ifApbMaster)::get(this,"","vifApbMaster",vifApbMaster)) begin
  `uvm_error("cApbMasterDriver","Can NOT get vifApbMaster!!!")
end
//Check the interrupt connection
if(!uvm_config_db#(virtual interface ifInterrupt)::get(this,"","vifInterrupt",vifInterrupt)) begin
  `uvm_error("cVSequencer","Can NOT get vifInterrupt!!!")
end
Một thông điệp lỗi sẽ báo thông qua macro `uvm_error nếu kết nối với interface mong muốn không được tìm thấy.
2) Cấu trúc và chức năng của Monitor
Monitor (cApbMasterMonitor.sv) được mở rộng từ class uvm_monitor và xây dựng một số chức năng riêng trong hai phase:

  1. build_phase
    1. Kiếm tra kết nối của Monitor trong môi trường. Cụ thể, hai interface vifApbMaster và vifInterrupt được kiểm tra.
    2. Tạo đối tượng của các TLM analysis port. Cụ thể, hai port được tạo là ap_toScoreboard và preset_toScoreboard.
    3. Tạo đối tượng coApbTransaction để lưu lại các transaction trên APB interface. Chú ý, đối tượng này phải cùng kiểu dữ liệu với kiểu dữ liệu sẽ được gửi qua analysis port ap_toScoreboard , kiểu dữ liệu này là cApbTransaction.
  2. run_phase sẽ thực hiện các task sau song song 
    1. collect_data() giám sát APB interface, phát hiện read/write transfer trên APB interface và lưu lại trong đối tượng coApbTransaction. Sau đó, method write() được sử dụng để gửi gói coApbTransaction đến TLM analysis port ap_toScoreboard. Phía Scoreboard sẽ nhận và xử lý
    2. detect_reset() giám sát tín hiệu reset, preset_n trong testTop.sv, của môi trường, lưu lại trong biến preset_n và gửi giá trị này qua analysis port preset_toScoreboard bằng method write().
    3. monitor_ifEn() giám sát các APB transaction ghi vào thanh ghi interrupt enable của DUT, DUT gồm 2 UART là UART-TX và UART-RX. Nếu phát hiện transaction ghi vào thanh ghi interrtupt enable, địa chỉ offset 16'h0010, giá trị ghi vào các bit interrupt enable sẽ được lau lại trong biến ifEn[4:0]. Biến này sẽ được dùng để điều khiển task detect_intf() với mục đích xác định xem một tín hiệu interrupt tích cực, chuyển từ mức 0 sang mức 1, có đúng hay không.
    4. detect_intf() giám sát tất cả các tín hiệu interrupt thông qua interface vifInterrupt. Nếu một interrupt tích cực nhưng không được enable, `uvm_error sẽ thông báo. Nếu một interrupt tích cực và được enable, một `uvm_warning sẽ thông báo cho người test biết để kiểm tra lại xem có đúng như mong muốn hay không. Một biến ifSta được sử dụng để đảm bảo các thông điệp `uvm_error và `uvm_warning chỉ in ra một lần khi interrupt bắt đầu tích cực. Nếu không có biến này, các thông điệp sẽ được in liên tục trong suốt quá trình tín hiệu interrupt tích cực sau mỗi cạnh lên xung clock.
Hình 2: Cấu trúc và chức năng chính của Monitor (cApbMasterMonitor.sv)
3) Cấu trúc và chức năng của Scoreboard
Scoreboard (cScoreboard.sv) được mở rộng từ class uvm_scoreboard và thêm các chức năng riêng như sau:

  1. build_phase tạo ra các đối tượng của analysis implementation port gồm:
    1. aimp_frmMonitorTX kết nối với analysis port ap_toScoreboard của Monitor trong agent coApbMasterAgentTx, xem cEnv.sv.
    2. aimp_frmMonitorRX kết nối với analysis port ap_toScoreboard của Monitor trong agent coApbMasterAgentRx, xem cEnv.sv.
    3. aimp_resetfrmTX kết nối với analysis port preset_toScoreboard của Monitor trong agent coApbMasterAgentTx, xem cEnv.sv. Như đã trình bày ở trên, do reset của hệ thống là chung nên chỉ cần kết nối một port để giám sát trạng thái reset, không cần kết nối port đến agent coApbMasterAgentRx.
  2. run_phase thực thi các task write<suffix> với suffix được khai báo bởi `uvm_analysis_imp_decl. Các task này gọi là các "analysis implementation" dùng để xử lý các transaction nhận trên analysis implementation port. Các bạn xem lại mục TLM trong bài viết số 3.
    1. write_resetfrmTX() giám sát trạng thái reset được Monitor gửi đến là tích cực cờ trạng thái rst_flg=1 nếu reset tích cực mức 0.
    2. write_frmMonitorTX() giám sát việc đọc dữ liệu từ thanh ghi dữ liệu của UART-TX (uart_0), địa chỉ offset 16'h000C. Nếu có một tranasaction đọc thanh ghi dữ liệu, dữ liệu đọc trên prdata[7:0] sẽ so sánh với dữ liệu truyền phía UART-RX (uart_1). Nếu giá trị dữ liệu đọc trùng khớp thì thông điệp báo SUCCESS sẽ được in ra, nếu dữ liệu đọc bị sai thì thông điệp báo lỗi FAIL sẽ được in ra.
    3. write_frmMonitorRX() giám sát việc đọc dữ liệu từ thanh ghi dữ liệu của UART-RX (uart_1), địa chỉ offset 16'h000C. Nếu có một tranasaction đọc thanh ghi dữ liệu, dữ liệu đọc trên prdata[7:0] sẽ so sánh với dữ liệu truyền phía UART-TX (uart_0). Nếu giá trị dữ liệu đọc trùng khớp thì thông điệp báo SUCCESS sẽ được in ra, nếu dữ liệu đọc bị sai thì thông điệp báo lỗi FAIL sẽ được in ra.
  3. report_phase sẽ kiểm tra lại số lượng dữ liệu mong muốn truyền trên UART-TX và UART-RX. Nếu vẫn còn dữ liệu cần truyền ở UART-TX nhưng chưa được đọc và kiểm tra trên UART-RX hoặc ngược lại thì Scoreboard sẽ cảnh báo với `uvm_warning. Chú ý, đây không phải là một lỗi (error) vì nó phụ thuộc vào mục đích test của người viết testbench (testcase).
Hình 3: Cấu trúc và chức năng của Scoreboard (cScoreboard.sv)
Để kiểm tra dữ liệu truyền nhận giữa 2 UART, Scoreboard thực hiện như sau:
  1. Lưu lại dữ liệu cần truyền trên mỗi UART trong một hàng đợi (queue), nó tương tự như một FIFO
  2. Tại UART phía đối diện, UART nhận, mỗi dữ liệu đọc ra sẽ được so sánh với dữ liệu trong FIFO đã lưu ở bước trên.
  3. Sau mỗi lần so sánh, dữ liệu đã so sánh trong FIFO sẽ được xóa.
Thuật toán chi tiết để kiểm tra dữ liệu truyền từ UART-TX đến UART-RX sẽ được trình bày sau đây. Chiều từ UART-RX đến UART-TX thực hiện tương tự nên sẽ không trình bày chi tiết.
Đầu tiên, phần xử lý cập nhật dữ liệu truyền vào hàng đợi (FIFO) như sau:
1. Kiểm tra trạng thái reset thông qua cờ rst_flg Nếu cờ này bằng 1, tức là có reset, thì Scoreboard sẽ khởi động lại và xóa toàn bộ FIFO lưu dữ liệu truyền
queueTransTX.delete();
uartEnTX = 1'b0;
2. Nếu không phải trong trạng thái reset, rst_flg=0, bit báo trạng thái enable của UART-TX sẽ luôn được cập nhật đầu tiên nếu có bất cứ transaction ghi đến thanh ghi SE, offset là 16'h0004.
if (TransOnTX.pwrite && (TransOnTX.paddr[15:0] == 16'h0004)) begin
  uartEnTX = TransOnTX.pwdata[0];
end
 3. Nếu UART-TX được enable, uartEnTX=1, và có transaction ghi vào thanh ghi dữ liệu, địa chỉ offset là 16'h000C thì dữ liệu này là dữ liệu cần truyền nên sẽ lưu vào cuối FIFO. Chú ý, chỉ lưu 8 bit LSB, 24 bit đầu là 0.
else if (TransOnTX.pwrite && (TransOnTX.paddr[15:0] == 16'h000C) && uartEnTX) begin  queueTransTX.push_back(TransOnTX.pwdata & 32'h0000_00ff);
end
Phần xử lý lưu dữ liệu truyền vào FIFO của UART-TX được thực hiện bởi method write_frmMonitorTX. Method này lấy APB transaction từ Monitor cảu agent coApbMasterAgentTx.
Hình 4: Giai thuật cập nhật dữ liệu truyền của Scoreboard
Tiếp theo, phần xử lý kiểm tra dữ liệu đọc, dữ liệu truyền từ UART-TC đến UART-RX, trên UART-RX được thực hiện như sau:
1. Kiểm tra trạng thái reset thông qua cờ rst_flg Nếu cờ này bằng 1, tức là có reset, thì Scoreboard sẽ không thực thi quá trình kiểm tra dữ liệu đọc.
2. Nếu không phải trong trạng thái reset, rst_flg=0, bit báo trạng thái enable của UART-RX sẽ luôn được cập nhật đầu tiên nếu có bất cứ transaction ghi đến thanh ghi SE, offset là 16'h0004.
if (TransOnRX.pwrite && (TransOnRX.paddr[15:0] == 16'h04)) begin
  uartEnRX = TransOnRX.pwdata[0];
end
 3. Nếu UART-RX được enable, uartEnRX=1, và có transaction đọc từ thanh ghi dữ liệu, địa chỉ offset là 16'h000C thì dữ liệu này là dữ liệu sẽ được kiểm tra.
4. Lấy phần tử đầu tiên từ FIFO lưu dữ liệu truyền của UART-TX, queueTransTX.
queueCompRX = queueTransTX[0];
4. So sánh 8 bit dữ liệu đọc được với phần tử đầu tiên lấy từ FIFO.
  • Nếu hai giá trị bằng nhau, in ra thông điệp báo SUCCESS
  • Nếu hai giá trị khác nhau, in ra thông điệp báo FAIL
if ((TransOnRX.prdata & 32'h0000_00ff) == queueCompRX) begin  `uvm_info("SB INFO", $sformatf("[%t] SUCCESS on UART-RX: transfer data = %2h, queueTransTX size = %d", $time, TransOnRX.prdata, queueTransTX.size()), UVM_LOW);
endelse begin  `uvm_error("SB ERROR", $sformatf("[%t] FAIL on UART-RX: read data = %2h, expected data =%2h, queueTransTX size = %d", $time, TransOnRX.prdata, queueCompRX, queueTransTX.size()))
end
5. Kiểm tra lại số phần tử trong FIFO queueTransTX.
  • Nếu FIFO rỗng, in ra thông điệp báo dữ liệu đọc được không phải là dữ liệu được truyền từ UART-TX. Điều này xảy ra khi phát một transaction đọc thanh ghi dữ liệu khi nó rỗng.
  • Nếu FIFO không rỗng, thực hiện xóa phần tử đầu tiên của FIFO, phần tử vừa dùng để so sánh ở bước trên
if (queueTransTX.size() != 0) begin
  queueTransTX.delete(0);
endelse begin  uvm_warning("SB UNFINISH-RX", "Read data but do NOT have any transmitted data");
end
Phần xử lý kiểm tra dữ liệu đọc trên UART-RX được thực hiện trong method write_frmMonitorRX.
Hình 5: Giải thuật kiểm tra dữ liệu nhận trên UART của Scoreboard
Các bạn hãy tải source code trên Github để vừa đọc vừa so sánh. Mọi góp ý, các bạn có thể comment dưới bài viết.

Dữ liệu có thể dowload:
Môi trường UVM bản Draff trên Github

Lịch sử cập nhật:
1/ 2019.09.15 - Tạo lần đầu

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

Chủ Nhật, 11 tháng 8, 2019

[UVM] Bài 5 - System Verilog code của môi trường UVM cho UART-APB (bản draff)

Hiện nhóm tác giả đã xây dựng xong phiên bản thô (draff) của môi trường UVM cho UART-APB. Đến thời điểm bài viết này được xuất bản, môi trường vẫn chưa được kiểm tra và debug hoàn chỉnh. Bài viết này sẽ mô tả về cấu trúc các file, cách tạo testcase và chạy mô phỏng trên môi trường hiện tại để các bạn đọc quan tâm có thể tìm hiểu và nghiên cứu từ bây giờ.
Trước khi đọc bài này, các bạn nên đọc kỹ lại các bài viết trước đó như bài 1, bài 2, bài 3, bài 4các bài trình bày về class của System Verilog. Đặc biệt, nếu bạn là người mới bắt đầu, hãy tập trung đọc hiểu và thực hành các ví dụ cơ bản của các bài trước để hiểu trước khi đi vào môi trường UVM này.

1) Cấu trúc môi trường UVM
Môi trường UVM cho UART-APB đã được mô tả trong lần phần tích đầu tiên ở bài 2. Trong quá trình xây dựng môi trường, một số thành phần khác cần được thêm vào để giải quyết một số vấn đề khi xây dựng môi trường thực tế.
Cấu trúc môi trường UVM hiện tại sẽ được mô tả trong phần này. Các bạn có thể đọc và so sánh với bài 2 để tìm điểm giống và khác nhau giữa cấu trúc mô tả trong bài 2 và cấu trúc trong bài này.
Chú ý, các phần cơ bản hầu như không đổi, chỉ một vài thành phần mới được thêm vào giúp việc xây dựng testcase dễ dàng hơn.
Phương pháp để bạn đọc dễ đọc và tìm hiểu source code của môi trường này:
  1. Phân tích sơ đồ cấu trúc môi trường.
    • Tên trong sơ đồ là tên thực tế xuất hiện trong môi trường.
    • Tên ngoài dấu ngoặc đơn là tên instance hoặc tên đối tượng (object)
    • Tên trong dấu ngoặc đơn là tên module hoặc tên class
  2. Tìm file code tương ứng với thành phần đã mô tả trong sơ đồ
  3. Kiểm chứng lại mối liên hệ giữa các thành phần trong source code và sơ đồ cấu trúc môi trường
Hình 1: Cấu trúc môi trường UVM hiện tại
Cấu trúc source code:
  • checker
    • apb_protocol_checker.sv - Phần lõi thực thi chức năng kiểm tra giao thức APB
    • apb_protocol_checker_top.sv - Phần kết nối APB checker với các instance của UART trong dut_top
    • uart_protocol_checker.sv - Phần lõi thực thi chức năng kiểm tra giao thức UART
    • uart_protocol_checker_top.sv - Phần kết nối UART checker với các instance của UART trong dut_top
  • dut
    • uart_apb_if.v - module chứa các thanh ghi cấu hình và trạng thái, kết nối với bus APB
    • uart_receiver.v - module truyền dữ liệu nối tiếp theo chuẩn UART
    • uart_transmitter.v - module nhận dữ liệu nối tiếp theo chuẩn UART
    • uart_top.v - TOP level của lõi IP UART-APB, kết nối các module chức năng
    • dut_top.v - TOP level của DUT kết nối 2 instance UART.
  • pat
    • trialPat/cVSequence.sv - Testcase (test pattern, testbench) của người kiểm tra. Mỗi thư mục là 1 testcase và đều chứa một file testcase suy nhất tên cVSequence.sv
    • ...
  • sim
    • uart_define.h - Chứa các define của UART
    • testTop.sv - Kết nối các thành phần môi trường mức TOP, gọi và chạy testcase
    • run_sim.do - Script chạy mô phỏng trên QuestaSim GUI, được dùng để chạy testcase khi cần debug
    • add_wave.do - Dùng cho script run_sim.do
    • run_qsim.pl - Script Perl chạy mô phỏng trên Cygwin terminal ở chế  độ batch mode
  • uvm_comp
    • cApbMasterAgent.sv - Kết nối Sequencer, Driver và Monitor
    • cApbMasterDriver.sv - Driver lái APB interface
    • cApbMasterMonitor.sv - Monitor giám sát APB interface và Interrupt interface
    • cApbMasterSequencer.sv - Lấy APB transaction từ Sequence chuyển đến Driver
    • cApbTransaction.sv - APB transaction, là dữ liệu và thông tin điều khiển APB interface sẽ gửi cho Driver
    • cCommonSequence.sv - Chứa các class chung được sử dụng để tạo ra các transaction.
    • cEnv.sv - Môi trường liên kết các Agent, Scoreboard và cVSequencer
    • cScoreboard.sv - Scoreboard dùng để kiểm tra dữ liệu trên 2 instance UART, dữ liệu kiểm tra lấy từ Monitor
    • cTest.sv - Thành phần cố định thực thi testcase, testcase viết trong cVSequence.sv sẽ được gọi và thực thi trong class này. Class này sẽ được gọi và thực thi ở method run_test() trong testTop.sv
    • cVSequence.sv - là một bản copy của testcase được thực thi lấy từ thư mục pat/<thư mục testcase>/cVSequence.sv
    • cVSequencer.sv - là thành phần trung gia chuyển các transaction đến các Agent
    • ifDut.sv - là các định nghĩa interface của DUT
    • uMacro.svh - là các macro được sử dụng trong testcase (pat/<thư mục testcase>/cVSequence.sv)
2) Làm thế nào để tạo một testcase?
2.1) Bước 1: Tạo class sequence chung để điều khiển các transaction
File: cCommonSequence.sv

Mục đích: Tạo class cho phép sinh ra các transaction như mong muốn của người test

Giải thích: Theo cấu trúc transaction, các biến pwrite, paddr, pwdata, pstrb, apbSeqEnapbConEn là các biến random. Giá trị của các biến này sẽ được tạo ngẫu nhiên khi method randomize() được thực thi. Để cấu hình các thanh ghi UART-APB đến giá trị mong muốn, chúng ta cần có các class ràng buộc lại giá trị của các biến trên.

Ví dụ, xét class cApbMasterWriteSeq, đây là class tạo ra transaction ghi trên APB. Để thực hiện điều này, biến pwrite của transaction luôn gán là 1 và transaction luôn được enable (apbSeqEn=1). Các giá trị như địa chỉ, dữ liệu ghi và pstrb có thể được gán thông qua biến addr, data và be.

Ví dụ 1: class cApbMasterWriteSeq
class cApbMasterWriteSeq extends uvm_sequence#(cApbTransaction);
     `uvm_object_utils(cApbMasterWriteSeq)
     `uvm_declare_p_sequencer(cApbMasterSequencer)

   cApbTransaction coApbTransaction;

   rand logic conEn;
     rand logic [31:0] addr;
     rand logic [31:0] data;
     rand logic [ 3:0] be;

     function new (string name = "cApbMasterWriteSeq");
         super.new(name);
     coApbTransaction = cApbTransaction::type_id::create("coApbTransaction");
     endfunction

     virtual task body();
         start_item(coApbTransaction);
     //coApbTransaction.randomize();
         assert(coApbTransaction.randomize() with {
       coApbTransaction.apbSeqEn  == 1;
       coApbTransaction.apbConEn  == conEn;
             coApbTransaction.paddr  == addr;
             coApbTransaction.pwdata == data;
             coApbTransaction.pstrb  == be;
             coApbTransaction.pwrite == 1;
         });
         finish_item(coApbTransaction);
     endtask
 endclass

2.2) Bước 2: Tạo Macro để sử dụng các đối tượng class sequence đã tạo ở bước 1
File: uMacro.svh

Mục đích: Sử dụng các đối tượng (object) tạo từ class sequence dễ dàng hơn.

Giải thích: đối tượng của class đã tạo ở bước 1 sẽ được gọi và khởi tạo trong testcase (pat/<thư mục testcase>/cVSequence.sv). Người test sẽ thao tác trên đối tượng này để gửi các transaction đến Driver và lái DUT. Dòng code để thực hiện điều này có thể dài nên nó sẽ được thay bằng một macro giúp rút gọn code của testbench.

Ví dụ, xét macro ApbWriteTX, macro này giúp tạo một transaction ghi gửi đến APB interface của uart_0. Người test có thể điền địa chỉ ghi và dữ liệu ghi mong muốn.

Ví dụ 2: ApbWriteTX(address,value)
`define ApbWriteTX(address,value) \
 `uvm_do_on_with(WriteSeq, p_sequencer.coApbMasterAgentTx.coApbMasterSequencer, { \
                WriteSeq.conEn      == 1'b0; \
                WriteSeq.addr[31:0] == address; \
                WriteSeq.data[31:0] == value; \
                WriteSeq.be[3:0]    == 4'b1111; \
                })

Trong đoạn code ví dụ trên, WriteSeq là đối tượng tạo từ class cApbMasterWriteSeq. Đối tượng này sẽ được khởi tạo trong file testcase (testbench).

2.3) Bước 3: Tạo testcase sử dụng các macro đã tạo ở bước 2
File: pat/<tên thư mục testcase>/cVSequence.sv

Mục đích: Ghi/đọc các thanh ghi của UART để điều khiển DUT hoạt động theo mục đích test.

Giải thích: Lõi IP UART-APB là một APB slave. Để kiểm tra các hoạt động của lõi IP, người test cần cấu hình lõi IP đến chế độ hoạt động mong muốn và giám sát các giá trị cấu hình hoặc bit trạng thái của lõi IP. Điều này thực hiện bằng cách ghi/đọc cá thanh ghi lõi IP thông qua APB interface sử dụng các macro đã tạo ở bước 2.

Ví dụ, để enable uart_0, người test cần ghi "1" vào bit 0 của thanh ghi SE, mô tả ở bài 1. Sau khi ghi, bit này được đọc lại để kiểm tra xem đã được cấu hình đúng hay chưa.

Ví dụ 3: Cấu hình bit enable và kiểm tra lại giá trị cấu hình
`ApbWriteTX(32'h00000004,32'h00000001)
`ApbReadTX(32'h00000004,32'h00000001,32'h00000001)

Chú ý:
  1. testcase của bạn nằm trong pat/<tên thư mục testcase>/cVSequence.sv, bạn có thể viết code tùy ý để tạo trường hợp test mong muốn mà không cần phải làm bước 1 và 2. Việc thực hiện bước 1 và 2 chỉ là 1 trong nhiều cách hỗ trợ tạo testcase.
  2. Trong môi trường này, mỗi testcase là một file cVSequence.sv được đặt trong một thư mục với tên riêng gợi nhớ chức năng test của testcase
  3. Thư mục các testcase để trong UvmEnvUartApb/pat/
Hình 2: Ba bước để tạo một testcase trong môi trường UVM
3) Làm thế nào để chạy một testcase?
Trong môi trường hiện tại, run_test() trong sim/testTop.sv sẽ thực thi class cTest để bắt đầu quá trình mô phỏng. cTest là class sẽ khởi tạo các thành phần UVM, đối tượng coEnv, và chạy testcase của bạn ở run_phase() thông qua đối tượng coVSequence.
Làm sao run_test() biết class mà nó cần thực thi là cTest chứ không phải class nào khác? cTest được truyền đến run_test() thông qua biến UVM_TESTNAME, khai báo ở lệnh chạy mô phỏng trong file run_sim.do hoặc run_qsim.pl.
Hình 3: cTest được khai báo ở lệnh chạy mô phỏng trong script và truyền đến run_test() trong testTop
Để thực thi mô phỏng với 1 testcase, các bạn thực hiện như sau:
cd /UvmEnvUartApb/sim/
Hiện tại môi trường hỗ trợ 2 script:
  1. Chạy mô phỏng trên QuestaSim GUI
    1. Mở QuestaSim GUI (chạy phần mềm Questasim)
    2. Tạo một project mới trong thư mục sim với tên testTop
    3. link hoặc copy đường dẫn file testcase của bạn, ví dụ như ../pat/trialPat/cVSequence.sv đến thư mục /uvm_comp/
    4. Chạy mô phỏng với lệnh do run_sim.do trên cửa sổ transcript của QuestaSim
    5. Chọn "No" khi cửa số báo finish xuất hiện
    6. Xem kết quả trên cửa sổ Transcript hoặc waveform
  2. Chạy mô phỏng ở chế độ batch mode trên Cygwin (có cài Perl)
    1. Sửa biến $SIM_TOOL trong file ./run_qsim.pl để chỉ đến thư mục cài đặt QuestaSim của bạn, ví dụ như C:/questasim64_10.2c/win64. Chú ý, đây là đường dẫn đến đúng thư mục chứa các tool của QuestaSim chứ không phải thư mục cài đặt ngoài cùng.
    2. Chạy mô phỏng với lệnh sau:
./run_qsim.pl <đường dẫn thư mục testcase>
Ví dụ như:
./run_qsim.pl ../pat/trialPat/cVSequence.sv
Script này sẽ copy testcase của bạn đến thư mục /uvm_comp để tổng hợp và chạy mô phỏng.

Nhóm tác giả lưu ý lại một lần nữa, môi trường hiện tại là bản draff, một số thành phần chưa hoàn chỉnh và chưa được debug đầy đủ. Tuy nhiên, việc chạy các testcase để cấu hình và sử dụng UART đã có thể thực hiện được. Vì vậy, các bạn có thể tải về để tìm hiểu.

Dữ liệu có thể dowload:

Lịch sử cập nhật:
1/ 2019.08.11 - Tạo lần đầu
2/ 2019.08.18 - Update hình 1, sửa minh họa kết nối trên scoreboard từ hình <> thành hình tròn

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

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