模块与端口:从零开始搭积木
各位同学,今天我们来聊聊RTLA里最基础、也最核心的东西——模块与端口。
你想想看,数字芯片设计说白了就是搭积木。每个模块就是一块积木,端口就是积木上的插头和插座。模块之间通过端口互相连接,最终拼成一个完整的系统。
我个人习惯把模块比作一个黑盒子。你不需要知道盒子里面怎么实现的,只要知道它有哪些输入、哪些输出,就能把它用起来。这就是模块化的思想。
module声明:给你的积木起个名字
在RTLA里,声明一个模块很简单。用module关键字,后面跟上模块名,然后是一对括号,里面放端口列表。
module counter (
input wire clk,
input wire rst_n,
output reg [7:0] count
);
// 模块内部逻辑
endmodule
嗯,这里要注意:模块名最好用有意义的英文单词,别用a、b、c这种。我在项目中见过有人用module m1,三个月后他自己都看不懂了。
端口方向:数据该往哪走
端口方向有三种:input、output、inout。
| 方向 | 含义 | 典型用途 |
|---|---|---|
| input | 数据流入模块 | 时钟、复位、数据输入 |
| output | 数据流出模块 | 计算结果、状态信号 |
| inout | 双向数据 | I2C数据线、三态总线 |
inout用得比较少,但一旦用上就得小心。我曾经在一个I2C控制器里把inout的方向搞反了,仿真怎么都通不过。后来发现是驱动使能信号写反了,折腾了一整天。
端口类型:wire、reg、logic
端口类型决定了这个信号怎么被赋值。
- wire:连线型。由组合逻辑驱动,或者直接连到其他模块的输出。
- reg:寄存器型。在always块里赋值,会综合成触发器或锁存器。
- logic:RTLA里新增的类型。它既可以当wire用,也可以当reg用。
我个人强烈推荐用logic。为什么?因为它省事。你不用纠结这个信号到底该声明成wire还是reg,统统用logic就对了。
module adder (
input logic [3:0] a,
input logic [3:0] b,
output logic [4:0] sum
);
always_comb begin
sum = a + b;
end
endmodule
你看,这里sum在always_comb里赋值,用logic完全没问题。如果换成Verilog,你得声明成reg,但实际综合出来是组合逻辑,容易让人困惑。
参数化模块:让模块更灵活
参数化是模块设计的精髓。通过parameter,你可以让一个模块适应不同的位宽、不同的深度。
module fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16
) (
input logic clk,
input logic rst_n,
input logic wr_en,
input logic [DATA_WIDTH-1:0] wr_data,
input logic rd_en,
output logic [DATA_WIDTH-1:0] rd_data,
output logic full,
output logic empty
);
// FIFO实现
endmodule
实例化的时候,你可以覆盖这些参数:
fifo #(
.DATA_WIDTH(16),
.DEPTH(32)
) u_fifo (
.clk(clk),
.rst_n(rst_n),
// ...
);
我记得有一次做项目,需要同时用8位和16位的FIFO。如果写两个模块,代码量翻倍不说,维护也麻烦。用参数化模块,改一下参数就行,省心多了。
知识体系总览
下面这张图帮你理清模块与端口的关系:
这张图很直观:左边是输入端口,右边是输出端口,中间是模块内部逻辑。参数化配置让模块可以灵活调整。
避坑总结
最后,我把自己踩过的坑总结一下:
- 端口方向别搞反:input和output写反了,仿真能过,但综合会报错。我吃过这个亏。
- 参数默认值要合理:给parameter设个合理的默认值,别人用你的模块时不用每次都改。
- 端口名别太短:
clk、rst没问题,但d、q这种就太抽象了。多写几个字母,代码可读性翻倍。 - inout慎用:除非你真的很清楚三态门怎么用,否则别碰inout。
好了,模块与端口就讲到这里。记住一句话:模块是骨架,端口是血脉。骨架搭好了,血脉通顺了,芯片设计就成功了一半。
公众号:蓝海资料掘金营,微信deep3321