🏠

ifft4096_2d_app.cpp 深度解析

文件概述

ifft4096_2d_app.cppifft4096_2d 子模块的测试平台实现文件,负责将核心的 2D IFFT Graph 封装成一个完整的可测试单元。这个文件展示了如何在 AIE 编程模型中连接算法核心与外部世界(PLIO 接口),并精确控制硬件资源布局。


核心组件:dut_graph

类定义与职责

class dut_graph : public graph {
public:
  ifft4096_2d_graph dut;                                    // 被测设备实例
  std::array< input_plio,ifft4096_2d_graph::TP_SSR>  front_i;  // 前端输入 PLIO 数组
  std::array< input_plio,ifft4096_2d_graph::TP_SSR>  back_i;   // 后端输入 PLIO 数组
  std::array<output_plio,ifft4096_2d_graph::TP_SSR>  front_o;  // 前端输出 PLIO 数组
  std::array<output_plio,ifft4096_2d_graph::TP_SSR>  back_o;   // 后端输出 PLIO 数组

设计意图:采用组合而非继承的方式包装核心 Graph,保持核心算法的纯粹性,同时添加测试所需的 I/O 能力。

构造函数详解

构造函数执行三个关键任务:

1. PLIO 接口创建(数据注入/收集层)

for ( unsigned ff=0; ff < ifft4096_2d_graph::TP_SSR; ff++) {
  // 文件名生成:每个 SSR 通道对应独立的输入输出文件
  std::string fname_i0 = "data/front_i_" + std::to_string(ff) + ".txt";
  std::string fname_i1 = "data/back_i_" + std::to_string(ff) + ".txt";
  std::string fname_o0 = "data/front_o_" + std::to_string(ff) + ".txt";
  std::string fname_o1 = "data/back_o_" + std::to_string(ff) + ".txt";
  
  // PLIO 名称生成:用于调试和追踪
  std::string pname_i0 = "PLIO_front_in_" + std::to_string(ff);
  // ...
  
  // 创建 PLIO 实例
  front_i[ff] = input_plio::create(pname_i0, plio_64_bits, fname_i0);
  // ...
}

关键参数说明:

  • plio_64_bits:指定 64 位数据宽度,意味着每个时钟周期传输 64 位数据
  • 文件名模式:data/{front|back}_{i|o}_{channel}.txt,清晰标识数据方向和通道

2. 数据流连接(逻辑拓扑层)

connect<>( front_i[ff].out[0],  dut.front_i[ff]   );  // PL → AIE (前端输入)
connect<>( back_i[ff].out[0],  dut.back_i[ff]   );   // PL → AIE (后端输入)
connect<>( dut.front_o[ff],     front_o[ff].in[0] ); // AIE → PL (前端输出)
connect<>( dut.back_o[ff],     back_o[ff].in[0] );  // AIE → PL (后端输出)

连接语义:

  • front_i[ff].out[0]:PLIO 的输出端口(对 AIE 而言是输入)
  • dut.front_i[ff]:核心 Graph 的输入端口
  • 使用 connect<> 模板函数建立单向数据流连接

3. 硬件资源布局(物理映射层)

这是最关键的部分,通过条件编译 #ifndef __X86SIM__ 仅在非仿真模式下生效:

Tile 布局策略
// 前半部分 SSR 通道 (ff < 4)
location<kernel>(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0]) = tile(34+ff, 4);
location<kernel>(dut.ifft4096_2d.backFFTGraph[ff].FFTwinproc.m_fftKernels[0])  = tile(30+ff, 4);

// 后半部分 SSR 通道 (ff >= 4)
location<kernel>(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0]) = tile(34+ff-4, 5);
location<kernel>(dut.ifft4096_2d.backFFTGraph[ff].FFTwinproc.m_fftKernels[0])  = tile(30+ff-4, 5);

布局分析:

        列 30    31    32    33    34    35    36    37
       ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
行 5   │     │     │     │     │ f4  │ f5  │ f6  │ f7  │  ← Front FFT (后半)
       │     │     │     │     │     │     │     │     │    Twiddle Rot (后半)
       ├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
行 4   │ b0  │ b1  │ b2  │ b3  │ f0  │ f1  │ f2  │ f3  │  ← Back FFT (前半)
       │     │     │     │     │     │     │     │     │    Front FFT (前半)
       └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘         Twiddle Rot (前半)
       
       f = frontFFTGraph, b = backFFTGraph

为什么这样布局?

  1. 计算-通信分离:Front FFT 和 Back FFT 位于不同的 tile 组,避免内存访问冲突
  2. 旋转因子就近:Twiddle Rotation Kernel 与 Front FFT 同 tile 放置
    location<kernel>(dut.ifft4096_2d.m_fftTwRotKernels[ff]) = 
        location<kernel>(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0]);
    
  3. Shim 接口优化:PLIO 绑定到最近的 shim tile
    • 前半部分 (ff < 4): shim(16) for front, shim(14) for back
    • 后半部分 (ff >= 4): shim(15) for front, shim(13) for back
内存 Bank 分配策略
// 输入缓冲区:Bank 0
single_buffer(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0].in[0]);
location<buffer>(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0].in[0]) = bank(34+ff, 4, 0);

// FFT 输出缓冲区:Bank 1
single_buffer(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0].out[0]);
location<buffer>(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0].out[0]) = bank(34+ff, 4, 1);

// Twiddle 输出缓冲区:Bank 2
single_buffer(dut.ifft4096_2d.m_fftTwRotKernels[ff].out[0]);
location<buffer>(dut.ifft4096_2d.m_fftTwRotKernels[ff].out[0]) = bank(34+ff, 4, 2);

// 堆栈和参数:Bank 3
location<stack>(dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0]) = bank(34+ff, 4, 3);
location<parameter>(dut.ifft4096_2d.m_fftTwRotKernels[ff].param[0]) = bank(34+ff, 4, 3);
location<parameter>(dut.ifft4096_2d.m_fftTwRotKernels[ff].param[1]) = bank(34+ff, 4, 3);

内存布局解读:

每个 Front FFT tile 的 4 个 bank 分配如下:

Bank 用途 说明
0 输入缓冲区 接收来自 PLIO 的数据
1 FFT 输出 存储 FFT 计算结果
2 Twiddle 输出 旋转因子计算结果
3 堆栈 + 参数 运行时栈和旋转因子参数表

设计权衡:

  • 使用 single_buffer() 显式声明单缓冲,节省内存但牺牲一定的流水线弹性
  • Bank 3 共享堆栈和参数,因为参数表在初始化后只读,不会与栈操作冲突

Main 函数执行流程

int main(void) {
  aie_dut.init();    // 初始化:配置 DMA、复位状态机
  aie_dut.run(8);    // 运行 8 次迭代
  aie_dut.end();     // 清理:刷新缓冲区、释放资源
  return 0;
}

执行语义:

  • init():完成所有 kernel 的初始化,包括旋转因子表的加载
  • run(8):启动数据流图,处理 8 帧数据(每帧 \(4096 \times 4096\) 复数样本)
  • end():优雅关闭,确保所有输出数据都写入文件

关键设计决策分析

1. 为什么使用 std::array 而不是原始数组?

std::array<input_plio, TP_SSR> front_i;  // 类型安全,大小在编译期确定
  • 类型安全:防止缓冲区溢出
  • RAII 语义:自动管理 PLIO 对象生命周期
  • 编译期大小:与模板参数 TP_SSR 保持一致

2. 路径深度的权衡

代码中出现了深层的路径访问:

dut.ifft4096_2d.frontFFTGraph[ff].FFTwinproc.m_fftKernels[0].in[0]

这暴露了 Vitis Libraries 的内部结构,带来了紧耦合。但这是必要的,因为:

  • AIE 编译器需要明确的层次路径来定位资源
  • 库的设计允许这种细粒度控制
  • 替代方案(抽象接口)会增加运行时开销

3. 条件编译的边界选择

#ifndef __X86SIM__
  // 硬件约束
#endif

为什么不把所有约束都放在这里?

  • location<kernel>(m_fftTwRotKernels[ff]) 放在外面是因为它在 characterize 版本中也存在
  • 这表明 Twiddle Rotation Kernel 的位置关联是一个功能需求(必须与前级同 tile),而其他约束是性能优化(bank 分配、shim 绑定)

常见陷阱与调试建议

1. Tile 坐标越界

如果修改 TP_SSR 或 tile 坐标公式,可能导致:

Error: Tile coordinate (38, 4) is out of bounds for device xcvc1902

验证方法: 查阅目标器件的数据手册,确认 tile 阵列尺寸。

2. Bank 冲突

多个缓冲区分配到同一个 bank:

location<buffer>(...) = bank(34, 4, 0);
location<buffer>(...) = bank(34, 4, 0);  // 冲突!

症状: 编译错误或运行时内存访问异常

3. Shim 接口限制

每个 shim tile 有有限的 PLIO 接口数量。过度集中会导致:

Error: Shim tile 16 has too many PLIO connections

当前设计的分布: shim(13), shim(14), shim(15), shim(16) —— 分散负载

4. 文件路径问题

std::string fname_i0 = "data/front_i_" + std::to_string(ff) + ".txt";

注意: 相对路径是相对于工作目录,不是相对于可执行文件位置。确保在正确的目录下运行仿真。


与其他文件的对比

本文件(完整版本)与 ifft4096_2d_characterize/ifft4096_2d_app.cpp 的主要区别:

特性 完整版本 Characterize 版本
TP_SSR 8 1
Tile 布局约束 完整(bank、stack、shim) 仅 kernel 关联
用途 硬件部署 算法验证
复杂度

这种分离允许团队并行工作:算法工程师专注于 characterize 版本验证正确性,系统工程师在完整版本上优化硬件布局。

On this page