🏠

Channelizer Graph Application 技术深度解析

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

想象你正在处理一个宽频带信号——就像一条包含多个车道的超级高速公路,每个车道承载着不同的通信信道。传统的做法是用一组并行的滤波器来分离这些信道,但当信道数量达到数百甚至数千时,这种方法在硬件资源上变得不可行。

Channelizer(信道化器) 是一种高效的数字信号处理架构,它使用 多相滤波器组(Polyphase Filter Bank) 配合 IFFT(逆快速傅里叶变换) 来实现大规模并行信道分离。具体来说,这个模块实现了一个 32通道的信道化器,核心参数包括:

  • 4096点 IFFT(提供频率分辨率)
  • 32个并行子通道(TP_SSR = 32,Super Sample Rate)
  • TDM(时分复用)FIR滤波器组进行预处理

为什么不用简单的方案?因为 Versal AI Engine 的片上内存和计算资源有限, naive 的实现会导致路由拥塞、内存溢出和时序违例。这个模块的设计精髓在于:通过精心的资源映射和布局约束,将计算密集型的滤波操作与数据重排逻辑高效地映射到 AIE 阵列上


心智模型:如何理解这个架构?

把 channelizer 想象成一个 "信号分拣工厂"

┌─────────────────────────────────────────────────────────────┐
│                    输入宽频带信号流                          │
└───────────────────────┬─────────────────────────────────────┘
                        ▼
┌─────────────────────────────────────────────────────────────┐
│  Stage 1: TDM FIR Bank (分拣预处理)                          │
│  • 32个并行FIR核,每个处理一路子带                            │
│  • 时分复用技术让单个核处理多个相位                            │
│  • 输出经过抽取的多相分量                                     │
└───────────────────────┬─────────────────────────────────────┘
                        ▼
┌─────────────────────────────────────────────────────────────┐
│  Stage 2: 2D-IFFT (频域分析)                                 │
│  • 前向FFT:将时域样本转换到频域                              │
│  • 旋转/重排:调整数据顺序以匹配信道映射                       │
│  • 后向FFT:完成最终的信道分离                                │
└───────────────────────┬─────────────────────────────────────┘
                        ▼
┌─────────────────────────────────────────────────────────────┐
│                    32路独立信道输出                           │
└─────────────────────────────────────────────────────────────┘

关键抽象概念:

  1. firbank_graph —— 多相滤波器组图

    • 封装了 tdmfir(TDM FIR)内核数组
    • 每个内核对应一个子带的抽取滤波
    • 通过 TP_SSR 模板参数控制并行度
  2. ifft4096_2d_graph —— 二维IFFT处理图

    • 采用 2D分解策略\(4096 = 64 \times 64\)
    • 先进行64点FFT("front"阶段),再进行64点FFT("back"阶段)
    • 中间包含 twiddle factor 旋转和数据重排
  3. dut_graph —— 顶层Device Under Test图

    • 组合上述两个子图
    • 定义PLIO(Programmable Logic IO)接口连接外部DMA
    • 施加详细的 location constraints(位置约束) 指导AIE编译器进行物理布局

架构与数据流

组件关系图

graph TB subgraph "Top Level: dut_graph" PLI[PL Input Interfaces
sig_i, front_i, back_i] PLO[PL Output Interfaces
sig_o, front_o, back_o] subgraph "channelizer_graph" FB[firbank_graph
32 TDM FIR kernels] FFT[ifft4096_2d_graph
2D-FFT pipeline] end end PLI -->|data/filterbank_i_*.txt| FB FB -->|filtered subbands| PLO PLI -->|data/fft_front_i_*.txt| FFT FFT -->|channelized output| PLO style FB fill:#e1f5fe style FFT fill:#fff3e0

详细数据流追踪

路径1:滤波器组处理(Filterbank Path)

// channelizer_graph.h 中的连接
for (unsigned ff=0; ff < firbank_graph::TP_SSR; ff++) {
    connect<>( sig_i[ff],       firbank.sig_i[ff] );      // 输入 → FIR
    connect<>( firbank.sig_o[ff], sig_o[ff] );            // FIR → 输出
}

数据流向:

  1. PL端DMA 通过 input_plio 写入 sig_i[ff] 端口
  2. TDM FIR内核 (dut.firbank.tdmfir.m_firKernels[ii]) 接收数据
  3. 内核执行 多相抽取滤波,系数来自 channelizer_init_taps.h 中的 TAPS_INIT_0
  4. 结果通过 sig_o[ff]output_plio 返回PL端

路径2:IFFT处理(IFFT Path)

// channelizer_graph.h 中的连接
for (unsigned ff=0; ff < ifft4096_2d_graph::TP_SSR; ff++) {
    connect<>( front_i[ff],             ifft4096_2d.front_i[ff] );
    connect<>( ifft4096_2d.front_o[ff], front_o[ff] );
    connect<>( back_i[ff],              ifft4096_2d.back_i[ff] );
    connect<>( ifft4096_2d.back_o[ff],  back_o[ff] );
}

数据流向:

  1. Front输入 (front_i[ff]) → Front FFT内核Twiddle旋转内核
  2. 中间数据暂存于tile本地内存
  3. Back输入 (back_i[ff]) → Back FFT内核Back输出 (back_o[ff])

注意:front_i/front_oback_i/back_o分开的PLIO接口,允许灵活的数据流控制(例如插入外部重排逻辑)。


核心组件深度解析

1. dut_graph —— 顶层测试图

文件: channelizer_app.cpp

这是整个设计的入口点,继承自 ADF(Adaptive DataFlow)框架的 graph 基类。

class dut_graph : public graph {
public:
  channelizer_graph dut;                                    // 被测设备
  std::array< input_plio,firbank_graph::TP_SSR>      sig_i; // 滤波器输入
  std::array<output_plio,firbank_graph::TP_SSR>      sig_o; // 滤波器输出
  std::array< input_plio,ifft4096_2d_graph::TP_SSR>  front_i;// IFFT前级输入
  std::array< input_plio,ifft4096_2d_graph::TP_SSR>   back_i;// IFFT后级输入
  std::array<output_plio,ifft4096_2d_graph::TP_SSR>  front_o;// IFFT前级输出
  std::array<output_plio,ifft4096_2d_graph::TP_SSR>   back_o;// IFFT后级输出

构造函数的关键设计决策

决策1:手动布局约束(Manual Placement Constraints)

代码中大量的 location<> 调用不是装饰性的——它们是 物理布局指令,告诉AIE编译器将特定内核放在哪个tile的什么位置。

// FIR内核布局示例
location<kernel>(dut.firbank.tdmfir.m_firKernels[ii]) = tile(start_fb+FBX[ii], FBY[ii]);
location<stack>(dut.firbank.tdmfir.m_firKernels[ii])  = bank(start_fb+FBX[ii], FBY[ii], 3);
location<buffer>(dut.firbank.tdmfir.m_firKernels[ii].in[0]) = bank(start_fb+FBX[ii], FBY[ii], 0);

这里的 FBXFBY 数组定义了一个 交错式布局模式

Col:    0    1   2    3   4   5  6  7
      ------------------------------
Row:3 | 12  28  14  30  13  29 15 31
    2 |  8  24  10  26   9  25 11 27
    1 |  4  20  06  22   5  21  7 23
    0 |  0  16  02  18   1  17  3 19
      ------------------------------

为什么这样布局? 注释中明确说明:

"Goal is to reduce routing congestion by preventing routing of all split/merge units to all columns"

如果不加约束,编译器可能将所有32个FIR内核放在相邻的tile,导致:

  • 水平方向的路由资源耗尽
  • 垂直方向的stream连接过长
  • 时序难以收敛

这种 checkerboard-like(棋盘式) 分布将计算负载分散到8列×4行的区域,平衡了片上网络(NoC)的流量。

决策2:单缓冲 vs 双缓冲策略

single_buffer(dut.firbank.tdmfir.m_firKernels[ii].in[0]);

对于输入缓冲区使用 single_buffer,意味着:

  • 生产者写入和消费者读取必须严格同步
  • 节省50%的内存占用(相比ping-pong双缓冲)
  • 适用于确定性数据流(如本设计中的固定速率采样)

但代价是 没有容错余量——如果消费者稍慢,就会覆盖数据。

决策3:条件编译区分仿真与硬件

#ifdef AIE_SIM_ONLY
  sig_i[ii] = input_plio::create("PLIO_i_"+std::to_string(ii), plio_64_bits, file_i0);
#else
  sig_i[ii] = input_plio::create("PLIO_i_"+std::to_string(ii), plio_64_bits);
#endif
  • 仿真模式 (AIE_SIM_ONLY):PLIO从文本文件读取激励数据
  • 硬件模式:PLIO连接到实际的AXI-Stream接口,由外部HLS内核驱动

这种设计允许同一套C++代码用于:

  1. 纯软件仿真验证算法正确性
  2. 硬件协同仿真(HW co-sim)
  3. 实际板卡部署

2. channelizer_graph —— 功能子图

文件: channelizer_graph.h

这是一个 可复用的组件图,只关注功能连接,不涉及具体布局。

class channelizer_graph : public graph {
public:
  std::array<port<input>, firbank_graph::TP_SSR>  sig_i;
  std::array<port<output>, firbank_graph::TP_SSR> sig_o;
  // ... 类似声明 for IFFT ports

  firbank_graph firbank;           // 实例化滤波器组
  ifft4096_2d_graph ifft4096_2d;   // 实例化IFFT

  channelizer_graph(void) : firbank{TAPS_INIT_0} {  // 传递抽头系数
    // 连接逻辑...
  }
};

设计模式:组合优于继承

channelizer_graph 不直接实现FIR或FFT逻辑,而是 组合 更细粒度的子图。这体现了ADF框架的核心哲学:

  • 模块化:每个子图可以独立测试和优化
  • 层次化:复杂系统由简单组件递归构建
  • 关注点分离:功能定义(channelizer_graph)与物理实现(dut_graph中的约束)解耦

依赖分析与模块交互

向上依赖(What this module calls)

依赖项 类型 用途
adf.h 系统头文件 ADF框架核心API(graph, kernel, port等)
firbank_graph.h 项目头文件 TDM FIR滤波器组实现
ifft4096_2d_graph.h 项目头文件 2D-FFT流水线实现
channelizer_init_taps.h 项目头文件 滤波器抽头系数初始化数据

向下依赖(What calls this module)

根据模块树,channelizer_graph_application 位于:

AIE_ML_Design_Graphs
└── channelizer_ifft_and_tdm_fir_graphs
    └── channelizer_graph_application (current)

它是该分支的 叶子节点,没有直接的子模块。但从系统设计角度,它与以下模块紧密相关:

  1. channelizer_hls_stream_and_dma_kernels —— PL端的DMA和流处理内核
  2. channelizer_vitis_system_kernels —— Vitis系统集成层

数据契约(Data Contracts)

PLIO接口规范

  • 位宽:plio_64_bits(64位每时钟周期)
  • 协议:AXI4-Stream(由ADF框架封装)
  • 数据格式:复数样本(通常cint16或cfloat,具体取决于下游配置)

文件命名约定(仿真模式):

"data/filterbank_i_" + std::to_string(ii) + ".txt"   // 滤波器输入
"data/filterbank_o_" + std::to_string(ii) + ".txt"   // 滤波器输出
"data/fft_front_i_" + std::to_string(ff) + ".txt"    // IFFT前级输入
"data/fft_back_i_" + std::to_string(ff) + ".txt"     // IFFT后级输入

设计权衡与决策分析

权衡1:SSR并行度选择

选择TP_SSR = 32

考虑因素

  • AIE阵列尺寸:Versal AI Core器件的典型AIE阵列是几十×几十的tile网格
  • 内存带宽:每个FIR内核需要访问系数表和循环缓冲区
  • 路由可行性:过高的SSR会导致tile间stream连接过于密集

替代方案

  • SSR=16:减少50%的计算资源,但需要两倍的时钟周期处理相同吞吐量
  • SSR=64:理论上更高吞吐,但超出典型器件的资源容量

权衡2:2D-FFT分解策略

选择\(4096 = 64 \times 64\) 的2D分解

优势

  • 将大点数FFT分解为两次小点数FFT,降低蝴蝶网络的复杂度
  • 中间 transpose 操作可以利用AIE的本地内存层次结构

代价

  • 需要在两次pass之间存储完整的64×64矩阵
  • 引入了额外的twiddle factor乘法(旋转因子计算)

对比方案

  • 直接4096点FFT:需要更大的radix单元,不适合AIE的向量宽度
  • \(128 \times 32\) 分解:不同的延迟/面积权衡

权衡3:显式布局约束 vs 自动布局

选择:在 dut_graph 中手动指定所有kernel/buffer/stack的位置

优势

  • 可预测的性能和布线结果
  • 避免编译器的次优决策(特别是对于规则的数据流图)

风险

  • 代码与特定器件的AIE阵列尺寸耦合(如VC1902 vs VC2802)
  • 修改设计时需要重新计算所有坐标

缓解措施

  • 使用相对坐标(start_fb, start_plio 基准点)
  • 通过 FBX/FBY 查找表抽象具体位置计算

新贡献者注意事项

常见陷阱

  1. 忽略 __X86SIM__ 宏的作用

    #ifndef __X86SIM__
        // 这些约束只在非x86仿真时生效
        location<kernel>(...)
    #endif
    

    x86仿真器不支持物理位置约束,因此这部分代码被条件排除。如果你在仿真中看到奇怪的内核位置,检查是否误用了硬件专用API。

  2. 修改 FBX/FBY 数组而不更新注释中的示意图 代码顶部的ASCII艺术图是布局的文档。如果更改了映射逻辑,务必同步更新图表,否则后续维护者会困惑。

  3. 混淆 frontback IFFT端口

    • front_i/front_o:第一维FFT(行变换)
    • back_i/back_o:第二维FFT(列变换) 数据必须先经过front再经过back,顺序不能颠倒。
  4. 忘记 single_buffer 的同步要求 如果你修改了数据生产/消费速率(例如添加异步FIFO),single_buffer 可能导致数据损坏。此时需要改为默认的双缓冲或手动管理同步。

调试技巧

查看最终布局: 编译后检查生成的 aiesimulator_output/ 目录中的 .xsa 或可视化报告,确认kernel是否按预期放置。

验证数据流: 使用 aie::print 或 ADF的 event_trace 功能捕获运行时数据流,确保样本按预期通过filterbank和IFFT。

性能瓶颈定位: 如果吞吐量不达标,检查:

  • PLIO的 plio_64_bits 是否足够(可能需要升级到128位)
  • FIR内核的抽头数是否过多导致计算延迟
  • IFFT的twiddle factor访问是否成为内存瓶颈

扩展点

如果你想修改这个设计:

  1. 改变信道数量:修改 TP_SSR 模板参数,并相应扩展 FBX/FBY 数组
  2. 替换滤波器系数:编辑 channelizer_init_taps.h 中的 TAPS_INIT_0
  3. 调整IFFT点数:修改 ifft4096_2d_graph 的定义,注意2D分解因子的选择
  4. 添加新的处理阶段:在 channelizer_graph 中实例化新的子图,并在 dut_graph 中添加对应的PLIO和约束

参考链接

On this page