4、结构化建模:门级原语、模块实例化与层次化设计
各位同学,今天我们聊一个非常实在的话题——结构化建模。
说白了,就是怎么用Verilog把一块数字电路“搭”出来。不是那种高大上的行为级描述,而是真正贴近底层硬件的那种搭法。我个人觉得,这是理解FPGA内部工作原理的必经之路。
4.1 门级原语:最底层的积木
Verilog里有一组内置的门级原语,比如and、or、not、nand、nor、xor、xnor。这些玩意儿,就是数字电路最基础的积木块。
我记得刚学FPGA那会儿,总觉得用assign语句写个“a & b”就完事了,干嘛还要用and门原语?后来做项目才明白,门级原语能让你精确控制电路的拓扑结构。有些老工程师做关键路径优化时,就喜欢用门级原语手动搭建。
用法很简单,看个例子:
// 两输入与门
and u_and1 (out, a, b);
// 三输入或门
or u_or1 (out, a, b, c);
// 非门
not u_not1 (out, in);
// 异或门
xor u_xor1 (sum, a, b);
语法格式是:门类型 实例名 (输出, 输入1, 输入2, ...);
注意,第一个端口永远是输出,后面全是输入。这个顺序别搞反了,我曾经见过一个同事把输入输出写反了,仿真怎么跑都对,综合出来就是不对,查了两天才发现是端口顺序错了。
4.2 模块实例化:把积木拼起来
门级原语是基础积木,模块实例化就是拼积木的过程。你写好的一个模块,可以在另一个模块里“调用”它,这就是实例化。
举个例子,假设我们写了一个半加器模块:
module half_adder (
input a, b,
output sum, cout
);
xor u_xor (sum, a, b);
and u_and (cout, a, b);
endmodule
然后在全加器里实例化它:
module full_adder (
input a, b, cin,
output sum, cout
);
wire s1, c1, c2;
// 实例化两个半加器
half_adder u_ha1 (.a(a), .b(b), .sum(s1), .cout(c1));
half_adder u_ha2 (.a(s1), .b(cin), .sum(sum), .cout(c2));
assign cout = c1 | c2;
endmodule
这里我用了端口名连接的方式,也就是.端口名(连线)。我个人强烈推荐这种方式,虽然打字多一点,但可读性高,不容易出错。
还有一种是按位置连接,比如:
half_adder u_ha1 (a, b, s1, c1); // 按端口顺序
这种写法我建议你少用。为什么?因为一旦模块端口顺序变了,你的代码就全废了。我在项目中吃过这个亏,从那以后只用端口名连接。
4.3 层次化设计思想
层次化设计,说白了就是“分而治之”。一个大系统,拆成几个子系统;子系统再拆成功能模块;功能模块再拆成基本单元。每一层只关心自己的事,上层不管下层的实现细节。
你想想看,一个百万门的FPGA设计,如果全写在一个module里,那调试起来得多痛苦?
我一般这样划分层次:
- 顶层模块:只做实例化,连接各个子模块,不写任何逻辑
- 功能模块:比如UART、SPI、FIFO、状态机等,每个模块独立设计、独立仿真
- 底层单元:门级原语、基本触发器、组合逻辑
核心原则:每个模块只做一件事,并且把它做好。模块之间的接口越简单越好,信号数量控制在合理范围内。
举个例子,一个简单的数据采集系统:
module top (
input clk, rst_n,
input adc_data,
output [7:0] result
);
wire [7:0] data_out;
wire data_valid;
// ADC接口模块
adc_interface u_adc (
.clk(clk),
.rst_n(rst_n),
.adc_data(adc_data),
.data_out(data_out),
.data_valid(data_valid)
);
// 数据处理模块
data_processor u_proc (
.clk(clk),
.rst_n(rst_n),
.data_in(data_out),
.valid_in(data_valid),
.result(result)
);
endmodule
你看,顶层模块干干净净,只做连线。每个子模块的功能一目了然,调试时也能单独测试。
4.4 Lattice器件结构简介
聊完了设计方法,咱们看看实际芯片长什么样。以Lattice的iCE40系列为例,它的内部结构大致是这样的:
这张图展示的是Lattice iCE40系列的基本架构。核心是中间那块可编程逻辑块阵列,也就是PLB。每个PLB里包含若干个逻辑单元(LC),每个LC里有一个4输入查找表(LUT4)、一个触发器和进位链逻辑。
我刚开始用Lattice芯片时,总觉得它的LUT4比Xilinx的LUT6小气。后来做低功耗项目才发现,LUT4的粒度更细,对于小规模逻辑,资源利用率反而更高。这就是为什么iCE40在低功耗市场这么能打。
除了PLB,还有几个关键部件:
- IO单元(PIO):分布在芯片四周,负责芯片内部信号与外部引脚的连接。支持多种电平标准,比如LVCMOS、LVTTL等。
- Block RAM:嵌入式存储块,可以用来做FIFO、ROM、单口/双口RAM。iCE40每个BRAM是4Kbit大小。
- DSP模块:硬件乘法器,支持乘加运算。做数字信号处理时特别有用。
- 布线资源:连接所有模块的“高速公路”,包括可编程互连和全局时钟网络。
小技巧:用Lattice的Diamond或Radiant软件时,打开Chip Planner视图,你能直观看到每个逻辑单元在芯片上的物理位置。这对做时序约束和布局优化非常有帮助。
4.5 层次化设计与Lattice器件的结合
理解了器件结构,再回头看层次化设计,你会有更深的认识。
举个例子,你写了一个计数器模块,综合后它会映射到PLB里的几个逻辑单元。如果你在顶层实例化了8个计数器,那它们就会占用8组逻辑单元。层次化设计越清晰,综合工具就越容易做资源优化和时序分析。
我曾经接手过一个项目,前任工程师把所有逻辑都写在一个module里,足足3000多行。综合后时序一团糟,根本不知道哪里是瓶颈。后来我花了三天时间,把它拆成12个子模块,每个模块独立仿真验证,最后时序收敛了,调试也方便多了。
所以我的建议是:
- 每个模块控制在200行以内
- 模块接口信号不超过20个
- 顶层只做实例化,不写逻辑
- 每个模块有独立的仿真testbench
做到这几点,你的设计就会像搭积木一样清晰。不管是Lattice还是其他厂家的FPGA,这套方法论都通用。
总结一下:门级原语是基础积木,模块实例化是拼装方法,层次化设计是架构思想,Lattice器件是物理载体。把这四样东西串起来,你就能从底层到顶层,完整地理解一个FPGA设计是怎么诞生的。
公众号:蓝海资料掘金营,微信deep3321