模块的接口
模块的端口列表就是它的接口:外部世界能看到和接触到的一切。在模块内部,模块体根据输入计算输出。端口就是穿过这条边界的导线。
现代的 ANSI 风格声明把方向、类型和位宽都写在一行里:
module my_module(
input wire clk,
input wire reset,
input wire [7:0] data_in,
output reg [7:0] data_out,
output wire valid
);
// body
endmodule
整体结构就是这样。每个端口都有:
- 一个方向:
input、output或inout。 - 一个类型:
wire或reg(在 SystemVerilog 中还可以是logic)。 - 一个位宽:像
[7:0]这样的范围,省略则为单个位。 - 一个名字:在模块体内使用的标识符。
三种方向
input:由外部驱动
输入总是 wire。驱动源在模块外部:要么是上层模块的信号,要么是测试平台中的 reg。在这个模块内部,你可以在表达式中读取输入,但永远不能给它们赋值:
module reader(input wire [7:0] data);
initial $display("data = %h", data);
endmodule
在模块内部写 data = ... 会报错。
output:由内部驱动
输出由这个模块内部的某个东西驱动。它可以是 wire(由 assign 或子模块的输出驱动),也可以是 reg(由 always/initial 驱动)。
module driver(
input wire a,
input wire b,
input wire clk,
output wire y, // wire - driven by assign
output reg q // reg - driven by always
);
assign y = a & b; // OK because y is wire
always @(posedge clk)
q <= a; // OK because q is reg
endmodule
在 always 块内给 y 赋值,或者用 assign 驱动 q,都会导致编译错误。关键字的选择必须和驱动方式相匹配。
inout:双向
inout 端口是一种导线,可以轮流由模块自身或外部连接的器件来驱动。它们出现在芯片的边界上:I²C 的 SDA 线、双向 GPIO 引脚、共享数据总线。在模块内部,用三态写法来控制方向:
module bidir_pin(
input wire data_out,
input wire output_enable,
output wire data_in,
inout wire pin
);
assign pin = output_enable ? data_out : 1'bz;
assign data_in = pin;
endmodule
pin 是共享的导线。当 output_enable 为高时,模块把 pin 驱动为 data_out。为低时,模块把引脚释放为高阻态(z),让外部的驱动源使用它。data_in 始终观察引脚上当前的值。
在纯内部逻辑中很少用到 inout。如果你在做存储器控制器、CPU 核心或图像处理流水线,可能永远都用不上它。它是为芯片边界上的 I/O 环准备的功能。
单位端口与多位端口
位宽范围是可选的,省略它就得到一个单位端口:
input wire valid, // 1 bit
input wire [7:0] data, // 8 bits
input wire [31:0] addr, // 32 bits
位宽可以使用参数,参数化模块就是这样实现的:
module bus #(
parameter WIDTH = 32
)(
input wire [WIDTH-1:0] in,
output wire [WIDTH-1:0] out
);
assign out = in;
endmodule
ANSI 风格与 Verilog-1995 风格
偶尔你会看到一些旧代码,把端口列表和端口声明分开写:
// Old Verilog-1995 style - DON'T do this in new code
module foo(clk, data_in, data_out);
input clk;
input [7:0] data_in;
output reg [7:0] data_out;
// ...
endmodule
端口列表中只有名字,然后每个端口在模块体内单独声明。这种写法冗长,容易出现“端口列表里写了名字却没有声明”的错误,要敲的字也多了一倍。ANSI-2001 风格(这套文档中一直使用的写法)把两者合而为一:
module foo(
input wire clk,
input wire [7:0] data_in,
output reg [7:0] data_out
);
// ...
endmodule
请使用 ANSI 风格。工具从 2001 年起就支持它了。
完整示例
这一个模块包含了:
- 一个参数(
WIDTH)。 - 时钟、复位、加载使能、数据和移位使能这几个输入。
- 一个在
always块内驱动的reg输出(data_out)。 - 一个由连续赋值驱动的
wire输出(msb_out)。 - 一个完整的 ANSI 风格声明,在一行中列出每个端口的方向、类型和位宽。
你将来写的几乎每个可综合模块,都是这个样子。
接下来
下一篇文档 模块实例化 会展示如何把这个模块用到更大的设计中。你刚写的移位器单独存在时没什么用;只有当别的模块实例化它、把它接入系统时,它才能发挥作用。
常见问题
Verilog 中的三种端口方向是什么?
input、output 和 inout。input 由模块外部驱动。output 由模块内部驱动。inout 是双向的,适用于三态总线:模块既驱动又读取同一个引脚。内部端口绝大多数都是 input 或 output;inout 只出现在芯片引脚上。
output wire 和 output reg 有什么区别?
output wire 表示这个输出由连续赋值或子模块的输出驱动,也就是 always 块之外的一切。output reg 表示它由过程块(initial 或 always)驱动。这个关键字决定的是你能否在 always 内部给这个信号赋值,而不是综合出的硬件里有没有触发器。
Verilog 端口的 ANSI 风格是什么?
ANSI 风格直接在端口列表中声明每个端口的方向和类型:module foo(input wire [7:0] data, output reg [7:0] result);。较老的 Verilog-1995 风格只在端口列表里写名字,再在模块内部重新声明一遍。新代码请始终使用 ANSI 风格:更短,更不容易出错,也是所有现代代码的标准写法。
一个 Verilog 模块可以有多个输入和输出吗?
可以。一个模块需要多少端口就可以有多少,方向可以任意组合。在端口列表中用逗号分隔它们。端口列表的顺序决定了实例化时按位置连接的顺序,但你几乎总是使用按名称连接(.port(signal)),以免依赖顺序。