3、系统架构设计:顶层模块划分、数据流设计、控制流设计、时钟域与复位策略、功耗预算分配

好,咱们进入第三章。说实话,系统架构设计是整个项目里最考验经验的一环。很多刚入行的朋友喜欢一上来就写代码,结果写到一半发现模块之间信号对不上,时钟域乱成一锅粥。我个人的习惯是,先花30%的时间把架构想清楚,后面写代码反而快。

这一章咱们聊五个核心点:顶层模块怎么切、数据怎么流、控制怎么走、时钟复位怎么处理、功耗怎么分。嗯,一个一个来。

3.1 顶层模块划分:别切太碎,也别切太粗

模块划分这事儿,说白了就是「高内聚、低耦合」。但实际做起来,很多人容易走极端。

我记得有一次带新人做项目,他把一个简单的视频缩放模块切成了二十多个小模块,每个模块就几十行代码。结果呢?接口信号多到连他自己都记不住,仿真一跑全是时序违例。后来我让他合并成四个大模块,问题立马解决了。

我一般遵循这几个原则:

  • 功能边界清晰:每个模块只干一件事。比如DMA控制器就管数据搬运,别在里面塞算法逻辑。
  • 接口尽量简单:模块之间的信号线别超过20根。如果超过,说明你切得有问题。
  • 复用性考虑:有些模块(比如AXI接口、FIFO)以后其他项目也能用,单独拎出来。
  • 仿真友好:每个模块都能独立仿真。我习惯给每个模块配一个简单的testbench,哪怕只是验证接口时序。

举个例子,一个典型的图像处理加速器,我通常会这样切:

顶层模块:image_accelerator_top
  ├── axi_slave_if        // 配置接口,接收CPU指令
  ├── dma_controller      // 数据搬运,从DDR读/写
  ├── line_buffer         // 行缓存,用于图像滤波
  ├── processing_core     // 核心算法,比如卷积、缩放
  └── output_fifo         // 输出缓冲,平滑数据输出

你看,每个模块的名字就说明了它的职责。新同事接手一看就懂。

小技巧:模块命名用英文,但注释写中文。我团队里统一要求:模块名用snake_case,信号名用大写字母缩写(比如DATA_VALID)。这样代码可读性高。

3.2 数据流设计:画图比写代码重要

数据流设计,说白了就是搞清楚数据从哪来、经过哪、到哪去。我每次做架构,第一件事就是画数据流图。

下面这张图是我用SVG画的,展示了一个典型的数据流走向。你仔细看,箭头方向就是数据流动的方向。

DDR内存 DMA控制器 行缓存 处理核心 输出FIFO 读数据 写数据 行数据 结果 配置

你看,数据从DDR出来,经过DMA搬到行缓存,然后喂给处理核心,最后通过输出FIFO写回DDR。这个流程很清晰,每个模块只跟上下游打交道。

我遇到过最坑的事,就是数据流没画清楚就开始写代码。结果发现处理核心需要的数据格式跟DMA送过来的不一样,中间还得加个格式转换模块。你说这多折腾?

核心原则:数据流设计要保证「无阻塞、无死锁」。每个模块的输入输出带宽要匹配。比如处理核心每时钟周期处理一个像素,那DMA就得保证每时钟周期能送来一个像素。

3.3 控制流设计:状态机是你的好朋友

控制流,说白了就是谁在什么时候干什么。我习惯用状态机来实现控制逻辑。

举个例子,一个简单的DMA控制器,状态机大概长这样:

typedef enum {
    IDLE,
    READ_REQ,
    READ_DATA,
    WRITE_REQ,
    WRITE_DATA,
    DONE
} dma_state_t;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state <= IDLE;
    end else begin
        case (state)
            IDLE: begin
                if (start) state <= READ_REQ;
            end
            READ_REQ: begin
                if (axi_ready) state <= READ_DATA;
            end
            READ_DATA: begin
                if (axi_valid) state <= WRITE_REQ;
            end
            // ... 后面类似
        endcase
    end
end

你看,状态机把控制逻辑写得明明白白。每个状态只做一件事,跳转条件也很清楚。

我曾经犯过一个错误:在状态机里嵌了太多组合逻辑,结果时序跑不过。后来我学乖了,状态机的输出全部用寄存器打一拍。虽然多了一个时钟周期延迟,但时序好多了。

注意:状态机别写太复杂。如果一个状态机超过20个状态,建议拆成多个小状态机。我见过有人写了一个50个状态的状态机,调试的时候差点崩溃。

3.4 时钟域与复位策略:跨时钟域是魔鬼

时钟域设计,这是FPGA设计里最容易出问题的地方。我刚开始做项目时,就因为跨时钟域没处理好,导致芯片在高温下偶尔死机。查了整整两周才找到原因。

我的经验是:

  • 尽量少用跨时钟域:能在一个时钟域里解决的问题,就别搞两个时钟。比如处理核心和行缓存,我通常用同一个时钟。
  • 跨时钟域必须用同步器:单比特信号用两级触发器同步,多比特信号用异步FIFO。别偷懒。
  • 复位策略要统一:我习惯用异步复位、同步释放。这样既保证了复位瞬间的可靠性,又避免了复位信号毛刺。

举个例子,跨时钟域同步器的代码:

// 单比特同步器
always_ff @(posedge clk_dst or negedge rst_n) begin
    if (!rst_n) begin
        sync_reg1 <= 1'b0;
        sync_reg2 <= 1'b0;
    end else begin
        sync_reg1 <= data_src;
        sync_reg2 <= sync_reg1;
    end
end
assign data_dst = sync_reg2;

你看,就两级触发器。但很多人会问:为什么不用一级?因为一级可能采到亚稳态。两级能把亚稳态概率降到几乎为零。

避坑指南:我曾经在项目里用了一级同步器,结果仿真没问题,上板子就随机出错。后来换成两级,问题消失。从那以后,我所有跨时钟域信号都至少两级同步。

3.5 功耗预算分配:别等到最后才想功耗

功耗这事儿,很多人觉得是后端的事。其实不然。架构设计阶段就要把功耗预算分好。

我一般这样分:

模块 功耗占比 说明
处理核心 40% 算法逻辑,动态功耗大
DMA控制器 20% 频繁读写,但逻辑简单
行缓存/存储器 25% BRAM占大头,静态功耗
控制逻辑 10% 状态机,功耗低
时钟网络 5% 时钟树,但不可忽视

你看,处理核心占了40%。所以降功耗的重点就在那里。我常用的手段:

  • 门控时钟:模块不工作时,把时钟关掉。比如DMA在空闲时,时钟可以停掉。
  • 数据使能:处理核心只在有数据时才翻转寄存器。用valid信号控制。
  • 降低频率:如果带宽够用,把频率降下来。比如处理核心跑200MHz就够了,别硬上300MHz。

我记得有个项目,功耗预算只有5W。刚开始没注意,综合完一看功耗8W,超标了。后来加了门控时钟,又把一些模块的频率从250MHz降到150MHz,最终压到4.8W。嗯,刚刚好。

总结一下:架构设计阶段,把模块切好、数据流画清楚、控制流用状态机、跨时钟域处理好、功耗提前分好。后面写代码就是水到渠成的事。

好,这一章就聊到这儿。下一章咱们讲RTL编码规范,到时候我会分享一些我踩过的坑,保证让你少走弯路。

可可精品资料库