Menu

Phép gán blocking và non-blocking trong Verilog: = hay <=

Chủ đề gây nhầm lẫn nhiều nhất với người mới học Verilog. = và <= thực sự có nghĩa gì bên trong khối always, và quy tắc giúp tránh hầu hết race condition.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Hai toán tử

Bên trong các khối always và initial, Verilog có hai toán tử gán:

  • = là phép gán blocking. Cập nhật vế trái ngay bây giờ, trước khi chuyển sang câu lệnh tiếp theo.
  • <= là phép gán non-blocking. Tính vế phải ngay bây giờ, lên lịch cập nhật vế trái vào cuối bước thời gian hiện tại.

Bên ngoài các khối thủ tục (trong assign) chỉ có =: assign a <= b là lỗi cú pháp. Bên trong khối thủ tục, cả hai đều hợp lệ, và chọn đúng toán tử là quyết định quan trọng nhất khi mới học Verilog.

Blocking làm gì

Blocking hoạt động đúng như bạn chờ đợi ở một ngôn ngữ phần mềm: từng dòng một, từ trên xuống dưới. Câu lệnh N hoàn tất trước khi câu lệnh N+1 bắt đầu:

Đây là thực thi tuần tự thuần túy. Mỗi câu lệnh thấy được tác động của các câu lệnh phía trên nó. Điều đó khớp với trực giác lập trình phần mềm.

Non-blocking làm gì

Non-blocking được thiết kế theo phần cứng. Mọi vế phải được tính bằng giá trị ở đầu bước thời gian. Mọi vế trái được cập nhật ở cuối bước thời gian. Thứ tự các câu lệnh trong mã nguồn không làm thay đổi sự phụ thuộc giữa các tín hiệu:

Đoạn code đó xoay vòng ba giá trị a → b → c → a trong một bước. Với blocking, bạn sẽ cần một biến tạm để khỏi ghi đè mất một giá trị. Với non-blocking, phép hoán đổi diễn ra trọn vẹn vì mọi vế phải đều đọc giá trị trước bước.

Đây chính xác là cách ba flip-flop hoạt động tại một sườn clock: tất cả cùng lưu đầu vào vào đúng một thời điểm, bất kể sự phụ thuộc giữa chúng.

Quy tắc cứu bạn

Dùng <= trong khối theo clock. Dùng = trong khối tổ hợp.

Chỉ vậy thôi. Hãy ghi nhớ và viết theo nó như phản xạ. Phần lớn race condition, tình trạng mô phỏng và tổng hợp không khớp, và những bug kiểu "mô phỏng thì chạy nhưng tổng hợp thì không" đều đến từ việc vi phạm quy tắc này.

Quy tắc này có lý do từ phần cứng: khối theo clock mô phỏng các flip-flop cùng lấy mẫu đầu vào một lúc. Khối tổ hợp mô phỏng logic lan truyền nhanh nhất có thể. Ngữ nghĩa của toán tử gán khớp với những hành vi đó.

Mỗi <= đọc giá trị hiện tại của thanh ghi nguồn và lên lịch để thanh ghi đích nhận giá trị đó vào cuối bước thời gian. Kết quả cuối cùng đúng như những gì một thanh ghi dịch phần cứng làm: mỗi flip-flop lưu giá trị của flip-flop bên cạnh, tất cả trên cùng một sườn clock, không có race.

Giờ hãy xem điều gì sẽ xảy ra nếu dùng phép gán blocking:

// WRONG - this is not a shift register!
always @(posedge clk) begin
    out[3] = out[2];   // out[3] becomes out[2]
    out[2] = out[1];   // out[2] becomes out[1], which we just set above
    out[1] = out[0];
    out[0] = in;
end

Mỗi câu lệnh ghi đè lên nguồn trước khi câu lệnh tiếp theo đọc nó. Chỉ trong một chu kỳ clock, in sẽ lan truyền thẳng đến out[3], vì mỗi dòng thấy giá trị vừa được ghi của dòng phía trên. Hành vi trên phần cứng thật (dùng ngữ nghĩa non-blocking) sẽ hoàn toàn khác với những gì trình mô phỏng hiển thị.

Khối tổ hợp: blocking là đúng

Với always @(*), phép gán blocking là đúng. Không có flip-flop, không có quy tắc lưu đồng thời nào cần tuân theo, và các biến trung gian rất hữu ích:

sum được tính trước, rồi result dùng giá trị vừa tính đó. Logic tổ hợp được gộp thành một khối phần cứng duy nhất: result = ~(a + b). Không có flip-flop nào xuất hiện vì không có clock.

Nếu bạn dùng <= ở đây, trình mô phỏng vẫn sẽ cập nhật sum trước khi result được tính lại dựa trên nó (vì cả hai cập nhật đều diễn ra ở cuối bước), nhưng thứ tự sẽ khác đi một cách khó thấy và nhiều công cụ tổng hợp sẽ cảnh báo. Đừng trộn; hãy chọn toán tử khớp với loại khối.

Sai lầm gây hại nhiều nhất

Đây là nó: một khối theo clock dùng phép gán blocking.

// BUG: race condition waiting to happen
always @(posedge clk) begin
    a = b;
    b = c;
    c = a;
end

Khi mô phỏng, trình mô phỏng có thể xử lý a trước, rồi b, rồi c, cho ra một bộ giá trị. Phần cứng sẽ cho ra một bộ khác, vì flip-flop thật lưu dữ liệu đồng thời. Hai kết quả lặng lẽ khác nhau và bạn sẽ mất cả ngày để tìm ra bug. Hãy dùng <= trong khối theo clock.

Vì sao lại có hai toán tử

Những người thiết kế Verilog có thể chọn một ngữ nghĩa gán duy nhất và giữ nguyên nó. Họ không làm vậy, vì ngôn ngữ phải mô phỏng hai hành vi phần cứng khác nhau:

  • Logic tổ hợp: tín hiệu lan truyền liên tục, sự phụ thuộc là quan trọng, câu hỏi "cổng này tính ra gì" có ý nghĩa.
  • Logic tuần tự: một sườn clock xảy ra, mọi flip-flop lưu dữ liệu cùng lúc, sự phụ thuộc giữa đầu vào và đầu ra của flip-flop được tách rời.

Blocking dành cho loại thứ nhất. Non-blocking dành cho loại thứ hai. Toán tử chọn ngữ nghĩa; trình mô phỏng lo phần còn lại.

Tiếp theo là gì

Giờ bạn đã có đủ quy tắc để viết đúng mọi khối thủ tục. Chương tiếp theo đi từ từng khối riêng lẻ đến các cấu trúc điều khiển nằm bên trong chúng: if/else, case và vòng lặp for. Quy tắc blocking và non-blocking vẫn áp dụng bên trong tất cả các cấu trúc đó.

Câu hỏi thường gặp

Phép gán blocking và non-blocking trong Verilog khác nhau thế nào?

Blocking (=) cập nhật đích ngay lập tức, trước khi câu lệnh tiếp theo chạy. Nó mô phỏng việc thực thi tuần tự. Non-blocking (<=) lên lịch cập nhật vào cuối bước thời gian hiện tại: mọi vế phải non-blocking được tính bằng giá trị cũ của tất cả tín hiệu, rồi mọi vế trái được cập nhật trong một bước phối hợp duy nhất. Non-blocking chính là cách flip-flop thực sự hoạt động.

Khi nào dùng = và khi nào dùng <= trong Verilog?

Quy tắc: dùng <= trong các khối theo clock always @(posedge clk), và dùng = trong các khối tổ hợp always @(*). Chỉ một quy tắc đó đã ngăn được toàn bộ nhóm race condition sinh ra khi trộn hai kiểu gán. Bên trong các khối initial của testbench, = là lựa chọn thông thường; <= hiếm khi xuất hiện ở đó.

Vì sao Verilog cần phép gán non-blocking?

Vì trong phần cứng, mọi flip-flop đều lưu đầu vào của chúng cùng lúc tại một sườn clock. Nếu bạn dùng = (blocking) trong code theo clock, thứ tự các câu lệnh trong tệp sẽ quyết định tín hiệu nào thấy giá trị mới của tín hiệu nào, gây ra race condition giữa mô phỏng và phần cứng thật. <= khớp với hành vi phần cứng bằng cách tính mọi vế phải trước, rồi mới thực hiện tất cả các cập nhật.

Điều gì xảy ra nếu trộn = và <= trong cùng một khối always?

Bạn sẽ gặp race condition. Cách trộn này tạo ra phần cứng có hành vi phụ thuộc vào cơ chế bên trong trình mô phỏng (thứ tự lập lịch sự kiện), và tệ hơn, kết quả mô phỏng có thể không khớp với những gì công cụ tổng hợp tạo ra. Hầu hết công cụ lint đánh dấu đây là lỗi. Cách sửa là chọn hẳn một kiểu dựa trên vai trò của khối: non-blocking cho khối theo clock, blocking cho khối tổ hợp.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu