模块化设计思想:模块划分原则、模块接口定义、模块内部结构规范

说实话,模块化设计这事儿,我入行头三年都没真正搞明白。那时候总觉得,能跑就行,管它分不分模块。直到有一次接手一个同事留下的代码——一个模块写了三千行,所有功能揉在一起,我看了整整一周才敢动手改。嗯,从那以后,我成了模块化设计的坚定拥护者。

你想想看,芯片设计本质上就是搭积木。每个模块就是一块积木,接口清晰、功能单一,才能搭出稳固的系统。今天我就把这几年的心得掰开揉碎讲给你听。

一、模块划分原则

模块怎么切?我个人的习惯是,先问自己三个问题:

  • 这个模块的功能能不能用一句话说清楚? 如果说不清楚,说明切得太粗了。
  • 这个模块的输入输出是不是明确的? 如果接口模糊,后面联调会痛不欲生。
  • 这个模块能不能独立测试? 不能独立测的模块,debug时就是噩梦。

具体来说,我总结了四条原则:

原则一:高内聚,低耦合

模块内部的功能要紧密相关,模块之间的依赖要尽可能少。我在项目中遇到过,有人把地址译码和FIFO控制写在一个模块里,结果改地址映射时把FIFO也搞坏了。这就是典型的内聚不够。

原则二:单一职责

一个模块只做一件事。比如,一个UART模块就只负责串行数据的收发,别把波特率生成也塞进来——那应该单独做个波特率发生器。

原则三:接口最小化

模块对外暴露的信号越少越好。我曾经见过一个模块,接口信号有80多个,光看端口列表就花了半小时。后来重构时砍到30个,功能一点没少。

原则四:可复用性

写模块时,想想下一个项目能不能直接用。比如,一个AHB-to-APB桥,如果参数化做得好,换个项目改个位宽就能用。

二、模块接口定义

接口定义这事儿,说白了就是定规矩。规矩定好了,团队协作才顺畅。我建议你注意以下几点:

2.1 接口命名规范

我个人习惯用统一的命名风格:

  • 输入信号:以 i_ 开头,比如 i_clk、i_rst_n
  • 输出信号:以 o_ 开头,比如 o_data、o_valid
  • 双向信号:以 io_ 开头,比如 io_sda

小技巧:接口信号的名字最好能体现它的功能。比如 o_fifo_full 比 o_full 好一万倍。你想想看,debug时看到信号名就知道它干嘛的,多省事。

2.2 接口时序约定

每个接口都要明确时序关系。我一般会在模块头部的注释里写明:

//------------------------------------------------
// 模块:uart_rx
// 功能:UART接收模块
// 接口时序:
//   - i_clk: 系统时钟,上升沿采样
//   - i_rst_n: 异步复位,低有效
//   - i_rx: 串行输入,空闲时为高
//   - o_data: 并行数据输出,o_valid为高时有效
//   - o_valid: 数据有效标志,持续一个时钟周期
//------------------------------------------------

2.3 接口信号列表

我建议用表格来管理接口信号,一目了然:

信号名 方向 位宽 描述
i_clk 输入 1 系统时钟
i_rst_n 输入 1 异步复位,低有效
i_rx 输入 1 串行数据输入
o_data 输出 8 并行数据输出
o_valid 输出 1 数据有效标志

三、模块内部结构规范

模块内部怎么写?我有个「三段式」的习惯,用了十几年,屡试不爽。

3.1 三段式结构

每个模块内部,我建议分成三个部分:

  1. 寄存器定义区:所有内部寄存器和wire信号的定义
  2. 组合逻辑区:用 always @(*) 或 assign 实现的组合逻辑
  3. 时序逻辑区:用 always @(posedge clk) 实现的时序逻辑

举个例子:

module fifo_controller (
    input  wire        i_clk,
    input  wire        i_rst_n,
    input  wire        i_wr_en,
    input  wire        i_rd_en,
    output reg         o_full,
    output reg         o_empty
);

    //========================================
    // 1. 寄存器定义区
    //========================================
    reg [3:0] wr_ptr;
    reg [3:0] rd_ptr;
    reg [4:0] fifo_cnt;

    //========================================
    // 2. 组合逻辑区
    //========================================
    assign o_full  = (fifo_cnt == 5'd16);
    assign o_empty = (fifo_cnt == 5'd0);

    //========================================
    // 3. 时序逻辑区
    //========================================
    always @(posedge i_clk or negedge i_rst_n) begin
        if (!i_rst_n) begin
            wr_ptr   <= 4'd0;
            rd_ptr   <= 4'd0;
            fifo_cnt <= 5'd0;
        end else begin
            // 写指针更新
            if (i_wr_en && !o_full)
                wr_ptr <= wr_ptr + 1'b1;
            // 读指针更新
            if (i_rd_en && !o_empty)
                rd_ptr <= rd_ptr + 1'b1;
            // FIFO计数更新
            case ({i_wr_en, i_rd_en})
                2'b10: fifo_cnt <= fifo_cnt + 1'b1;
                2'b01: fifo_cnt <= fifo_cnt - 1'b1;
                default: ;
            endcase
        end
    end

endmodule

注意:组合逻辑和时序逻辑一定要分开写。我曾经见过有人把两者混在一起,结果综合出来的电路和仿真对不上,查了三天才发现是组合逻辑里用了非阻塞赋值。这种坑,踩一次就够了。

3.2 代码布局规范

我建议每个模块都保持一致的布局:

  • 模块头部:功能描述、作者、修改历史
  • 参数定义:用 parameter 或 localparam
  • 端口声明:输入在前,输出在后
  • 内部信号:wire在前,reg在后
  • 逻辑实现:组合逻辑在前,时序逻辑在后

3.3 注释规范

注释不是越多越好,而是要「该有的地方有」。我个人的习惯是:

  • 每个模块头部写清楚功能
  • 每个always块前面写一句注释,说明它在干嘛
  • 复杂的组合逻辑,写清楚真值表或算法
  • 不要写「废话注释」,比如 // 时钟信号 这种

四、知识体系总览

下面这张图,是我对模块化设计思想的总结。你把它打印出来贴在工位上,写代码时瞄一眼,能少走很多弯路。

模块化设计思想知识体系 模块化设计 模块划分原则 模块接口定义 内部结构规范 高内聚低耦合 单一职责 接口最小化 命名规范 时序约定 信号列表 三段式结构 代码布局 注释规范 目标:可读、可测、可复用、可维护

我的经验之谈:模块化设计不是一蹴而就的。刚开始你可能觉得「分模块好麻烦」,但等你debug时发现,一个模块出问题,其他模块完全不受影响,那种爽感,值得你花时间去养成这个习惯。

好了,关于模块化设计,我就说这么多。记住一句话:好的模块化设计,让代码像乐高积木一样,拼起来是系统,拆开来是零件。你写代码时多想想这句话,慢慢就能找到感觉。

可可精品资料库