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, ...);

注意,第一个端口永远是输出,后面全是输入。这个顺序别搞反了,我曾经见过一个同事把输入输出写反了,仿真怎么跑都对,综合出来就是不对,查了两天才发现是端口顺序错了。

注意:门级原语的输出端口必须连线到wire类型变量,不能是reg。这是硬件描述语言的硬性规定。

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 FPGA 内部结构示意 可编程逻辑块阵列 (PLB) 包含多个逻辑单元 (LC) 每个LC含LUT4 + 触发器 + 进位链 IO单元 (PIO) IO单元 (PIO) Block RAM DSP模块 布线资源 可编程互连 全局时钟网络

这张图展示的是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