Verilog function là gì? Hướng dẫn cơ bản và ví dụ thực tế cho thiết kế mạch số

目次

1. Verilog function là gì? (Khái niệm cơ bản và vai trò)

Verilog HDL (Hardware Description Language) là ngôn ngữ mô tả phần cứng được sử dụng để thiết kế và mô phỏng mạch số. Trong đó, function (hàm) là một cơ chế giúp đóng gói các xử lý cụ thể để dễ dàng tái sử dụng. Hiểu rõ Verilog function không chỉ giúp cải thiện khả năng đọc và bảo trì mã, mà còn góp phần nâng cao hiệu quả trong thiết kế mạch. Bài viết này sẽ giải thích khái niệm cơ bản về Verilog function và cách sử dụng chúng trong thực tế.

Function là gì?

Verilog functionmột khối thực hiện tính toán hoặc xử lý cụ thể và trả về một giá trị duy nhất. Việc sử dụng function giúp giảm bớt sự lặp lại mã và làm cho thiết kế mạch đơn giản hơn.

Đặc điểm của function

  • Có thể có một hoặc nhiều đầu vào (chỉ sử dụng input)
  • Chỉ có một đầu ra (giá trị trả về)
  • Không thể chứa độ trễ thời gian (ví dụ #10)
  • Bên trong function luôn mô tả logic tổ hợp (combinational logic)
  • Function được định nghĩa bên ngoài khối always và được đánh giá ngay lập tức, khác với task

Khi nào sử dụng Verilog function

Verilog function thường được dùng trong các trường hợp sau:

1. Mô tả mạch tổ hợp

Vì function trả về kết quả ngay lập tức theo đầu vào, nó thường được sử dụng trong các mạch tổ hợp (Combinational Logic). Ví dụ: cộng, trừ, encoder, decoder.

2. Tăng khả năng tái sử dụng mã

Loại bỏ mã lặp lại, gom các xử lý thường dùng vào một hàm duy nhất. Ví dụ: công thức tính toán phức tạp với nhiều điều kiện.

3. Giảm lỗi thiết kế

Việc gom xử lý vào một nơi duy nhất giúp giảm sai sót khi chỉnh sửa. Ví dụ: tính CRC hoặc kiểm tra parity.

Sự khác nhau giữa function và task

Ngoài function, Verilog còn có task. Hai khái niệm này tương tự nhưng có những khác biệt:
Mụcfunctiontask
Đầu raChỉ 1Nhiều
Đầu vào
Biến nội bộ
Độ trễ (#10)Không
Sử dụng trong alwaysKhông
Cách gọitên_function(tham_số)tên_task(tham_số);

Khi nên dùng function

  • Cần kết quả tính toán ngay
  • Xử lý logic không có độ trễ
  • Xử lý đơn giản trả về một giá trị duy nhất

Khi nên dùng task

  • Có độ trễ thời gian (ví dụ #10)
  • Cần nhiều đầu ra
  • Xử lý debug trong mô phỏng (ví dụ ghi log)

Tóm tắt

  • Verilog functionhàm nhận đầu vào và trả về một giá trị duy nhất.
  • Phù hợp cho mô tả mạch tổ hợp, không hỗ trợ độ trễ.
  • Giúp giảm mã lặp lại, cải thiện khả năng đọc.
  • Khác với task, cần chọn đúng tùy trường hợp.

2. Cách viết Verilog function 【Ví dụ đơn giản cho người mới bắt đầu】

Trong phần trước, chúng ta đã tìm hiểu khái niệm cơ bản của Verilog function. Ở đây, chúng ta sẽ đi sâu vào cách viết function trong Verilog.

Cú pháp cơ bản của function

Verilog function được viết theo cấu trúc sau:
function [độ_rộng_đầu_ra] tên_function;
    input [độ_rộng_đầu_vào] tên_input1, tên_input2, ...;
    begin
        tên_function = biểu_thức_tính_toán;
    end
endfunction

Điểm cần chú ý

  • Khai báo bằng từ khóa function
  • Biến có cùng tên với function chính là giá trị trả về
  • Khai báo đầu vào với input (không dùng output hay inout)
  • Xử lý logic được viết trong begin ... end
  • Function được định nghĩa ngoài khối always

Ví dụ đơn giản về Verilog function

Ví dụ dưới đây minh họa một function cộng 8-bit:
module example;
    function [7:0] add_function;
        input [7:0] a, b;
        begin
            add_function = a + b;
        end
    endfunction

    reg [7:0] x, y, sum;

    initial begin
        x = 8'b00001100; // 12
        y = 8'b00000101; // 5
        sum = add_function(x, y);
        $display("Sum: %d", sum); // Sum: 17
    end
endmodule

Giải thích

  • add_function nhận 2 đầu vào 8-bit a, b và trả về tổng
  • Dùng sum = add_function(x, y); để gọi hàm và gán kết quả
  • Bên trong khối initial sử dụng $display để in kết quả

Khai báo đầu vào và đầu ra trong function

Khai báo đầu vào

Function chỉ nhận input làm tham số.
function [7:0] my_function;
    input [7:0] in1, in2;
    begin
        my_function = in1 & in2; // Phép AND
    end
endfunction
Lưu ý: Không thể dùng output. Đầu ra chính là biến có cùng tên với function.

Function có điều kiện rẽ nhánh

Có thể sử dụng if hoặc case trong function.
function [3:0] max_function;
    input [3:0] a, b;
    begin
        if (a > b)
            max_function = a;
        else
            max_function = b;
    end
endfunction
Hàm này trả về giá trị lớn hơn giữa a và b.

Tóm tắt

  • Định nghĩa bằng từ khóa function và trả về một giá trị duy nhất
  • Chỉ có thể dùng input (không dùng output)
  • Kết quả trả về bằng cách gán cho tên_function
  • Có thể sử dụng điều kiện if hoặc case

3. Cách sử dụng Verilog function 【Kèm ví dụ thực tế】

Trong phần trước, chúng ta đã học cú pháp và cách viết cơ bản của Verilog function. Ở phần này, chúng ta sẽ tìm hiểu cách áp dụng function trong thiết kế thực tế thông qua các ví dụ cụ thể.

Cách gọi function

Verilog function được gọi giống như biến thông thường, theo cú pháp tên_function(tham_số1, tham_số2, ...). Ví dụ dưới đây định nghĩa một hàm XOR 8-bit và sử dụng trong module:
module function_example;
    function [7:0] xor_function;
        input [7:0] a, b;
        begin
            xor_function = a ^ b;
        end
    endfunction

    reg [7:0] x, y, result;

    initial begin
        x = 8'b11001100;
        y = 8'b10101010;
        result = xor_function(x, y); // gọi function
        $display("XOR Result: %b", result); // XOR Result: 01100110
    end
endmodule

Điểm cần nhớ

  • Gọi function bằng cú pháp biến = function(tham_số);
  • Có thể dùng trong khối always hoặc initial
  • Hoạt động như mạch tổ hợp (combinational logic)

Sử dụng function trong mạch tổ hợp

Vì function luôn được đánh giá ngay lập tức, nên rất hữu ích khi xây dựng mạch tổ hợp. Ví dụ dưới đây triển khai bộ giải mã 2-to-4 bằng function:
module decoder_example;
    function [3:0] decoder;
        input [1:0] sel;
        begin
            case (sel)
                2'b00: decoder = 4'b0001;
                2'b01: decoder = 4'b0010;
                2'b10: decoder = 4'b0100;
                2'b11: decoder = 4'b1000;
                default: decoder = 4'b0000;
            endcase
        end
    endfunction

    reg [1:0] select;
    wire [3:0] decoded_output;

    assign decoded_output = decoder(select); // sử dụng function

    initial begin
        select = 2'b01;
        #10; // thêm độ trễ để quan sát trong mô phỏng
        $display("Decoded Output: %b", decoded_output); // Decoded Output: 0010
    end
endmodule

Giải thích

  • decoder nhận đầu vào 2-bit và chuyển thành đầu ra 4-bit
  • Dùng case để quyết định đầu ra theo giá trị đầu vào
  • assign dùng để gán đầu ra của function cho decoded_output → Function có thể được dùng như một phần của mạch tổ hợp

Sự khác nhau giữa always và function 【Bảng so sánh】

Cả Verilog function và always đều dùng để mô tả logic, nhưng chúng có mục đích và giới hạn khác nhau.
Mụcfunctionalways
Vị trí viếtBên ngoài khối alwaysBên trong khối always
Đầu vàoChỉ inputCó thể là reg hoặc wire
Đầu ra1 giá trị duy nhấtCó thể cập nhật nhiều giá trị
Độ trễ (#10)Không
Lưu trạng tháiKhông (đánh giá ngay lập tức) (có thể dùng như flip-flop)
Ứng dụng chínhMạch tổ hợpMạch tuần tự hoặc xử lý theo sự kiện

Nguyên tắc sử dụng

  • Dùng function cho các phép toán logic đơn giản (mạch tổ hợp)
  • Dùng always cho các mạch có trạng thái (flip-flop, bộ đếm)
  • Nếu cần độ trễ (#10), hãy dùng always thay vì function

Tóm tắt cách sử dụng Verilog function

✅ Gọi bằng cú pháp tên_function(tham_số)Thích hợp cho thiết kế mạch tổ hợp, khác với always ✅ Có thể dùng caseif để viết logic linh hoạt ✅ Ứng dụng trong decoder, tính toán số học, v.v.

4. Ứng dụng của Verilog function (Thiết kế Decoder và ALU)

Chúng ta đã tìm hiểu cú pháp và cách sử dụng cơ bản của Verilog function. Trong phần này, chúng ta sẽ xem xét cách áp dụng function trong thiết kế mạch số thực tế như Decoder và ALU (Arithmetic Logic Unit).

Triển khai function cho Decoder (2-to-4 Decoder)

Decoder là mạch chuyển đổi số bit đầu vào nhỏ thành số bit đầu ra lớn hơn. Ví dụ: 2-bit đầu vào → 4-bit đầu ra. Ta có thể viết Decoder 2-to-4 bằng function như sau:
module decoder_example;
    function [3:0] decoder;
        input [1:0] sel;
        begin
            case (sel)
                2'b00: decoder = 4'b0001;
                2'b01: decoder = 4'b0010;
                2'b10: decoder = 4'b0100;
                2'b11: decoder = 4'b1000;
                default: decoder = 4'b0000;
            endcase
        end
    endfunction

    reg [1:0] select;
    wire [3:0] decoded_output;

    assign decoded_output = decoder(select); // sử dụng function

    initial begin
        select = 2'b00; #10;
        $display("Decoded Output: %b", decoded_output);
        select = 2'b01; #10;
        $display("Decoded Output: %b", decoded_output);
        select = 2'b10; #10;
        $display("Decoded Output: %b", decoded_output);
        select = 2'b11; #10;
        $display("Decoded Output: %b", decoded_output);
    end
endmodule

Triển khai function cho ALU (Cộng, Trừ, AND, OR)

ALU (Arithmetic Logic Unit) là mạch trung tâm của CPU, thực hiện cộng, trừ và các phép toán logic (AND, OR). Dưới đây là ví dụ thiết kế một ALU 8-bit đơn giản bằng function:
module alu_example;
    function [7:0] alu;
        input [7:0] a, b;
        input [1:0] op; // tín hiệu điều khiển 2-bit
        begin
            case (op)
                2'b00: alu = a + b; // Cộng
                2'b01: alu = a - b; // Trừ
                2'b10: alu = a & b; // AND
                2'b11: alu = a | b; // OR
                default: alu = 8'b00000000;
            endcase
        end
    endfunction

    reg [7:0] x, y;
    reg [1:0] opcode;
    wire [7:0] result;

    assign result = alu(x, y, opcode); // gọi function

    initial begin
        x = 8'b00001100; // 12
        y = 8'b00000101; // 5

        opcode = 2'b00; #10;
        $display("Addition Result: %d", result); // 12 + 5 = 17

        opcode = 2'b01; #10;
        $display("Subtraction Result: %d", result); // 12 - 5 = 7

        opcode = 2'b10; #10;
        $display("AND Result: %b", result); // AND

        opcode = 2'b11; #10;
        $display("OR Result: %b", result); // OR
    end
endmodule

Tóm tắt

Function có thể áp dụng cho mạch tổ hợp như Decoder và ALUDùng case để mô tả các phép toán linh hoạtCải thiện khả năng đọc và dễ tái sử dụng codeFunction phù hợp cho mạch tổ hợp, không phù hợp cho mạch tuần tự (có độ trễ)

5. Lưu ý khi sử dụng Verilog function

Verilog function là công cụ mạnh mẽ giúp tăng khả năng đọc và tái sử dụng mã, nhưng cũng có một số hạn chế. Phần này sẽ giải thích chi tiết những điểm cần chú ý khi dùng function.

Không thể gọi đệ quy

Trong Verilog, function không được phép gọi đệ quy (recursive call). Tức là function không thể tự gọi lại chính nó.

❌ Ví dụ sai: Hàm có đệ quy

function [3:0] factorial;
    input [3:0] n;
    begin
        if (n == 0)
            factorial = 1;
        else
            factorial = n * factorial(n - 1); // ❌ không hợp lệ
    end
endfunction
Đoạn code này sẽ gây lỗi khi mô phỏng.

✅ Giải pháp: dùng vòng lặp

Nếu muốn xử lý lặp, hãy sử dụng vòng lặp trong always hoặc dùng task.
task factorial_task;
    input [3:0] n;
    output [15:0] result;
    integer i;
    begin
        result = 1;
        for (i = 1; i <= n; i = i + 1)
            result = result * i;
    end
endtask
→ Dùng vòng lặp thay cho đệ quy.

Không thể dùng độ trễ (#10) trong function

Verilog function luôn được đánh giá ngay lập tức (combinational logic), nên không thể chứa lệnh trễ (#10).

❌ Ví dụ sai: dùng độ trễ trong function

function [7:0] delay_function;
    input [7:0] in;
    begin
        #10; // ❌ không hợp lệ
        delay_function = in + 1;
    end
endfunction
→ Đoạn code trên sẽ báo lỗi biên dịch.

✅ Giải pháp: dùng always hoặc task

Nếu cần độ trễ, hãy dùng task hoặc always block.
task delay_task;
    input [7:0] in;
    output [7:0] out;
    begin
        #10;
        out = in + 1;
    end
endtask
→ Các xử lý có độ trễ nên dùng task.

Phân biệt function và task

Ngoài function, Verilog còn có task. Chúng có sự khác nhau rõ rệt, cần sử dụng đúng mục đích.
Mụcfunctiontask
Đầu ra1 giá trị duy nhất (tên hàm)Nhiều giá trị (dùng biến output)
Đầu vàoinput duy nhấtinputoutput đều có thể
Biến nội bộ
Độ trễ (#10)Không
Dùng trong alwaysKhông
Cách gọitên_function(tham_số)tên_task(tham_số);

Khi nên dùng function

✅ Khi cần kết quả tính toán ngay lập tức (cộng, trừ, logic) ✅ Khi mô tả mạch tổ hợp không có độ trễ ✅ Khi chỉ cần một giá trị đầu ra

Khi nên dùng task

✅ Khi cần xử lý có độ trễ (#10) ✅ Khi có nhiều đầu ra ✅ Khi cần debug trong mô phỏng (ghi log, giám sát)

Function không thể định nghĩa trong always

Function trong Verilog không được khai báo bên trong always. Chúng phải được định nghĩa bên ngoài module hoặc always.

❌ Ví dụ sai: định nghĩa function trong always

always @(a or b) begin
    function [7:0] my_function; // ❌ không hợp lệ
        input [7:0] x, y;
        begin
            my_function = x + y;
        end
    endfunction
end

✅ Cách đúng

Khai báo function bên ngoài, rồi gọi trong always.
function [7:0] add_function;
    input [7:0] x, y;
    begin
        add_function = x + y;
    end
endfunction

always @(a or b) begin
    result = add_function(a, b); // ✅ gọi function
end

Tóm tắt

Function có nhiều hạn chếKhông hỗ trợ đệ quy (dùng vòng lặp hoặc task thay thế) ✅ Không chứa độ trễ (dùng always hoặc task) ✅ Không được định nghĩa trong always (phải ở ngoài) ✅ Chỉ có một giá trị trả về (nhiều đầu ra → dùng task) ✅ Cần phân biệt rõ giữa function và task để dùng đúng

6. 【FAQ】Các câu hỏi thường gặp về Verilog function

Chúng ta đã tìm hiểu từ cơ bản đến ứng dụng và lưu ý khi dùng Verilog function. Phần này tổng hợp những câu hỏi thường gặp và câu trả lời.

Khác nhau giữa function và task?

Q. Sự khác nhau giữa Verilog function và task là gì? Khi nào nên dùng?

A. Function dùng để trả về một giá trị duy nhất ngay lập tức, trong khi task có thể có nhiều đầu ra hoặc chứa độ trễ.

Mụcfunctiontask
Đầu ra1 giá trị duy nhất (tên hàm)Nhiều giá trị (biến output)
Đầu vàoinput duy nhấtinput / output đều có thể
Biến nội bộ
Độ trễ (#10)Không
Dùng trong alwaysKhông
Cách gọitên_function(tham_số)tên_task(tham_số);

Khi nên dùng function

✅ Khi cần kết quả tính toán ngay (cộng, trừ, phép logic) ✅ Khi mô tả mạch tổ hợp không có độ trễ ✅ Khi chỉ cần 1 giá trị trả về

Khi nên dùng task

✅ Khi cần xử lý có độ trễ (#10) ✅ Khi cần nhiều đầu ra ✅ Khi debug trong mô phỏng (monitor, display)

Có thể dùng reg trong function?

Q. Có thể khai báo reg trong function không?

A. Không thể, nhưng có thể dùng integer thay thế.

Ví dụ:
function [7:0] multiply;
    input [3:0] a, b;
    integer temp;
    begin
        temp = a * b;
        multiply = temp;
    end
endfunction

Khi nào nên dùng function?

Q. Function phù hợp dùng trong trường hợp nào?

A. Function phù hợp cho các phép toán đơn giản hoặc mô tả mạch tổ hợp.

Ví dụ:
  • Phép toán số học (cộng, trừ, logic)
  • Decoder hoặc Encoder
  • So sánh giá trị (tìm max, min)
  • Kiểm tra lỗi (ví dụ parity check)
⚠️ Không nên dùng cho mạch tuần tự (có flip-flop).

Có thể gọi function khác bên trong function không?

Q. Có thể gọi một function khác trong function không?

A. Có thể, nhưng cần cẩn thận về sự phụ thuộc giữa các function.

Ví dụ:
function [7:0] add;
    input [7:0] a, b;
    begin
        add = a + b;
    end
endfunction

function [7:0] double_add;
    input [7:0] x, y;
    begin
        double_add = add(x, y) * 2; // gọi function khác
    end
endfunction

Phân biệt function và always?

Q. Nên dùng function hay always trong Verilog?

A. Function dùng cho mạch tổ hợp, còn always dùng cho mạch tuần tự.

Mụcfunctionalways
Độ trễ (#10)Không
Lưu trạng tháiKhông (đánh giá ngay) (dùng làm flip-flop, counter)
Ứng dụngMạch tổ hợp (xử lý tức thì)Mạch tuần tự (có trạng thái)
Ví dụ:

✅ Function (mạch tổ hợp)

function [7:0] add;
    input [7:0] a, b;
    begin
        add = a + b;
    end
endfunction

✅ Always (mạch tuần tự)

always @(posedge clk) begin
    sum <= a + b; // hoạt động như flip-flop
end

Tóm tắt

Function phù hợp cho tính toán đơn giản và mạch tổ hợpTask dùng khi có nhiều đầu ra hoặc cần độ trễAlways phù hợp cho mạch tuần tự có trạng tháiFunction không hỗ trợ #delay và không lưu trạng thái