Cú pháp quen thuộc, tư duy khác hẳn
if/else trông giống hệt C:
if (condition) begin
// ... statements ...
end else begin
// ... statements ...
end
Nhưng quy tắc thì khác, vì Verilog không phải phần mềm. Có hai điều cần nhớ:
if/elsechỉ sống bên trong khối thủ tục. Bạn không thể viết một câu lệnhifđứng riêng ở cấp module.if/elseđược tổng hợp thành gì phụ thuộc vào loại khối. Trong khối tổ hợp, nó trở thành bộ ghép kênh hoặc bộ mã hóa ưu tiên. Trong khối theo clock, nó trở thành flip-flop có logic cập nhật theo điều kiện.
Bên trong khối tổ hợp
Khối tổ hợp always @(*) chạy lại mỗi khi a, b hoặc c thay đổi. Chuỗi if/else chọn một nhánh, gán max, rồi khối kết thúc. Vì max luôn được gán (nhánh nào cũng có phép gán), công cụ tổng hợp tạo ra logic tổ hợp thuần, không có latch.
Lưu ý max được khai báo reg dù phần cứng không hề có flip-flop. Quy tắc vẫn như mọi khi: thứ gì được gán bên trong always thì phải là reg.
Cái bẫy latch
Đây là bug phổ biến nhất trong code tổ hợp của người mới:
// WRONG - infers a latch
always @(*) begin
if (enable)
out = data;
// no else! when `enable` is low, what does `out` do?
end
Công cụ tổng hợp hiểu là "khi enable ở mức thấp, out không được gán" và kết luận out phải nhớ giá trị trước đó. Nhớ một giá trị cần một ô lưu trữ, nên công cụ chèn vào một latch. Latch trong thiết kế đồng bộ gây ra vấn đề về timing, khó reset và gần như không bao giờ là điều bạn muốn.
Có hai cách sửa:
Cả hai đều tạo ra cùng một phần cứng tổ hợp: một mux 2 sang 1. Mẫu "giá trị mặc định ở đầu khối" mở rộng tốt hơn khi bạn có nhiều phép gán có điều kiện cho cùng một tín hiệu.
Bên trong khối theo clock
Để ý những điểm khác so với trường hợp tổ hợp:
- Khối là
always @(posedge clk), địa hạt của flip-flop. - Phép gán dùng
<=(non-blocking). - Không có
elsecho trường hợp "không reset cũng không enable". Như vậy không sao. Trong khối theo clock, khi không nhánh nào chạy, flip-flop chỉ đơn giản giữ giá trị cũ, đúng như cách flip-flop hoạt động về mặt vật lý. Không có latch nào bị suy ra vì tín hiệu vốn đã là một thanh ghi.
Đây là chỗ duy nhất bỏ else mà vẫn an toàn. Bên ngoài khối theo clock, luôn xử lý mọi nhánh.
Chuỗi else if: bộ mã hóa ưu tiên
Một chuỗi câu lệnh else if có thứ tự ưu tiên ngầm: điều kiện đứng trước được ưu tiên hơn điều kiện đứng sau:
requests[0] có ưu tiên cao nhất: nếu nó được bật, kết quả cấp quyền là 0 bất kể các bit có chỉ số cao hơn ra sao. Công cụ tổng hợp biến chuỗi này thành một dãy mux nối tầng: kiểm tra bit 0 trước, rồi bit 1, rồi bit 2, rồi bit 3. Mỗi tầng thêm một chút độ trễ.
Nếu các điều kiện loại trừ lẫn nhau, chẳng hạn khi giải mã một đầu vào one-hot, câu lệnh case (bài tiếp theo) tạo ra phần cứng phẳng hơn và nhanh hơn chuỗi else if. Hãy dùng dạng case khi không có yêu cầu ưu tiên thật sự.
if không có else trong code theo clock
Khối theo clock không cần else vì "giữ giá trị cũ" là mặc định. Đó là cách bạn dựng tín hiệu enable:
always @(posedge clk) begin
if (load) target <= incoming;
// no else: when load is low, target keeps its value
end
Đó là một thanh ghi có enable. Phần lớn thanh ghi pipeline, bộ đếm và thanh ghi cấu hình đều dùng mẫu này.
begin/end và câu lệnh đơn
Giống C, bạn có thể bỏ begin/end khi chỉ có một câu lệnh:
if (a) out = 1;
else out = 0;
Từ hai câu lệnh trở lên, hãy dùng khối:
if (a) begin
out = 1;
flag = 1;
end else begin
out = 0;
flag = 0;
end
Hai cách viết có thể dùng xen kẽ. Các bộ quy tắc viết code thường khuyên luôn dùng begin/end để sau này thêm câu lệnh thứ hai không gặp rắc rối.
Tiếp theo
Bài tiếp theo, Câu lệnh case, nói về case, công cụ phù hợp cho giải mã nhiều nhánh (máy trạng thái, phân nhánh theo opcode, bảng ROM). Sau đó là vòng lặp for, khác vòng lặp phần mềm ở một điểm tinh tế: chúng được trải phẳng ở bước elaboration.
Câu hỏi thường gặp
Câu lệnh if trong Verilog hoạt động thế nào?
if (cond) statement; chạy statement khi cond khác 0. Bạn có thể gói nhiều câu lệnh trong begin ... end. Thêm else statement; cho nhánh còn lại, hoặc nối tiếp bằng else if (other_cond) .... if/else chỉ tồn tại bên trong khối thủ tục, initial hoặc always, chứ không nằm ở cấp cao nhất của module.
Latch bị suy ra (inferred latch) trong Verilog là gì?
Là latch mà công cụ tổng hợp tự tạo ra dù bạn không yêu cầu, vì khối always tổ hợp của bạn không gán một tín hiệu ở mọi nhánh. Công cụ thấy 'nếu a thì out = 1' mà không có else, kết luận trường hợp không được gán phải nhớ giá trị trước đó và tạo ra một latch. Latch gần như luôn là sai; cách sửa là cho mỗi tín hiệu một giá trị mặc định ở đầu khối hoặc một nhánh else tường minh.
Làm sao tránh suy ra latch trong Verilog?
Trong khối tổ hợp always @(*), hãy chắc rằng mọi reg đầu ra đều được gán ở mọi nhánh code. Cách gọn nhất là đặt giá trị mặc định ở đầu khối rồi ghi đè theo điều kiện. Trình biên dịch thường cảnh báo khi suy ra latch: hãy coi cảnh báo đó là lỗi.
Chuỗi if-else trong Verilog được tổng hợp thành gì?
Một bộ mã hóa ưu tiên. if đầu tiên có ưu tiên cao nhất, else if tiếp theo chỉ được xét khi điều kiện đầu sai, và cứ thế. Trong phần cứng, nó trở thành một chuỗi mux với thứ tự ưu tiên cố định sẵn. Nếu các điều kiện loại trừ lẫn nhau, câu lệnh case với cùng logic thường được tổng hợp thành phần cứng phẳng hơn và dễ đọc hơn.