Mở đầu: Con bug 1 dòng code không ai nhìn ra bằng mắt
Hãy tưởng tượng bạn viết một thanh ghi dịch 2 tầng (2-stage shift register) — mạch cơ bản đến mức sách
giáo khoa nào cũng có. Bạn mô phỏng, waveform hiện ra, tín hiệu vẫn "nhấp nháy" đúng nhịp clock, không có
báo lỗi đỏ nào. Nhưng khi so kỹ với bảng chân trị kỳ vọng, đầu ra trễ ít hơn 1 chu kỳ so
với thiết kế — như thể một trong hai flip-flop đã "biến mất" khỏi mạch. Không có gì sai về mặt cú pháp.
Không có cảnh báo tổng hợp. Chỉ có 1 ký tự bạn gõ nhầm: dấu = thay vì
<=. Đây là cạm bẫy được xem là "kinh điển" nhất trong toàn bộ HDL — đến mức gần như mọi kỹ
sư RTL đều từng dính ít nhất một lần, thường là ở dự án đầu tiên.
Bài này giải quyết đúng cạm bẫy đó, đồng thời xây nền tảng logic tuần tự: always_ff khai báo
ý định flip-flop rõ ràng hơn cú pháp cũ, sự khác biệt không thể xem nhẹ giữa gán blocking
(=) và non-blocking (<=), và 2 kiểu reset (đồng bộ/bất đồng bộ) với đánh đổi
riêng của từng kiểu. Cuối bài, bạn sẽ tự tay dựng một Counter 4-bit và bộ tạo PWM số —
bản song song kỹ thuật số của mạch PWM analog dùng IC 555 ở series Điện Tử.
always_comb/case trước khi học đối chiếu với logic tuần tự. Nên có: Series 10 Bài 14 (IC 555 & PWM analog) — nền tảng so sánh với PWM số ở demo cuối bài.
Tài nguyên ngoài: HDLBits, ChipVerify.
1. always_ff @(posedge clk) — khai báo rõ ý định flip-flop
Logic tổ hợp (Bài 2) không có "trí nhớ" — đầu ra luôn được tính lại từ đầu dựa trên đầu vào hiện tại.
Logic tuần tự thì ngược lại: đầu ra phụ thuộc vào giá trị trước đó, được lưu trong
flip-flop, và chỉ cập nhật tại đúng thời điểm cạnh lên của tín hiệu clock (rising edge / posedge). Verilog
cũ dùng always @(posedge clk) — cú pháp này chỉ là quy ước, trình biên dịch
không kiểm tra gì thêm để đảm bảo đây thực sự là 1 flip-flop hợp lệ.
// Verilog cũ: chỉ là quy ước, không được kiểm tra
module old_style_ff (
input wire clk, d,
output reg q
);
always @(posedge clk)
q <= d;
endmodule
// SystemVerilog: always_ff khai báo RÕ RÀNG đây là flip-flop
module new_style_ff (
input logic clk, d,
output logic q
);
always_ff @(posedge clk)
q <= d;
endmodule
always_ff giúp công cụ tổng hợp xác nhận tĩnh (static check) rằng khối này
thực sự mô tả 1 flip-flop hợp lệ — nếu bạn vô tình viết logic không đúng mẫu tuần tự chuẩn (vd trộn lẫn
blocking/non-blocking sai cách, xem Mục 2), một số công cụ sẽ cảnh báo ngay tại bước lint, trước cả khi mô
phỏng.
| Đặc điểm | Tổ hợp (always_comb, Bài 2) |
Tuần tự (always_ff, bài này) |
|---|---|---|
| Có "trí nhớ"? | Không — luôn tính lại từ đầu vào hiện tại | Có — giữ giá trị qua flip-flop |
| Khi nào cập nhật? | Ngay khi bất kỳ input đổi | Chỉ tại cạnh lên (posedge) của clock |
| Tổng hợp ra | Cổng logic thuần (AND/OR/MUX...) | Flip-flop (D-FF) + logic tổ hợp phía trước |
2. Blocking = vs Non-blocking <= — cạm bẫy số 1 của HDL
Đây là điểm khác biệt quan trọng nhất mà bài này muốn bạn nhớ suốt series: bên trong
always_ff, luôn dùng non-blocking <=. Lý do nằm ở cách 2 toán tử này định nghĩa thời điểm đọc giá trị vế phải:
-
Blocking
=: giống hệt ngôn ngữ lập trình — câu lệnh thực thi xong mới tới câu tiếp theo, và câu sau thấy được giá trị vừa gán của câu trước. -
Non-blocking
<=: mọi vế phải trong cùng 1 khốialways_ffđều được đọc từ giá trị TRƯỚC cạnh clock này (như "đóng băng" toàn bộ trạng thái tại đầu chu kỳ), rồi mọi phép gán mới được áp dụng đồng loạt ở cuối. Đây chính xác là cách hàng nghìn flip-flop vật lý hoạt động: tất cả cùng "chốt" giá trị mới tại đúng 1 cạnh clock, không có thứ tự trước sau.
❌ Sai — dùng blocking trong always_ff:
always_ff @(posedge clk) begin
q1 = d; // Gán ngay, q1 đổi tức thời
q2 = q1; // Đọc q1 VỪA gán ở trên (giá trị MỚI = d) — mất 1 tầng trễ!
end
Vì = là blocking, dòng thứ 2 đọc q1 đã bị dòng thứ 1 ghi đè trong CÙNG chu
kỳ. Kết quả: q1 và q2 luôn bằng nhau, đổi cùng lúc — 2 flip-flop
vật lý vẫn được tổng hợp ra, nhưng hành vi mô phỏng lại giống như chỉ có 1 tầng trễ,
không phải 2. Sai lệch này cực khó phát hiện bằng mắt vì sóng vẫn "nhấp nháy" bình thường.
✅ Đúng — dùng non-blocking:
always_ff @(posedge clk) begin
q1 <= d; // Đọc d tại đầu chu kỳ
q2 <= q1; // Đọc q1 CŨ (trước cạnh clock này) — đúng 2 tầng trễ
end
Đừng chỉ tin lời giải thích — hãy tự kiểm chứng bằng engine thật bên dưới. Bấm
Step Clock nhiều lần với 1 xung d, quan sát waveform: bản đúng cho
q2 trễ đúng 2 nhịp so với d; bản sai cho q1 và q2 luôn
thay đổi cùng lúc.
Bấm d để bật xung đầu vào 1 nhịp, rồi bấm Step Clock nhiều lần và theo
dõi waveform. Đổi giữa ❌ Blocking / ✅ Non-blocking và so sánh trực tiếp độ trễ của q2.
module shift_bad (
input logic clk, d,
output logic q1, q2
);
always_ff @(posedge clk) begin
q1 = d;
q2 = q1;
end
endmodule
module shift_good (
input logic clk, d,
output logic q1, q2
);
always_ff @(posedge clk) begin
q1 <= d;
q2 <= q1;
end
endmodule
<= trong always_ff (tuần tự) —
Blocking = trong always_comb (tổ hợp). Trộn lẫn 2 quy tắc này
là nguồn cạm bẫy phổ biến thứ nhì sau latch inference (Bài 2).
3. Reset đồng bộ vs bất đồng bộ
Hầu hết mạch tuần tự cần 1 tín hiệu reset để đưa thanh ghi về giá trị khởi tạo biết trước (vd counter về 0). Có 2 cách kích hoạt reset, mỗi cách một đánh đổi:
// Reset ĐỒNG BỘ: chỉ có hiệu lực tại cạnh clock (nằm trong danh sách sensitivity list @())
always_ff @(posedge clk) begin
if (rst)
count <= 4'b0000;
else
count <= count + 1;
end
// Reset BẤT ĐỒNG BỘ: có hiệu lực NGAY LẬP TỨC, không cần chờ cạnh clock
// (Lưu ý: cú pháp "or posedge rst" ngoài tập con VeriLite của trang này — chỉ minh hoạ)
always_ff @(posedge clk or posedge rst) begin
if (rst)
count <= 4'b0000;
else
count <= count + 1;
end
| Kiểu reset | Ưu điểm | Nhược điểm |
|---|---|---|
| Đồng bộ | Không cần thêm phần cứng đặc biệt, không rủi ro metastability | Cần clock đang chạy mới reset được — không dùng được nếu clock bị treo |
| Bất đồng bộ | Reset ngay lập tức, hoạt động kể cả khi clock dừng | Nếu tín hiệu reset thả xuống (release) quá gần cạnh clock, có thể gây metastability — cần thêm synchronizer (sẽ học ở Bài 7) |
Demo tương tác của bài (Mục 4) và file tải về đều dùng reset đồng bộ — đúng tập con mà VeriLite (engine của trang này) hỗ trợ mô phỏng chính xác.
4. Demo tương tác: Counter 4-bit + Bộ tạo PWM số
Counter là mạch tuần tự cơ bản nhất: mỗi cạnh clock, giá trị tăng thêm 1, tự "cuộn vòng" về 0 khi tràn số
(4-bit: 15 → 0). Ghép thêm 1 bộ so sánh tổ hợp (count < duty), bạn có ngay 1 bộ tạo
PWM số — thay vì dùng tụ điện nạp/xả như
IC 555 (mạch PWM analog đã học ở series Điện Tử), độ
rộng xung được điều khiển hoàn toàn bằng số đếm nhị phân.
Bấm Step Clock để đi từng nhịp, hoặc Run 16 nhịp để xem trọn 1 chu
kỳ counter. Đổi duty threshold rồi Run lại — tỉ lệ thời gian pwm=1 trên waveform thay
đổi đúng theo threshold/16.
module pwm_digital (
input logic clk, rst,
input logic [3:0] duty,
output logic pwm_out
);
logic [3:0] count;
always_ff @(posedge clk) begin
if (rst)
count <= 4'b0000;
else
count <= count + 1;
end
assign pwm_out = (count < duty) ? 1'b1 : 1'b0;
endmodule
count,
duty) — cùng 1 thiết kế chạy trên FPGA/ASIC bất kỳ đều cho
chính xác cùng 1 kết quả, không lệch dù chỉ 1 phần nghìn giây, đổi lại cần 1 nguồn
clock ổn định làm "nhịp đếm".
Tóm lược
Bài này chuyển từ logic tổ hợp (không nhớ) sang logic tuần tự (có nhớ qua clock):
-
✅
always_ff @(posedge clk)khai báo rõ ý định flip-flop, được kiểm tra tĩnh — an toàn hơnalways @(posedge clk)cũ. -
✅ Luôn dùng non-blocking
<=trong always_ff — blocking=có thể làm "mất" cả 1 tầng flip-flop trong thanh ghi dịch nhiều tầng. - ✅ Reset đồng bộ an toàn hơn (không metastability) nhưng cần clock chạy; bất đồng bộ phản hồi ngay nhưng cần synchronizer (Bài 7).
- ✅ Counter + bộ so sánh tổ hợp = PWM số, xác định 100%, khác PWM analog dùng linh kiện R/C.
Bài tiếp theo chuyển sang máy trạng thái hữu hạn (FSM) — cách mô hình hoá hành vi có
nhiều "chế độ" khác nhau, xây trên chính nền tảng always_ff vừa học.
Trắc nghiệm ôn tập
Câu 1
Trong 1 khối always_ff, phép gán non-blocking (<=) đọc giá trị vế phải từ
đâu?
Câu 2
Thanh ghi dịch 2 tầng viết bằng blocking (q1 = d; q2 = q1;) trong
always_ff sẽ có hành vi gì so với thiết kế đúng (non-blocking)?
Câu 3
Reset bất đồng bộ (asynchronous reset) có rủi ro chính nào?
Câu 4
PWM số (digital PWM) khác PWM analog (IC 555) ở điểm cốt lõi nào?
Tải file code thực hành minh họa bài học
File SystemVerilog gồm counter 4-bit, PWM số, và cả 2 phiên bản thanh ghi dịch (đúng/sai) kèm testbench so sánh trực tiếp. Chạy được trên Verilator hoặc dán vào EDA Playground:
Tải về sequential_flops.sv
Bình luận