🏠

Prime Factor FFT Vitis System Integration 模块深度解析

概述:这个模块解决什么问题?

想象你正在设计一个高速数字信号处理系统,需要计算长度为1008点的FFT(快速傅里叶变换)。传统的Cooley-Tukey算法虽然通用,但需要大量的"旋转因子"乘法运算——这些额外的计算会消耗宝贵的AI Engine资源。素因子算法(Prime Factor Algorithm, PFA)提供了一个优雅的替代方案:当变换长度 \(N = N_1 \times N_2 \times ...\) 且各因子互质时,可以将一维DFT分解为多维DFT,完全消除级间的旋转因子乘法

然而,这种优雅是有代价的。PFA要求对输入输出数据进行复杂的重排序(permutation),这种重排序涉及模运算寻址模式,无法直接用AI Engine Memory Tile的DMA Buffer Descriptor实现。这就是prime_factor_fft_vitis_system_integration模块存在的意义——它作为PL(Programmable Logic)与AIE(AI Engine)之间的数据编排层,通过Vitis HLS实现的专用内核,完成以下关键任务:

  1. DMA数据搬运:从LPDDR内存高效读取/写入FFT数据
  2. 输入重排序:将线性顺序的时域样本转换为PFA所需的3D立方体索引顺序
  3. 输出重排序:将频域结果从PFA内部顺序还原为标准顺序

可以把整个系统想象成一个精密的交响乐团:AIE中的DFT-7、DFT-9、DFT-16内核是演奏家,而这个模块则是乐谱分发员和座位安排员——确保每个演奏家在正确的时间拿到正确的音符。


架构设计与数据流

系统拓扑图

flowchart LR LPDDR["LPDDR Memory
时域/频域数据"] DMA_SRC["dma_src
DMA Source Kernel"] PERM_I["permute_i
Input Permutation"] AIE["AIE Array
DFT-7 → Transpose0 → DFT-9 → Transpose1 → DFT-16"] PERM_O["permute_o
Output Permutation"] DMA_SNK["dma_snk
DMA Sink Kernel"] LPDDR <-->|AXI4-Full| DMA_SRC DMA_SRC -->|AXI4-Stream| PERM_I PERM_I -->|AXI4-Stream| AIE AIE -->|AXI4-Stream| PERM_O PERM_O -->|AXI4-Stream| DMA_SNK DMA_SNK <-->|AXI4-Full| LPDDR

组件角色与职责

1. dma_src / dma_snk —— PL侧的"数据泵"

这对HLS内核负责在LPDDR和PL之间建立高速数据通道。它们采用经典的双缓冲(ping-pong)策略

  • dma_src:从LPDDR读取128位宽的数据块,存入内部BRAM缓冲区,然后以流式方式输出到下游的permute_i
  • dma_snk:接收来自permute_o的流式数据,存入内部缓冲区,最后写回LPDDR

关键设计参数:

  • 数据宽度:128位(对应4个32位复数样本)
  • 缓冲区深度:1008个样本(即1008×128位)
  • 目标时钟:312.5 MHz
// dma_src的核心循环结构
void load_buffer(TT_DATA mem[DEPTH], TT_DATA (&buff)[DEPTH]) {
  LOAD_BUFF: for (int mm=0; mm < DEPTH; mm++) {
#pragma HLS PIPELINE II=1
    buff[mm] = mem[mm];
  }
}

2. permute_i —— 输入端的"索引翻译器"

这是整个系统中最精妙的组件之一。PFA-1008算法将一维序列映射到3D数据立方体(7×9×16),而permute_i的任务是将线性输入地址转换为符合第一级DFT-7需求的访问模式。

核心机制

  • 使用四副本存储:每个样本同时写入4个不同的bank位置,支持后续的单周期4样本并行读取
  • 采用LUT驱动的地址生成:预计算的PERM_I_ADDR表定义了252个独特的读地址模式(每个模式读取4个样本)
  • 乒乓缓冲:两个物理buffer交替进行读写,隐藏流水线延迟
// permute_i的四副本写入策略
buff[ping][0][0][wr_addrA] = permute_i[0];
buff[ping][0][1][wr_addrA] = permute_i[0];  // 同一数据,4个位置
buff[ping][0][2][wr_addrA] = permute_i[0];
buff[ping][0][3][wr_addrA] = permute_i[0];

为什么需要四副本?因为PFA的重排序模式要求在一个时钟周期内读取可能分散在不同bank的4个样本。通过预先复制数据到所有可能的bank位置,可以用简单的多路选择器(mux)而非复杂的多端口RAM实现高吞吐访问。

3. permute_o —— 输出端的"逆翻译器"

功能上与permute_i对称,但寻址逻辑不同。它将DFT-16输出的3D立方体数据重新映射回线性频域顺序。

关键差异

  • 写地址遵循DFT-16的输出顺序(stride=63的步进模式)
  • 读地址由PERM_O_ADDR LUT驱动,产生标准频域顺序
  • 同样采用四副本策略和乒乓缓冲

数据流完整追踪

让我们跟随一个FFT变换的数据旅程:

阶段1:数据注入(Host → LPDDR → dma_src)

  1. Host处理器将1008点复数样本写入LPDDR的指定区域
  2. dma_src通过AXI4-Full接口发起burst读取,填满内部BRAM缓冲区
  3. #pragma HLS DATAFLOW确保加载和传输阶段流水线化执行

阶段2:输入重排序(dma_src → permute_i → AIE)

  1. dma_src以每周期4样本(128位)的速率输出数据
  2. permute_i接收数据后,根据当前写地址将每个样本复制4份存入ping buffer的不同bank
  3. 当写完1008个样本后,切换ping/pong标志,开始从pong buffer读取
  4. 读取阶段,PERM_I_ADDR LUT提供重排序后的地址,通过多路选择器选出正确的4个样本
  5. 输出流向AIE的DFT-7内核,此时数据的索引顺序已满足PFA第一维的要求

阶段3:AIE内部处理

数据依次经过:

  • DFT-7:计算7点DFT,输出维度变为(7×9×16)的变换结果
  • Transpose0:Memory Tile执行的矩阵转置,重排为(9×16×7)
  • DFT-9:计算9点DFT
  • Transpose1:第二次转置,重排为(16×7×9)
  • DFT-16:计算16点DFT,完成完整PFA

阶段4:输出重排序与回收(AIE → permute_o → dma_snk → LPDDR)

  1. DFT-16的输出进入permute_o,以stride=63的模式写入ping buffer
  2. 四副本策略再次应用,确保任意重排序读取都能单周期完成
  3. PERM_O_ADDR LUT驱动读取地址,产生标准频域顺序
  4. dma_snk捕获流式输出,支持多轮迭代中的选择性存储(通过loop_sel参数)
  5. 最终数据通过AXI4-Full写回LPDDR

设计决策与权衡分析

1. 为什么选择HLS而非RTL实现?

决策:使用Vitis HLS从C++模型综合出PL内核

考量因素

  • 开发效率:C++模型可以快速验证算法正确性,MATLAB生成的LUT可以直接嵌入
  • 可移植性:同一套C++代码可以针对不同频率目标重新综合
  • 权衡代价:相比手工RTL,HLS可能在面积和时序上有轻微损失,但对于这种控制密集型(而非计算密集型)逻辑,差距在可接受范围

2. 四副本存储的面积-性能权衡

决策:每个样本存储4份,换取单周期任意4样本读取能力

数学依据

  • PFA-1008的输入重排序需要在一个周期内读取4个可能位于任意位置的样本
  • 真双端口BRAM每周期最多提供2个独立地址的读取
  • 通过4副本策略,任何4个目标样本必然分布在4个不同的bank中,可以用4个并行读取+多路选择实现

资源成本

  • 原始数据量:1008 × 32位 = 4KB
  • 实际BRAM使用:2(ping-pong)× 4(副本)× 4KB = 32KB
  • 换来的是确定的II=1流水线性能

3. 为何将重排序放在PL而非AIE Memory Tile?

关键限制:Memory Tile的Buffer Descriptor不支持模运算寻址

PFA的重排序公式形如:

\[P = mod(C \times D \times R + R \times D \times C + R \times C \times D, 1008)\]

这种非线性的模运算地址模式超出了Memory Tile BD的表达能力(BD只支持线性stride和wrap模式)。因此,必须在PL中用自定义逻辑实现。

4. 统一的312.5 MHz时钟域

配置id=2:dma_src.ap_clk,permute_i.ap_clk,permute_o.ap_clk,dma_snk.ap_clk

设计意图

  • 简化跨时钟域(CDC)设计,避免异步FIFO带来的面积和延迟开销
  • 与AIE的1250 MHz时钟形成简单的4:1比例,便于PLIO接口的速率匹配
  • 128位@312.5 MHz = 32位@1250 MHz,与AIE的向量宽度对齐

关键实现细节

HLS优化指令解读

// permute_i_wrapper的关键pragma
#pragma HLS interface mode=ap_ctrl_none port=return  // 自由运行,无握手
#pragma HLS pipeline II=1                             // 每周期一个输出

// permute函数内部的存储优化
#pragma HLS array_partition variable=buff dim=1       // ping-pong维度展开
#pragma HLS bind_storage variable=buff type=RAM_T2P impl=bram  // 真双端口BRAM
#pragma HLS dependence variable=buff type=intra false // 声明无伪依赖,允许激进调度

ap_ctrl_none的选择至关重要——这使得内核可以连续流水处理无限长的样本流,而不需要每帧都进行启动/停止握手。

延迟隐藏机制

// dma_src中的虚拟样本注入
static constexpr int LATENCY = NFFT/2;  // 504 cycles
RUN_LATENCY: for (int dd=0; dd < LATENCY; dd++) {
#pragma HLS PIPELINE II=1
  sig_o.write( TT_DATA(0) );
}

这段看似奇怪的"零值填充"实际上是精心计算的延迟补偿。permute_ipermute_o各自引入约NFFT/4的流水线延迟,通过在数据源端预注入虚拟样本,确保整个系统的数据对齐正确。

LUT生成与验证

输入输出重排序表由MATLAB脚本gen_permute_tables.m生成,基于PFA的数学定义。这些LUT被硬编码为C数组:

// pfa1008_permute_i_luts.h
#define PERM_I_ADDR {\
   0, 144, 288, 432, 576, 720, 864, 112, ... \
}

注意LUT的双重存储:permute_lut[2][NFFT],这是为了配合ping-pong缓冲的双bank架构,允许读写阶段并行访问各自的LUT副本。


新贡献者须知:陷阱与注意事项

1. 隐式的数据格式契约

所有PL内核假设数据以打包的128位字形式组织,每个字包含4个32位复数样本(实部16位,虚部16位)。修改数据宽度需要同步调整:

  • TT_DATA类型定义(ap_uint<NBITS>
  • AIE PLIO接口宽度配置
  • MATLAB参考模型的输出格式

2. 启动延迟与有效数据窗口

permute_ipermute_o都有startup计数器逻辑:

if ( startup == ap_uint<10>(NFFT/4-1) ) {
  running = 1;  // 开始输出有效数据
}

这意味着每个内核在启动后会丢弃前252个周期的输出。系统集成时必须确保:

  • dma_src持续发送足够的数据(包括虚拟填充)
  • 下游AIE内核能够容忍初始的无效数据周期
  • 整体吞吐量计算要考虑这252周期的"预热期"

3. 多帧迭代的loop_sel行为

dma_snk支持捕获多轮迭代中的特定一轮:

if ( ll == loop_sel ) {
  buff[dd] = val;  // 只保存选定的那一轮
}

如果loop_cnt=8loop_sel=3,只有第3轮(从0计数)的数据会被写回DDR。这在调试和验证时很有用,但生产环境中通常设置loop_sel=loop_cnt-1来捕获最后一轮结果。

4. 时钟约束的连锁反应

system.cfg中定义的时钟ID=2是一个全局约定。如果修改时钟频率:

  • 必须重新运行HLS综合以确保时序收敛
  • 可能需要调整LATENCY常量以匹配新的流水线深度
  • Vivado实现策略(phys_opt_design等)可能需要相应调整

5. 与AIE Graph的版本兼容性

pfa1008_graph.h中的PLIO定义:

#ifdef AIE_SIM_ONLY
  sig_i = input_plio::create("PLIO_sig_i", plio_64_bits, "data/sig_i_aie.txt");
#else
  sig_i = input_plio::create("PLIO_sig_i", plio_64_bits);
#endif

注意仿真模式使用64位PLIO,但实际硬件连接也是64位配置。这是因为AIE-ML的PLIO在硬件上会自动适配到实际的128位PL侧接口。混淆这一点可能导致仿真通过但硬件失败。


相关模块与扩展阅读


总结

prime_factor_fft_vitis_system_integration模块是Versal异构计算的典型范例——它展示了如何将AIE的高效能计算与PL的灵活数据编排相结合,解决纯软件或纯硬件方案都难以应对的复杂问题。理解这个模块的关键在于把握数据重排序的硬件实现本质:通过四副本存储、LUT驱动寻址和乒乓缓冲的组合,将数学上复杂的索引计算转化为高效的流水线操作。

对于新加入团队的工程师,建议从修改loop_cnt和观察波形开始,逐步理解数据在系统中的流动节奏,然后再深入HLS内核的优化细节。

On this page