Module và instance: nối hai bộ cộng mà không nhầm cổng

Khoảng 25 phútMức 1/3Bản nháp: một số khẳng định chưa đối chiếu spec

Tiên quyết

Sau bài này bạn sẽ

  • Phân biệt tên module, tên instance và tên tín hiệu nối cổng
  • Đọc kết nối .port(signal) từ parent sang child
  • Tính số bit cần cho tổng và phát hiện mất carry ở biên module
  • Đọc đường dẫn hierarchy và giải thích giới hạn của hierarchy sau synthesis

Một bản thiết kế, nhiều instance

Bạn có một khối cộng hai số 4 bit. Hệ thống cần cộng hai cặp số cùng lúc: cặp bên trái và cặp bên phải. Viết lại hai bản code phép cộng sẽ khó bảo trì: sửa bên trái rồi quên bên phải là một lỗi rất thực tế.

Tách phép cộng thành module nibble_sum, rồi tạo hai instance từ cùng định nghĩa. Có ba loại tên cần tách bạch:

  • Tên module nibble_sum: định nghĩa giao diện và logic của khối.
  • Tên instance u_left, u_right: hai chỗ dùng cụ thể trong parent twin_sum.
  • Tên tín hiệu a0, b0, a1, b1: các dây phía parent được nối tới cổng child.

Đừng đọc instantiation như lời gọi hàm chạy lần lượt. Hai instance của ví dụ là hai nhánh logic tổ hợp trong mô hình phần cứng; không cần gọi u_left rồi mới gọi u_right. Module này không có clock và không giữ kết quả của lần cộng trước.

Đọc cổng theo hai phía

Trong khai báo nibble_sum, input và output mô tả hướng nhìn từ chính child. a, b đi vào child; total đi ra child. Một kết nối ở parent là:

nibble_sum u_left (.a(a0), .b(b0), .total(total0));

Đọc .a(a0) thành “cổng a của instance này nối với tín hiệu a0 ở parent”. Phía trước dấu ngoặc là tên cổng trong định nghĩa child; phía trong ngoặc là tín hiệu của parent. nibble_sum chọn loại module, còn u_left đặt tên cho instance.

Đổi tên dây parent từ a0 sang left_operand thì kết nối thành .a(left_operand); không tự đổi cổng child thành .left_operand(...). Đổi tên cổng child là thay đổi giao diện và phải cập nhật những nơi dùng nó.

Đây là cách giải thích quy tắc ngôn ngữ cần tiếp tục đối chiếu với [2]. Bài đang ở draft; kết quả tool ở phần sau chỉ là bằng chứng của các chương trình cụ thể, không thay thế toàn bộ đặc tả ngôn ngữ.

Hai bộ cộng, kiểm tra toàn bộ đầu vào

Mỗi đầu vào 4 bit unsigned nhận từ 0 đến 15. Tổng lớn nhất là 15 + 15 = 30, cần 5 bit. Trong child, phép cộng mở rộng rõ ràng cả hai toán hạng bằng {1'b0, a} và {1'b0, b} để giữ carry.

Trước khi chạy tool, tính tay ba kỳ vọng:

  • Bên trái 15 + 1 = 16, bên phải 3 + 4 = 7.
  • Bốn đầu vào độc lập, mỗi đầu vào có 16 giá trị: 16 × 16 × 16 × 16 = 65536 trường hợp.
  • Ở tổ hợp cuối, cả hai cặp là 15 + 15, nên cả hai output là 30.

Hai instance, hai bộ cổng độc lập

Đối chiếu cách nối cổng theo tên với 65 536 tổ hợp đầu vào và quan sát việc mất carry khi nối output 5 bit vào dây 4 bit.

nibble_sum.sv RTL
`timescale 1ns/1psmodule nibble_sum (  input  wire [3:0] a,  input  wire [3:0] b,  output wire [4:0] total);  assign total = {1'b0, a} + {1'b0, b};endmodule
twin_sum.sv RTL
`timescale 1ns/1psmodule twin_sum (  input  wire [3:0] a0, b0, a1, b1,  output wire [4:0] total0, total1);  nibble_sum u_left  (.a(a0), .b(b0), .total(total0));  nibble_sum u_right (.a(a1), .b(b1), .total(total1));endmodule
tb_modules.sv testbench
module tb_modules;  timeunit 1ns;  timeprecision 1ps;  logic [3:0] a0, b0, a1, b1;  wire [4:0] total0, total1;  int cases_checked = 0;  twin_sum dut (.a0(a0), .b0(b0), .a1(a1), .b1(b1),                .total0(total0), .total1(total1));  initial begin    for (int i = 0; i < 16; i++)      for (int j = 0; j < 16; j++)        for (int k = 0; k < 16; k++)          for (int l = 0; l < 16; l++) begin            a0 = 4'(i); b0 = 4'(j); a1 = 4'(k); b1 = 4'(l);            #1;            if (int'(total0) != i + j || int'(total1) != k + l)              $fatal(1, "wrong port connection at case %0d", cases_checked);            cases_checked++;            if (i == 15 && j == 1 && k == 3 && l == 4)              $display("PORTS RESULT left=%0d right=%0d hier=%0d",                       total0, total1, dut.u_left.total);          end    $display("MODULES RESULT cases=%0d left=%0d right=%0d",             cases_checked, total0, total1);    $finish;  endendmodule

Đã kiểm chứng thế nào

  • Phân tích và elaborate theo LRM (pyslang 12.1.0)
  • Lint -Wall (Verilator 5.050)
  • Mô phỏng, exit 0 và output khớp kỳ vọng tính tay (Verilator 5.050, 2-state)
  • Tổng hợp, số cell khớp kỳ vọng (Yosys 0.70)

Output phải khớp: PORTS RESULT left=16 right=7 hier=16MODULES RESULT cases=65536 left=30 right=30

Kỳ vọng sau tổng hợp: twin_sum: $_*DFF* = 0

Verilator 2-state; các đầu vào trong thử nghiệm chỉ là 0/1. Không dùng kết quả này để kết luận về X/Z, multiple drivers hay gate delay.

Kiểm tra lint riêng nibble_sum; parent/child được elaborate và mô phỏng chung. Bản narrow cố ý lệch độ rộng và có thể sinh warning.

Synthesis chỉ kiểm tra không có FF. Không khẳng định hierarchy hoặc hai bộ cộng còn nguyên sau tối ưu.

Các kiểm tra chạy lúc xây dựng nội dung (2026-11-06), không chạy trong trình duyệt. Kỳ vọng được tính tay trước khi chạy.

Mở tb_modules.sv để đọc cách tự kiểm tra. Nó không chỉ in kết quả: sau khi đổi đầu vào, #1 cho logic tổ hợp có thời gian cập nhật trong mô phỏng, rồi so cả hai output với phép cộng số nguyên của testbench. Sai bất kỳ trường hợp nào thì $fatal; chỉ khi đi hết vòng lặp mới in dòng kết quả cuối.

Synthesis xác nhận ví dụ không có flip-flop. Không dự đoán chính xác số cổng hoặc khẳng định chip giữ nguyên hai hộp u_left/u_right: tối ưu có thể biến đổi hoặc flatten hierarchy.

Đường đi trong hierarchy

Testbench tạo instance dut của twin_sum. Bên trong dut có u_left và u_right:

 tb_modules
 └─ dut              (twin_sum)
    ├─ u_left        (nibble_sum)
    │  └─ total
    └─ u_right       (nibble_sum)
       └─ total

dut.u_left.total đi theo tên instance, rồi tới tín hiệu total trong child. Testbench thực sự đọc đường dẫn này ở tổ hợp 15+1 và quan sát 16. Nó không viết dut.nibble_sum.total: hai instance cùng kiểu module nên tên kiểu không xác định bạn muốn nhánh nào.

Nếu dây nhận thiếu một bit?

Child đã có output 5 bit, nhưng parent vẫn có thể nối sai độ rộng:

wire [3:0] short_total;
nibble_sum dut (.a(a), .b(b), .total(short_total));

Với a=15, b=1, tổng 5 bit là 10000. Dây nhận chỉ giữ bốn bit thấp 0000, tức là 0. “Trong child đã khai báo đủ bit” chưa đủ để bảo đảm kết quả đi tới nơi nhận còn đủ bit.

Biến thể narrow trong metadata chạy chính tb_narrow.sv chứa kết nối này. Kỳ vọng tính tay là NARROW RESULT short_total=0; output thực của biến thể được lưu trong verification.json. [4]

Nếu chỉ thử các cặp có tổng nhỏ hơn 16, lỗi mất carry có thể bị bỏ qua. Vì vậy cần thử biên: 0+0, 15+0, 15+1, 15+15, và so sánh trên đủ độ rộng. Compiler có thể cảnh báo lệch width; hãy đọc warning, không chỉ nhìn việc tạo được executable.

Cách sửa là dùng dây nhận 5 bit, như total0/total1 trong bản chính. Không sửa bằng cách đổi đáp án mong đợi thành 0 nếu yêu cầu là giữ đủ tổng.

Lỗi dễ gặp và cách debug

  1. Nhầm tên module với instance. Tìm dòng định nghĩa module nibble_sum rồi dòng dùng nibble_sum u_left (...). Khi đọc hierarchy, đi theo u_left.
  2. Đúng tên cổng nhưng sai tín hiệu. Kiểm từng .port(signal) theo hai phía. Test với hai cặp đầu vào cho kết quả khác nhau; hai nhánh cùng nhận một cặp số có thể che lỗi nối nhầm.
  3. Chỉ child đủ rộng. Lần theo cả đường đi: toán hạng, kết quả trong child, cổng output và dây parent. Ở ví dụ này mọi tổng cần đi qua đường 5 bit.
  4. Compile thành công là đủ. Kiểm tra lint, rồi chạy self-checking testbench với trường hợp biên. Parser không đọc được ý định thiết kế.
  5. Coi mỗi module là một thanh ghi. Ví dụ không có clock; tên module và số instance không tự tạo ra trạng thái. Không dùng kết quả synthesis của ví dụ để kết luận mọi module đều là logic tổ hợp.

Tự kiểm tra

Module, instance và cổng

  1. Trong twin_sum của ví dụ, có bao nhiêu instance trực tiếp của nibble_sum?
  2. Trong .a(a0), tên nào là cổng của child?
  3. Trong biến thể narrow, a=15 và b=1. short_total 4 bit nhận giá trị nào?
  4. Đường dẫn nào được testbench dùng để đọc total của instance bên trái?

Checklist khi ghép module

  • Tên module chọn định nghĩa; tên instance xác định một chỗ dùng trong hierarchy.
  • Với .port(signal), kiểm riêng tên cổng child và tín hiệu parent.
  • Kiểm hướng, độ rộng và chức năng của cả đường nối, không chỉ câu lệnh cộng.
  • Dự đoán output trước khi chạy; dùng các giá trị làm hai nhánh cho kết quả khác nhau.
  • Giữ bản chất của bằng chứng: mô phỏng 0/1, synthesis không có FF, chưa đối chiếu đầy đủ LRM và không kiểm tra timing vật lý.

Nguồn và mức độ đối chiếu

  1. Thử nghiệm module/ports: content/code/ex.a6.modules/verification.json, Kết quả thử nghiệm. Trích dẫn; nguồn đã đọc toàn văn.
  2. IEEE 1800-2023 SystemVerilog LRM, LRM: modules và port connections. Chưa đối chiếu với văn bản nguồn (chưa mở).
  3. Thử nghiệm module/ports: content/code/ex.a6.modules/verification.json, Bằng chứng mô phỏng. Trích dẫn; nguồn đã đọc toàn văn.
  4. Thử nghiệm module/ports: content/code/ex.a6.modules/verification.json, Biến thể narrow. Trích dẫn; nguồn đã đọc toàn văn.
  5. IEEE 1800-2023 SystemVerilog LRM, Modules; module instantiation and port connections. Chưa đối chiếu với văn bản nguồn (chưa mở).
  6. IEEE 1800-2023 SystemVerilog LRM, Module instantiation and port connections. Chưa đối chiếu với văn bản nguồn (chưa mở).
  7. Thử nghiệm module/ports: content/code/ex.a6.modules/verification.json, verification.json: variants.narrow.observed. Trích dẫn; nguồn đã đọc toàn văn.
  8. Thử nghiệm module/ports: content/code/ex.a6.modules/verification.json, verification.json: observed PORTS RESULT. Trích dẫn; nguồn đã đọc toàn văn.
  9. Thử nghiệm module/ports: content/code/ex.a6.modules/verification.json, content/code/ex.a6.modules/verification.json. Trích dẫn; nguồn đã đọc toàn văn.