3. 模块与端口:module/endmodule结构、input/output/inout端口、端口位宽声明
好,咱们今天聊聊模块与端口。这是Verilog里最基础、也最核心的东西。说白了,你写Verilog就是在画一个又一个的“黑盒子”,而模块就是这个盒子,端口就是盒子上的引脚。
3.1 module/endmodule结构——你的第一个“黑盒子”
每个Verilog设计,都是从module开始,到endmodule结束。这个结构就像C语言里的main()函数,但更灵活。一个模块可以调用另一个模块,这就是层次化设计。
我个人习惯,写模块时先把端口声明写完,再写内部逻辑。这样思路清晰,不容易漏东西。
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
output reg [7:0] count // 8位计数器输出
);
// 内部逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else
count <= count + 1'b1;
end
endmodule
你看,这个结构很清晰。模块名是counter,端口列表写在括号里,最后用endmodule收尾。嗯,这里要注意:endmodule后面不要加分号,这是新手常犯的错。
3.2 input/output/inout端口——三种引脚类型
端口有三种类型,我分别说说。
3.2.1 input(输入端口)
输入端口,数据从外部流入模块内部。它必须是wire类型。为什么?因为输入端口是被外部驱动的,模块内部只能读它,不能写它。
module and_gate (
input wire a, // 输入a
input wire b, // 输入b
output wire y // 输出y
);
assign y = a & b;
endmodule
3.2.2 output(输出端口)
输出端口,数据从模块内部流向外部。它可以是wire或reg类型。用assign赋值时用wire,用always块赋值时用reg。
我在项目中遇到过一个问题:有个同事把输出端口声明为wire,却在always块里给它赋值,结果编译报错。说白了,wire只能被连续赋值,reg才能被过程赋值。
3.2.3 inout(双向端口)
双向端口,既能输入也能输出。这个用得不多,但在总线接口(比如I2C的SDA线)里很常见。使用时要特别小心,需要三态门控制。
module bidir (
inout wire data, // 双向数据线
input wire en, // 输出使能
input wire din // 要输出的数据
);
assign data = en ? din : 1'bz; // en=1时输出,en=0时高阻
endmodule
你想想看,如果两个模块同时驱动一根线,会发生什么?短路!所以inout端口必须用三态门,不输出时就置为高阻z。
3.3 端口位宽声明——别让你的数据“溢出”
端口位宽,说白了就是这根“线”能传多少位数据。声明方式很简单:[MSB:LSB]。
| 声明方式 | 含义 | 示例 |
|---|---|---|
input [7:0] data |
8位输入,data[7]是最高位 | data[7:0] = 8'hFF |
output [15:0] result |
16位输出,result[15]是最高位 | result[15:0] = 16'hABCD |
inout [3:0] bus |
4位双向总线 | bus[3:0] = 4'bz |
我曾经犯过一个错:声明了一个[0:7]的端口,结果后面代码里用了[7:0]的索引,仿真时数据全乱了。所以,我建议统一用[MSB:LSB](高位在左,低位在右)的写法,别混用。
核心要点:
- 端口位宽必须与内部逻辑匹配,否则综合时会出警告
- 未声明位宽的端口默认是1位
- 位宽声明写在端口名前面,不是后面
3.4 知识体系结构图
下面这张图,帮你理清模块与端口的关系。我画了张SVG,你看一眼就明白了。
小技巧:写端口声明时,我习惯把同一类型的端口放在一起,按位宽从大到小排列。这样代码看起来整齐,也方便后续维护。
警告:千万别把input端口声明成reg类型!综合工具会报错。记住:input只能是wire,output可以是wire或reg,inout只能是wire。
好了,模块与端口这部分就讲到这里。记住:模块是设计的骨架,端口是骨架上的关节。骨架搭好了,后面的逻辑填充才能顺风顺水。
公众号:蓝海资料掘金营,微信deep3321