Thứ Bảy, 11 tháng 1, 2020

[Q&A] Hỏi đáp về công nghệ vi mạch trong quý 1 năm 2020

Tập hợp các câu hỏi và trả lời về công nghệ vi mạch thảo luận trong nhóm VLSI Technology.
Phần mềm sử dụng:
  • Tổng hợp: Quartus II 32-bit Version 12.0 Build 178 05/31/2012
  • Mô phỏng: QuestaSim-64 10.2c


Câu hỏi 1 (2020.01.01):
Khi mô tả phần cứng bằng ngôn ngữ Verilog HDL, Latch được tạo ra trong trường hợp nào? và làm thế nào để tránh tạo Latch không mong muốn?

Trả lời 1a:
Khi mô tả phần cứng bằng ngôn ngữ Verilog HDL, Latch được tạo ra trên một ngõ ra khi thuộc một trong các trường hợp sau:
  1. Ngõ ra được mô tả bằng các phát biểu điều kiện if/else hoặc case (case/casex/casez) trong khối always dùng danh sách độ nhạy theo mức nhưng không được gán giá trị hoặc được gán bằng chính nó ở một trong các tổ hợp có thể xảy ra của điều kiện. Giải thích từ ngữ:
    • Sử dụng always có danh sách độ nhạy theo mức, không phải theo cạnh (posedge hoặc negedge)
    • Thiếu gán giá trị ở một trong các tổ hợp có thể xảy ra của điều kiện. Ví dụ, điều kiện là sel[1:0] có 2 bit thì có 4 tổ hợp có thể xảy ra là b00, b01, b10 và b11. Chú ý, các tổ hợp này không bao gồm giá trị (don't care) và (high impedance).
    • "Gán bằng chính nó" (tồn tại đường hồi tiếp từ ngõ ra) là ngõ ra được gán lại bằng chính ngõ ra mà không tạo ra bất cứ logic nào khác, ví dụ như y=y.
    • "Ở một trong các tổ hợp điều kiện" chứ không phải trên toàn bộ các tổ hợp có thể của điều kiện. 
  2. Ngõ ra được mô tả bằng toán tử điều kiện "?" trong khối always hoặc phát biểu assignđược gán bằng chính nó ở một trong các nhánh điều kiện.
Latch được tạo ra để "giữ lại giá trị ngõ ra trước đó" khi điều kiện rơi vào tổ hợp mà tại đó ngõ ra không được gán giá trị.

Ví dụ 1: Tạo Latch khi thiếu ít nhất một tổ hợp giá trị của điều kiện
always @ (*) begin
  if (sel == 2'd0) y = a;
  else if (sel == 2'd1) y = b;
  else if (sel == 2'd2) y = 0;
end
hoặc
always @ (*) begin
  case (sel)
    2'd0: y = a;
    2'd1: y = b;
    2'd2: y = 0;
  endcase
end
Trong ví dụ 1, Latch được tạo ra vì tổ hợp giá trị 2'd3 của tín hiệu điều kiện sel[1:0] không được mô tả. Trong trường hợp này, khi sel=2'd3 thì y sẽ giữ nguyên giá trị trước đó và Latch được tạo ra khi tổng hợp.
Hình 1: Mạch logic của ví dụ 1, Latch xuất hiện ở ngõ ra y
Trong hình trên, tín hiệu enable của Latch là ENA chỉ tích cực khi sel = 0, 1 hoặc 2. Nếu sel=3, ENA không tích cực và y giữ nguyên giá trị cũ.
Ví dụ 2: Tạo Latch vì thiếu gán giá trị ở một trong các tổ hợp điều kiện
always @ (*) begin
  case (sel[1:0])
    2'd0: begin
y = a;
x = b;
    end
   2'd1: begin
y = b;
x = a;
   end
   2'd2: begin
       y = 0;
       x = 1;
   end
   2'd3: begin
       x = 0;
   end
  endcase
end
Trong ví dụ 2, tất cả các tổ hợp điều kiện đã được mô tả. Tuy nhiên, ngõ ra y không được gán giá trị ở tổ hợp sel=3 nên Latch được tạo ra trên ngõ ra y.
Hình 2: Mạch logic của ví dụ 2, Latch được tạo ra ở ngõ ra y 
Ví dụ 3: Tạo Latch vì gán giá trị ngõ ra bằng chính nó
assign y = (sel==0)? a: y;
hoặc
always @ (*) begin
  if (sel == 2'd0) y = a;
  else y = y;
end
Trong ví dụ 3, ngõ ra y được gán bằng chính nó khi sel khác 0.
Hình 3: Mạch logic của ví dụ 3, Latch được tạo ra trên ngõ ra y
Ví dụ 4: Tạo Latch khi ngõ ra được gán bằng chính nó kèm toán tử logic
always @ (*) begin
  if (sel == 2'd0) y = a;
  else y = &y;
end
Trong ví dụ 4, ngõ ra y=&y nhưng toán tử "&" không có tác dụng tạo ra một cổng logic vì y là tín hiệu 1 bit. Dòng code này tương đương với y=y nên Latch được tạo ra giống như ví dụ 3.
Nếu ngõ ra được gán bằng chính nó và kèm với mô tả có tác dụng tạo ra cổng logic như NOT, OR, AND, XOR, ... thì một mạch tuần tự bất đồng bộ (asynchronous circuit) sẽ được tạo ra chứ không phải Latch.
Ví dụ 5: Ngõ ra được gán với bù của chính nó
always @ (*) begin
  if (sel == 2'd0) y = a;
  else y = ~y;
end
Trong ví dụ 5, ngõ ra y=~y, một logic cần được tạo ra, ví dụ như cổng NOT, để thực hiện đảo giá trị của y. Trường hợp này, Latch không được tạo ra ở y. Chú ý, trong các thiết kế đồng bộ, loại mạch tuần tự bất đồng bộ như ví dụ này không được sử dụng.
Hình 4: Mạch logic của ví dụ 5, mạch tuần tự bất đồng bộ được tạo ra ở y
Đối với trường hợp gán ngõ ra bằng chính nó, nếu phép gán này được thực hiện trên tất cả các tổ hợp điều kiện có thể thì Latch cũng không được tạo ra.
Ví dụ 6: Gán ngõ ra bằng chính nó trên tất cả các tổ hợp điều kiện
always @ (*) begin
  if (sel == 2'd0) y = &y;
  else if (sel == 2'd1) y = ^y;
  else if (sel == 2'd2) y = y;
  else y = |y;
end
Trong ví dụ 6, ngõ ra y=y trong tất cả các tổ hợp điều kiện. Điều này ứng với ngõ ra y không được lái bởi bất cứ ngõ vào nào. Tùy trình tổng hợp y có thể được gán bằng 0 hoặc 1.

Trả lời 1b:
Như vậy để tránh tạo Latch không mong muốn trên một ngõ ra thì:
  • Ngõ ra phải được gán giá trị trong tất cả tổ hợp của điều kiện khi mô tả logic dùng phát biểu điều kiện trong khối always có danh sách độ nhạy theo mức.
  • Nếu tồn tại đường hồi tiếp (feedback) từ ngõ ra thì phải có logic trên đường hồi tiếp này. Lúc này, một mạch tuần tự bất đồng bộ được tạo ra. Chú ý, trong thiết kế đồng bộ, chúng ta sẽ không sử dụng loại mạch tuần tự bất đồng bộ này. Điều này được nêu ở đây chỉ với mục đích "tránh tạo Lacth".
  • Sử dụng phát biểu function
  • Sử dụng thuộc tính (attribute) full_case đối với phát biểu case để chỉ dẫn tổng hợp
Ví dụ 7: Gán giá trị ngõ ra trong tất cả tổ hợp của điều kiện trong phát biểu case
always @ (*) begin
  case (sel)
    2'd0: y = a;
    2'd1: y = b;
    2'd2: y = 0;
    2'd3: y = 1;
  endcase
end
Trong ví dụ trên, ngõ ra y được gán giá trị ở tất cả tổ hợp điều kiện trong phát biểu case.
Hình 5: Mạch logic của ví dụ 7
Ví dụ 8: Gán giá trị ngõ ra ở tất cả tổ hợp điều kiện trong phát biểu if/else, trường hợp sel chỉ là 1 bit
always @ (*) begin
  if (sel) y = a;
  else if (!sel) y = b;
end
Trong ví dụ trên, sel chỉ 1 bit, y được gán giá trị trong cả 2 trường hợp sel=0sel=1. Trình tổng hợp Quartus II không tạo Latch trong trường hợp này dù có warning.
Warning (10240): Verilog HDL Always Construct warning at latch_ref.v(95): inferring latch(es) for variable "y", which holds its previous value in one or more paths through the always construct
Hình 6: Mạch logic của ví dụ 8
Ví dụ 9: Gán giá trị ngõ ra ở tất cả tổ hợp điều kiện trong phát biểu if/else, trường hợp sel là 2 bit
always @ (*) begin
  if (sel == 2'd0) y = a;
  else if (sel == 2'd1) y = b;
  else if (sel == 2'd2) y = 0;
  else if (sel == 2'd3) y = 1;
end
Trong ví dụ trên, y được gán giá trị ở tất cả tổ hợp có thể của điều kiện trong phát biểu if/else. Chúng ta mong muốn Latch không được tạo ra nhưng với một điều kiện có số bit nhiều hơn 1 thì trình tổng hợp có thể vẫn tạo Latch trong trường hợp này.
Hình 7: Mạch logic của ví dụ 9, Latch được tạo ra dù ngõ ra được gán ở tất cả các tổ hợp có thể của điều kiện trong pahts biểu if/else
Nếu bạn sử dụng Design Compiler (DC) của Synopsys thì kết quả có thể tương tự như Quartus II. Đối với phát biểu if/else trong always, DC sẽ tạo Latch nếu thiếu mô tả nhánh else.
When an if statement used in a Verilog always block or VHDL process as part of a continuous assignment does not include an else clause, Design Compiler creates a latch
Như vậy đối với phát biểu if, việc mô tả đầy đủ các tổ hợp điều kiện mà không có nhánh else không đảm bảo tránh tạo Latch mà tùy thuộc vào trình tổng hợp. Tương tự, đối với phát biểu case nên dùng default để đảm bảo đã đầy đủ tất cả các tổ hợp điều kiện.
Ví dụ 10: Tránh tạo Latch không mong muốn trong phát biểu case bằng cách dùng default
always @ (*) begin
  case (sel)
    2'd0: y = a;
    2'd1: y = b;
    2'd2: y = 0;
    default: y = 1;
  endcase
end
Việc "gán giá trị trong tất cả tổ hợp của điều kiện" còn có thể đạt được bằng cách khác là mô tả giá trị mặc định của ngõ ra trước các phát biểu if case. Cách mô tả này có tác dụng tương đương như việc sử dụng else hoặc default.
Ví dụ 11: Mô tả giá trị mặc định của ngõ ra trước khi mô tả phát biểu điều kiện để tránh tạo Latch
always @ (*) begin
  y = 0;
  if (sel == 2'd0) y = a;
  else if (sel == 2'd1) y = b;
end
hoặc:
always @ (*) begin
  y = 0;
  case (sel)
    2'd0: y = a;
    2'd1: y = b;
  endcase
end
Trong ví dụ trên, y=0 nếu sel[1:0] khác 0 và khác 1.
Hình 8: Mạch logic của phát biểu case trong ví dụ 11
Một cách khác để tránh tạo Latch không mong muốn mà không cần phải mô tả đầy đủ các tổ hợp điều kiện có thể là dùng phát biểu function. Giá trị ngõ ra sẽ được gán giá trị 0 hoặc 1 tùy trình tổng hợp nếu nó không được gán trong tất cả các tổ hợp điều kiện.
Ví dụ 12: Sử dụng function để tránh tạo Latch không mong muốn
always @ (*) begin
  y = out(sel, a, b);
end 
function reg out;
  input [1:0] in0;
  input in1, in2;

  case (in0)
    2'd0: out = in1;
    2'd1: out = in2;
  endcase

endfunction
hoặc:
always @ (*) begin
  y = out(sel, a, b);
end

function reg out;
  input [1:0] in0;
  input in1, in2;

  if (in0 == 2'd0) out = in1;
  else if (in0 == 2'd1) out = in2;

endfunction
Trong ví dụ trên, function out chỉ xác định giá trị ngõ ra trong 2 trường hợp khi in0=0in0=1. Khi tổng hợp, y=out sẽ có giá trị 0 hoặc 1 nếu sel=in0=2 hoặc sel=in0=3.
Hình 9: Mạch logic cho function dùng phát biểu if trong ví dụ 12, y được gán 0 khi sel khác 0 và khác 1
Một cách khác để đạt được kết quả tổng hợp như function là dùng thuộc tính chỉ dẫn tổng hợp full_case cho phát biểu case.
Ví dụ 13: Dùng thuộc tính full_case để tránh tạo Latch không mong muốn
always @ (*) begin
  (* full_case *)
  case (sel)
    2'd0: y = a;
    2'd1: y = b;
  endcase
end
hoặc:
always @ (*) begin
  case (sel) // synthesis full_case
    2'd0: y = a;
    2'd1: y = b;
  endcase
end
Trong ví dụ trên, cách mô tả thuộc tính dùng (* *) được khuyến cáo sử dụng vì các trình mô phỏng và tổng hợp có thể kiểm tra lỗi cú pháp (syntax) nếu mô tả sai. Cách mô tả thuộc tính như một comment, dùng //, không được khuyến khích vì:
  • Nếu viết sai chính tả ví dụ như full_case thanh fu_case thì trình tổng hợp sẽ bỏ qua, xem như đó là 1 comment và không kiểm tra syntax
  • Mặt khác, tùy trình tổng hợp, việc hỗ trợ từ khóa cho comment này khác nhau. Ví dụ, DC hỗ trợ // synopsys ... còn Quartus II hỗ trợ cả // synthesis ... // synopsys ...
Hình 10: Mạch logic cho ví dụ 13
Trong mạch logic trên, nếu sel=2 hoặc sel=3, ngõ ra mạch so sánh bằng 0 và y=a.
Việc dùng function hoặc thuộc tích full_case để mô tả một mạch tổ hợp không có đầy đủ các tổ hợp điều kiện trong một số trường hợp sau:
  • Tạo ra một mạch không cần quan tâm (don't care) đến giá trị ngõ ra trong một số tổ hợp điều kiện
  • Hoặc tạo ra một mạch có một vài tổ hợp điều kiện không bao giờ xảy ra
Khi dùng với một trong hai mục đích trên, mạch tổ hợp có thể được mô tả bằng một cách tương đương khác là gán giá trị "x" cho ngõ ra trong các trường hợp không sử dụng. Điều này vừa tránh tạo Latch, vừa tạo được logic tối ưu như mong muốn.
Ví dụ 14: Gán giá trị "x" cho ngõ ra
always @ (*) begin  case (sel)
    2'd0: y = a;
    2'd1: y = b;
    default: y = 1'bx;  endcaseend
Việc gán giá trị "x" cho ngõ ra trong các trường hợp điều kiện không sử dụng hoặc không quan tâm giá trị sẽ giúp trình tổng hợp có thể tối ưu logic tốt hơn, nhất là khi số bit điều kiện và số bit ngõ ra lớn. Tuy nhiên, một điểm bất lợi là kết quả mô phỏng giữa mức RTL code và mức cổng (gate netlist) sẽ khác nhau vì khi rơi vào nhánh gán giá trị "x".
  • Mức RTL code, ngõ ra là "x"
  • Mức cổng, ngõ ra là 0 hoặc 1
Điểm bất lợi này cũng xảy ra với phát biểu function, như ví dụ 12.
  • Mức RTL code, ngõ ra giữ nguyên giá trị trước đó, hành vi này giống như ngõ ra có Latch
  • Mức cổng, ngõ ra là 0 hoặc 1
Ngoài ra:
Đối với System Verilog, bạn có thể dùng unique case hoặc priority case để tránh tạo Latch khi mô tả mạch tổ hợp. Nếu  unique hoặc priority được sử dụng thì case không cần phải mô tả đầy đủ các trường hợp nhưng Latch vẫn sẽ không tạo ra. Sự khác nhau của unique priority, các bạn tham khảo trong tài liệu SV.

Ví dụ 15: Dùng unique hoặc priority case để tránh tạo Latch
  unique case (sel)
    2'd0: y = a;
    2'd1: y = b;
  endcase
hoặc
  priority case (sel)
    2'd0: y = a;
    2'd1: y = b;
  endcase
Hình 11: Mạch logic của ví dụ 15



Câu hỏi 2 (2020.01.06):
Khi truyền 1 tín hiệu 1 bit, async_in, từ miền clock A sang miền clock B, tại miền clock B, tín hiệu được đồng bộ bằng 2 Flip-Flop theo miền clock B. Điều này giúp loại bỏ hiện tượng metastable ở ngõ ra mạch đồng bộ, synch_out. Đúng hay sai?

Hình 11: Mạch đồng bộ tín hiệu 1 bit dùng 2 Flip-Flop

Trả lời câu 2:
SAI.
Hiện tượng tín hiệu ngõ ra synch_out bất ổn định (metastable) không thể bị loại bỏ hoàn toàn bởi mạch đồng bộ 2 FF. Mạch đồng bộ 2 FF chỉ có thể "hạn chế" việc xảy ra hiện tượng này. Đây là hiện tượng có tính chất xác suất. Việc mắc nối tiếp 2 hoặc nhiều FF để đồng bộ tín hiệu 1 bit giúp giảm xác suất xảy ra hiện tượng bất ổn định.
Chi tiết được trình bày ở mục 4. MTBF trong bài viết "đồng bộ tín hiệu bất đồng bộ".



Câu hỏi 3 (2020.01.13): 
Trình bày về phép gán blocking và non-blocking trong ngôn ngữ Verilog HDL (định nghĩa, cách sử dụng, hoạt động, những lưu ý, ...)?

Trả lời câu 3: Trả lời này gồm 3 phần
  1. Trình bày về phép gán blocking và nonblocking
  2. Phân tích việc sử dụng 2 phép gán khi mô tả mạch tổ hợp
  3. Phân tích việc sử dụng 2 phép gán khi mô tả mạch tuần tự
1) Tổng quan

Phép gán blocking (chặn) và nonblocking (không chặn) là loại phát biểu gán thủ tục (procedural assignment statement). Các phép gán này được sử dụng trong các thủ tục always, initial, task function.
Ký hiệu:
  • Phép gán blocking: = (Verilog) và các toán tử gán mới +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <<<=, >>>= (System Verilog)
  • Phép gán nonblocking: <=
Trong một khối tuần tự (sequential block), một phép gán blocking phải được thực thi trước khi thực thi các phát biểu khác mô tả sau phép gán này. Nghĩa là, phép gán blocking chặn (block) sự thực thi của các phát biểu nằm phía sau nó. Khối tuần tự là khối được đóng gói trong cặp từ khóa begin/end.
Trong một khối song song (parallel block), một phép gán blocking không chặn sự thực thi của các phát biểu khác được mô tả sau phép gán này. Khối song song là khối được định nghĩa trong cặp từ khóa fork/(join/join_any/join_none), gọi chung là fork/join.
Phép gán nonblocking cho phép sắp xếp việc thực thi gán giá trị mà không chặn (block) sự thực thi của các phát biểu khác mô tả sau phép gán này. Trong tài liệu IEEE, nó là “without blocking the procedural flow”, nghĩa là không chặn quá trình thực thi các thủ tục.
Theo các quy định trên đây về phép gán blocking và nonblocking thì các nhận xét chung chung như “Phép gán blocking được thực hiện tuần tự hay thực thi theo thứ tự còn phép gán nonblocking được thực hiện song song” là chưa chính xác vì hai phép gán này đều có thể thực hiện tuần tự hoặc đồng thời tùy thuộc vào việc nó được mô tả trong một khối begin/end hay fork/join.
2) Hoạt động của phép gán blocking và nonblocking
Verilog và System Verilog là ngôn ngữ xử lý theo mô hình sự kiện rời rạc. Một quá trình mô phỏng được chia thành các khe thời gian. Mỗi khe thời gian được chinh thành nhiều miền sự kiện khác nhau. Mỗi sự kiện sẽ được sắp xếp (schedule) vào các miền này để thực thi. Các bạn đọc thêm ở bài viết về các miền sự kiện của Verilog và System Verilog.

Phép gán blocking sẽ được thực thi việc ước lượng giá trị vế phải và gán sang vế trái trong một bước trong miền ACTIVE của 1 khe thời gian. Trong khi đó, phép gán nonblocking được thực thi bằng 2 bước:
  • Ước lượng giá trị bên vế phải phép gán tại miền ACTIVE và sắp xếp việc gán giá trị vào miền NBA (NonBlocking Assigment) của một khe thời gian
  • Gán giá trị từ vế phải sang vế trái tại miền NBA
Hình 1: Miền thực thi của phép gán blocking và nonblocking trong một khe thời gian Verilog (trái) và SV (phải)
Ví dụ sau đây sẽ giải thích sự khác nhau của phép gán blocking và nonblocking.
Ví dụ 1: Phép gán blocking
initial begin
  y = #5 1'b1;
  y = #3 1'b0;
end
initial fork
  x = #5 1'b1;
  x = #3 1'b0;
join
Trong ví dụ trên y được gán giá trị trong một khối tuần tự begin/endx được gán giá trị trong một khối song song fork/join. Một giá trị delay được dùng trên mỗi phép gán.
Đối với y, sau 5 đơn vị thời gian, y sẽ được gán bằng 1. Sau 3 đơn vị thời gian nữa, T=8, y được gán bằng 0. Điều này là vì, trong một khối tuần tự, các phép gán blocking sẽ thực hiện tuần tự từ trên xuống, phép gán y = #5 1’b1 phải thực hiện trước rồi mới đến y = #3 1’b0. Trường hợp này, phép gán y = #5 1’b1 chặn sự thực thi của phép gán y = #3 1’b0.
Đối với x, sau 3 đơn vị thời gian x=0 và sau thêm 2 đơn vị thời gian (thời điểm t=5) nữa thì x=1. Điều này là vì, trong một khối song song, các phép gán blocking thực thi đồng thời, phép gán trước không chặn phép gán sau.
Hình 2: waveform của ví dụ 1
Ví dụ 2: Phép gán nonblocking
initial begin
  y <= #5 1'b1;
  y <= #3 1'b0;
end
initial fork
  x <= #5 1'b1;
  x <= #3 1'b0;
join
Trong ví dụ trên đây, x y có waveform như nhau. Sau 3 đơn vị thời gian, x=y=0 và sau thêm 2 đơn vị thời gian nữa thì x=y=1. Trong khối tuần tự, phép gán nonblocking y <= #5 1'b1 không chặn việc thực thi của phép gán y <= #3 1'b0. Khi thực thi mô phỏng, giá trị 0 được ước lượng và việc gán được sắp xếp vào vùng NBA tại thời điểm T=3, giá trị 1 được ước lượng và việc gán được sắp xếp vào thời điểm T=5. Trong khối song song, việc thực thi cũng tương tự.
Hình 3: waveform của ví dụ 2
Hành vi của phép gán blocking phù hợp để mô tả mạch tổ hợp. Ngõ ra một mạch tổ hợp đáp ứng ngay lập tức theo sự thay đổi của ngõ vào, không tính độ trễ vật lý. Một phép gán blocking cập nhật vế trái ngay lập tức, không trì hoãn, khi giá trị vế phải thay đổi nên nó phù hợp cho việc mô tả mạch tổ hợp.
Hành vi của phép gán nonblocking phù hợp để mô tả mạch tuần tự dùng Latch hoặc Flip-Flop. Ngõ ra các mạch tuần tự, từ một Latch hoặc FF, thay đổi đồng thời tại mức/cạnh tích cực của clock. Một phép gán nonblocking xác định giá trị hiện tại của tất cả các ngõ vào trên các Latch và FF khi có mức/cạnh tích cực của clock. Sau đó, thực thi việc gán giá trị này đến ngõ ra của Latch và FF. Thời điểm thực hiện phép gán, sự thay đổi giá trị của ngõ vào không ảnh hưởng đến giá trị (đã được ước lượng xong) được gán cho ngõ ra. Như vậy, phép gán nonblocking phù hợp cho việc mô tả mạch tuần tự.
Trong mô tả RTL code khả tổng hợp, phép gán blocking và nonblocking được dùng trong khối tuần tự (procedural block) dùng always hoặc fucntion kết hợp cặp từ khóa begin/end. Việc dùng phép gán blocking hoặc nonblocking đều có thể tạo ra được mạch tổ hợp hoặc tuần tự như mong muốn. Tuy nhiên, phép gán blocking chỉ phù hợp để mô tả mạch tổ hợp và nonblocking chỉ phù hợp để mô tả mạch tuần tự. Phần sau đây sẽ phân tích chi tiết điều này.
3) Mô tả mạch tổ hợp
Ví dụ 3: Mô tả mạch tổ hợp bằng phép gán blocking (nên dùng)
always @* begin
  temp = in0 & in1;
  y = ~ temp;
end
Ví dụ 4: Mô tả mạch tổ hợp bằng phép gán nonblocking (không nên dùng)
always @* begin  temp <= in0 & in1;
  y <= ~ temp;
end
Ví dụ 5: Mô tả mạch tổ hợp bằng phép gán nonblocking trong always_comb (không nên dùng)
always_comb begin
  x <= in0 & in1;
  y <= ~x;
end
Tất cả các cách mô tả trên đây đều tổng hợp ra đúng mạch logic mong muốn nhưng hành vi thực thi mô phỏng thì không giống nhau.
Hình 3: Mạch logic của ví dụ 3, 4 và 5
Để kiểm chứng kết quả và hành vi thực thi mô phỏng, chúng ta thực hiện như sau:
  • Thêm task $display vào cuối khối always để kiểm tra kết quả các biến trong vùng ACTIVE và số lần vùng ACTIVE được thực thi.
  • Thêm task $strobe vào cuối khối always để kiểm tra kết quả các biến trong vùng POSTPONED, Đây là thời điểm kết thúc quá trình mô phỏng trong 1 khe thời gian nên nó thể hiện kết quả cuối cùng của các biến trong một khe thời gian.
  • Thêm một khối initial để khởi tạo giá trị in0 = 0 tại thời điểm mô phỏng T=0.
  • Kiểm tra thông điệp hiển thị trên terminal hoặc cửa sổ transcript của trình mô phỏng. Đồng thời, kiểm tra waveform của các tín hiệu.
Ví dụ 6: Code kiểm tra việc thực thi mô phỏng của phép gán blocking (nên dùng)
always @ (in0, in1) begin
  temp = in0 & in1;
  y = ~temp;
  $display("[%t] ACTIVE: in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
  $strobe("[%t] POSTPONED: in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
end
initial begin
  in0 = 0;
end
Đoạn code trên thực thi như sau:
  • Tại thời điểm T=0, tại vùng ACTIVE, in0 được gán bằng 0 và khởi động always.
  • x được gán bằng 0 vì vế phải in0 & in1 bằng 0 tại vùng ACTIVE
  • y được gán bằng ~temp=1 tại vùng ACTIVE
Kết quả hiển thị trên terminal cho thấy rằng khối always thực thi 1 lần và cho kết quả đúng ngay ở vùng ACTIVE.
# [ 0] ACTIVE: in0 = 0, in1 = x, temp = 0, y = 1
# [ 0] POSTPONED: in0 = 0, in1 = x, temp = 0, y = 1
Hình 4: waveform của ví dụ 6
Kết quả mô phỏng và hành vi thực thi tương ứng như một mạch tổ hợp. Nếu dùng phép gán blocking, cho dù danh sách độ nhạy được mô tả bằng cách nào thì kết quả mô phỏng vẫn chính xác. Các cách mô tả danh sách độ nhạy:
always @ (in0, in1)
hoặc:
always @ (*) //tương đương với always @ (in0, in1)
hoặc:
always @* //tương đương với always @ (in0, in1, temp)
hoặc:
always_comb //hỗ trợ bởi System Verilog
Tuy nhiên đối với phép gán nonblocking, sự thực thi mô phỏng và kết quả mô phỏng sẽ khác nhau tùy vào danh sách độ nhạy. Chúng ta cùng xem xét loạt ví dụ sau đây, các ví dụ này có mô tả logic code như nhau nhưng danh sách độ nhạy khác nhau.
Ví dụ 7: Dùng phép gán nonblocking với danh sách độ nhạy tường minh (không sử dụng)
always @ (in0, in1) begin
  temp <= in0 & in1;
  y <= ~temp;
  $display("[%t] in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
  $strobe("[%t] POSTPONED: in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
end
initial begin
  in0 = 0;
end
Kết quả mô phỏng của ví dụ trên là temp=0y=x (don’t care), một giá trị không xác định vì:
  • Tại thời điểm T=0, in0=0 kích hoạt khối always thực thi.
  • Tại vùng ACTIVE, in0 & in1 = 0 nhưng không được gán cho temp. ~temp là một giá trị không xác định nhưng không được gán cho y. Vì vậy temp y đều là giá trị “x” (don’t care) khi kết thúc vùng ACTIVE. Hai phép gán giá trị cho temp y được sắp xếp đến vùng NBA.
  • Tại vùng NBA, temp được gán bằng 0, y được gán một giá trị không xác định.
Trường hợp này, kết quả mô phỏng không đúng với mạch logic thực tế.
Hiển thị trên terminal cho thấy khối always thực thi 1 lần, giá trị của temp y không được gán tại vùng ACTIVE. Kết thúc thời điểm T=0, vùng POSTPONED, giá trị của y thể hiện không đúng với hành vi của một mạch tổ hợp.
# [ 0] ACTIVE: in0 = 0, in1 = x, temp = x, y = x
# [ 0] POSTPONED: in0 = 0, in1 = x, temp = 0, y = x
Hình 5: waveform của ví dụ 7
Ví dụ 8: Dùng phép gán nonblocking với danh sách độ nhạy bao gồm biến trung gian (không sử dụng)
always @ (in0, in1, temp) begin
  temp <= in0 & in1;
  y <= ~temp;
  $display("[%t] in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
  $strobe("[%t] POSTPONED: in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
end
initial begin
  in0 = 0;
end
Kết quả mô phỏng của ví dụ trên là temp=0 y=1 vì:
  • Tại thời điểm T=0, in0=0 kích hoạt khối always thực thi.
  • Tại vùng ACTIVE, in0 & in1 = 0 nhưng không được gán cho temp. ~temp là một giá trị không xác định nhưng không được gán cho y. Vì vậy temp và y đều là giá trị “x” (don’t care) khi kết thúc vùng ACTIVE. Hai phép gán giá trị cho temp và y được sắp xếp đến vùng NBA.
  • Tại vùng NBA, temp được gán bằng 0, y được gán một giá trị không xác định. Vì temp có trong danh sách độ nhạy nên nó kích hoạt khối always thực thi lần nữa.
  • Tại vùng ACTIVE, in0 & in1 = 0 nhưng không được gán cho temp, temp đang bằng 0. ~temp là 1 nhưng không được gán cho y. Vì vậy temp=0 và y=x khi kết thúc vùng ACTIVE. Hai phép gán giá trị cho temp và y được sắp xếp đến vùng NBA.
  • Tại vùng NBA, temp được gán bằng 0 (giữ nguyên giá trị trước đó), y được bằng 1.
Trường hợp này, kết quả cuối cùng đúng với mạch logic thực tế nhưng:
  • Cần thêm biến trung gian vào danh sách độ nhạy
  • Giá trị ngõ ra của logic tổ hợp đạt được giá trị mong muốn bằng cách thực thi always nhiều lần.
Hiển thị trên terminal cho thấy khối always thực thi 2 lần tại thời điểm T=0.
# [ 0] ACTIVE: in0 = 0, in1 = x, temp = x, y = x
# [ 0] ACTIVE: in0 = 0, in1 = x, temp = 0, y = x
# [ 0] POSTPONED: in0 = 0, in1 = x, temp = 0, y = 1
Hình 6: waveform của ví dụ 8
Danh sách độ nhạy ngầm hiểu @(*) tương đương @ (in0, in1) nên kết quả sẽ giống với ví dụ 7. Còn @* tương đương với @(in0, in1, temp) nên kết quả sẽ giống với ví dụ 8. Tuy nhiên, một số trình mô phỏng có thể đối xử với @(*)@* theo một cách khác. Ví dụ như QuestaSim phiên bản 10.2c, @(*)@* xem danh sách độ nhạy ngầm hiểu gồm cả các thay đổi bên trong always.
Ví dụ 9: Dùng phép gán nonblocking với danh sách độ nhạy @(*) (không sử dụng)
always @(*) begin
  temp <= in0 & in1;
  y <= ~temp;
  $display("[%t] in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
  $strobe("[%t] POSTPONED: in0 = %b, in1 = %b, temp = %b, y = %b", $time, in0, in1, temp, y);
end
initial begin
  in0 = 0;
end
Thực thi mô phỏng trên QuestaSIM 10.2c như sau:
  • Tại thời điểm T=0, tại vùng ACTIVE, in0 được gán bằng 0 và khởi động always.
  • Tại vùng ACTIVE, in0 & in1 = 0 nhưng không được gán cho x. ~x là một giá trị không xác định nhưng không được gán cho y. Vì vậy x và y đều là giá trị “x” (don’t care) khi kết thúc vùng ACTIVE
  • Tại vùng NBA, x được gán 0, y được gán một giá trị không xác định. Khối always tiếp tục lặp lại vì x thay đổi giá trị.
  • Tại vùng ACTIVE, in0 & in1 = 0 nhưng không được gán cho x, x đang bằng 0. ~x là 1 nhưng không được gán cho y. Lúc này, x=0 và y là giá trị không xác định khi kết thúc vùng ACTIVE
  • Tại vùng NBA, x vẫn bằng 0, y được gán là 1. Khối always tiếp tục lặp lại vì y thay đổi giá trị. Chú ý, y đổi giá trị làm always thực thi lại một lần nữa
  • Tại vùng ACTIVE, in0 & in1 = 0 nhưng không được gán cho x, x đang bằng 0. ~x là 1 nhưng không được gán cho y, y đang bằng 1. Lúc này, x=0 và y=1 khi kết thúc vùng ACTIVE
Kết quả trên termial cho thấy khối always được lặp lại 3 lần tại thời điểm T=0:
# [ 0] ACTIVE: in0 = 0, in1 = x, temp = x, y = x
# [ 0] ACTIVE: in0 = 0, in1 = x, temp = 0, y = x
# [ 0] ACTIVE: in0 = 0, in1 = x, temp = 0, y = 1
# [ 0] POSTPONED: in0 = 0, in1 = x, temp = 0, y = 1
Trường hợp này, kết quả cuối cùng đúng với mạch logic thực tế nhưng:
  • Giá trị ngõ ra của logic tổ hợp đạt được giá trị mong muốn bằng cách thực thi always nhiều lần.
Kết quả mô phỏng với @(*) và @* là như nhau và như mô tả trên đây khi dùng QuestaSim 10.2c. Kết quả này có thể khác khi bạn dùng trình mô phỏng khác hoặc phiên bản khác của QuestaSim.
Nếu dùng System Verilog, bạn có thể dùng always_comb, và kết quả và sự thực thi giống với trường hợp @(in0, in1) trong cùng ví dụ đang phân tích. Chú ý, trong ví dụ này, always_comb @(In0, in1) cho kết quả giống nhau. Điều này không có nghĩa là hai cách mô tả này tương đương nhau.
Với những phân tích trên đây, blocking hoàn toàn phù hợp để mô hình hóa một mạch tổ hợp trên cả phương diện tổng hợp và mô phỏng. Nonblocking không phù hợp để mô tả một mạch tổ hợp vì có thể kết quả mô phỏng có thể không chính xác hoặc tốn nhiều bước tính toán để đạt được kết quả cuối cùng.
4) Mô tả mạch tuần tự có FF
Đối với mạch tuần tự có FF, việc dùng phép gán blocking hay nonblocking đều có thể tổng hợp được logic mong muốn. Tuy nhiên, việc dùng phép gán blocking để mô tả mạch tuần tự sẽ có nhiều rủi ro nên phép gán này không được dùng khi mô tả mạch tuần tự.
Giả sử chúng ta cần mô tả một mạch tuần tự có 2 FF nối tiếp giữa ngõ vào in0 và ngõ ra y.
Ví dụ 10: Mô tả mạch tuần tuần tự bằng phép gán nonblocking (nên dùng)
always @ (posedge clk) begin
  temp <= in0;
  y <= temp;
end
hoặc:
always @ (posedge clk) begin
  temp <= in0;
  y <= temp;
end
hoặc:
always @ (posedge clk) begin
  temp <= in0;
end
always @ (posedge clk) begin
  y <= temp;
end

Ví dụ 11: Mô tả mạch tuần tự bằng phép gán blocking trong cùng một khối always (không nên dùng)
always @ (posedge clk) begin  y = temp;
  temp = in0;
end

Ví dụ 12: Mô tả mạch tuần tự bằng phép gán blocking trong hai khối always (không nên dùng)
always @ (posedge clk) begin
  temp = in0;
end
always @ (posedge clk) begin
  y = temp;
end
Kết quả tổng hợp của cả 3 ví dụ trên đây đều như nhau.
Hình 7: Mạch logic của ví dụ 10, 11 và 12
Đối với ví dụ 10, kết quả tổng hợp và mô phỏng luôn đúng và giống nhau cho cả 3 trường hợp. Điểm khác nhau duy nhất của 3 đoạn code là thứ tự thực hiện phép gán tại vùng NBA:
  • Thứ tự thực thi gán cho đoạn code đầu tiên
    • temp được gán
    • y được gán
  • Thứ tự thực thi gán cho đoạn code thứ 2
    • y được gán
    • temp được gán
  • Thứ tự gán cho đoạn code thứ 3 là tùy ý, tùy vào trình mô phỏng sắp xếp
Đối với ví dụ 11, kết quả tổng hợp và mô phỏng đúng như mong muốn. Tuy nhiên, điểm bất lợi là phải mô tả đúng theo thứ tự này. Theo đó, trong một chuỗi FF, FF của tầng sau phải được mô tả trước, hướng từ ngõ ra đến ngõ vào.
Hình 8: Thứ tự mô tả phép gán khi dùng blocking để mô tả mạch tuần tự
Ví dụ 11 được điều chỉnh lại như sau để có thể kiểm chứng kết quả mô phỏng:
  • Thêm task $display vào cuối khối always để kiểm tra kết quả các biến trong vùng ACTIVE và số lần vùng ACTIVE được thực thi.
  • Thêm task $strobe vào cuối khối always để kiểm tra kết quả các biến trong vùng POSTPONED, Đây là thời điểm kết thúc quá trình mô phỏng trong 1 khe thời gian nên nó thể hiện kết quả cuối cùng của các biến trong một khe thời gian.
  • Thêm một khối initial để khởi tạo giá trị in0 = 1 tại thời điểm mô phỏng T=0 và tạo xung clock clk.
  • Kiểm tra thông điệp hiển thị trên terminal hoặc cửa sổ transcript của trình mô phỏng. Đồng thời, kiểm tra waveform của các tín hiệu
Ví dụ 13: Code dùng để phỏng ví dụ 11
always @ (posedge clk) begin
  y = temp;
  temp = in0;
  $display("[%t] ACTIVE: in0 = %b, temp = %b, y = %b", $time, in0, temp, y);
  $strobe("[%t] POSTPONED: in0 = %b, temp = %b, y = %b", $time, in0, temp, y);
end
initial begin
  clk = 0;
  in0 = 1;
  forever #1 clk = ~clk;
end
Trình tự thực thi mô phỏng như sau:
  • Tại T=1, cạnh lên xung clock clk xuất hiện, khối always được kích hoạt
  • Giá trị temp được ước lượng là x và y được gán giá trị x trong vùng ACTIVE
  • Giá trị in0 được ước lượng là 1 và temp được gán là 1 trong vùng ACTIVE
  • Tại T=3, cạnh lên xung clock clk xuất hiện, khối always được kích hoạt
  • Giá trị temp được ước lượng là 1 và y được gán giá trị 1 trong vùng ACTIVE
  • Giá trị in0 được ước lượng là 1 và temp vẫn giữ giá trị 1 trong vùng ACTIVE
Kết quả hiện thị trên terminal cho thấy always được thực thi 1 lần tại thời điểm cạnh lên clk, T=1 và T=3.
# [ 1] ACTIVE: in0 = 1, temp = 1, y = x
# [ 1] POSTPONED: in0 = 1, temp = 1, y = x
# [ 3] ACTIVE: in0 = 1, temp = 1, y = 1
# [ 3] POSTPONED: in0 = 1, temp = 1, y = 1
Hình 9: waveform của ví dụ 13
Nếu thứ tự được đảo lại như ví dụ sau đây, cả kết quả tổng hợp và mô phỏng đều sai.
Ví dụ 14: Code minh họa việc tổng hợp và mô phỏng sai logic tuần tự khi dùng phép gán blocking
always @ (posedge clk) begin
  temp = in0;  y = temp;  $display("[%t] ACTIVE: in0 = %b, temp = %b, y = %b", $time, in0, temp, y);
  $strobe("[%t] POSTPONED: in0 = %b, temp = %b, y = %b", $time, in0, temp, y);
end
initial begin
  clk = 0;
  in0 = 1;
  forever #1 clk = ~clk;
end
Việc mô phỏng được thực thi như sau:
  • Tại T=1, cạnh lên xung clock clk xuất hiện, khối always được kích hoạt
  • Giá trị in0 được ước lượng là 1 và temp được gán là 1 trong vùng ACTIVE
  • Giá trị temp được ước lượng là 1 và y được gán giá trị 1 trong vùng ACTIVE
Hình 10: waveform của ví dụ 14
Khi tổng hợp, chỉ một FF được tạo ra giữa in0 và y.
Hình 11: Mạch logic của ví dụ 14
Đối với ví dụ 12, hai phép gán được mô tả trong 2 khối always độc lập. Kết quả tổng hợp sẽ có được 2 FF giữa in0 và y như mong muốn vì đây là hai khối always độc lập. Tuy nhiên, việc mô phỏng RTL code thì có vấn đề.
Ví dụ 14: Code minh họa việc mô phỏng sai logic tuần tự khi dùng phép gán blocking như ví dụ 12
always @ (posedge clk) begin
  temp = in0;
  $display("[%t][temp] ACTIVE: in0 = %b, temp = %b, y = %b", $time, in0, temp, y);
  $strobe("[%t][temp] POSTPONED: in0 = %b, temp = %b, y = %b", $time, in0, temp, y);
end
always @ (posedge clk) begin
  y = temp;
  $display("[%t][y] ACTIVE: in0 = %b, temp = %b, y = %b", $time, in0, temp, y);
end
initial begin
  clk = 0;
  in0 = 1;
  forever #1 clk = ~clk;
end
Kết quả mô phỏng là không thể xác định do chạy đua (race condition) xảy ra giữa hai khối always. Trường hợp này, thứ tự thực thi hai phép gán trong vùng ACTIVE không thể xác định. Thứ tự này phụ thuộc trình mô phỏng sắp xếp vì hai phép gán thuộc hai khối always độc lập. Kết quả là:
  • Nếu temp = in0 được thực thi trước thì kết quả giống như giữa in0 và y chỉ có 1 FF. Trên terminal, ngay thời điểm T=1, cạnh lên đầu tiên của clk, y=temp=1:
# [ 1][temp] ACTIVE: in0 = 1, temp = 1, y = x
# [ 1][y] ACTIVE: in0 = 1, temp = 1, y = 1
# [ 1][temp] POSTPONED: in0 = 1, temp = 1, y = 1
  • Nếu y = temp được thực thi trước thì kết quả giống như giữa in0 và y có 2 FF. Trên terminal, T=1, cạnh lên clk thứ 1, chỉ temp=in0=1; đến cạnh lên clk tiếp theo, T=3, y=temp=1:
# [ 1][y] ACTIVE: in0 = 1, temp = x, y = x
# [ 1][temp] ACTIVE: in0 = 1, temp = 1, y = x
# [ 1][temp] POSTPONED: in0 = 1, temp = 1, y = x
# [ 3][y] ACTIVE: in0 = 1, temp = 1, y = 1
# [ 3][temp] ACTIVE: in0 = 1, temp = 1, y = 1
# [ 3][temp] POSTPONED: in0 = 1, temp = 1, y = 1
Trên QuestaSim, bạn có thể đảo thứ tự code của hai khối always để thấy được điều này. Chú ý, theo quy chuẩn, thứ tự của 2 khối always trong một module không quyết định thứ tự thực thi của chúng. Thứ tự thực thi của chúng hoàn toàn do trình mô phỏng quyết định. Ở đây, có thể QuestaSim đang thực thự theo trình tự xuất hiện trong code nên bạn có thể dùng cách đảo vị trí khối always để thử nghiệm. 
Với nhưng ví dụ đã nêu, blocking sẽ không được dùng để mô tả mạch tuần tự vì nó có thể làm cho kết quả tổng hợp và mô phỏng sai với mong muốn. Nonblocking phù hợp để mô hình hóa mạch tuần tự vì nó cho kết quả tổng hợp và mô phỏng như nhau trong tất cả các trường hợp mô tả RTL code.


Câu hỏi 4 (2020.02.02):
Liệt kê các kỹ thuật có thể áp dụng để đồng bộ một tín hiệu nhiều bit (hai bit trở lên) giữa hai miền clock bất đồng bộ? Hãy nêu rõ vấn đề gặp phải, lý do áp dụng và ưu nhược điểm của kỹ thuật bạn đã nói nếu có thể.

Trả lời:
Tùy vào trường hợp cụ thể, một trong các phương pháp đồng bộ sau đây có thể được áp dụng để đồng bộ tín hiệu nhiều bit:
  • Sử dụng logic kiểm tra giá trị tín hiệu nhận được để đảm bảo nhận được giá trị đúng
  • Sử dụng tín hiệu cho phép (enable) để xác định thời điểm giá trị hợp lệ
  • Sử dụng cơ chế chuyển đổi giá trị thay đổi nhiều bit thành giá trị chỉ thay đổi 1 bit
  • Sử dụng giao thức bắt tay giữa hai miền clock để nhận biết khi nào giá trị được truyền và khi nào giá trị đã được nhận. Có hai loại giao thức bắt tay phổ biến là:
    • Bắt tay 4 pha (4-phase handshake)
    • Bắt tay 2 pha (2-phase handshake)
  • Sử dụng bộ nhớ đệm giữa hai miền như FIFO, RAM, ...
Chi tiết về các kỹ thuật đồng bộ, ưu điểm, nhược điểm và phạm vi ứng dụng được trình bày ở đây.



Câu hỏi 5 (2020.02.04):
Các phương pháp thiết kế giúp giảm công suất tiêu thụ (low power) trong mạch số mà bạn biết?

Trả lời:
Một số phương pháp thiết kế giúp giảm công suất tiêu thụ:

  • Phân chia miền clock (multi clock domain)
  • Hiệu chỉnh tần số clock (clock frequency scaling)
  • Tối ưu tài nguyên thiết kế (resource optimization)
  • Clock gating
  • Power gating
  • Phân chia miền điện áp (multi voltage hoặc multi power domain)
  • Sử dụng đa mức điện áp ngưỡng (multi threshold)

Chi tiết được mô tả trong bài viết "Các kỹ thuật thiết kế giúp giảm công suất tiêu thụ"



Lịch sử cập nhật:
2020.01.12 - Hoàn thành câu 1
2020.01.15 - Thêm chú thích về việc không dùng mạch tuần tự bất đồng bộ
2020.01.15 - Trả lời câu 2
2020.01.17 - Thêm mô tả unique/priority case cho câu 1
2020.01.19 - Trả lời câu 3
2020.02.02 - Trả lời câu 4
2020.03.01 - Trả lời câu 5

Tài liệu tham khảo:
[1] IEEE Computer Society and the IEEE Standards Association Corporate Advisory Group; IEEE Standard for SystemVerilog— Unified Hardware Design, Specification, and Verification Language; IEEE Std 1800™-2017; 6 December 2017
[2] Clifford E. Cummings, Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!; Sunburst Design, Inc; SNUG-2000

0 bình luận:

Đăng nhận xét