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实现的专用内核,完成以下关键任务:
- DMA数据搬运:从LPDDR内存高效读取/写入FFT数据
- 输入重排序:将线性顺序的时域样本转换为PFA所需的3D立方体索引顺序
- 输出重排序:将频域结果从PFA内部顺序还原为标准顺序
可以把整个系统想象成一个精密的交响乐团:AIE中的DFT-7、DFT-9、DFT-16内核是演奏家,而这个模块则是乐谱分发员和座位安排员——确保每个演奏家在正确的时间拿到正确的音符。
架构设计与数据流
系统拓扑图
时域/频域数据"] 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_idma_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_ADDRLUT驱动,产生标准频域顺序 - 同样采用四副本策略和乒乓缓冲
数据流完整追踪
让我们跟随一个FFT变换的数据旅程:
阶段1:数据注入(Host → LPDDR → dma_src)
- Host处理器将1008点复数样本写入LPDDR的指定区域
dma_src通过AXI4-Full接口发起burst读取,填满内部BRAM缓冲区#pragma HLS DATAFLOW确保加载和传输阶段流水线化执行
阶段2:输入重排序(dma_src → permute_i → AIE)
dma_src以每周期4样本(128位)的速率输出数据permute_i接收数据后,根据当前写地址将每个样本复制4份存入ping buffer的不同bank- 当写完1008个样本后,切换ping/pong标志,开始从pong buffer读取
- 读取阶段,
PERM_I_ADDRLUT提供重排序后的地址,通过多路选择器选出正确的4个样本 - 输出流向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)
- DFT-16的输出进入
permute_o,以stride=63的模式写入ping buffer - 四副本策略再次应用,确保任意重排序读取都能单周期完成
PERM_O_ADDRLUT驱动读取地址,产生标准频域顺序dma_snk捕获流式输出,支持多轮迭代中的选择性存储(通过loop_sel参数)- 最终数据通过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的重排序公式形如:
这种非线性的模运算地址模式超出了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_i和permute_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_i和permute_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=8而loop_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_pipeline_graphs:AIE侧的DFT和Transpose内核详细设计
- prime_factor_fft_hls_kernels:本模块依赖的HLS内核源码组织
- prime_factor_fft_system_integration:更高层次的系统集成视角
总结
prime_factor_fft_vitis_system_integration模块是Versal异构计算的典型范例——它展示了如何将AIE的高效能计算与PL的灵活数据编排相结合,解决纯软件或纯硬件方案都难以应对的复杂问题。理解这个模块的关键在于把握数据重排序的硬件实现本质:通过四副本存储、LUT驱动寻址和乒乓缓冲的组合,将数学上复杂的索引计算转化为高效的流水线操作。
对于新加入团队的工程师,建议从修改loop_cnt和观察波形开始,逐步理解数据在系统中的流动节奏,然后再深入HLS内核的优化细节。