Viên gạch nền: D flip-flop
Mọi thứ trong thiết kế đồng bộ đều quy về một linh kiện phần cứng nhỏ xíu: D flip-flop. Nó có một đầu vào clock, một đầu vào dữ liệu và một đầu ra dữ liệu. Ở mỗi sườn lên của clock, nó chốt giá trị của D và giữ nguyên cho đến sườn kế tiếp. Chỉ vậy thôi.
Trong Verilog:
always @(posedge clk) begin
q <= d;
end
Ba dòng, một flip-flop. Toán tử <= là phép gán non-blocking, đúng với hành vi của flip-flop thật (xem bài Blocking và non-blocking). Danh sách độ nhạy posedge clk chính là thứ khiến khối này trở thành logic tuần tự.
Xếp nhiều flip-flop như vậy với logic tổ hợp ở giữa, bạn có thể dựng bất kỳ mạch đồng bộ nào mình muốn.
Thanh ghi có reset
Thiết kế thật luôn cần reset, tức một cách đưa hệ thống về trạng thái đã biết khi cấp nguồn hoặc khi cần:
Đây là reset đồng bộ: điều kiện reset được xét ở sườn clock như mọi đầu vào khác. Công cụ tổng hợp tạo ra một flip-flop có mux 2 sang 1 ở đầu vào dữ liệu: khi reset ở mức cao, mux đưa vào số 0; ngược lại nó đưa vào d.
Với phần lớn thiết kế, reset đồng bộ là lựa chọn đúng. Timing đơn giản hơn, chạy tốt trên FPGA, và tín hiệu reset có thể đến từ bất cứ đâu, không cần một nguồn riêng căn theo clock.
Thanh ghi có enable
Bạn thường cần một thanh ghi chỉ cập nhật khi có tín hiệu cho phép. Hãy đặt một câu lệnh if bên trong khối theo clock, và dựa vào hành vi ngầm "giữ giá trị cũ" khi thiếu else:
Đây là mẫu "thanh ghi có enable" kinh điển. Nó xuất hiện khắp nơi: thanh ghi cấu hình, tầng pipeline chỉ tiến khi tầng sau đã sẵn sàng, bộ đếm có thể tạm dừng rồi chạy tiếp. Việc bỏ else cuối cùng là có chủ ý và an toàn trong khối theo clock: flip-flop vốn đã nhớ giá trị trước đó, nên "không làm gì" nghĩa là "giữ nguyên".
Trong một khối tổ hợp, cùng đoạn code đó sẽ suy ra latch. Quy tắc khác nhau, cú pháp như nhau.
Bộ đếm
Bộ đếm là một thanh ghi có giá trị kế tiếp bằng giá trị hiện tại cộng một:
Bộ đếm tăng lên ở mỗi chu kỳ clock khi enable ở mức cao. Sau 16 chu kỳ nó quay vòng về 0 (vì ta khai báo 4 bit và 15 + 1 tràn thành 0). Việc quay vòng đó là bản chất của số học N bit và cũng chính là cách bộ đếm phần cứng thật hoạt động.
Thanh ghi dịch
Xếp chuỗi các flip-flop cho bạn một thanh ghi dịch. Mẹo là thực hiện toàn bộ phép dịch trong một câu lệnh non-blocking duy nhất:
Thân khối là out <= {out[WIDTH-2:0], in}: ghép các bit thấp của out hiện tại với in mới, rồi gán cả cụm. Vì đây là phép gán non-blocking, vế phải đọc giá trị out cũ trước khi vế trái được cập nhật. Kết quả là một phép dịch N bit gọn gàng trong một chu kỳ clock.
Đây là mẫu thanh ghi dịch ở dạng nhỏ gọn nhất. Nó mở rộng ra thành LFSR, bộ truyền nối tiếp, bộ chuyển nối tiếp sang song song, tức mọi thiết kế mà dữ liệu đi qua một chuỗi flip-flop theo từng clock.
Pipeline
Pipeline là một chuỗi thanh ghi ngăn cách bởi logic tổ hợp. Mỗi tầng xử lý dữ liệu từ tầng trước rồi chuyển cho tầng sau:
Ba tầng, độ trễ ba chu kỳ, nhưng khi pipeline đã đầy thì mỗi chu kỳ cho ra một kết quả mới. Độ trễ là 3 clock vì dữ liệu đi qua ba flip-flop; thông lượng là 1 phép tính mỗi clock vì cả ba tầng cùng làm việc song song trên các đầu vào khác nhau.
Đây là cách các thiết kế hiệu năng cao đạt mục tiêu thông lượng: giữ mỗi tầng ngắn, chia pipeline sâu hơn và để tính song song gánh việc.
Reset bất đồng bộ (khi bạn cần)
Đôi khi bạn không thể chờ sườn clock để kích hoạt reset: chip đang tắt nguồn, clock bị ngắt, một watchdog bên ngoài đang kéo đường reset. Khi đó:
always @(posedge clk or negedge reset_n) begin
if (~reset_n) q <= 0;
else q <= d;
end
Lúc này danh sách độ nhạy có cả sườn clock lẫn sườn reset. Flip-flop phản ứng ngay với bất kỳ sườn nào. Theo quy ước, reset_n tích cực mức thấp (được kích hoạt khi ở mức thấp), đó là lý do điều kiện kiểm tra là ~reset_n.
Reset bất đồng bộ có đánh đổi: khó phân tích timing hơn, có thể gây metastability nếu không xử lý cẩn thận lúc nhả reset, và không dùng được trên mọi kiến trúc FPGA. Mặc định hãy dùng reset đồng bộ, chỉ dùng bất đồng bộ khi thiết kế thật sự đòi hỏi.
Tiếp theo
Giờ bạn đã dựng được mọi đường dữ liệu đồng bộ. Bài tiếp theo, Máy trạng thái hữu hạn, kết hợp một thanh ghi theo clock với câu lệnh case để tạo ra mẫu FSM chuẩn: trái tim của mọi bộ điều khiển, bộ xử lý giao thức và khối ra quyết định trong thiết kế số.
Câu hỏi thường gặp
Logic theo clock trong Verilog là gì?
Là logic chỉ cập nhật theo nhịp của một tín hiệu clock. Cách viết chuẩn là always @(posedge clk) target <= next_value;: ở mỗi sườn lên của clk, target chốt giá trị next_value. Chỉ một dòng đó đã mô tả một flip-flop trong phần cứng. Xếp nhiều flip-flop với logic tổ hợp ở giữa, bạn dựng được bộ đếm, thanh ghi dịch, pipeline, tức mọi thứ đồng bộ.
Reset đồng bộ và reset bất đồng bộ trong Verilog khác nhau thế nào?
Reset đồng bộ dùng always @(posedge clk) if (reset) ...: reset được lấy mẫu ở sườn clock như mọi đầu vào khác. Reset bất đồng bộ dùng always @(posedge clk or negedge reset_n) if (~reset_n) ...: khối được kích hoạt bởi sườn clock hoặc khi reset được kích hoạt, nên reset có hiệu lực ngay lập tức. Reset đồng bộ là lựa chọn mặc định; reset bất đồng bộ dùng khi reset phải chắc chắn có hiệu lực kể cả lúc clock đã ngừng.
Làm sao xây dựng pipeline trong Verilog?
Xếp nhiều tầng always @(posedge clk) stageN_reg <= stageN_combinational;: logic tổ hợp của mỗi tầng cấp dữ liệu cho thanh ghi của tầng sau, và mọi thanh ghi cùng chốt ở một sườn clock. Kết quả là một pipeline nhận dữ liệu mới ở mỗi chu kỳ và cho ra kết quả sau N chu kỳ, với thông lượng một kết quả mỗi clock.
Thanh ghi dịch trong Verilog là gì?
Là một chuỗi flip-flop trong đó đầu ra của flip-flop này nối vào đầu vào của flip-flop kế tiếp. Mỗi sườn clock dịch mọi bit đi một vị trí. Phiên bản Verilog kinh điển dùng phép gán non-blocking: out <= {out[N-2:0], in};, một dòng tạo ra thanh ghi dịch N bit nhận một bit từ in mỗi chu kỳ.