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.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 1 (Từ cổng logic đến RTL) — cần hiểu HDL mô tả phần cứng song song trước khi đọc tiếp bài này.
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.

logic_vs_reg.sv
// 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)
🕳️ Cạm bẫy: & khác &&
Đây là lỗi gõ nhầm phổ biến nhất khi mới học HDL. 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).

🕳️ Cạm bẫy: Latch inference khi thiếu else/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
🔍 Tự kiểm chứng: engine "từ chối" code thiếu else
Đang thử:
y = ?

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ị.

latch_bug.sv
module latch_bug (
  input logic en, a,
  output logic y
);
  always_comb begin
    if (en)
      y = a;
    // thiếu else
  end
endmodule
latch_fixed.sv
module latch_fixed (
  input logic en, a,
  output logic y
);
  always_comb begin
    if (en)
      y = a;
    else
      y = 1'b0;
  end
endmodule
🔬 Đào sâu: Vì sao VeriLite (engine của trang này) "từ chối" bản thiếu else?
Bấm thử tab ❌ Thiếu else ở trên: 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.

🎛️ ALU 4-bit mini — 8 phép toán qua case
Opcode:
a=3 b=1 → y=0 (0000)

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.

alu4.sv
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:

  • logic thay thế wire/reg — 1 kiểu duy nhất, rõ ràng hơn; reg KHÔ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_comb an toàn hơn always @(*) cũ; luôn viết else/default đầy đủ để tránh latch inference ngoài ý muốn.
  • case tổ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

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 1: Từ cổng logic đến RTL Bài 3: SystemVerilog tuần tự & Thanh ghi Quay lại Lộ trình Series Thiết Kế Vi Mạch Số & FPGA

Bình luận