Sau khi đã hiểu về mô hình mô phỏng sự kiện của Verilog và System Verilog qua bài 1, bài 2, bài 3 và bài 4 với những ví dụ minh họa cụ thể. Bài này sẽ trình bày về hiện tượng “chạy đua” (race condition) và những nguyên tắc để hạn chế hiện tượng chạy đua trong Verilog và System Verilog. Đồng thời giải thích vì sao có hiện tượng này khi chạy mô phỏng.
1) Chạy đua là gì?
Trong cuộc sống, chạy đua là thi xem ai đến đích trước. Việc ai đến đích trước sẽ làm cho kết quả cuộc thi khác nhau. Tương tự, thuật ngữ chạy đua dựa trên ý tưởng hai tín hiệu “đua” nhau để xem tín hiệu nào sẽ tác động đến đầu ra trước. Thứ tự tác động của chúng làm cho đầu ra có thể mang các giá trị khác nhau và sinh ra lỗi không mong muốn cho một thiết kế, một hệ thống hoặc một quá trình xử lý. Tóm lại, điều kiện chạy đua là một sai sót thể hiện ở việc một đầu ra phụ thuộc không mong muốn vào định thời (timing) hoặc thứ tự sắp xếp (ordering) của các sự kiện.
2) Phân loại “chạy đua”
Chạy đua có thể được chia làm 2 nhóm là chạy đua phần cứng (hardware race) và chạy đua sinh ra do mô phỏng (simulation induced race)
2.1) Chạy đua phần cứng
Chạy đua phần cứng xuất hiện trong mạch tổ hợp do bản chất vật lý của mạch. Trên thực tế, các tín hiệu trong một mạch tổ hợp có độ trễ lan truyền khác nhau. Khi đầu vào của một cổng logic thay đổi trạng thái thì phải mất một khoảng thời gian trễ trước khi thay đổi này truyền đến ngõ ra của cổng logic. Trong khoảng thời gian này, ngõ ra có thể thay đổi đến một trạng thái không mong muốn trước khi chuyển đến trạng thái ổn định. Trạng thái không mong muốn này gọi là glitch. Tuy nhiên các cuộc chạy đua tạm thời này thường không ảnh hưởng đến hoạt động chính xác của hệ thống, nếu nó không phải là đường clock hoặc reset bất đồng bộ. Đường clock và rest bất đồng bộ rất nhạy bởi sự thay đổi tín hiệu nên glitch trên các đường này là vô cùng nguy hiểm.
Một ví dụ về chạy đua phần cứng mà các bạn có thể dễ tìm gặp là mạch sau:
Hình 1: Minh họa chạy đua phần cứng |
Giả sử cồng NOT trễ 1ns, cổng AND trễ 2ns, dây nối có độ trễ không đáng kể. Khi in chuyển từ mức “0” đến mức “1” thì sau 1ns in’ mới bằng “0”. Như vậy, trong 1ns trước khi in’ chuyển đến trạng thái đúng, cả 2 ngõ vào cổng AND bằng “1”. Sau 2ns, ngõ ra out sẽ thể hiện mức 1 này. Một ví dụ điển hình khác về chạy đua là trạng thái cấm của Latch S-R trong hình minh họa sau:
Hình 2: Mô hình R-S Latch |
Khi hai ngõ vào S và R được gán đến giá trị “1” đồng thời thì hai ngõ ra của cổng NOR sẽ là “0”. Lúc này, do đường hồi tiếp từ Q và /Q đến ngõ vào của 2 cổng NOR nên nếu chỉ có S hoặc R chuyển trạng thái:
- S chuyển sang trạng thái “0” thì hai ngõ ra Q và /Q sẽ ổn định về “01”.
- R chuyển sang trạng thái “0” thì hai ngõ ra Q và /Q sẽ ổn định về “10”.
Nhưng nếu cả S và R cùng chuyển trạng thái từ “1” sang “0” đồng thời thì xảy ra chạy đua và trạng thái cuối cùng của hai ngõ ra Q và /Q là không thể dự đoán được. Các chạy đua phần cứng là bản chất vậy lý bên trong mỗi hệ thống, các kỹ thuật thiết kế thích hợp và công cụ có thể ngăn chặn hiệu quả điều này.
2.2) Chạy đua sinh ra do mô phỏng
Chạy đua sinh ra do mô phỏng không do bản chất bên trong của thiết kế hay tính chất vật lý của nó mà là hệ quả tự nhiên không mong muốn của thuật toán mô phỏng theo sự kiện sử dụng trong Verilog và System Verilog.
Trong khi bản chất thực tế của phần cứng có thể xử lý song song đồng thời thì trình mô phỏng chỉ có thể xử lý một sự kiện tại một thời điểm, nên trong cùng một khe thời gian, một chuỗi các sự kiện sẽ được lập thời gian biểu để thực hiện tuần tự. Như vậy, các hoạt động đồng thời của phần cứng sẽ được mô hình hóa bằng một tập hợp các hoạt động theo thứ tự của trình mô phỏng. Độ lệch của mô hình này so với phần cứng thực tế gây ra các chạy đua không có trong phần cứng thực tế, mà do trình mô phỏng tạo ra.
Những chạy đua này có thể làm cho trình mô phỏng cho kết quả:
- Trường hợp 1: Thiết kế lỗi trong khi thực tế thiết kế là chính xác
- Trường hợp 2: Thiết kế có vẻ đúng trong khi thực tế thiết kế không chính xác.
Trường hợp thứ hai nguy hiểm hơn rất nhiều so với trường hợp thứ nhất vì chúng ta sẽ không phát hiện được bug cho đến khi chạy phần cứng thực tế. Một nguyên nhân thường gặp của trường hợp thứ 2 là do code thiết kế vô tình theo đúng thứ tự cụ thể của thuật toán mô phỏng. Chính vì vậy mà Verilog và System Verilog quy định một vùng sự kiện cụ thể (ví dụ như Active, Re-Active, …) cho phép xử lý các phát biểu theo thứ tự tùy ý, nhưng các phát biểu trong mỗi block begin-end phải theo đúng thứ tự.
3) Làm sao để phát hiện điều kiện chạy đua sinh ra do mô phỏng?
Trường hợp thứ nhất có thể được phát hiện và chỉnh sửa trong quá trình chạy mô phỏng trên phần mềm vì kết quả mô phỏng sẽ gây ra lỗi không mong muốn.
Đối với trường hợp thứ 2, chúng ta có thể xây dựng các hệ thống phần cứng thực tế dùng FPGA để có thể chạy thử nghiệm RTL code. Trên thực tế, hiện nay, một số công ty chuyên cung cấp công cụ và phần mềm mô phỏng còn phát triển các hệ thống mô phỏng kết hợp giữa phần cứng và phần mềm để giúp:
- Mô phỏng code thiết kế gần giống thực tế nhất
- Thời gian mô phỏng nhanh hơn
- Có thể mô phỏng toàn bộ các thành phần của một hệ thống lớn ví dụ như một SoC với đầy đủ code RTL, hệ điều hành, chương trình ứng dụng thực tế, …
Một trong các hệ thống như vậy là HAPS (High-performance ASIC Prototyping Systems) của hãng Synopsys.
Hình 3: Hệ thống HAPS của Synopsys |
Bản chất của chạy đua là đầu ra (ngõ ra) hoặc kết quả sinh ra sẽ không thể xác định trước nên mỗi trình mô phỏng khác nhau có thể cho kết quả khác nhau khi mô phỏng cùng một code có điều kiện chạy đua. Hoặc, cùng một trình mô phỏng nhưng phiên bản khác nhau cũng có thể cho kết quả khác nhau khi mô phỏng trên cùng một code. Vì vậy, chúng ta có thể mô phỏng trên nhiều phần mềm khác nhau và hãy luôn chú ý cập nhật phiên bản phần mềm mới.
Bên cạnh đó, một số tool mô phỏng hiện nay có hỗ trợ phát hiện và báo các điều kiện chạy đua trong code (xem mục 6 - cuối bài viết để biết thêm chi tiết).
Bên cạnh đó, một số tool mô phỏng hiện nay có hỗ trợ phát hiện và báo các điều kiện chạy đua trong code (xem mục 6 - cuối bài viết để biết thêm chi tiết).
4) Làm thế nào để tránh chạy đua sinh ra do mô phỏng
Nếu các bạn tìm google với từ khóa “race condition” các bạn có thể dễ dàng tìm thấy các lời khuyên (tip) để làm thế nào tránh hoặc hạn chế chạy đua khi sử dụng Verilog và System Verilog. Một tài liệu khá nổi tiếng là “Clifford E. Cummings , Arturo Salz , SystemVerilog Event Regions, Race Avoidance & Guidelines” của Sunburst Design. Theo bài viết này, việc tuân thủ các lời khuyên sau đây giúp loại bỏ từ 90% đến 100% các điều kiện chạy đua trong code RTL. Tuy không thể kiếm chứng nhưng khi phân tích kỹ thì hầu hết các lời khuyên là xác đáng. Ở đây ngoài việc dẫn lại một số lời khuyên cho việc coding thì bài này sẽ tập trung vào phân tích và đưa ra các ví dụ cụ thể.
4.1) Dùng phép gán nonblocking cho mô tả mạch tuần tự (dành cho code RTL)
Theo mô hình mô phỏng sự kiện của System Verilog và Verilog, phép gán nonblocking được thực thi trong 2 vùng là:
- Vùng Active: Ước lượng giá trị vế phải của phép gán nonblocking. Hoạt động này giúp xác định giá trị hiện tại của vế phải.
- Vùng NBA: Cập nhật giá trị bên trái của phép gán nonblocking. Hoạt động này đảm bảo tất cả các biến vế trái được cập nhật giá trị đã được xác định ở vùng Active chứ không phải giá trị vừa được gán mới trong vùng này.
Hành vi mô phỏng của phép gán nonblocking phù hợp với việc mô hình mạch tuần tự theo clock vì tại cạnh lên clock tất cả các ngõ ra Q của FF được cập nhật giá của ngõ vào D ở cùng một thời điểm. Nếu có nhiều FF mắc nối tiếp thì ngõ ra Q sẽ lấy giá trị ngõ vào D trước khi nó bị cập nhật bởi FF trước đó.
always @ (posedge clk) begin
q0 <= din;
q1 <= q0;
end
Hình 4: Hai FF mắc nối tiếp |
Hai FF mắc nối tiếp như trên, nếu din=0, q0=1 và q1=0, khi cạch lên clk xuất hiện thì cùng lúc q0=0 và q1=1. Về hoạt động mô phỏng:
- Tại vùng Active: din được xác định là “0” và q1 được xác định là “1”
- Tại vùng NBA: q0 được cập nhật giá trị “0” và q1 được cập nhật giá trị q0=1 đã được xác định ở vùng Active chứ không phải giá trị q0 mới.
Nếu chúng ta dùng phép gán blocking:
always @ (posedge clk) begin
q0 = din;
q1 = q0;
end
Vì các phép gán blocking đều được thực thi trong vùng Active nên q0=din=0 và q1=q0=0. q1 mang giá trị mới của q0, điều này không đúng với mô hình phần cứng mong muốn.
4.2) Dùng phép gán blocking cho mô tả mạch tổ hợp sử dụng always (dành cho code RTL)
Theo mô hình mô phỏng sự kiện của System Verilog và Verilog, các phép gán blocking cùng được thực thi trong vùng Active. Tại vùng này, sự kiện cập nhật các biến bên trái phép gán blocking sẽ được lặp lại liên tục cho đến khi chúng có được giá trị mới nhất. Hành vi này phù hợp với mô hình mạch tổ hợp là ngõ ra cập nhật ngay lập tức theo sự thay đổi của ngõ vào nếu bỏ qua các độ trễ vật lý.
Ví dụ, nếu ta có 2 process sau:
always @(*) begin
q0 = din;
end
always @(*) begin
q1 = q0;
end
Hai phép gán blocking của hai process này có thể được thực thi theo bất kỳ thứ tự nào trong vùng Active. Giả sử din chuyển từ “0” sang 1 và q1=q0 được thực hiện trước thì trình tự thực thi sẽ như sau:
q1=q0=0;
q0=din=1;
q1=q0=1;
Kết thúc vùng Active, q1=q0=din=1 đúng với hành vi mạch tổ hợp.
4.3) Dùng phép gán nonblocking cho việc mô tả Latch (dành cho code RTL)
FF hoạt động theo cạnh clock còn Latch hoạt động theo mức của một tín hiệu enable.
Hình 5: Mô hình 2 D Latch mắc nối tiếp và timing |
Nếu mắc 2 Latch nối tiếp nhau như hình minh họa trên thì q0 sẽ được cập nhật giá trị trước và q1 sẽ được cập nhật giá trị sau. Nếu mô hình Latch bằng phép gán nonblocking như sau:
always @ (*) begin if (en) begin q0 <= din;
q1 <= q0;
endend
thì q0 và q1 sẽ được gán theo đúng thứ tự như phần cứng thực tế do phép gán nonblocking thực thi trong 2 vùng (xem cách thực thi của phép hán nonblocking tại 4.1). Nhưng nếu đoạn code trên được mô tả bằng phép gán blocking thì tại vùng Active, sau khi q0=din=1 thì ngay sau đó q1=q0=1.
4.4) Không dùng chung cả phép gán blocking và nonblocking trong cùng một khối always
Giải sử chúng ta mô hình 2 FF mắc nối tiếp bằng cách dùng cả phép gán blocking và nonblocking như sau:
always @ (posedge clk) begin
q0 = din;
q1 <= q0;
end
Tại vùng Active, hai hoạt động sau được thực hiện:
- Gán q0=din
- Ước lượng vế phải của phép gán q1<=q0 bằng giá trị q0 đã được cập nhật từ din
Tại vùng NBA, q1 được gán bằng q0 đã cập nhật giá trị mới. Điều này không đúng với mô hình 2 FF mắc nối tiếp thực tế và kết quả mô phỏng sẽ sai.
4.5) Không dùng các phát biểu gán cho cùng một biến trong nhiều khối always (dành cho mô tả RTL)
Khi một biến được gán trong nhiều khối always khác nhau rõ ràng là khó (hoặc không thể) kiểm soát được giá trị của nó khi thực thi mô phỏng. Ví dụ:
always @ (*) begin q = 0;
end
always @ (*) begin q = 1;
end
Trường hợp này, trình mô phỏng sẽ không thể xác định giá trị q bằng bao nhiêu, giá trị “x” được thể hiện trên q.
4.6) Sử dụng $strobe để hiển thị giá trị được gán bằng phép gán nonblocking
$strobe có chức năng hiển thị giống $display nhưng vị trí thực thi trong một khe thời gian là khác nhau. Trong khi $display thực thi tại vùng Active hoặc Reactive thì $strobe thực thi tại vùng Postpone. Đây là vùng cuối cùng của một khe thời gian, sau khi tất cả các phép gán nonblocking đã đã cập nhật giá trị mới tại vùng NBA hoặc Re-NBA.
5) Một số ví dụ chạy đua sinh ra do mô phỏng và cách khắc phục
Chú ý, các đoạn code dưới đây được đặt trong một module để chạy mô phỏng thử.
5.1) Chạy đua Đọc-Ghi (Read-Write)
assign p = q;
initial begin
q = 1;
#1 q = 0;
$display(p);
end
Khi q được gán bằng 0, sự kiện cập nhật p=q được sinh ra. Lúc này cả 2 sự kiện p=q và $display đều đang trong vùng Active thuộc 2 process khác nhau. Theo nguyên tắc, chúng có thể được thực thi theo bất cứ thứ tự nào nên một trình mô phỏng có thể thực thị $display trước hoặc gán p=q trước rồi mới thực thi $display. Đây là kiểu chạy đua Đọc-Ghi vì phép gán assign cố gắng “ghi” một giá mới vào p còn $display cố gắng “đọc” giá trị của “p”.
Trường hợp chạy đua được dùng làm ví dụ trong chuẩn System Verilog nhưng làm thế nào để khắc phục? Nếu muốn $display thực hiện trong cùng một khe thời gian, chúng ta có thể sử dụng độ trễ #0.
initial begin q = 1;
#1 q = 0;
#0 $display(p);
end
Lúc này, $display sẽ được thực hiện sau khi p=q được gán vì p=q thực hiện trong vùng Active còn $display với #0 thực hiện trong vùng Inactive. Vùng này nằm ngay sau vùng Active.
Một cách khác là chúng ta có thể chuyển $display sang một khe thời gian khác ngay sau khe thời gian này mà tại đó không có sự kiện cập nhật giá trị mới của p. Nghĩa là dùng một giá trị độ trễ khác 0 ví dụ như #1, #2, …
5.2) Chạy đua Ghi-Ghi (Write-Write)
reg a;
initial #10 a = 0;
initial #10 a = 1;
initial #11
if (a) $display("0 win in the race");
else $display("1 win in the race");
Tại thời gian T=10, hai process initial chạy đua để ghi vào biến a vì cả 2 process này đều được thực thi ở vùng Active và có thể theo bất kỳ thứ tự nào. Nếu kết quả cuối cùng a = 1, tức phép gán a=0 được thực hiện trước, sau đó đến a=1. Ngược lại nếu kết quả cuối cùng a=0 thì a=1 đến trước, sau đó a=0 mới được thực thi. Như vậy, tại T=11, #display có thể rơi vào nhánh if hoặc else.
Khi mô phỏng trên Questa Sim64 10.2c, chỉ cần đổi thứ tự hai dòng code initial của a=0 và a=1 bạn có thể thấy được hai kết quả khác nhau. Cách khắc phục là gì? Thực tế, chúng ta sẽ không sử dụng cách viết code như vậy, tương tự như always, một biến không nên được gán tại 2 khối initial khác nhau để tránh chạy đua. Nếu bạn muốn gán 2 giá trị khác nhau đến một biến thì mỗi giá trị gán đến biến cần trong 2 thời điểm khác nhau.
6) Phát hiện chạy đua bằng phần mềm mô phỏng
6) Phát hiện chạy đua bằng phần mềm mô phỏng
Như đã trình bày ở trên, một số phần mềm mô phỏng hỗ trợ phát hiện, debug và báo cáo các "chạy đua" xuất hiện trong code. Ví dụ, phần mềm VCS của Synopsys có hai cơ chế phát hiện chạy đua như sau:
- Phát hiện chạy đua động: phát hiện trong quá trình chạy mô phỏng (run-time). Trường hợp có thể phát hiện là:
- Điều kiện chạy đua đọc-ghi
- Điều kiện chạy đua ghi-ghi
- Phát hiện điều kiện chạy đua tĩnh: phát hiện trong quá trình biên dịch code (compile-time). Trường hợp có thể phát hiện là khi một một nhóm các phát biểu các khác nhau có mối liên quan đến nhau và hoạt động như một "vòng lặp". Ví dụ:
always @( A or C ) beginTrong đoạn code trên, C được gán ở phát biểu assign và C có trong danh sách độ nhạy cảu always. Khối always thực hiện phép gán giá trị cho B, giá trị này ảnh hưởng đến C, lúc này, các always và assign vô tình tổ hợp với nhau như một vòng lặp. VCS có thể báo cáo điều kiện này như sau:
D = C;
B = A;
end
assign C = B;
Race-[CLF] Combinational loop found
"source.v", 35: The trigger ’C’ of the always blockcan causethe following sequence of event(s) which can againtriggerthe always block.
"source.v", 37: B = A;which triggers ’B’.
"source.v", 40: assign C = B;which triggers ’C’.
Lịch sử cập nhật:
1) 2019.09.28 - Sửa q1 và q2 thành q0 và q1 ở mô tả hình 4
Tham khảo:
1/ Clifford E. Cummings , Arturo Salz , SystemVerilog Event Regions, Race Avoidance & Guidelines1) 2019.09.28 - Sửa q1 và q2 thành q0 và q1 ở mô tả hình 4
Tham khảo:
2/ IEEE Standard for SystemVerilog - Unified Hardware Design, Specification, and Verification Language, IEEE Std 1800™-2012
3/ Synopsys, VCS®/VCSi™ User Guide
0 bình luận:
Đăng nhận xét