Công cụ chủ lực của Verilog hành vi
assign mô tả logic tổ hợp gồm một phương trình duy nhất. Ngay khi cần if/else, case hoặc bộ nhớ, bạn sẽ dùng đến always. Khối always là một đoạn code thủ tục chạy lại mỗi khi những tín hiệu cụ thể thay đổi. Các tín hiệu kích hoạt việc chạy lại đó gọi là sensitivity list (danh sách độ nhạy).
Có hai dạng always mà bạn sẽ gặp nhiều nhất:
always @(*): chạy lại mỗi khi bất kỳ tín hiệu nào được đọc trong khối thay đổi. Tạo ra logic tổ hợp.always @(posedge clk): chỉ chạy lại ở sườn lên củaclk. Tạo ra logic tuần tự theo clock (flip-flop).
Còn có các dạng khác (@(a or b), @(negedge clk), @(posedge clk or negedge reset_n)), nhưng hai dạng trên chiếm gần như mọi khối tổng hợp được mà bạn sẽ viết.
always @(*) tổ hợp
Ba điều cần để ý:
outlàreg. Mọi thứ được gán bên trongalwaysđều phải làreg. Từ khóa này không có nghĩa "đây là một flip-flop"; ở đây nó chỉ có nghĩa "tôi được ghi từ một khối thủ tục".always @(*). Dấu*nghĩa là "thức dậy mỗi khi bất cứ thứ gì tôi đọc thay đổi". Trình mô phỏng tự xác định sensitivity list. Bạn có thể tự viết danh sách, ví dụalways @(sel), nhưng@(*)an toàn hơn vì bỏ sót một tín hiệu là nguồn bug kinh điển.- Không có clock. Khối này mô tả logic tổ hợp. Công cụ tổng hợp tạo ra một khối logic tính
outtrực tiếp từsel: không có flip-flop, không cần chân clock.
Nhánh default không bắt buộc về cú pháp, nhưng về bản chất thì nên coi là bắt buộc. Bỏ nó đi thì mọi giá trị đầu vào không được liệt kê sẽ khiến out giữ giá trị cũ, và điều đó được tổng hợp thành một latch ngoài ý muốn. Luôn thêm nhánh default.
always @(posedge clk) tuần tự
Những khác biệt chính so với bản tổ hợp:
always @(posedge clk). Khối chỉ chạy lại ở sườn lên củaclk. Giữa hai sườn không có gì xảy ra.- Phép gán non-blocking
<=. Bên trong một khối theo clock, đây là toán tử đúng. Nó có nghĩa "lên lịch đểcountnhận giá trị mới vào cuối bước thời gian", đúng như cách một flip-flop hoạt động. Lý do và cách làm thay thế có trong Blocking và non-blocking. - Không cần
default. Lệnhifđã bao cả hai nhánh (reset và không reset). Không có nguy cơ sinh latch.
Công cụ tổng hợp nhận ra dạng này (sensitivity theo clock, phép gán non-blocking) và tạo ra một thanh ghi 4 bit (bốn flip-flop), cùng logic tổ hợp tính count + 1 và bộ mux chọn giữa reset và tăng giá trị.
Khác biệt khi tổng hợp
Cùng một mã nguồn module có thể mô tả hai phần cứng hoàn toàn khác nhau, tùy vào dạng của khối always:
| Khối | Phần cứng |
|---|---|
always @(*) y = expr; | Logic tổ hợp thuần. Không có bộ nhớ. |
always @(posedge clk) y <= expr; | Flip-flop. Lưu expr một lần mỗi chu kỳ clock. |
always @(*) if (en) y = expr; | Latch, thường là bug. Trường hợp "else" giữ giá trị cũ. |
always @(posedge clk) if (en) y <= expr; | Flip-flop có enable. Chỉ lưu khi en ở mức cao. |
Trường hợp thứ ba là cái bẫy latch. Latch là một ô nhớ trong suốt, giữ nguyên đầu ra khi đầu vào không được kích hoạt: hữu ích trong một số thiết kế đặc thù, nhưng gần như luôn là bug khi xuất hiện ngoài ý muốn. Hầu hết công cụ tổng hợp đều cảnh báo rõ khi suy ra một latch mà bạn không yêu cầu. Hãy coi cảnh báo đó là lỗi.
Các biến thể của sensitivity list
Bạn sẽ gặp một vài dạng sensitivity list ít phổ biến hơn:
always @(a or b or c): danh sách tường minh. Verilog-2001 thêm dấu phân cách,:always @(a, b, c). Cách nào cũng được.always @(posedge clk or negedge reset_n): reset bất đồng bộ. Khối chạy ở sườn lên của clock hoặc sườn xuống của reset. Dùng khi reset phải có hiệu lực ngay lập tức, không chờ đến clock kế tiếp.always @(negedge clk): chạy theo sườn xuống của clock. Hiếm gặp; một số thiết kế dùng nó cho flip-flop "kích hoạt sườn âm", lưu dữ liệu ở sườn xuống thay vì sườn lên.
Với thiết kế mới, hãy ưu tiên always @(*) cho logic tổ hợp và always @(posedge clk) cho logic tuần tự. Chỉ dùng reset bất đồng bộ khi thiết kế thực sự cần.
Hai khối là hai phần cứng
Nhiều khối always trong cùng một module là độc lập với nhau, mỗi khối trở thành một phần cứng riêng:
Khối theo clock tạo ra một thanh ghi flip-flop. Khối tổ hợp tạo ra một cổng XOR. Chúng nằm cạnh nhau và không khối nào biết đến khối kia. Hai đầu ra thay đổi theo những lịch hoàn toàn khác nhau.
Những gì khối always không làm được
Một vài điều trông hấp dẫn nhưng không được phép:
- Gán cho một
wire: đích phải làreg. Trình biên dịch bắt buộc điều này. - Gán cho cùng một
regtừ hai khốialwayskhác nhau: gây ra hành vi không xác định khi mô phỏng và không tổng hợp được. Mỗi tín hiệu chỉ có một nguồn điều khiển. - Đọc và ghi cùng một tín hiệu trong cùng một khối tổ hợp theo cách tạo ra vòng phản hồi:
always @(*) x = x + 1;là một vòng lặp không độ trễ mà trình mô phỏng không giải được.
Hai lỗi đầu trình biên dịch bắt được. Lỗi thứ ba đôi khi chỉ lộ ra lúc mô phỏng dưới dạng chương trình bị treo.
Tiếp theo là gì
Tài liệu tiếp theo, Khối initial, nói về người anh em của always: một khối chạy đúng một lần khi bắt đầu mô phỏng. Đó là công cụ chủ lực của testbench. Sau đó là các quy tắc về phép gán blocking và non-blocking, quyết định khối theo clock của bạn có làm đúng điều bạn muốn hay không.
Câu hỏi thường gặp
Khối always trong Verilog là gì?
always mở đầu một khối thủ tục chạy lại mỗi khi các tín hiệu trong sensitivity list của nó thay đổi. Có hai dạng: always @(*) tạo logic tổ hợp (chạy lại khi bất kỳ đầu vào nào thay đổi), và always @(posedge clk) tạo logic tuần tự (chạy lại ở mỗi sườn lên của clk). Thân của khối always có thể chứa if, case, for và các phép gán thủ tục.
always @(*) và always @(posedge clk) khác nhau thế nào?
always @(*) nhạy với mọi tín hiệu được đọc trong khối; nó tạo ra logic tổ hợp không có bộ nhớ. always @(posedge clk) chỉ nhạy với sườn lên của clk; nó tạo ra các flip-flop lưu trạng thái một lần mỗi chu kỳ clock. Dạng thứ nhất không có clock và không có thanh ghi; dạng thứ hai có cả hai.
Sensitivity list trong Verilog là gì?
Là danh sách tín hiệu sau @ quyết định khi nào một khối always chạy lại. @(*) là cách viết tắt của 'mọi tín hiệu được đọc trong khối'. @(posedge clk) chỉ chạy ở sườn lên của clk. @(posedge clk or negedge reset_n) chạy khi một trong hai sự kiện xảy ra, dùng cho reset bất đồng bộ. Viết sai sensitivity list là một trong những nguyên nhân phổ biến nhất khiến kết quả mô phỏng và tổng hợp không khớp nhau.
Có gán được cho wire bên trong khối always không?
Không. Khối always chỉ gán được cho reg (hoặc logic trong SystemVerilog). Trình biên dịch bắt buộc điều này. Nếu muốn một wire là đầu ra của logic thủ tục, hãy khai báo một reg trung gian, điều khiển nó bên trong always, rồi dùng assign để gán wire từ reg đó ở bên ngoài, hoặc đơn giản là đổi wire thành reg.