模块化基础:模块化设计三原则与划分粒度

做FPGA设计久了,你会发现一个有意思的现象:同样的功能,有人写出来的代码像搭积木,改哪里都顺手;有人写出来的代码像一团乱麻,动一根线整个工程就崩了。区别在哪?说白了,就是模块化设计的功底。

我刚开始做项目那会儿,也吃过不少亏。记得有一次做视频处理项目,整个设计就一个顶层文件,里面塞了上万行代码。后来客户要改分辨率,我光是理清信号关系就花了两天。从那以后,我彻底明白了——模块化不是花架子,是保命的。

模块化设计三原则

模块化设计有三个核心原则,我管它叫「三驾马车」:高内聚、低耦合、接口清晰。这三个原则,缺一个都不行。

1. 高内聚:一个模块只干一件事

高内聚的意思是,模块内部的功能要紧密相关。说白了,就是「一个萝卜一个坑」。

举个例子,你写一个UART模块,它就应该只负责串口收发。别把波特率计算、数据缓存、协议解析全塞进去。我见过有人把FIFO管理逻辑也写在UART模块里,结果后来要换FIFO深度,改得满头包。

高内聚的判断标准:

  • 模块内部所有逻辑都服务于同一个功能目标
  • 修改一个功能时,只需要改这一个模块
  • 模块的命名能准确描述它的功能

我个人习惯,写模块前先问自己三个问题:这个模块到底要干什么?它依赖哪些外部信号?如果明天要复用,我需要改多少代码?想清楚了再动手。

2. 低耦合:模块之间少说话

低耦合,就是模块之间的依赖关系要尽量少。你想想看,如果两个模块之间牵一发动全身,那还叫什么模块化?

我曾经接手过一个项目,模块A和模块B之间直接传递了20多个控制信号。后来要加一个新功能,结果发现模块A的改动影响了模块B、模块C、甚至模块D。那感觉,就像拆炸弹一样紧张。

降低耦合的技巧:

  • 用标准接口协议(如AXI-Stream、AXI-Lite)代替自定义信号
  • 模块间只传递必要的数据和控制信号
  • 避免模块内部状态直接暴露给外部

嗯,这里要注意一点:低耦合不是零耦合。模块之间完全不通信是不可能的,关键是要把通信的「通道」设计得简单、标准、可控。

3. 接口清晰:信号定义要像说明书

接口清晰,说白了就是让别人一看就知道这个模块怎么用。我见过最头疼的代码,信号命名全是a、b、c、d,注释也没有。这种代码,别说别人看不懂,过两个月自己都忘了。

接口清晰包括几个方面:

  • 命名规范:信号名要能反映功能,比如 tx_datarx_valid,别用 sig1sig2
  • 方向明确:每个信号是输入还是输出,要一目了然
  • 时序说明:关键信号的时序关系要写清楚,比如「数据在时钟上升沿采样」
  • 参数化:用parameter定义可配置的参数,比如数据位宽、FIFO深度

避坑指南:我曾经因为接口定义不清晰,导致两个模块的握手信号时序对不上。仿真跑得好好的,上板就出错。后来花了三天才定位到问题——原来是valid信号早了半个时钟周期。从那以后,我每个模块的接口都写详细的时序图。

模块划分粒度:粗了不行,细了也不行

模块划分的粒度,是个技术活。太粗了,一个模块几千行代码,跟没划分一样;太细了,一个模块就几十行代码,接口满天飞,管理成本反而更高。

我一般遵循几个原则:

粒度 特点 适用场景
粗粒度(>2000行) 功能复杂,难以复用 顶层模块、系统级控制
中粒度(200-2000行) 功能独立,易于复用 大多数功能模块,如FIFO、UART、SPI
细粒度(<200行) 功能单一,接口简单 基础单元,如寄存器、计数器、状态机

我个人习惯,把模块分成三个层次:

  • 顶层模块:负责系统级连接,不包含具体逻辑
  • 功能模块:每个模块实现一个独立功能,比如图像缩放、数据打包
  • 基础单元:通用的基础电路,比如D触发器、加法器、FIFO

举个例子,一个图像处理系统,我会这样划分:

// 顶层模块:只做信号连接
module image_system_top (
    input  clk,
    input  rst_n,
    // ... 其他接口
);

// 功能模块:图像缩放
module image_scaler (
    input  [7:0] pixel_in,
    input        valid_in,
    output [7:0] pixel_out,
    output       valid_out
);

// 基础单元:行缓存FIFO
module line_buffer_fifo #(
    parameter DATA_WIDTH = 8,
    parameter FIFO_DEPTH = 1024
) (
    input  clk,
    input  rst_n,
    input  [DATA_WIDTH-1:0] wr_data,
    input                   wr_en,
    output [DATA_WIDTH-1:0] rd_data,
    input                   rd_en
);

你想想看,如果我把行缓存逻辑直接写在图像缩放模块里,那以后换FIFO深度或者换FIFO类型,就得改整个模块。现在拆出来,改起来就轻松多了。

模块划分的黄金法则:

  1. 一个模块的代码量控制在200-2000行之间
  2. 模块的接口信号不超过20个(复杂模块可适当放宽)
  3. 模块的功能描述能用一句话说清楚
  4. 模块的复用率越高越好

最后说一句,模块化设计不是一蹴而就的。我刚开始做的时候也经常划分不合理,后来慢慢积累经验,才找到感觉。记住一句话:好的模块化设计,是改出来的,不是设计出来的。