Menu

Module Verilog đầu tiên: hướng dẫn từng bước

Viết module Verilog hoàn chỉnh đầu tiên từ con số 0: khai báo, cổng, một đoạn logic tổ hợp và một testbench để điều khiển nó. Chạy được ngay trên trình duyệt.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Module là đơn vị của Verilog

Mọi thứ trong Verilog đều nằm trong một module. Một module đóng gói một phần mạch: nó khai báo những tín hiệu nào đi vào, những tín hiệu nào đi ra, và các wire, thanh ghi, logic nào nằm giữa chúng. Mọi con chip bạn từng thấy đều là một cây các module tạo instance cho module khác, xuống tận các cổng nguyên thủy do nhà sản xuất cung cấp.

Hình dạng luôn giống nhau:

module name(port_list);
    // declarations: wire, reg, parameter
    // body: assigns, instantiations, always blocks
endmodule

Chúng ta sẽ xây dựng một module hoàn chỉnh qua từng giai đoạn.

Module hữu ích nhỏ nhất: cổng AND hai đầu vào

Ta cần một thứ đơn giản và tự đứng được. Cổng AND hai đầu vào là lựa chọn hoàn hảo: hai đầu vào, một đầu ra, một dòng logic.

Nhấn Chạy. Bạn sẽ thấy bốn hàng của bảng chân trị AND. Cùng đi qua từng phần nhé.

Đọc phần khai báo module

module and_gate(
    input  wire a,
    input  wire b,
    output wire y
);
  • module and_gate khai báo một module tên là and_gate. Các module khác sẽ dùng tên này để tạo instance cho nó.
  • Danh sách trong ngoặc là danh sách cổng: các tín hiệu nhìn thấy được từ bên ngoài.
  • input wire a: a là cổng đầu vào; nó là wire (được điều khiển từ bên ngoài).
  • output wire y: y là cổng đầu ra, được điều khiển bởi thứ gì đó bên trong module.

Nếu muốn viết gọn, bạn có thể viết input a thay cho input wire a, vì riêng chiều cổng đã mặc định kiểu là wire. Nhưng viết tường minh là một thói quen đáng rèn luyện. Cổng module trình bày đầy đủ các dạng cổng.

Phần thân

assign y = a & b;

Đó là một phép gán liên tục. Nó nói rằng "mỗi khi a hoặc b thay đổi, tính lại y bằng phép AND từng bit của hai tín hiệu". Không có clock, không có định thời: mối quan hệ này luôn đúng. Đó là logic tổ hợp thuần túy.

endmodule đóng khối lại. Module đã xong.

Testbench

Bạn không thể chạy riêng and_gate. Bạn cần một module thứ hai để điều khiển đầu vào và theo dõi đầu ra của nó. Đó là testbench, và theo quy ước ta đặt tên là test, tb hoặc <design>_tb.

module test;
    reg  a, b;
    wire y;

    and_gate dut(.a(a), .b(b), .y(y));
    ...
endmodule

Ba điều cần chú ý:

  • Không có danh sách cổng. Testbench là cấp cao nhất của mô phỏng: không có gì bên ngoài nó.
  • reg a, b và wire y. Đầu vào của thiết kế được kiểm thử (DUT) là reg trong testbench vì ta điều khiển chúng từ một khối thủ tục. Đầu ra là wire vì DUT điều khiển nó.
  • and_gate dut(.a(a), .b(b), .y(y)). Đây là tạo instance. Ta tạo ra một bản sao của and_gate và gọi nó là dut (một tên phổ biến, viết tắt của "design under test"). Cú pháp .a(a) nghĩa là "nối cổng tên a của instance với tín hiệu cục bộ tên a". Tạo instance module đi sâu hơn.

Tác động đầu vào

initial begin
    a = 0; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 0; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 1; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 1; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
    $finish;
end

Khối initial chạy một lần khi mô phỏng bắt đầu. Bên trong nó:

  • a = 0; b = 0; điều khiển các đầu vào. Đây là các phép gán blocking: thứ tự quan trọng, và mỗi phép gán xảy ra trước phép tiếp theo.
  • #1 cho thời gian mô phỏng tiến thêm 1 đơn vị. Ta cần điều này để y có thời gian ổn định sau khi đầu vào thay đổi. Nếu không có #1, $display sẽ in ra giá trị cũ của y.
  • $display(...) in ra console mô phỏng. Chuỗi định dạng hoạt động giống printf của C: %b là nhị phân, %d là thập phân, %h là thập lục phân, %t là thời gian mô phỏng.
  • $finish kết thúc mô phỏng. Không có nó, trình mô phỏng sẽ tiếp tục cho thời gian tiến mãi để chờ một sự kiện không bao giờ đến.

Thử làm hỏng nó

Sửa module thành cổng OR (đổi & thành |) rồi chạy lại. Bảng chân trị thay đổi. Giờ thử XOR:

Cùng một bộ khung. Khác toán tử. Đó là toàn bộ cuộc chơi với logic tổ hợp: khai báo cổng, viết các assign, quét qua các đầu vào, quan sát đầu ra.

Module có hai đầu ra

Một module có thể có nhiều hơn một đầu ra. Đây là bộ bán cộng (half-adder): hai đầu vào, tổng và bit nhớ:

Hai câu lệnh assign nằm cạnh nhau nhưng diễn ra song song: không có chuyện "tính tổng trước rồi mới tính bit nhớ". Cả hai luôn đúng. Đó chính là tính đồng thời mà chúng ta đã nói trong Phần cứng và phần mềm, giờ được thể hiện cụ thể.

Những gì bạn đã biết

Bạn đã thấy toàn bộ bộ khung của một tệp Verilog: một module thiết kế với các cổng đã khai báo và phần thân, cùng một module testbench tạo instance cho nó, điều khiển đầu vào trong khối initial và báo cáo kết quả. Gần như mọi tệp mã nguồn Verilog bạn sẽ đọc đều theo mẫu này. Phần còn lại của ngôn ngữ chỉ là lấp đầy bằng logic phong phú hơn, tín hiệu nhiều bit, hành vi theo clock và testbench lớn hơn.

Tiếp theo: chú thích và phong cách viết code, để các module của bạn luôn dễ đọc khi chúng lớn dần.

Câu hỏi thường gặp

Module Verilog đơn giản nhất là gì?

Module Verilog hợp lệ nhỏ nhất chỉ là module name; endmodule: không cổng, không thân. Module hữu ích nhỏ nhất là một module có một đầu ra: module and_gate(input wire a, input wire b, output wire y); assign y = a & b; endmodule. Đó là một mạch logic tổ hợp thật mà bạn có thể đặt vào bất kỳ thiết kế lớn hơn nào.

Làm sao để chạy một module Verilog?

Bạn không thể chạy riêng một module: nó là bản mô tả mạch, không phải chương trình. Bạn viết một module testbench để tạo instance cho thiết kế và điều khiển các đầu vào, sau đó biên dịch cả hai bằng iverilog -o sim design.v test.v và chạy vvp sim. Editor trên trình duyệt ở trang này làm cả hai bước cho bạn khi bạn nhấn Chạy.

Testbench trong Verilog là gì?

Testbench là một module thứ hai, thường không có cổng, có nhiệm vụ thử nghiệm thiết kế của bạn. Nó tạo instance cho thiết kế, thay đổi các đầu vào qua một khối initial, quan sát đầu ra bằng $display hoặc $monitor, rồi gọi $finish khi xong. Testbench không tổng hợp được; nó chỉ tồn tại để kiểm chứng hành vi.

Vì sao code Verilog cần $finish?

Vì phần cứng không bao giờ dừng. Trình mô phỏng giả lập thời gian trôi qua, và nếu không có $finish tường minh, nó sẽ tiếp tục tiến mãi để chờ sự kiện mới. $finish báo cho trình mô phỏng 'xong rồi, thoát gọn gàng đi'. Trong testbench, đó là dòng cuối của khối initial: chạy bài kiểm tra, rồi kết thúc.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu