Mở đầu: Cạm bẫy im lặng nhất của logic tổ hợp
Bài 1 đã chứng minh RTL là bản thiết kế phần cứng song song, không phải chương trình tuần
tự — nhưng biết điều đó chưa đủ để viết code không lỗi. Có một loại lỗi HDL nguy hiểm hơn hẳn lỗi cú pháp:
nó biên dịch sạch, mô phỏng "gần đúng", thậm chí chạy ổn trên FPGA test ban đầu — rồi vài tuần sau xuất
hiện timing violation ngẫu nhiên mà không ai hiểu vì sao. Thủ phạm gần như luôn là 1 dòng code bạn
quên viết, không phải dòng bạn viết sai: thiếu 1 nhánh else, thiếu 1 dòng
default. Trình tổng hợp sẽ không báo lỗi — nó âm thầm "sáng tạo" thêm 1 phần tử nhớ (latch)
mà bạn chưa từng định nghĩa.
Bài này bắt đầu viết SystemVerilog thật: khai báo module/port, kiểu dữ liệu logic (thay thế 2
kiểu wire/reg hay gây hiểu lầm của Verilog cũ), toán tử số học/bitwise trên
vector nhiều bit, và cách always_comb mô tả logic tổ hợp phức tạp. Điểm nhấn chính là cạm bẫy
latch inference nói trên — bạn sẽ tự tay thử code thiếu else ngay trên
trang, xem engine "từ chối" nó ra sao, trước khi áp dụng toàn bộ kiến thức vào xây 1
ALU 4-bit hoàn chỉnh.
Tài nguyên ngoài: HDLBits, ChipVerify.
1. Module/port và kiểu logic (thay wire/reg cũ)
Verilog cũ (1984) chia tín hiệu thành 2 kiểu tách biệt: wire (chỉ được gán bằng
assign, mô tả dây nối tổ hợp) và reg (chỉ được gán bên trong khối
always). Cái tên reg gây hiểu lầm lớn nhất lịch sử Verilog —
nó không có nghĩa là thanh ghi (register/flip-flop) thật, chỉ đơn thuần là "được gán
trong always block". Một reg hoàn toàn có thể tổng hợp ra logic tổ hợp thuần túy (không có
flip-flop nào cả) nếu khối always đó là always_comb.
| Kiểu (Verilog cũ) | Được gán bằng | Ngộ nhận phổ biến |
|---|---|---|
wire |
assign (continuous) |
Đúng — luôn là tổ hợp |
reg |
bên trong always |
❌ "reg = có flip-flop" — SAI, chỉ đúng khi always đó nhạy cạnh clock |
SystemVerilog (2005) gộp cả hai thành 1 kiểu duy nhất: logic. Bạn dùng logic
cho hầu hết mọi tín hiệu — trình biên dịch sẽ tự xác định nó là tổ hợp hay tuần tự dựa vào
cách bạn gán (qua assign/always_comb hay
always_ff), không phải dựa vào từ khóa khai báo.
// Verilog cũ: 2 kiểu tách biệt, dễ nhầm
module old_style (
input wire a, b,
output reg y
);
always @(*) y = a & b; // "reg" nhưng vẫn là tổ hợp!
endmodule
// SystemVerilog: 1 kiểu duy nhất, rõ ràng hơn
module new_style (
input logic a, b,
output logic y
);
always_comb y = a & b; // Rõ ràng: always_comb = chắc chắn tổ hợp
endmodule
wire vẫn hợp lệ và vẫn được dùng (đặc biệt khi nối dây giữa các module con), nhưng
logic là lựa chọn mặc định cho code mới — series này dùng logic xuyên suốt, chỉ
nhắc lại wire/reg khi đọc code legacy.
2. assign, toán tử số học & bitwise, vector [N:0]
Một tín hiệu không chỉ có thể là 1 bit — logic [3:0] a khai báo
vector 4 bit (bus), đánh số từ bit 3 (MSB)
xuống bit 0 (LSB). Đây là cách biểu diễn số nguyên nhị phân
trong phần cứng, giống hệt khái niệm
biểu diễn số trong Series C nhưng ở mức dây dẫn vật lý thay vì
byte trong RAM.
logic [3:0] a; // vector 4-bit: a[3] a[2] a[1] a[0]
logic [7:0] byte_data; // vector 8-bit, y hệt 1 byte
assign a = 4'b1010; // gán literal nhị phân 4-bit = 10 thập phân
assign a[1:0] = 2'b11; // gán riêng 2 bit thấp (bit-slicing)
SystemVerilog phân biệt rõ 2 nhóm toán tử dễ nhầm lẫn nhất với người mới: bitwise (thao tác từng bit độc lập, kết quả có cùng độ rộng với input) và logical (thao tác trên toàn bộ giá trị như một điều kiện đúng/sai, kết quả luôn là 1 bit).
| Nhóm | Toán tử | Ý nghĩa | Ví dụ (a=4'b1010, b=4'b0110) |
|---|---|---|---|
| Bitwise | & |
AND từng bit | a & b = 4'b0010 |
| |
OR từng bit | a | b = 4'b1110 |
|
^ |
XOR từng bit | a ^ b = 4'b1100 |
|
~ |
Đảo từng bit (NOT) | ~a = 4'b0101 |
|
| Logical | && |
AND logic (coi cả vector là 1 điều kiện) | a && b = 1'b1 (cả 2 khác 0) |
|| |
OR logic | a || b = 1'b1 |
|
! |
NOT logic | !a = 1'b0 (a khác 0 nên !a sai) |
& khác &&a & b cho ra
vector cùng độ rộng với a/b (AND từng bit), còn a && b luôn cho ra
đúng 1 bit (coi cả a, cả b là điều kiện đúng/sai). Gõ nhầm & thành
&& trong 1 phép tổ hợp bus sẽ làm mất toàn bộ độ rộng bit mà trình biên dịch không
báo lỗi cú pháp nào — chỉ sai kết quả khi mô phỏng.
Toán tử số học (+ - * / %) hoạt động như số học nhị phân thông thường, nhưng luôn bị giới hạn
bởi độ rộng bit khai báo — 4-bit chỉ biểu diễn được 0–15 (không dấu). Phép cộng tràn số
(overflow) sẽ tự động "cuộn vòng" (wrap-around) chứ không báo lỗi:
4'b1111 + 4'b0001 = 4'b0000 (15 + 1 = 16, nhưng 16 không vừa 4 bit nên chỉ còn 0).
Mặc định vector là không dấu (unsigned). Muốn biểu diễn số âm, khai báo
signed: logic signed [3:0] a; — khi đó bit MSB (bit 3) là bit dấu (0 = dương, 1
= âm, biểu diễn theo bù 2 — two's complement).
3. always_comb với if/case — cạm bẫy latch inference
always_comb là cách viết logic tổ hợp phức tạp (nhiều điều kiện) mà assign đơn
dòng không diễn đạt nổi. Nó thay thế cú pháp cũ always @(*) — điểm khác biệt quan trọng:
always_comb được trình biên dịch kiểm tra tĩnh (static check) để đảm bảo
đúng là logic tổ hợp, trong khi always @(*) chỉ là quy ước, không được kiểm tra gì cả.
// Verilog cũ: always @(*) — không kiểm tra gì, dễ viết sai mà không biết
always @(*) begin
if (sel)
y = a;
else
y = b;
end
// SystemVerilog: always_comb — công cụ tổng hợp bắt lỗi ngay nếu thiếu nhánh
always_comb begin
if (sel)
y = a;
else
y = b;
end
Đây chính là lúc cạm bẫy nghiêm trọng nhất của tổ hợp logic xuất hiện:
latch inference — khi 1 khối always_comb/case không gán giá trị
output ở mọi nhánh có thể xảy ra (thiếu else hoặc thiếu
default).
❌ Sai — thiếu else, sinh ra latch ngoài ý muốn:
always_comb begin
if (en)
y = a;
// KHÔNG có else! Khi en=0, y phải giữ giá trị gì?
end
Khi en=0, code không nói rõ y bằng gì. Phần cứng thật
không có khái niệm "giữ nguyên giá trị cũ" cho logic tổ hợp thuần túy — combinational
logic phải luôn xuất ra 1 giá trị xác định cho mỗi tổ hợp input. Để "giải quyết" việc thiếu thông tin
này, trình tổng hợp buộc phải chèn thêm 1 latch (bộ nhớ nhạy mức, level-sensitive) để
giữ giá trị cũ khi en=0 — bạn vô tình có 1 phần tử nhớ trong mạch mà không hề định nghĩa
nó, dễ gây lỗi thời gian (timing) khó debug ở bước sau.
✅ Đúng — có else, chắc chắn là tổ hợp thuần túy:
always_comb begin
if (en)
y = a;
else
y = 1'b0; // Luôn xác định — không còn lý do để sinh latch
end
Bấm nút en/a để đổi input. Với bản ✅ Có else, y luôn
phản hồi đúng theo bảng chân trị.
module latch_bug (
input logic en, a,
output logic y
);
always_comb begin
if (en)
y = a;
// thiếu else
end
endmodule
module latch_fixed (
input logic en, a,
output logic y
);
always_comb begin
if (en)
y = a;
else
y = 1'b0;
end
endmodule
y sẽ luôn hiện ? (không
phản hồi en/a) — vì VeriLite chỉ nhận diện được cặp if/else đầy
đủ là 1 phép gán hợp lệ. Đây không phải giới hạn ngẫu nhiên: một linter SystemVerilog thật (Verilator
-Wall, Vivado, Design Compiler) cũng phát hiện và cảnh báo ngay "not all
outputs assigned in all paths" trước khi bạn kịp mô phỏng sai — VeriLite mô phỏng đúng tinh thần đó, chỉ
là ở mức "im lặng không có driver" thay vì in ra thông báo lỗi.
case hoạt động tương tự if/else if nhưng rõ ràng hơn khi có nhiều
hơn 2-3 nhánh — và cùng một cạm bẫy latch áp dụng: thiếu default có nghĩa là
còn tổ hợp input nào đó chưa được xử lý, sinh ra latch y hệt thiếu else. Luôn viết
default: ở cuối mọi khối case tổ hợp, kể cả khi bạn nghĩ đã liệt kê đủ mọi giá
trị — đây chính là kỹ thuật demo tiếp theo: ALU 4-bit với 8 opcode chọn qua case.
4. Demo tương tác: ALU 4-bit mini
ALU là khối tổ hợp trung tâm của mọi CPU — nhận 2 toán hạng
a, b (4-bit) và 1 mã lệnh opcode (3-bit, chọn 1 trong 8 phép toán),
xuất ra kết quả y. Đây chính xác là ví dụ kinh điển của case tổ hợp: mỗi nhánh
case là 1 phép toán khác nhau, default đảm bảo không rơi vào latch.
Bấm nút chọn opcode và toggle từng bit của a/b — sơ đồ tô sáng đúng khối phép toán đang chạy qua
MUX. Đây là cách case thực sự tổng hợp: N khối tính toán song song + 1 mux chọn theo
opcode, không phải "if/else lồng nhau tuần tự" như khi đọc code.
module alu4 (
input logic [3:0] a, b,
input logic [2:0] opcode,
output logic [3:0] y
);
always_comb begin
case (opcode)
3'b000: y = a + b; // ADD
3'b001: y = a - b; // SUB
3'b010: y = a & b; // AND
3'b011: y = a | b; // OR
3'b100: y = a ^ b; // XOR
3'b101: y = ~a; // NOT
3'b110: y = a << 1; // SHL
3'b111: y = a >> 1; // SHR
default: y = 4'b0000; // bắt buộc — tránh latch inference
endcase
end
endmodule
Tóm lược
Bài này đi từ khai báo tín hiệu đến logic tổ hợp phức tạp hơn:
-
✅
logicthay thếwire/reg— 1 kiểu duy nhất, rõ ràng hơn;regKHÔNG có nghĩa là flip-flop thật. -
✅ Phân biệt toán tử bitwise (
& | ^ ~, giữ nguyên độ rộng bit) và logical (&& || !, luôn ra 1 bit). - ✅ Vector
[N:0]biểu diễn bus nhiều bit; mặc định không dấu, tràn số tự cuộn vòng. -
✅
always_comban toàn hơnalways @(*)cũ; luôn viếtelse/defaultđầy đủ để tránh latch inference ngoài ý muốn. - ✅
casetổng hợp thành N khối song song + 1 mux chọn, không phải if/else tuần tự.
Bài tiếp theo chuyển sang phần tuần tự (sequential): always_ff, thanh ghi,
và cạm bẫy blocking vs non-blocking assignment — cạm bẫy kinh điển thứ 2 của HDL.
Trắc nghiệm ôn tập
Câu 1
Trong Verilog cũ, từ khóa reg có ý nghĩa chính xác là gì?
Câu 2
Với a = 4'b1010 (10 thập phân), kết quả của a && 0 là gì?
Câu 3
Latch inference xảy ra khi nào trong 1 khối always_comb?
Câu 4
case tổ hợp (vd ALU 8 opcode) tổng hợp thành cấu trúc phần cứng nào?
Tải file code thực hành minh họa bài học
File SystemVerilog ALU 4-bit đầy đủ 8 opcode kèm testbench tự kiểm tra từng phép toán trên nhiều tổ hợp a/b. Chạy được trên Verilator hoặc dán trực tiếp vào EDA Playground:
Tải về alu4.sv
Bình luận