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

    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

    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

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

    Vi mạch và Ứng dụng

Chủ Nhật, 13 tháng 8, 2017

[Verilog][System Verilog] Máy trạng thái hữu hạn FSM

Bài viết này phân tích cấu trúc của máy trạng thái hữu hạn FSM (Finite State Machine), cách mô tả RTL code của FSM và các vấn đề liên quan khác trên một ví dụ cụ thể về điều khiển đèn giao thông.

1. Tổng quan về FSM

1.1 Khái niệm

Máy trạng thái hữu hạn, viết tắt là FSM, là một thành phần được sử dụng phổ biến trong thiết kế vi mạch số với ưu điểm là dễ kiểm soát quá trình hoạt động của thiết kế và dễ debug hoạt động của thiết kế.
Hình 1. Mô hình cơ bản của FSM
 FSM gồm có 3 thành phần cơ bản như sau:
  1. Mạch tạo trạng thái kế tiếp (Next state logic) là mạch tổ hợp phụ thuộc vào ngõ vào FSM và giá trị trạng thái hiện tại lấy từ bộ nhớ trạng thái (state memory)
  2. Bộ nhớ trạng thái (state memory) là phần tử lưu trạng thái hiện tại của FSM nó có thể là Flip-Flop, Latch, ... lấy ngõ vào từ mạch tạo trạng thái kế tiếp. Bộ nhớ trạng thái thường được sử dụng trong các thiết kế đồng bộ là FF hoạt động theo xung clock. Một tín hiệu reset có thể phải sử dụng để khởi động FSM đến một giá trị ban đầu. Reset không cần sử dụng đối với các FSM luôn hoạt động đúng dù giá trị ban đầu của FF là bao nhiêu.
  3. Mạch tạo ngõ ra (output logic) là mạch tổ hợp tạo giá trị ngõ ra tương ứng với trạng thái hiện tại của FSM. Mạch này lấy ngõ vào là giá trị trạng thái hiện tại và có thể tổ hợp thêm ngõ vào của FSM
1.2 Phân loại

FSM được chia làm 2 loại:
  1. FSM Moore là loại có mạch tạo ngõ ra không phụ thuộc trực tiếp vào ngõ vào FSM
  2. FSM Mealy là loại có mạch tạo ngõ ra phụ thuộc trực tiếp vào ngõ vào FSM
Hình 2. FSM Moore
Hình 3: FSM Mealy
Với FSM Mealy, mạch tạo trạng thái kế tiếp và ngõ ra có thể được thay bằng ROM trong một số FPGA. Thay vì sử dụng mạch logic để tính toán giá trị ngõ ra và trạng thái kế tiếp thì bộ ROM được sử dụng để lưu các giá trị này ứng với mỗi trạng thái hiện tại. Lúc này ngõ vào và giá trị trạng thái hiện tại được sử dụng để điều khiển và tạo địa chỉ truy cập ROM.
Hình 4. FSM sử dụng ROM để lưu giá trị ngõ ra và trạng thái kế tiếp
Các bạn có thể phân biệt đâu là FSM Moore và đâu là FSM Mealy ở các mạch trong hình sau đây? (Cấu trả lời có ở cuối bài viết nhưng các bạn hãy thử suy nghĩ và cho đáp án của riêng mình).
Hình 5.1 Phân biệt FSM Moore và FSM Mealy
1.3 So sánh FSM Moore và FSM Mealy

FSM Moore có tính an toàn cao hơn FSM Mealy vì ngõ ra FSM được tổ hợp từ giá trị FF nên nếu được kết nối đến các khối khác thì tín hiệu này có thể được sử dụng làm đường hồi tiếp trở lại tổ hợp với các mạch tạo ngõ vào cho FSM. Còn đối với FSM Mealy thì việc nay sẽ tạo ra mạch bất đồng bộ.
Hình 5.2 Sử dụng FSM Moore để tránh tạo mạch bất đồng bộ không mong muốn khi hồi tiếp ngõ ra

Độ trễ ngõ ra FSM Moore được xác định rõ ràng và giới hạn trong FSM tính từ FF đi qua mạch tổ hợp tạo ngõ ra. Trong khi ngõ ra FSM Mealy phụ thuộc cả vào ngõ vào nên độ trễ của các tầng trước nối đến ngõ vào FSM cũng được tính vào độ trễ ngõ ra FSM và có thể là đường critical path (đường có độ trễ lớn nhất).
Hình 5.3 Độ trễ ngõ ra của FSM Mealy phụ thuộc vào độ trễ ngõ vào nhưng FSM Moore thì không
Mealy có ưu điểm là số trạng thái ít hơn Moore với cùng một chức năng và ngõ ra đáp ứng ngay theo sự thay đổi của ngõ vào mà không cần chờ đến cạnh lên xung clock. Nếu dùng Mealy thì cần đặc biệt chú ý đến vấn đề hồi tiếp và độ trễ cộng dồn như đã trình bày ở trên.

2. Thiết kế FSM

2.1 Yêu cầu
Sử dụng FSM để thiết kế một bộ điều khiển đèn giao thông tại một ngã tư đường với yêu cầu:
  1. Thời gian dừng tại mỗi trạng thái đèn có thể cấu hình được trước khi biên dịch RTL code
  2. Tần số điều khiển đèn là 1 Hz, tương ứng với chu kỳ 1 s
  3. Sự chuyển trạng thái đèn được xác định như bảng sau
  4. Mỗi đường sẽ có 3 tín hiệu điều khiển đèn tương ứng với 3 màu Green/Yellow/Red
  5. Trạng thái khởi động là trạng thái đèn trên cả STREET A và STREET B đều đỏ (Red)
Hình 6. Bảng chuyển trạng thái đèn giao thông

Hình 7. Sơ đồ ngã tư với đèn giao thông
2.2 Phân tích tổng quan
Từ yêu cầu ban đầu, chúng ta sẽ có một thiết kế gồm các tín hiệu như sau:
Hình 8. Sơ đồ tín hiệu giao tiếp của bộ điều khiển đèn giao thông
Trong đó:
  1. clk là xung clock tần số 1 Hz
  2. rst_n là tín hiệu dùng để khởi động bộ điều khiển đến trạng thái ban đầu Red-Red khi nó tích cực mức thấp
  3. street_a[2:0] là tín hiệu điều khiển đèn trên đường STREET A. Thứ tự bit từ 0 đến 2 tương ứng với trạng thái Red/Yellow/Green. Tích cực mức 1.
  4. street_b[2:0] là tín hiệu điều khiển đèn trên đường STREET B. Thứ tự bit từ 0 đến 2 tương ứng với trạng thái Red/Yellow/Green. Tích cực mức 1.
Bảng giá trị ngõ ra tương ứng với các trạng thái đèn như sau:
Hình 9. Bảng giá trị ngõ ra bộ điều khiển tương ứng với trạng thái đèn
Căn cứ trên hoạt động mà yêu cầu đề ra, sơ đồ khối của bộ điều khiển đèn giao thông như sau:
  1. TIME_COUNTER: dùng để đếm xunh clock xác định thời gian duy trì trạng thái của đèn. Đây là thời gian có thể cấu hình được trước khi biên dịch như yêu cầu đặt ra. Ba tín hiệu g_end, r_end và y_end báo thời điểm kết thúc của trạng thái GREEN, RED và YELLOW. Ba tín hiệu fsm_g, fsm_r và fsm_y báo trạng thái GREEN, REG và YELLOW để bộ đếm hoạt động theo giá trị cấu hình phù hợp.
  2. FSM: là máy trạng thái sẽ tạo ngõ ra street_a, street_b và fsm_g, fsm_r, fsm_y
Hình 10. Sơ đồ khối của bộ điều khiển đèn giao thông

2.3 Phân tích khối FSM
Ứng với 6 cặp trạng thái nên chúng ta có FSM 6 trạng thái như sau:
Hình 11. FSM điều khiển đèn giao thông
Trong đó, ký hiệu A và B ứng với STREET A và STREET B. G là GREEN, R là RED và Y là YELLOW. Ví dụ, AG_BR nghĩa là trạng thái STREET A đang GREEN, STREET B đang RED.

Yêu cầu đặt ra, sau khi reset thì cả hai đèn phải RED-RED nên bạn có thể chọn AR_BR1 hoặc AR_BR2. Ở đây, trạng thái AR_BR1 được chọn làm trạng thái khởi động.

Việc chuyển trạng thái chỉ xảy ra khi tín hiệu báo độ trễ tương ứng của trạng thái đó tích cực. Ví dụ, khi đèn trên STREET A hoặc STREET B đang xanh thì g_end phải tích cực thì mới chuyển sang trạng thái tiếp theo.
Hình 12. Bảng giá trị ngõ ra FSM
Sơ đồ nguyên lý tương ứng của FSM như sau:
Hình 13. Sơ đồ nguyên lý của FSM
2.4 Phân tích khối TIME_COUNTER
Vì bài này chỉ tập trung vào việc giải thích cho FSM nên khối này chỉ được đưa ra sơ đồ nguyên lý chứ không giải thích thêm để bạn đọc có thể tự nghiên cứu. Chú ý, GREEN_TIME, YELLOW_TIME và RED_TIME là các hằng số được cấu hình trước khi biên dịch RTL code. Với tần số 1 Hz, nếu muốn thời gian dừng của đèn xanh là 30s, đèn vàng là 5s và trạng thái cả hai đèn cùng đỏ là 3s thì cần gán giá trị như sau:
  • GREEN_TIME = 29
  • YELLOW_TIME = 4
  • RED_TIME = 2
Hình 14. Sơ đồ nguyên lý khối TIME_COUNTER

2.5 Hướng dẫn mô tả RTL code cho FSM

Một FSM gồm 3 phần:
  1. Mạch tạo trạng thái kế tiếp (Next state logic)
  2. Bộ nhớ trạng thái (state memory)
  3. Mạch tạo ngõ ra (output logic)
Để mô tả một FSM hoàn chỉnh, bạn cần mô tả đầy đủ 3 phần trên và thêm mô tả mã hóa trạng thái (State code). Các bạn hãy đọc RTL code và só sánh với hình 11 và hình 13 để hiểu rõ hơn.

RTL code của khối FSM:
module fsm (/*AUTOARG*/
   // Outputs
   street_a, street_b, fsm_g, fsm_y, fsm_r,
   // Inputs
   clk, rst_n, g_end, y_end, r_end
   );
//
//interface
//
input clk;
input rst_n;
input g_end;
input y_end;
input r_end;
output reg [2:0] street_a;
output reg [2:0] street_b;
output wire fsm_g;
output wire fsm_y;
output wire fsm_r;
//
//Internal signals
//
reg [2:0] current_state, next_state;
//STATE code
localparam AG_BR  = 3'd0;
localparam AY_BR  = 3'd1;
localparam AR_BR1 = 3'd2;
localparam AR_BG  = 3'd3;
localparam AR_BY  = 3'd4;
localparam AR_BR2 = 3'd5;
//Next state logic
always @ (*) begin
  case (current_state[2:0])
    AG_BR: begin
      if (g_end) next_state[2:0] = AY_BR;
      else next_state[2:0] = current_state[2:0];
    end
    AY_BR: begin
      if (y_end) next_state[2:0] = AR_BR1;
      else next_state[2:0] = current_state[2:0];
    end
    AR_BR1: begin
      if (r_end) next_state[2:0] = AR_BG;
      else next_state[2:0] = current_state[2:0];
    end
    AR_BG: begin
      if (g_end) next_state[2:0] = AR_BY;
      else next_state[2:0] = current_state[2:0];
    end
    AR_BY: begin
      if (y_end) next_state[2:0] = AR_BR2;
      else next_state[2:0] = current_state[2:0];
    end
    AR_BR2: begin
      if (r_end) next_state[2:0] = AG_BR;
      else next_state[2:0] = current_state[2:0];
    end
    default: next_state[2:0] = current_state[2:0];
  endcase
end
//STATE MEMORY
always @ (posedge clk) begin
  if (~rst_n) current_state[2:0] <= AR_BR1;
  else current_state[2:0] <= next_state[2:0];
end
//Output logic
always @ (*) begin
  case (current_state[2:0])
    AG_BR: street_a[2:0] = 3'b100;
    AY_BR: street_a[2:0] = 3'b010;
    default: street_a[2:0] = 3'b001;
  endcase
end
//
always @ (*) begin
  case (current_state[2:0])
    AR_BG: street_b[2:0] = 3'b100;
    AR_BY: street_b[2:0] = 3'b010;
    default: street_b[2:0] = 3'b001;
  endcase
end
//
assign fsm_g = (current_state[2:0] == AG_BR) | (current_state[2:0] == AR_BG);
assign fsm_y = (current_state[2:0] == AY_BR) | (current_state[2:0] == AR_BY);
assign fsm_r = (current_state[2:0] == AR_BR1) | (current_state[2:0] == AR_BR2);
endmodule
2.6 RTL code và testbench đầy đủ

Download: https://www.mediafire.com/file/v2y6d42gw9zgu7w/rtl_code_traffic_light_controller_nguyenquanicd.zip

pass (nếu có): nguyenquanicd

3. FSM là một dạng biểu diễn của mạch tuần tự


FSM thực chất chỉ là một dạng biểu diễn của mạch tuần tự. Bất kỳ mạch tuần tự dùng FF nào cũng có thể biểu diễn được dưới dạng FSM và ngược lại. Hãy xem xét một ví dụ đơn giản như sau để thấy được sự tương quan giữa một mạch tuần tự dùng FF và FSM.

Xét một bộ đếm lên 2 bit có mạch nguyên lý như sau:
Hình 15. Mạch nguyên lý bộ đếm lên 1 đơn vị
Bộ đếm này có thể được biểu diễn dưới dạng FSM như sau:
Hình 16. Biểu diễn dạng FSM của bộ đếm lên 2 bit

Sơ đồ nguyên lý của FSM trên như sau:
Hình 17. Sơ đồ nguyên lý của FSM của bộ đếm lwn 2 bit
Chúng ta sẽ xem xét mối tương quan giữa sơ đồ nguyên lý hình 15 và hình 17, thanh ghi bộ đếm inc_counter[1:0] tương ứng với thanh ghi trạng thái state[1:0].
Hình 18. So sánh tìm mối tương quan giữa biểu diễn mạch tuần tự và biểu diễn FSM
Nếu các trạng thái D0, D1, D2 và D3 được mã hóa theo thứ tự là 0, 1, 2 và 3 thì giá trị inc_counter[1:0] luôn bằng với giá trị state[1:0]. Với mạch tạo trạng thái kế tiếp, khi counter_en bằng 0 thì thanh ghi trạng thái state[1:0] giữ nguyên giá trị cũ tại tất cả các trạng thái nên có thể thay đổi vị trí của nhóm MUX phụ thuộc counter_en và MUX phụ thuộc state[1:0]. Kết quả đạt được thể hiện ở hình sau:
Hình 19. Biến đổi tương đương của sơ đồ nguyên lý FSM của bộ đếm 2 bit
Mạch nguyên lý của FSM hình 19 chỉ khác mạch nguyên lý hình 15 ở bộ MUX 4 ngõ vào. Khi counter_en bằng 1, trạng thái kế tiếp có giá trị như sau:
  • Bằng D1(1) khi state[1:0] bằng D0(0)
  • Bằng D2(2) khi state[1:0] bằng D1(1)
  • Bằng D3(3) khi state[1:0] bằng D2(2)
  • Bằng D0(0) khi state[1:0] bằng D3(3)
Giá trị trạng thái kế tiếp tăng tuần tự 1 đơn vị giống chức năng của bộ cộng. Như vậy mạch nguyên lý ở hình 19 có thể được thay thế tương đương bằng mạch nguyên lý ở hình 15.

Qua ví dụ đơn giản trên, bạn đọc có thể thấy một mạch tuần tự dùng FF có thể biểu diễn dưới dạng một FSM hoặc ngược lại.

4. Mã hóa trạng thái

Một vấn đề quan trọng trong biểu diễn FSM là mã hóa trạng thái. Mỗi trạng thái FSM cần được gán 1 giá trị khác nhau để phân biệt, giá trị này gọi là mã của trạng thái và việc gán các giá trị này gọi là mã hóa trạng thái.

Việc mã hóa trạng thái khác nhau sẽ cho kết quả tổng hợp mạch khác nhau hay netlist khác nhau. Xét lại ví dụ về bộ đếm lên ở mục 3. Nếu các trạng thái từ D0 đến D3 không được mã hóa tuần tự từ 0 đến 3 thì mạch sau khi tổng hợp RTL code với biểu diễn FSM và RTL code với biểu diễn theo hình 15 sẽ có sự khác biệt lớn dù vẫn thực hiện cùng một chức năng.

Việc mã hóa trạng thái sẽ ảnh hưởng đến sự tối ưu của mạch theo hướng khác nhau như mã hóa để tối ưu về diện tích, tối ưu về công suất hay tăng tốc độ đáp ứng của mạch.

Nhiều loại mã có thể sử dụng để mã hóa trạng thái như:
  • Binary tuần tự là gán theo thứ tự 0, 1, 2, 3, 4, ... Ưu điểm là số bit trạng thái được sử dụng là tối thiểu.
  • Gray hoặc cách mã hóa làm chỉ làm 1 bit thay đổi khi chuyển trạng thái. Ưu điểm là số bit trạng thái được sử dụng là tối thiểu và tiết kiệm năng lượng vì khi hoạt động số chuyển mạch ít hơn.
  • One-hot: mỗi trạng thái được mã hóa bằng 1 bit, tại một thời điểm chỉ có 1 bit tích cực. Ưu điểm, mã hóa kiểu này thường dùng cho mạch có tốc độ đáp ứng cao, độ trễ thấp tuy nhiên số lượng FF sử dụng lớn nên tài nguyên tổng hợp và diện tích mạch tăng và có khả năng tiêu tốn năng lượng hơn. Tốc độ đáp ứng cao có được là do mạch tổ hợp ngõ ra của FSM có trạng thái được mã hóa one-hot được tối ưu ít logic hơn.
Quay lại với ví dụ về đèn giao thông, nếu sử dụng mã hóa one-hot, số bit trạng thái tăng lên 6 bit, mỗi bit ứng với 1 trạng thái nhưng logic tạo ngõ ra thì lại giảm đáng kể như mạch nguyên lý ở hình sau.
Hình 20. Mạch tổ hợp tạo ngõ ra của khối FSM bộ điều khiển đèn giao thông khi mã hóa trạng thái bằng one-hot
Có hai cánh mô tả RTL code của FSM được mã hóa trạng thái one-hot:
  • Mô tả như cách mã hóa binary thông thường là sử dụng current_state làm case_expression cho phát biểu case mô tả mạch tạo trạng thái kế tiếp. Tên trạng thái lúc này chính là giá trị của STATE
  • Mô tả sử dụng case_expression = 1. Tên trạng thái được sử dụng làm chỉ số xác định bit quy định trạng thái.
Cách mô tả FSM one-hot dùng tên trạng thái làm giá trị của STATE:
module fsm_onehot (/*AUTOARG*/
   // Outputs
   street_a, street_b, fsm_g, fsm_y, fsm_r,
   // Inputs
   clk, rst_n, g_end, y_end, r_end
   );
//
//interface
//
input clk;
input rst_n;
input g_end;
input y_end;
input r_end;
output wire [2:0] street_a;
output wire [2:0] street_b;
output wire fsm_g;
output wire fsm_y;
output wire fsm_r;
//
//Internal signals
//
reg [5:0] current_state, next_state;
//STATE code
localparam AG_BR  = 6'b000001;
localparam AY_BR  = 6'b000010;
localparam AR_BR1 = 6'b000100;
localparam AR_BG  = 6'b001000;
localparam AR_BY  = 6'd010000;
localparam AR_BR2 = 6'd100000;
//Next state logic
always @ (*) begin
  case (current_state[5:0])
    AG_BR: begin
      if (g_end) next_state[5:0] = AY_BR;
      else next_state[5:0] = current_state[5:0];
    end
    AY_BR: begin
      if (y_end) next_state[5:0] = AR_BR1;
      else next_state[5:0] = current_state[5:0];
    end
    AR_BR1: begin
      if (r_end) next_state[5:0] = AR_BG;
      else next_state[5:0] = current_state[5:0];
    end
    AR_BG: begin
      if (g_end) next_state[5:0] = AR_BY;
      else next_state[5:0] = current_state[5:0];
    end
    AR_BY: begin
      if (y_end) next_state[5:0] = AR_BR2;
      else next_state[5:0] = current_state[5:0];
    end
    AR_BR2: begin
      if (r_end) next_state[5:0] = AG_BR;
      else next_state[5:0] = current_state[5:0];
    end
    default: next_state[5:0] = current_state[5:0];
  endcase
end
//STATE MEMORY
always @ (posedge clk) begin
  if (~rst_n) current_state[5:0] <= AR_BR1;
  else current_state[5:0] <= next_state[5:0];
end
//Output logic
assign street_a[2] = current_state[0]; //AG_BR
assign street_a[1] = current_state[1]; //AY_BR
assign street_a[0] = ~(street_a[2] | street_a[1]);
//
assign street_b[2] = current_state[3]; //AR_BG
assign street_b[1] = current_state[4]; //AR_BY
assign street_b[0] = ~(street_b[2] | street_b[1]);
//
assign fsm_g = (current_state[0] | current_state[3]);
assign fsm_y = (current_state[1] | current_state[4]);
assign fsm_r = (current_state[2] | current_state[5]);
endmodule
Cách mô tả FSM one-hot với tên trạng thái được sử dụng làm chỉ số xác định bit quy định trạng thái. Ví dụ, AG_BR dùng để xác định vị trí bit 0 trong thanh ghi trạng thái, AY_BR dùng để xác định vị trí bit 1 trong thanh ghi trạng thái, ...


module fsm_onehot (/*AUTOARG*/
   // Outputs
   street_a, street_b, fsm_g, fsm_y, fsm_r,
   // Inputs
   clk, rst_n, g_end, y_end, r_end
   );
//
//interface
//
input clk;
input rst_n;
input g_end;
input y_end;
input r_end;
output wire [2:0] street_a;
output wire [2:0] street_b;
output wire fsm_g;
output wire fsm_y;
output wire fsm_r;
//
//Internal signals
//
reg [5:0] current_state, next_state;
//Bit position of STATE code
localparam AG_BR  = 3'd0;
localparam AY_BR  = 3'd1;
localparam AR_BR1 = 3'd2;
localparam AR_BG  = 3'd3;
localparam AR_BY  = 3'd4;
localparam AR_BR2 = 3'd5;
//Next state logic
always @ (*) begin
  next_state[5:0] = 6'b000000;
  case (1)
    current_state[AG_BR]: begin
      if (g_end) next_state[AY_BR] = 1'b1;
      else next_state[AG_BR] = 1'b1;
    end
    current_state[AY_BR]: begin
      if (y_end) next_state[AR_BR1] = 1'b1;
      else next_state[AY_BR] = 1'b1;
    end
    current_state[AR_BR1]: begin
      if (r_end) next_state[AR_BG] = 1'b1;
      else next_state[AR_BR1] = 1'b1;
    end
    current_state[AR_BG]: begin
      if (g_end) next_state[AR_BY] = 1'b1;
      else next_state[AR_BG] = 1'b1;
    end
    current_state[AR_BY]: begin
      if (y_end) next_state[AR_BR2] = 1'b1;
      else next_state[AR_BY] = 1'b1;
    end
    current_state[AR_BR2]: begin
      if (r_end) next_state[AG_BR] = 1'b1;
      else next_state[AR_BR2] = 1'b1;
    end
  endcase
end
//STATE MEMORY
always @ (posedge clk) begin
  if (~rst_n) current_state[5:0] <= 6'b000100;
  else current_state[5:0] <= next_state[5:0];
end
//Output logic
assign street_a[2] = current_state[0]; //AG_BR
assign street_a[1] = current_state[1]; //AY_BR
assign street_a[0] = ~(street_a[2] | street_a[1]);
//
assign street_b[2] = current_state[3]; //AR_BG
assign street_b[1] = current_state[4]; //AR_BY
assign street_b[0] = ~(street_b[2] | street_b[1]);
//
assign fsm_g = (current_state[0] | current_state[3]);
assign fsm_y = (current_state[1] | current_state[4]);
assign fsm_r = (current_state[2] | current_state[5]);
endmodule
5. Trả lời câu hỏi hình 5.1

Muốn biết mạch nào là FSM Moore, mạch nào là FSM Mealy thì chỉ cần nhìn vào các ngõ ra.
  • Chỉ cần một ngõ ra phụ thuộc trực tiếp vào ngõ vào (không qua bất kỳ FF nào) thì đó là Mealy
  • Tất cả các ngõ ra đều được tổ hợp từ các ngõ ra của FF thì đó là Moore
Các bạn nên nhớ trong mô hình FSM chỉ có STATE MEMORY là FF còn các mạch khác đều là mạch tổ hợp.

Đáp án: A, B và D là FSM Moore   -    C là FSM Mealy


Lịch sủ cập nhật:
1) 2019.11.10 - Sửa link hình ảnh

Thứ Bảy, 12 tháng 8, 2017

[Verification] Tổng quan về công việc kiểm tra và xác minh thiết kế

Bài viết này mô tả tổng quan về công việc kiểm tra và xác minh thiết kế (verification). Bên cạnh đó, bài viết cũng trình bày những khái niệm thuật ngữ liên quan và ý nghĩa của chúng. Hy vọng, qua bài viết, các bạn có thể hình dung một cách rõ ràng những vấn đề mà bạn sẽ gặp phải khi chọn công việc này.

1. Tổng quan

Nếu người thiết kế sáng tạo ra sản phẩm thì người mô phỏng là những người đảm bảo sản phẩm đó phải đáp ứng chất lượng tốt nhất khi đến tay khách hàng. Mục đích quan trọng nhất của người kiểm tra không phải là chỉ đi tìm bug (lỗi thiết kế) mà phải đảm bảo thiết kế hoạt động hoàn toàn chính xác theo đúng yêu cầu. Nghĩa là không chỉ chức năng thiết kế đúng mà các thông số khác như hiệu suất xử lý, tốc độ đáp ứng, ... nếu có thì cũng phải được đảm bảo.

Chi phí cho mỗi lần chế tạo, sản xuất vi mạch vô cùng đắt đỏ. Tùy vào công nghệ, nếu chỉ sản xuất thử nghiệm thì số tiền có thể từ hàng chục đến hàng trăm USD cho lô chip từ 20 đến 40 con. Chỉ một lỗi cũng có thể làm cho sản phẩm bị vứt bỏ hoàn toàn nên yêu cầu công việc kiểm tra ngày càng được chuẩn hóa, chuyên nghiệp và có những phương pháp cụ thể.

2. Các mức kiểm tra thiết kế

2.1 Phân chia mức kiểm tra dựa trên phạm vi kiểm tra

Một thiết kế sẽ cần nhiều mức kiểm tra khác nhau để đảm bảo tính chính xác cao. Dựa trên phạm vi kiểm tra, việc kiểm tra có thể chia làm các mức từ chi tiết đến tổng quan như sau:
  • Mức khối (block level)
  • Mức kết nối (connection level) hay mức tích hợp (integration level)
  • Mức hệ thống (system level) hay mức chip (chip level) hay mức TOP

Kiểm tra mức khối là mức thấp nhất trong các mức kiểm tra. Một thiết kế, được chia làm nhiều khối (block hoặc module) khác nhau. Mỗi khối này thực thi một chức năng nhiệm vụ cụ thể được người thiết kế quy định. Kiểm tra mức này giúp dễ dàng phát hiện ra các bug ẩn sâu trong từng khối. Đây là mức kiểm tra đầu tiên và cần thiết đối với các thiết kế mới.
Tùy vào từng thiết kế, cách hiểu mức block là khác nhau. Ví dụ, nếu thiết kế là một chip MCU như hình 1 thì kiểm tra mức block là kiểm tra các khối chức năng như Cortex-M3, UART, WDT, SPI, RTC, 5-layer AHB Matrix,....

Hình 1. Sơ đồ khối của một chip vi điều khiển MCU

Nếu thiết kế chỉ là một ngoại như UART như hình 2 thì kiểm tra mức block là kiểm tra các khối APB interface, Baud rate generator, Transmitter, Receiver,...

Hình 2. Sơ đồ khối của một UART

Kiểm tra mức kết nối hay mức tích hợp là kiểm tra sự hoạt động của của một block khi được kết nối đến các block khác kết nối trực . Mục tiêu chính của kiểm tra này chính là sự giao tiếp và đáp ứng giữa một block với các block khác kết nối trực tiếp đến nó. Cho dù giao tiếp giữa 2 block đã được chuẩn hóa thì công đoạn kiểm tra này cũng không thể bỏ qua vì cùng một quy định, cùng một tài liệu như mỗi người thiết kế có thể có cách nhìn và hiểu khác nhau nên việc hiểu sai và thực hiện sai hoàn toàn có thể xảy ra. Ví dụ, kiểm tra mức kết nối cho khối Receiver ở hình 2 là kiểm tra giao tiếp giữa Receiver và khối Baud rate generator, khối 32x12 receive FIFO, khối APB interface và Transmitter.

Kiểm tra mức hệ thống là kiểm tra hoạt động của thiết kế trong mô hình hệ thống thực tế mà thiết kế sẽ sử dụng. Từ "hệ thống" ở đây bao hàm một ý nghĩa rộng, hệ thống có thể đơn giản chỉ là kết nối thiết kế đến các mô hình chuẩn thông qua các giao tiếp để kiểm tra hoặc là một tổ hợp hoàn chỉnh của một con chip.

Ví dụ, với thiết kế là một MCU như hình 1, kiểm tra mức hệ thống tương đương với việc kiểm tra mức chip. Một môi trường kiểm tra được xây dựng với đầy đủ các thành phần như một con chip hoàn chỉnh, bạn sẽ kiểm tra hệ thống này như việc bạn lập trình một con chip MCU là viết code firmware (Assemble hoặc C), biên dịch thành mã nhị phân để đưa vào môi trường cho MCU chạy và kiểm tra kết quả.

Đối với thiết kế UART như hình 2, môi trường có thể gồm:

  • Một mô hình APB master để kết nối đến khối APB interface và điều khiển giao tiếp này
  • Một mô hình UART để kết nối với Transmiter, Receiver và FIFO stattus and Interrupt để thực thi giao thức truyền dữ liệu UART và các giao thức bắt tay mở rộng
  • Một bộ tạo và cấp clock đến chân UARTCLK
  • Một bộ giám sát tín hiệu ngắt UARTn interrupt
  • Một thành phần giám sát tất cả các giao tiếp
Hình 3. Kiểm tra mức hệ thống
2.2 Phân chia mức kiểm tra dựa trên quy trình thiết kế vi mạch

Quy trình thiết kế vi mạch gồm nhiều công đoạn khác nhau. Sau một số công đoạn quan trọng, thiết kế cần được kiểm tra lại. Có hai mức kiểm tra quan trọng là:

  • Kiểm tra mức RTL (RTL level) thực thi trên RTL code với mục đích chính là chỉ kiểm tra chức năng (function) của RTL code
  • Kiểm tra mức cổng (Gate level) thực thi trên netlist, thành phần được tạo ra sau khi tổng hợp RTL code, với mục đích chính là kiểm tra chức năng có tính đến timing như độ trễ trên các cổng logic, tần số xung clock mà thiết kế có thể đáp ứng, ...

Hình 4. Mức RTL code và mức Gate

3. Kế hoạch kiểm tra

Kế hoạch kiểm tra (verification plan) là một bản mô tả chi tiết và đầy đủ về các đặc điểm cần kiểm tra, kỹ thuật, phương pháp dùng để kiểm tra, thứ tự các bước sẽ thực hiện, cấu trúc môi trường kiểm tra.

Một kế hoạch kiểm tra sẽ trả lời rõ hai câu hỏi quan trọng là:
  1. Thiết kế sẽ được kiểm tra như thế nào? 
  2. Các trường hợp test, gọi là các test hoặc testcase hoặc testbench, sẽ được viết như thế nào?

Kế hoạch mô phỏng là văn bản định nghĩa các thông tin quan trọng sau:

  1. Các test sẽ được sử đụng để kiểm tra thiết kế bao gồm đầy đủ các test từ mức block đến mức TOP hay mức hệ thống
  2. Môi trường kiểm tra DUT bao gồm ngôn ngữ sử dụng, cấu trúc testbench, các lệnh đặc biệt, các model, các thư viện, các gói dữ liệu, cấu trúc file, các giao tiếp,...
  3. Môi trường xác thực cho DUT gồm định nghĩa phần mềm sử dụng, phương pháp báo lỗi, các loại lỗi.
Hình 5. Ví dụ về một mô tả cấu trúc kiểm tra register trong một kế hoạch kiểm tra
Hình 5 là một minh họa về cách mô tả cấu trúc kiểm tra register trong một thiết kế. Tôi sử dụng minh họa này để giải thích việc mô tả một môi trường mô phỏng trong một kế hoạch kiểm tra. Hai câu hỏi quan trọng của một kế hoạch kiểm tra được thể hiện như sau:
  1. Thiết kế sẽ được kiểm tra như thế nào? UVM TEST là testbench sẽ tạo ra các SEQUENCES đưa dữ liệu cho BUS SEQR và HW SEQ. Dữ liệu này được chuyển đến BUS DRIVER và HW DRIVER để lái các đầu vào của thiết kế DUT. Đầu ra của thiết kế được giám sát bằng HW MONITOR. Bên cạnh đó các thuộc tính và các hoạt động khác của DUT còn được giám sát bởi các Assertion, một đặc điểm được hỗ trợ bởi System Verilog. Đối với việc kiểm tra register. Một UVM register Model được sử dụng để kiểm tra giá trị register của DUT.
  2. Các trường hợp test, gọi là các test hoặc testcase hoặc testbench, sẽ được viết như thế nào? Các test sẽ được tạo bằng cách viết các UVM TEST khác nhau và các SEQUENCES khác nhau để truyền dữ liệu kiểm tra mong muốn đến DUT

    Trong minh họa hình 5, phương pháp mô phỏng được sử dụng là UVM (Universal Verification Methodology).

    4. Các loại testbench

    4.1 Directed test

    Test có hướng (Directed test) là loại test được viết để nhằm mục đích kiểm tra một chức năng hoặc đặc điểm cụ thể nào đó. Theo phương pháp truyền thống, khi nhận được một yêu cầu kiểm tra thiết kế, kỹ sư kiểm tra bắt đầu liệt kê tất cả các test theo mô tả của thiết kế và bắt đầu viết từng test cụ thể cho từng trường hợp. Mối test sẽ truyền những giá trị test cụ thể đến DUT.

    Hình 6. Minh họa không gian của một thiết kế có chưa các đặc điểm cần kiểm tra (feature) và các lỗi (bug). Nhiệm vụ của người kiểm tra là tạo ra các test bao phủ (cover) được tất cả các đặc điểm và tìm ra tất cả các lỗi

    Tên "directed test" là do mỗi test sẽ chỉ tập trung kiểm tra một hoặc một tập tính năng cụ thể của thiết kế. Như vậy, để một thiết kế được test 100% hoặc có mức độ bao phủ 100% (coverage 100%) thì người kiểm tra phải suy nghĩ và viết đầy đủ được các trường hợp cần test. Điều này hoàn toàn có thể thực hiện được nhưng tốn rất nhiều thời gian và công sức. Tuy nhiên, về mặt quản lý, phương pháp này cho thấy tiến trình và kết quả đều đặn của quá trình kiểm tra.

    *Coverage là thông số biểu thị số phần trăm đã được kiểm tra của thiết kế. Một thiết kế đã được test đầy đủ thì giá trị coverage là 100%.

    Hình 7. Biều đồ thời gian coverage khi thực thi directed test
    Tuy nhiên, nếu thiết kế tăng mức độ phức tạp, ví dụ như tăng gấp đôi thì thời gian để test sẽ cùng tăng gấp đôi hoặc hơn nữa. Đồng thời, có những đoạn thời gian mức độ coverage giữ nguyên trong quá trình test với directed test (tham khảo biểu đồ 7).

    Bạn cần phương pháp nhanh hơn nhưng vẫn phải đạt được mục tiêu coverage 100% đó chính là phương pháp test ngẫu nhiên (random test).

    4.2 Random test

    Trong khi directed test giúp chúng ta tìm ra các bug mà chúng ta dự đoán có thể xảy ra trong thiết kế thì random test có thể tìm ra các bug mà chúng ta không nghĩ đến.

    Như tên gọi, test ngẫu nhiên (random test) là testbench tạo ra các giá trị ngẫu nhiên một cách tự động (đặc điểm này được hỗ trợ bởi ngôn ngữ System Verilog) để cung cấp đến DUT. Nhưng đối với giao tiếp đã được quy chuẩn, ví dụ như APB, AHB, AXI, Avalon, .... có các tín hiệu hoạt động bắt tay theo quy định thì làm sao có thể sử dụng random test? Với trường hợp này, nếu sử dụng random test thuần túy, tạo giá trị ngẫu nhiên cho các tín hiệu điều khiển và dữ liệu, thì không thể kiểm tra đúng được. Vì vậy, phương pháp random được hỗ trợ thêm ràng buộc (constraint) để giới hạn khả năng random thông qua các điều kiện cụ thể. Đây cũng là đặc điểm được hỗ trợ bởi ngôn ngữ System Verilog. Test này gọi là "test ngẫu nhiên có ràng buộc" (constrained-random test).

    Có thể thấy, với random test số mẫu giá trị đưa vào DUT và thứ tự giá trị đưa đến DUT có thể ngẫu nhiên một cách tự động, có thể kèm ràng buộc nếu cần. Kết quả, tiến độ kiểm tra sẽ rất nhanh trong giao đoạn bắt đầu chạy test tuy việc xây dựng test loại này thường lâu hơn so với directed test do cần có các mô hình giám sát và kiểm tra các gái trị random.
    Hình 8. Biều đồ so sánh thời gian đạt coverage 100% khi sử dụng random test và directed test
    Về cuối giai đoạn mô phỏng với random test, phạm vi cần kiểm tra của thiết kế bị thu hẹp lại, một số trường hợp có thể phải thêm rất nhiều ràng buộc cho random test hoặc dùng directed test để kiểm tra các trường hợp này. Các trường hợp này gọi là các corner case.

    Tóm lại, trong khi directed test tạo chính xác giá trị cần test thì random test có thể tạo ra nhiều giá trị test khác nhau nên các random test có thể bị chồng lấp lên nhau, hai hoặc nhiều test cùng tạo ra cùng một trường hợp kiểm tra. Điều này không phải là vấn đề nghiêm trọng. Nhưng nếu random test tạo ra các giá trị test sai thì buộc phải thêm ràng buộc để giới hạn lại random test.

    Quy trình thực hiện kiểm tra mức độ coverage của directed test và random test khác nhau như sau:
    • Directed test: Chạy test -> kiểm tra coverage -> tìm điểm chưa được coverage (chưa được kiểm tra) -> hiệu chỉnh test để tạo ra test mới -> quay lại từ đầu
    • Random test: Chạy test nhiều lần với các chuỗi giá trị random khác nhau -> kiểm tra coverage -> tìm điểm chưa được coverage (chưa được kiểm tra) -> hiệu chỉnh test để tạo ra test mới (nếu có) -> thêm ràng buộc -> quay lại từ đầu
    Hình 9. Quy trình hội tu của directed test và random test

    *Note: bài viết có tham khảo cuốn System Verilog for Verification của CHRIS SPEAR



    Chủ Nhật, 6 tháng 8, 2017

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

    Bài viết này hướng dẫn các bạn viết một testbench đơn giản tự động kiểm tra một thiết kế. Đồng thời qua đó các bạn có thể hiểu cơ bản về việc mô phỏng và kiểm tra thiết kế. Nội dung của bài viết này dành cho các bạn mới bắt đầu tìm hiểu cách viết một testbench Verilog.

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

    Cấu trúc môi trường mô phỏng cơ bản gồm:
    1. DUT (Design Under Test) còn được gọi là UUT (Unit Under Test) chính là thiết kế cần mô phỏng và kiểm tra. DUT sẽ được gọi và kết nối với các thành phần kiểm tra ở bên trong môi trường mô phỏng hoặc trực tiếp trong testbench.
    2. Testbench
      1. Stimulus hoặc Driver hoặc Test Vector Generator là thành phần tạo và cung cấp dữ liệu đầu vào của DUT, lái các tín hiệu đầu vào của DUT
      2. Monitor hoặc Observer là thành phần giám sát và kiểm tra các giá trị ngõ ra của DUT
    Hình 1. Cấu trúc cơ bản của môi trường mô phỏng
    Xin lưu ý với các bạn, đây chỉ là cấu trúc cơ bản. Hiện nay, với sự phức tạp ngày càng tăng của các thiết kế môi trường mô phỏng cũng được xây dựng dựa trên các giao thức và phương pháp được chuẩn hóa ví dụ như OVM, VMM, UVM,... Các giao thức này phân chia môi trường thành nhiều thành phần với tên gọi khác nhau nhưng vẫn không nằm ngoài 2 mục đích là tạo dữ liệu đầu vào và giám sát dữ liệu đầu ra của thiết kế.
    2. Mô tả DUT 

    Phần này mô tả DUT sẽ sử dụng trong bài viết này. DUT là một bộ đếm Johnson được mô tả chi tiết ở link sau:

    RTL code của bộ đếm Johnson N bit:
    module johnson_counter (clk, rst_n, js_count);
    //
    parameter N  = 4;
    //
    //Interface
    //
    input clk;
    input rst_n;
    output reg [N-1:0] js_count;
    wire js_msb_inv;
    assign js_msb_inv = ~js_count[N-1];
    always @ (posedge clk) begin
     if (~rst_n)
       js_count[N-1:0] <= 0;
     else
       js_count[N-1:0] <= {js_count[N-2:0], js_msb_inv};
    end
    endmodule
    Bộ đếm này đã được mô phỏng trong bài viết sau:


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

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

    Khai báo đơn vị thời gian mô phỏng
    `timescale 1ns/1ns
    3.2 Tạo khối testbench
    module tb_auto;
       //testbench body
    endmodule
    3.3 Khai báo các tham số, hằng số
    parameter N = 4;
    parameter END_TIME = 100;
    parameter VALUE_NUM = 2*N;
    Trong đó:
    • N là số bit của DUT bộ đếm Johnson
    • VALUE_NUM là số giá trị cần phải kiểm tra của DUT
    • END_TIME là thời gian giới hạn của testbench khi chạy mô phỏng tính theo số chu kỳ xung clock clk. Ở đây, sau 100 xung clock clk thì mô phỏng sẽ kết thúc.
    3.4 Khai báo giao tiếp với DUT
    reg clk;
    reg rst_n;
    wire [N-1:0] js_count;
    Đây chính là các ngõ vào và ngõ ra của DUT, Trong đó:
    • Ngõ vào được khai báo kiểu biến (variable) để gán giá trị lái DUT
    • Ngõ ra khai báo kiểu net chỉ để giám sát
    3.5 Gọi DUT
    johnson_counter #(.N(N)) dut (.clk(clk), .rst_n(rst_n), .js_count(js_count));
    Dòng code trên gọi module johnson_counter và thiết lập số bit cho bộ đếm là N = 4 bit.

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

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

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

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

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

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

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

    3.8 Tạo Monitor

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

    Chuỗi giá trị sẽ là:
    1. Bộ đếm Binary 4 bit:    0000 -> 0001 -> 0010
    2. Bộ đếm Johnson 4 bit:  0000 -> 0001 -> 0011
    Vì vậy, testbench sẽ báo lỗi tại vị trí ngõ ra DUT bằng 0010 và giá trị cần test là 0011.
    Hình 4. Thông điệp báo lỗi bộ đếm
    Việc tạo một testbench kiểm tra cho phép kiểm tra nhiều trường hợp trong thời gian mô phỏng dài mà không cần tốn công dò dạng sóng. Việc xem waveform chỉ thực hiện khi bạn mới xây dựng testbench để kiểm tra tính đúng đắn của testbench và khi phát hiện lỗi để gỡ lỗi (debug).
    Trên đây chỉ là một hướng dẫn cơ bản giúp bạn nhanh chóng viết một testbench. Để một DUT được kiểm tra toàn diện và đảm bảo tính đúng đắn cao thì testbench và môi trường mô phỏng có thể cần thêm nhiều thành phần khác.

    Lịch sử cập nhật:
    1) 2019.11.10 - Cập nhật hình ảnh

    [Questa SIM] Hướng dẫn cài đặt và chạy mô phỏng cơ bản với Questa SIM

    1. Giới thiệu về Questa SIM

    Questa SIM là một trình mô phỏng của hãng Mentor Graphics hỗ trợ đa nền tảng như hệ điều hành UNIX, Linux và Windows. Nếu bạn nào đã từng sử dụng ModelSim, một trình mổ phỏng khác của Mentor Graphics, thì việc làm quen với Questa SIM rất nhanh chóng và dễ dàng.

    Questa SIM hỗ trợ biên dịch các thiết kế viết bằng Verilog HDL, VHDL và SystemC và hỗ trợ mô phỏng các đối tượng viết bằng Verilog HDL, VHDL, SystemC, System Verilog, PSL, UPF (Unified Power Format).

    Việc chạy mô phỏng với Questa SIM có thể thực hiện bằng giao diện phần mềm (GUI - Graphical User Interface), bằng chế độ lệnh (Command-line) hoặc bằng chế độ batch.
    • GUI: Chế độ sử dụng giao diện phần mềm để thực thi mô phỏng bằng các nút, menu tùy chọn hoặc bằng cách lệnh trong cửa sổ transcript mà giao diện hỗ trợ
    • Command-line: Dùng lệnh để chạy mô phỏng bằng cửa sổ lệnh (command prompt) của hệ điều hành. Questa SIM sẽ bắt đầu thực thi với các biến được đặc tả trong file modelsim.ini, một file được trình mô phỏng tự động tạo ra, hoặc một file DO tùy chọn do người dùng tạo ra.
    • Batch: Dùng lệnh để chạy mô phỏng bằng cửa sổ lệnh của hệ điều hành với các biến được nhập trực tiếp từ ngõ vào chuẩn (bàn phím) hoặc từ file; và xuất ra ngõ ra chuẩn (màn hình) hoặc file.
    Hình 1. Trình tự thực thi mô phỏng trong Questa SIM
    Trình tự mô phỏng gồm 5 bước cơ bản:
    1. Thu thập các file đầu vào (Verilog, VHDL, SystemC) và ánh xạ các thư viện. Bước này tự động tạo ra file modelsim.ini như đã nói ở trên.
    2. Phân tích kiểm tra code và biên dịch
    3. Tối ưu và tạo cơ sở dữ liệu đã biên dịch
    4. Mô phỏng
    5. Gỡ lỗi (Debug)
    2. Download gói cài đặt


    3. Cài đặt
    3.1/ Giải nén thư mục cài đặt
    Sau khi giải nén, thư mục QuestaSim10.2c\Mentor Graphics Questa sim SE (64bit) 10.2c chứa các file sau:
    1. questasim-gcc-4.5.0-mingw64.zip
    2. questasim-win64-10.2c.exe
    3. regassistuvm_4.3_win.exe
    4. Thuốc cho tool
    3.2/ Cài đặt Questa SIM
    Chạy file questasim-win64-10.2c.exe để cài đặt Questa SIM.
    Chọn Next → Agree → Next → Yes  → Yes
    Đến cửa sổ sau đây thì chọn NO
    Hình 2. Cài đặt Questa SIM
    3.3/ Cài đặt thưc viện UVM

    Đây là thư viện sử dụng cho phương pháp mô phỏng mới nhất hiện nay là UVM (Universal Verification Methodology).

    Chạy file regassistuvm_4.3_win.exe để cài đặt.
    Chọn Next -> chọn thư mục cài đặt và Next
    Hình 3. Cài đặt thư viện UVM
    3.4/ C/C++ compiler cho môi trường UVM
    Giải nén file questasim-gcc-4.5.0-mingw64.zip và copy thư mục "gcc-4.5.0-mingw64" vào thư mục cài đặt QuestaSim. Đây là gcc compiler dùng để tổng hợp DPI (Direct Programming Interface) dùng cho môi trường mô phỏng UVM.
    Lỗi thường gặp khi thiếu C/C++ compiler:
    ** Fatal: (vsim-7019) Can't locate a C/C++ compiler for 'DPI Export Compilation'.
    3.5/ Tạo thuốc cho Questa SIM

    Copy file MentorKG.exe và patch_dll.bat trong thư mục 
    QuestaSim10.2c\Mentor Graphics Questa sim SE (64bit) 10.2c\c-r-a-c-k vào thư mục cài đặt c:\questasim64_10.2c\win64\

    Chạy file patch_dll.bat sẽ tự động tạo ra file mgls.dll.

    Chạy file MentorKG.exe, cửa sổ hiện ra như sau:
    Hình 4. Chạy file MentorKG.exe
    Chờ cho đến khi file ".txt" sau được tạo ra và hiện lên như sau:
    Hình 5. File *.txt
    Lưu lại file trên dưới tên với đuôi mở rộng .dat trong thư mục cài đặt c:\questasim64_10.2c\win64\ 

    Bấm chuột phải lên Computer -> Properties -> Advanced system
    • Chọn Environment Variables tab -> Chọn new ở khung User variables for
    • Thêm:
      • Variable name: LM_LICENSE_FILE
      • Variable value: c:\questasim64_10.2c\win64\<tên file>.dat đã tạo ở trên
    Hình 6. Thiết lập biến môi trường cho Questa SIM
    Khởi động thử Questa SIM, hiện giao diện bình thường là OK.
    Hình 7. Giao diện Questa SIM-64 10.2c
    4. Chạy mô phỏng với giao diện (GUI) của Questa SIM

    4.1 Đầu vào mô phỏng
    Đầu vào mô phỏng gồm một file thiết kế (đây là bộ đếm Johnson tham khảo ở lilnk sau http://nguyenquanicd.blogspot.com/2017/08/verilog-rtl-code-mo-ta-cac-loai-bo-em.html) và một file testbench tạo giá trị ngõ vào cho việc mô phỏng thiết kế:
    1. File thiết kế: johnson_counter.v
    2. File testbench: tb_jc.v
    Ở đây file testbench chỉ cần tạo xung clock clk và reset ban đầu rst_n.

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

    File testbench:
    module tb_jc;
    parameter N = 4;
    //
    //Interface
    //
    reg clk;
    reg rst_n;
    wire [N-1:0] js_count;
    johnson_counter #(.N(N)) dut (.clk(clk), .rst_n(rst_n), .js_count(js_count));
    initial begin
      clk = 0;
    forever #10 clk = !clk;
    end
    initial begin
      rst_n = 0;
    #20
    rst_n = 1;
    end
    endmodule 
    4.2 Tạo project
    Chọn File->New->Project...

    Đặt tên project (Project name) và chọn đường dẫn lưu project (Project location) trong cửa sổ sau:
    Hình 8. Tạo project
    Tạo file thiết kế (Verilog, VHDL, SystemC) hoặc file mô phỏng mới (Create New File) hoặc thêm các file đã tạo sẵn vào project (Add Existing File) trong cửa sổ sau:
    Hình 9. Tạo mã nguồn mô phỏng
    Ở đây, tôi chọn Add Existing File và dẫn đến 2 file johnson_counter.v và tb_jc.v đã được viết trước. Open->OK->Close
    Hình 10. Chọn file
    4.3 Biên dịch
    Bấm chuột phải vào một trong các file và chọn Compile->compile All
    Hình 11. Biên dịch các file
    Chú ý, ký hiệu "?" ở cột Status biểu thị file chưa được biên dịch. Nếu biên dịch lỗi thì Status là dấu chéo màu đỏ, biên dịch tốt thì Status là dấu chỉ thị màu xanh.  Xem thông tin ở cửa sổ Transcript để thấy các thông tin cảnh báo. Ví dụ ở đây, file johnson_counter.v biên dịch thành công, file tb_jc.v biên dịch lỗi.
    Hình 12. Cảnh báo trên cửa sổ Transcript
    Nếu lỗi, hãy nhấp chột vào dòng cảnh báo trên cửa sổ Transcript để biết lý do và vị trí lỗi. Như ví dụ này, file tb_jc.v bị lỗi ở dòng 10 gần ký hiệu "#" và đây là lỗi cú pháp. Dòng 10 như sau:

    johnson_counter dut #(.N(N)) (.clk(clk), .rst_n(rst_n), .js_count(js_count));
    Tôi khai báo tên gọi module là dut đặt đằng trước việc gán giá trị tham số trong khi nó phải đặt sau như dòng code sau đây:
    johnson_counter #(.N(N)) dut (.clk(clk), .rst_n(rst_n), .js_count(js_count));
    Sau khi sửa, bạn hãy biên dịch lại là thành công như sau:
    Hình 13. Biên dịch lại các file

    4.4 Chạy mô phỏng
    Sau khi biên dịch thành công, đối tượng được biện dịch sẽ xuất hiện trong thư mục work của thẻ library như sau:
    Hình 14. Thẻ library
    Bấm chuột phải vào đối tượng testbench tb_jc để chọn chế độ chạy mô phỏng. Ở đây, tôi chọn simulate without optimization để tạo ra các tín hiệu có thể quan sát dạng sóng.
    Hình 15. Chạy mô phỏng
    Sau khi thực thi, một vài cửa số khác hiện lên gồm:
    • Cửa sổ sim thể hiện cấu trúc thứ bậc của các đối tượng đang được mô phỏng. Ví dụ, tb_jc gọi thiết kế có tên dut. Ngang cấp với dut là hai khối initial. Bên trong dut có 1 khối always và một phát biểu assign.
    • Cửa số Objects thể hiện các tín hiệu, biến tương ứng có trong đối tượng bên cửa sổ sim.
    • Cửa sổ Wave sẽ là nơi để quan sát dạng sóng các tín hiệu
    Lựa chọn các tín hiệu cần quan sát, bấm chuột phải và Add wave
    Hình 16. Lấy tín hiệu để xem dạng sóng
    Chú ý bên trên thanh công cụ có một nhóm các button để điều khiển việc chạy mô phỏng. Ở đây tôi chọn chạy mô phỏng với thời gian 500 ns. Bấm run cạnh ô 500 ns.
    Hình 17. Chạy mô phỏng và xem waveform

    Lịch sử cập nhật:
    1) 2019.July.13 - Thêm mục 3.4 C/C++ compiler cho môi trường UVM
    2) 2020.Jan.04 - Sửa link hình ảnh
    3) 2020.Apr.25 - Sửa link tải QuestaSim