Module bên trong module
Một thiết kế Verilog là một cây các module. Module cấp cao nhất (testbench, hoặc lớp bọc ngoài cùng của một con chip) khởi tạo các module cấp thấp hơn, các module này lại khởi tạo những module thấp hơn nữa, cho đến tận các cổng logic cơ bản do hãng cung cấp. Khởi tạo (instantiation) là cú pháp cho sự lồng nhau đó.
Bạn đã thấy dạng này rồi, chúng ta đã dùng nó trong bài Module đầu tiên:
and_gate dut(.a(a), .b(b), .y(y));
Dòng này tạo ra một instance của and_gate, đặt tên là dut và nối các port của nó với các tín hiệu cục bộ. Hãy cùng phân tích từng phần.
Cấu trúc của một lệnh khởi tạo
module_name instance_name (port_connections);
module_namephải khớp với tên trong một khai báomoduleở đâu đó trong dự án. Verilog phân biệt chữ hoa và chữ thường.instance_namelà nhãn bạn tự chọn, thường mô tả vai trò của instance này. Bạn sẽ dùng nó trong đường dẫn phân cấp và khi xem dạng sóng.port_connectionsnối các port của instance với tín hiệu cục bộ. Có hai cách để viết phần này.
Kết nối port theo tên (nên dùng)
Dạng theo tên trông như sau:
my_module instance_name(
.clk (clk),
.reset (reset_n),
.data_in(in_bus),
.data_out(out_bus),
.valid (out_valid)
);
Mỗi cặp .port(signal) có nghĩa là "nối port tên port của instance này với tín hiệu cục bộ tên signal". Thứ tự không quan trọng. Nếu bạn thêm một port mới vào khai báo module, các chỗ khởi tạo hiện có không bị hỏng, miễn là bạn cho port mới một giá trị mặc định hoặc cập nhật từng chỗ.
Hai lưu ý thực tế:
- Tên port (bên trái dấu ngoặc) phải khớp chính xác với khai báo của module.
- Tên tín hiệu (bên trong dấu ngoặc) thuộc phạm vi nơi đặt lệnh khởi tạo, thường là module cha.
Nếu một port không được nối, để trống bên trong: .optional_port(). Tín hiệu đó sẽ thả nổi (z) bên trong instance. Một vài công cụ tổng hợp sẽ cảnh báo; phần lớn chấp nhận.
Kết nối port theo vị trí (nên tránh)
Dạng ngắn gọn hơn liệt kê tín hiệu theo thứ tự danh sách port:
my_module instance_name(clk, reset_n, in_bus, out_bus, out_valid);
Cách này ngắn hơn nhưng dễ vỡ. Chỉ cần sắp xếp lại danh sách port của module (một kiểu refactor hoàn toàn có thể xảy ra) là mọi chỗ khởi tạo theo vị trí đều nối sai mà không báo lỗi. Đừng dùng kiểu theo vị trí trừ khi danh sách port chỉ có đúng một hoặc hai phần tử và gần như không bao giờ thay đổi.
Trường hợp vẫn chấp nhận được: các module tiện ích nhỏ mà thứ tự port là một phần của API. Một cổng hai đầu vào dùng theo vị trí thì ổn. Một bộ điều khiển bộ nhớ 30 port thì đang tự chuốc lấy rắc rối.
Ví dụ phân cấp hoàn chỉnh
Đây là một cây phân cấp ba tầng thật sự: test → full_adder → hai instance half_adder. Mỗi instance có bản sao riêng của các cổng bên trong half_adder; công cụ tổng hợp sẽ tạo ra một mạch cho mỗi lần khởi tạo.
Nhiều instance của cùng một module
Khi bạn khởi tạo cùng một module nhiều lần, mỗi instance là phần cứng độc lập. Chúng không dùng chung trạng thái. Chúng không dùng chung cổng logic. Hãy hình dung mỗi instance là một bản sao mới được dập ra từ thiết kế.
adder add0(.a(a0), .b(b0), .sum(s0));
adder add1(.a(a1), .b(b1), .sum(s1));
adder add2(.a(a2), .b(b2), .sum(s2));
adder add3(.a(a3), .b(b3), .sum(s3));
Đó là bốn bộ cộng riêng biệt chạy song song. Nếu adder chứa một thanh ghi, mỗi instance sẽ có bản sao riêng của thanh ghi đó, với trạng thái riêng.
Vòng lặp generate: nhân bản phần cứng lặp lại
Viết tay bốn instance thì không sao. Viết 64 cái thì rất mệt. Khối generate để trình elaboration gõ thay bạn:
Ba phần cú pháp mới:
genvar ikhai báo một biến lặp dùng được tronggenerate. Nó không phải tín hiệu lúc chạy, nó chỉ tồn tại ở giai đoạn elaboration.generate ... endgeneratebao quanh vòng lặp. Một số công cụ chấp nhận vòng lặp generate không có từ khóagenerate, nhưng viết rõ ra giúp ý định dễ hiểu hơn.begin : invert_loopđặt nhãn cho phạm vi generate. Nhãn này trở thành một phần của tên phân cấp của từng instance được sinh ra (dut.invert_loop[0].u_inv,dut.invert_loop[1].u_inv, v.v.).
Công cụ tổng hợp sẽ trải phẳng vòng lặp và tạo ra WIDTH bản sao của bit_inverter. Mỗi bản sao là phần cứng độc lập.
Ghi đè tham số khi khởi tạo
Nếu module có tham số, bạn có thể ghi đè chúng bằng #(.PARAM(value)) đặt giữa tên module và tên instance:
counter #(.WIDTH(16)) c16 (.clk(clk), .count(out16));
counter #(.WIDTH(32)) c32 (.clk(clk), .count(out32));
Cả hai instance dùng chung mã nguồn counter nhưng có độ rộng khác nhau. Chúng ta đã nói về cú pháp này trong bài Tham số; nó khớp gọn gàng vào lệnh khởi tạo.
Tên phân cấp
Khi đã có cây phân cấp, mỗi tín hiệu đều có một đường dẫn phân cấp:
test.dut.ha0.sum
Đọc là: trong module test, bên trong instance dut, bên trong instance ha0, tín hiệu tên sum. Bạn sẽ gặp các đường dẫn này trong trình xem dạng sóng, thông báo lỗi, và đôi khi trong một lệnh $display từ testbench chọc sâu vào một module con:
$display("internal carry1 = %b", dut.carry1);
Tham chiếu phân cấp như vậy chỉ dành cho testbench và gỡ lỗi: RTL tổng hợp được không truy cập vào bên trong module khác.
Các lỗi thường gặp
Sai tên port. .clk_in(clk) nối tín hiệu cục bộ clk vào port tên clk_in. Nếu port của module thực ra tên là clk, trình phân tích cú pháp sẽ báo cho bạn (có công cụ báo rõ ràng hơn công cụ khác).
Lệch độ rộng ở một port. Nối tín hiệu 4 bit vào port 8 bit sẽ âm thầm thêm số 0 ở đầu; chiều ngược lại sẽ âm thầm cắt bớt. Phần lớn công cụ có cảnh báo; nếu bạn không thấy cảnh báo nào, hãy tìm kỹ hơn.
Quên dấu # của tham số. counter (.WIDTH(8)) c(.clk(clk)) trông giống ghi đè tham số nhưng không phải: trình phân tích cú pháp cố coi (.WIDTH(8)) là kết nối port và báo lỗi. Cách đúng: counter #(.WIDTH(8)) c(.clk(clk)).
Dùng lại tên instance. Hai instance không thể trùng tên trong cùng một phạm vi. Thông báo lỗi thường rõ ràng; cái bẫy thật sự là thói quen sao chép và dán.
Tiếp theo là gì
Giờ bạn đã có thể nối các module với nhau thành một cây phân cấp thật. Bài tiếp theo hoàn thiện phần cấu trúc của Verilog, Phép gán liên tục, và đi sâu hơn vào câu lệnh assign mà chúng ta đã dùng thoải mái từ chương đầu tiên.
Câu hỏi thường gặp
Làm sao để khởi tạo một module trong Verilog?
Viết tên module, rồi tên instance, rồi danh sách kết nối port trong ngoặc đơn: my_module instance_name(.port(signal), ...);. Kiểu phổ biến nhất là kết nối theo tên (.port(signal)), khớp theo tên port bất kể thứ tự. Kiểu theo vị trí ngắn gọn hơn (my_module instance(signal1, signal2)) phụ thuộc vào thứ tự danh sách port và rất dễ gây lỗi khi bảo trì.
Kết nối port theo tên và theo vị trí khác nhau thế nào?
Kết nối theo vị trí liệt kê tín hiệu theo đúng thứ tự trong danh sách port của module: tín hiệu thứ nhất nối vào port thứ nhất, thứ hai vào thứ hai, cứ thế tiếp tục. Kết nối theo tên dùng .port_name(signal_name), khớp theo tên. Kiểu theo tên dài hơn nhưng không bị ảnh hưởng khi thứ tự port thay đổi và tự giải thích ngay tại chỗ gọi. Hãy dùng kiểu theo tên khi module có từ ba port trở lên.
Có thể khởi tạo cùng một module Verilog nhiều lần không?
Có, đó chính là mục đích của nó. Mỗi instance là một khối phần cứng độc lập với trạng thái riêng. Nếu bạn có module adder, bạn có thể khởi tạo nó 64 lần trong một khối SIMD, mỗi lần với đầu vào khác nhau. Vòng lặp generate là cú pháp chuẩn khi các instance giống nhau và được đánh chỉ số.
Khối generate trong Verilog là gì?
generate ... endgenerate là một cấu trúc ở thời điểm biên dịch để nhân bản phần cứng lặp lại. Một vòng lặp for trong generate tạo ra N instance của những gì nằm trong thân vòng lặp. generate chạy ở giai đoạn elaboration, trước khi mô phỏng bắt đầu: nó không phải vòng lặp lúc chạy mà là bộ sinh code cho công cụ tổng hợp.