3. 系统架构设计:整体架构框图、AXI4-Stream接口规范、数据通路设计

好,咱们进入第三章。这一章我打算把卷积加速器的骨架给你搭起来。说白了,就是回答三个问题:整个系统长什么样?数据怎么在模块间流动?接口怎么约定?

我个人习惯,做任何加速器之前,先画一张顶层框图。别急着写代码,先把框图画明白。我在项目中吃过亏,有一次架构没想清楚就开干,结果后面接口对不上,改得我头皮发麻。嗯,咱们今天就把这事一次搞定。

3.1 整体架构框图

先看这张图,这是整个卷积加速器的顶层架构。我把它画成了SVG,你仔细看看。

卷积加速器顶层架构框图 AXI4-Stream Slave 输入接口 输入FIFO 乒乓缓冲 卷积计算核心 PE阵列 4x4 乘加树 输出FIFO 乒乓缓冲 AXI4-Stream Master 输出接口 控制状态机 FSM + 配置寄存器 权重加载模块 从DDR加载权重 图例: AXI4-Stream接口 FIFO缓冲 计算核心 控制逻辑 权重加载 控制信号(虚线) 数据流(实线)

这张图里,数据从左往右流。输入数据先进AXI4-Stream Slave接口,然后进输入FIFO做缓冲。卷积核心从FIFO里拿数据做计算,结果送到输出FIFO,最后通过AXI4-Stream Master接口送出去。

你想想看,为什么中间要插两级FIFO?

原因很简单:速率匹配。输入接口可能跑200MHz,计算核心可能只能跑150MHz,没有FIFO缓冲,数据就会丢。我在一个项目中遇到过,输入数据源源不断,计算核心来不及处理,结果控制状态机直接崩了。从那以后,我每个加速器都老老实实加FIFO。

核心要点:整个架构围绕「数据流」设计。输入→缓冲→计算→缓冲→输出,这是最经典的流水线结构。控制状态机负责协调各个模块的启动、停止和状态切换。

3.2 AXI4-Stream接口规范

AXI4-Stream,说白了就是一组点对点的数据管道。没有地址线,只有数据线和握手信号。我刚开始学的时候觉得它很简单,后来才发现坑不少。

先看接口信号定义:

信号名 方向 位宽 描述
aclk 输入 1 时钟信号,所有信号在上升沿采样
aresetn 输入 1 异步复位,低电平有效
tvalid 主->从 1 主设备指示数据有效
tready 从->主 1 从设备指示可以接收数据
tdata 主->从 N 数据总线,N通常为8/16/32/64/128
tkeep 主->从 N/8 字节使能,指示哪些字节有效
tlast 主->从 1 指示当前传输是最后一个数据
tuser 主->从 M 用户自定义信号,可携带边带信息

这里有个关键点:握手协议。tvalid和tready必须同时为高,数据才算传输成功。我见过很多新手写代码时只检查tvalid,忽略了tready,结果数据丢了还不知道。

经验之谈:写AXI4-Stream从设备时,tready信号最好用寄存器打一拍再输出。这样可以避免组合逻辑路径过长,时序更容易收敛。我曾经在一个400MHz的设计里,就因为tready的组合逻辑太深,导致setup违例,折腾了两天才搞定。

再看一个典型的握手时序:

// AXI4-Stream 握手示例
// 主设备发送数据,从设备接收
always @(posedge aclk or negedge aresetn) begin
    if (!aresetn) begin
        // 复位状态
        tready <= 1'b0;
    end else begin
        // 当内部FIFO未满时,拉高tready
        if (fifo_wr_count < FIFO_DEPTH - 1)
            tready <= 1'b1;
        else
            tready <= 1'b0;
    end
end

// 数据采样条件:tvalid && tready
wire handshake = tvalid && tready;
always @(posedge aclk) begin
    if (handshake) begin
        fifo_wr_data <= tdata;
        fifo_wr_en   <= 1'b1;
    end else begin
        fifo_wr_en   <= 1'b0;
    end
end

这段代码里,我用了fifo_wr_count来判断FIFO是否快满了。为什么不用fifo_full信号?因为fifo_full在满的那一刻才拉高,但tready需要提前拉低,否则数据可能写进去又溢出。嗯,这个细节很多人不注意。

3.3 数据通路设计

数据通路,就是数据从输入到输出经过的路径。咱们这个卷积加速器,数据通路分三段:

  1. 输入段:AXI4-Stream Slave → 输入FIFO → 数据重排
  2. 计算段:数据重排 → PE阵列 → 累加器
  3. 输出段:累加器 → 输出FIFO → AXI4-Stream Master

每一段都有讲究。先说输入段。

输入数据进来的时候,是像素点按行扫描的顺序。但卷积计算需要的是滑动窗口。比如3x3的卷积核,你需要同时拿到9个像素点。所以输入FIFO后面必须跟一个数据重排模块,把串行数据变成并行窗口。

注意:数据重排模块是卷积加速器的关键瓶颈。如果窗口生成速度跟不上计算速度,整个流水线就会断流。我建议用双缓冲结构,一个缓冲在生成窗口,另一个缓冲在给PE阵列喂数据,交替工作。

计算段的核心是PE阵列。咱们用4x4的PE阵列做例子:

// PE阵列顶层接口
module pe_array #(
    parameter DATA_WIDTH = 8,
    parameter PE_ROWS    = 4,
    parameter PE_COLS    = 4
)(
    input  wire                     clk,
    input  wire                     rst_n,
    // 输入窗口数据
    input  wire [DATA_WIDTH*9-1:0]  window_data,  // 3x3窗口
    input  wire                     window_valid,
    // 权重数据
    input  wire [DATA_WIDTH*9-1:0]  weight_data,  // 3x3权重
    input  wire                     weight_valid,
    // 输出结果
    output wire [DATA_WIDTH*2-1:0]  result_data,  // 累加结果
    output wire                     result_valid
);

// 每个PE内部做乘加运算
// PE(i,j) = sum(window[k] * weight[k]) for k=0..8
// 然后按列累加,最后输出

endmodule

这里有个设计选择:权重是提前加载还是实时加载?

我个人建议提前加载。因为权重在推理过程中是不变的,提前加载到内部寄存器里,计算时直接从寄存器读,延迟最小。实时加载的话,每次计算都要从DDR读权重,带宽压力太大。

输出段相对简单。累加器的结果送到输出FIFO,然后通过AXI4-Stream Master接口发出去。但要注意输出数据的位宽。输入是8bit,经过乘加累加后,结果可能到16bit甚至32bit。输出FIFO的位宽要匹配,否则数据会截断。

数据通路设计原则:

  • 每一级之间用valid-ready握手,不要用单脉冲使能
  • FIFO深度要能覆盖最差情况下的延迟抖动
  • 数据位宽逐级增加,避免精度损失
  • 关键路径上插入流水线寄存器,提升频率

最后说一句,数据通路设计好了,整个加速器就成功了一半。剩下的就是控制状态机和细节优化。我在实际项目中,数据通路的调试占了整个项目60%的时间。所以这一章的内容,你值得多看两遍。

一个小技巧:写RTL之前,先用Excel或者Python把数据通路的数据流模拟一遍。把每个时钟周期数据在哪个模块、什么状态都列出来。这样写代码的时候心里有底,不容易出错。我每次做新架构都这么干,屡试不爽。

好了,这一章就到这里。架构框图有了,接口规范清楚了,数据通路也设计好了。下一章咱们开始写RTL代码,把今天画的框图变成真正的硬件。


可可精品资料库