🏠

Channelizer HLS Stream and DMA Kernels 模块深度解析

一句话概括

本模块是 Versal AIE-ML 多相信道化器(Polyphase Channelizer)的数据搬运与流重组层——它像一座精心设计的立交桥,将来自 DDR 的批量数据转换为 AI Engine 所需的并行流格式,并在不同带宽需求的处理阶段之间进行流的分拆与合并,确保 2 GSPS 采样率的数据在整个系统中畅通无阻。


问题空间:为什么需要这个模块?

核心挑战:速率不匹配与数据格式转换

在无线通信系统中,多相信道化器需要同时下变频数千个频分复用(FDM)信道。以本设计为例:

  • 采样率:2 GSPS(每秒 20 亿样本)
  • 信道数:4096 个
  • 每个信道带宽:488.28125 KHz
  • 输入数据类型cint16(32 位复数)
  • 输出数据类型cint32(64 位复数)

这里存在一个根本性的矛盾:DDR 存储器擅长批量传输,而 AI Engine 擅长并行流处理。想象一下:

  • DDR 就像一条宽阔但间歇性通车的高速公路(AXI4-MM 接口,突发传输效率高)
  • AI Engine 就像一组并行的城市快速路(AXI4-Stream 接口,持续流式处理)

这两者之间需要一个"交通枢纽"来完成:

  1. 数据格式转换:将 DDR 中的线性样本顺序转换为 AI Engine 所需的多相(polyphase)顺序
  2. 带宽适配:将窄而深的 DDR 访问转换为宽而浅的流式传输
  3. 流重组:在不同处理阶段(TDM FIR → IFFT)之间调整流的数量和宽度

为什么不直接用 DMA IP?

标准的 AMD DMA IP(如 axi_dma)可以完成基本的内存到流传输,但它无法解决以下问题:

  1. 数据重排:信道化器需要在样本级别进行复杂的多相分解和重排序
  2. 多流扇出/扇入:TDM FIR 输出 32 路流,而 2D IFFT 只需要 8 路;需要动态合并
  3. 位宽转换:不同处理阶段的样本位宽不同(cint16 vs cint32
  4. 时序对齐:需要精确控制数据到达时间以匹配 AI Engine 的处理节奏

因此,需要定制化的 HLS 内核来完成这些特定的数据搬运和重组任务。


心智模型:理解这个模块的四个关键抽象

抽象一:DMA 端点——"港口码头"

channelizer_dma_srcchannelizer_dma_snk 就像港口的两座码头:

  • dma_src(源码头):从 DDR "货轮" 卸载数据,重新打包成适合 AI Engine "列车" 的格式
  • dma_snk(目的码头):接收 AI Engine 处理后的数据,重新组装回 DDR 可存储的格式

它们使用 m_axi 接口连接 DDR,axis 接口连接 AI Engine,通过 s_axilite 接收主机配置。

抽象二:流合并器——"多车道汇入"

merge_4x1merge_8x4 就像高速公路的汇入匝道:

  • 将多条低速流(更多路、更窄)合并为少量高速流(更少路、更宽)
  • 同时进行数据格式的重新排列(多相顺序 ↔ 线性顺序)

抽象三:流分拆器——"多车道分流"

split_1x16 就像高速公路的分流匝道:

  • 将单条高速流分拆为多条并行流
  • 实现数据的多相分解,为 TDM FIR 的并行处理做准备

抽象四:乒乓缓冲——"双仓库调度"

所有内核内部都使用 hls::stream_of_blocks 实现生产者-消费者解耦

  • 想象两个仓库:一个装货时,另一个卸货
  • 这种"乒乓"机制允许读写并行,隐藏延迟

架构总览与数据流

flowchart LR subgraph DDR["DDR Memory"] MEM["Input/Output Buffers"] end subgraph PL["Programmable Logic (PL)"] subgraph DMA["DMA Endpoint Kernels"] SRC["channelizer_dma_src
2 streams @ cint16"] SNK["channelizer_dma_snk
4 streams @ cint32"] end subgraph SPLIT["Stream Split Kernel"] S116["split_1x16
1→16 streams"] end subgraph MERGE["Stream Merge Kernels"] M84["merge_8x4
8→4 streams"] M41["merge_4x1
4→1 stream"] end end subgraph AIE["AI Engine Array"] subgraph FIR["TDM FIR Bank"] FIRS["32 Parallel FIRs
SSR=32"] end subgraph IFFT["2D IFFT"] FFT2D["16 Tiles
SSR=8"] end end MEM -->|"m_axi
batch read"| SRC SRC -->|"axis
2 streams"| S116 S116 -->|"16 streams"| FIRS FIRS -->|"32 streams"| M84 M84 -->|"4 streams"| FFT2D FFT2D -->|"8 streams"| M41 M41 -->|"4 streams"| SNK SNK -->|"m_axi
batch write"| MEM

数据流详解

上行路径(DDR → AI Engine)

  1. channelizer_dma_src

    • 从 DDR 读取批量数据(cint16 样本)
    • 内部缓冲:TT_SAMPLE buff[NSTREAM][DEPTH*4],使用 URAM 实现
    • 输出:2 路 AXI4-Stream,每路 128 位 @ 312.5 MHz
    • 关键操作:将线性样本顺序转换为多相顺序
  2. split_1x16

    • 输入:1 路 128 位流(包含 4 个 cint16 样本)
    • 输出:16 路并行流
    • 关键操作:64 样本块的多相分解
    • II = 16:每 16 个周期处理 64 个样本
  3. TDM FIR Bank

    • 32 个并行 FIR 滤波器,每个处理 128 个信道
    • 输入:cint16,输出:cint32

下行路径(AI Engine → DDR)

  1. merge_8x4

    • 输入:8 路流(来自 TDM FIR 的 cint32 输出)
    • 输出:4 路流(供 2D IFFT 使用)
    • 关键操作:多相顺序到线性顺序的转换
    • II = 2:每 2 个周期产生 8 个输出样本
  2. 2D IFFT

    • SSR=8,16 个 AIE-ML tiles
    • 实现 4096 点 IFFT @ 2 GSPS
  3. merge_4x1

    • 输入:4 路流(来自 2D IFFT)
    • 输出:1 路流
    • 关键操作:最终的多相到线性转换
    • II = 8:每 8 个周期产生 8 个输出样本
  4. channelizer_dma_snk

    • 输入:4 路 AXI4-Stream
    • 输出:写入 DDR
    • 内部缓冲:TT_SAMPLE buff[NSTREAM][2*DEPTH],使用 URAM
    • 支持循环选择和计数(loop_sel, loop_cnt)用于调试

核心组件详解

1. DMA 源端点:channelizer_dma_src

文件位置hls/channelizer_dma_src/channelizer_dma_src.cpp

核心参数(来自头文件):

static constexpr unsigned NSTREAM = 2;        // 2 路输出流
static constexpr unsigned NCHAN = 4096;       // 4096 个信道
static constexpr unsigned NTRANSFORMS = 4;    // 4 次变换
static constexpr unsigned DEPTH = NTRANSFORMS*NCHAN/NSTREAM/4;  // 缓冲区深度
static constexpr unsigned NBITS = 128;        // 128 位数据总线

关键设计决策

决策 选择 原因
存储类型 URAM (bind_storage type=RAM_T2P impl=uram) 大容量(每 tile 288KB)、双端口,适合 ping-pong 缓冲
数组分区 array_partition variable=buff dim=1 对第一维(流索引)分区,支持并行访问多路流
依赖消除 dependence variable=buff type=intra false 告知 HLS 同一数组的不同元素无 RAW 依赖,允许流水线
流水线 II II=1 每个周期产生一个 DDR 读地址,最大化内存带宽利用率

数据重排逻辑

// 从 DDR 读取的每个 128 位字包含 4 个 cint16 样本
(val3,val2,val1,val0) = mem[mm];
// 重新分配到缓冲区的多相位置
buff[ss  ][dd  ] = val0;
buff[ss+1][dd  ] = val1;
buff[ss  ][dd+1] = val2;
buff[ss+1][dd+1] = val3;

这实现了**循环缓冲(circular buffer)**风格的样本分发,确保后续 TDM FIR 能以正确的多相顺序接收数据。


2. DMA 目的端点:channelizer_dma_snk

文件位置hls/channelizer_dma_snk/channelizer_dma_snk.cpp

核心参数

static constexpr unsigned NSTREAM = 4;        // 4 路输入流
static constexpr unsigned NFFT_1D = 64;       // 64 点 FFT 维度
static constexpr unsigned NFFT = NFFT_1D*NFFT_1D;  // 4096 点
static constexpr unsigned DEPTH = NFFT*NTRANSFORMS/NSTREAM/2;

独特功能:循环选择和计数

void capture_streams( TT_SAMPLE (&buff)[NSTREAM][2*DEPTH], 
                      TT_STREAM sig_i[NSTREAM],
                      const int& loop_sel, const int& loop_cnt )
  • loop_cnt:指定要捕获的循环次数
  • loop_sel:只保存指定循环的数据到缓冲区

这对于调试和验证非常有用:可以在长时间运行的设计中捕获特定时间段的数据,而不必存储所有中间结果。

数据打包逻辑

// 从 4 路流中读取样本,打包成 128 位字写入 DDR
TT_SAMPLE val0 = buff[ss0  ][addr0];
TT_SAMPLE val1 = buff[ss0+1][addr0];
mem[mm++] = ( val1, val0 );

注意这里的地址计算模式:通过 ss0addr0 的交替递增,实现了跨流的数据交织,恢复线性样本顺序。


3. 流合并器:merge_4x1merge_8x4

这两个内核展示了 HLS 中生产者-消费者模式的经典实现。

merge_4x1(4→1 合并)

架构

void consumer( TT_STREAM sig_i[NSTREAM_I], hls::stream_of_blocks<TT_BLOCK>& ss );
void producer( hls::stream_of_blocks<TT_BLOCK>& ss, TT_STREAM& sig_o );

关键特性

  • 使用 hls::stream_of_blocks 实现块级 FIFO
  • 存储绑定到 LUTRAM(bind_storage type=ram_s2p impl=lutram):小容量、低延迟
  • II = 8:每 8 个周期处理 8 个样本(4 个输入字 × 2 样本/字)

数据重排

// 输入是多相顺序:v00,v04,v01,v05,v02,v06,v03,v07
(v04,v00) = RL[ 0];
(v05,v01) = RL[ 1];
...
// 输出恢复线性顺序:(v01,v00), (v03,v02), (v05,v04), (v07,v06)
sig_o.write( (v01,v00) );

merge_8x4(8→4 合并)

更复杂的场景

  • 输入:8 路流
  • 输出:4 路流
  • 需要 4 个独立的 stream_of_blocks 缓冲区

存储选择:BRAM(bind_storage type=ram_t2p impl=bram

  • 比 LUTRAM 更大容量
  • 真双端口支持并发的生产者和消费者访问

时序优化

  • II = 2:每 2 个周期产生 8 个输出样本(4 路流 × 2 样本/流)
  • 有效吞吐率:625 MSamples/s(假设 312.5 MHz 时钟)

4. 流分拆器:split_1x16

文件位置hls/split_1x16/split_1x16.cpp

核心参数

static constexpr unsigned NSTREAM_I = 1;   // 1 路输入
static constexpr unsigned NSTREAM_O = 16;  // 16 路输出
static constexpr unsigned NSAMPLES = 64;   // 每块 64 样本
static constexpr unsigned DEPTH = NSAMPLES/4;  // 16 个 128 位字

操作模式

  1. Consumer:以 II=16 的速率读取 16 个 128 位字(共 64 个 cint16 样本)到块缓冲区
  2. Producer:一次性读取整个块,执行复杂的多相分解,输出到 16 路流

多相分解模式

// 输出到 16 路流的多相顺序
sig_o[ 0].write( (v48,v32,v16,v00) );  // 流 0: 样本 0,16,32,48
sig_o[ 1].write( (v49,v33,v17,v01) );  // 流 1: 样本 1,17,33,49
...
sig_o[15].write( (v63,v47,v31,v15) );  // 流 15: 样本 15,31,47,63

这实现了**循环移位(cyclic shift)**风格的样本分发,是 TDM FIR 高效处理的关键。


设计权衡与决策分析

权衡一:URAM vs BRAM vs LUTRAM

内核 存储类型 原因
channelizer_dma_src URAM 大容量需求(DEPTH×NSTREAM×4 样本),双端口支持读写并行
channelizer_dma_snk URAM 同上,且需要存储多个变换周期的数据
merge_4x1 LUTRAM 小容量(仅 4 个字),追求最低延迟和最高频率
merge_8x4 BRAM 中等容量,需要 4 个独立缓冲区,真双端口支持
split_1x16 默认(推测为 BRAM/URAM) 16 字深度,容量适中

未选择的替代方案

  • 全部使用 BRAM:会消耗过多 BRAM 资源,影响其他 PL 逻辑
  • 全部使用 URAM:LUTRAM 的小容量场景使用 URAM 会造成浪费

权衡二:DATAFLOW vs PIPELINE

所有内核都使用 #pragma HLS DATAFLOW,而非单个函数的 PIPELINE:

选择 DATAFLOW 的原因

  1. 任务级并行:明确分离生产者和消费者,两者可以真正并行执行
  2. 内存访问解耦:生产者填充缓冲区时,消费者可以同时读取另一个缓冲区
  3. 自然的乒乓实现hls::stream_of_blocks 在 DATAFLOW 区域自动实现 ping-pong

PIPELINE 的局限性

  • 适用于单函数内部的循环流水线
  • 难以表达跨函数的生产者-消费者关系

权衡三:流数量与位宽的平衡

设计约束

  • PLIO 接口固定为 128 位 @ 312.5 MHz
  • 要达到 2 GSPS,需要足够的并行度

计算

  • cint16 = 32 位 → 128 位可携带 4 个样本
  • 目标吞吐:2e9 样本/秒
  • 每路流吞吐:312.5e6 × 4 = 1.25e9 样本/秒
  • 所需流数:2e9 / 1.25e9 ≈ 1.6 → 向上取整为 2 路输入流

输出侧(cint32 = 64 位)

  • 128 位可携带 2 个样本
  • 每路流吞吐:312.5e6 × 2 = 625e6 样本/秒
  • 所需流数:2e9 / 625e6 = 3.2 → 实际使用 4 路输出流

权衡四:II(Initiation Interval)的选择

内核 II 原因
channelizer_dma_src load_buffer 1 最大化 DDR 带宽利用率
channelizer_dma_src transmit 1 持续流式输出
channelizer_dma_snk capture_streams 1 不丢失输入样本
channelizer_dma_snk read_buffer 1 最大化 DDR 写入带宽
merge_4x1 8 8 个样本的处理粒度,匹配数据依赖性
merge_8x4 2 更高的并行度需求,4 路输出流
split_1x16 16 64 样本块的批处理,摊销控制开销

关键洞察:II 的选择不是越小越好,而是要在吞吐需求资源消耗数据依赖性之间取得平衡。


新贡献者必读:陷阱与注意事项

1. 数据类型与位宽约定

容易混淆的地方

// channelizer_dma_src(输入侧)
typedef ap_uint<128>     TT_DATA;    // 4 x cint16
typedef ap_uint<32>      TT_SAMPLE;   // 1 x cint16

// channelizer_dma_snk(输出侧)
typedef ap_uint<128>     TT_DATA;    // 2 x cint32
typedef ap_uint<64>      TT_SAMPLE;   // 1 x cint32

陷阱:虽然都是 TT_DATATT_SAMPLE,但位宽含义完全不同!修改代码时必须确认当前命名空间。

2. stream_of_blocks 的生命周期

hls::write_lock<TT_BLOCK> WL(ss);   // 获取写锁
WL[0] = ...;                        // 写入数据
// WL 析构时自动提交块到流

陷阱

  • 必须在 DATAFLOW 区域内使用
  • 锁对象必须在函数作用域内创建和销毁
  • 不能跨函数边界传递锁

3. 地址计算的隐式假设

channelizer_dma_snk 中的地址计算:

int addr0 = rr;
int ss0=0;
for (int cc=0; cc < NSTREAM; cc++) {
    // ...
    ss0+=2;
    if ( ss0 == NSTREAM ) {
        addr0++;
        ss0=0;
    }
}

隐式假设NSTREAM 必须是偶数(这里是 4),且 ss0 以 2 为步长递增。

如果修改 NSTREAM 为 2 或 8

  • 代码仍然能编译
  • 但数据布局会完全错误!
  • 必须同步修改地址计算逻辑

4. LOOP_TRIPCOUNT 的作用

#pragma HLS LOOP_TRIPCOUNT min=1 max=8
for (int ll=0; ll < loop_cnt; ll++) {

重要:这只是给 HLS 工具的提示,用于综合时的性能估计:

  • 不影响实际硬件功能
  • 如果实际 loop_cnt 超出范围,硬件仍能正确工作
  • 但性能估计会不准确

5. 时钟域 crossing

所有 HLS 内核配置为 3.2ns 时钟周期(约 312.5 MHz):

clock=3.2ns

注意:这与 AI Engine 的 1.25 GHz 时钟不同。PL 和 AIE 之间的数据传输通过 PLIO 接口自动处理时钟域转换,但设计者需要确保:

  • 数据率匹配(考虑两边的位宽和频率)
  • 缓冲区深度足够吸收瞬时速率差异

6. 测试向量生成

merge_8x4channelizer_dma_src 包含 MATLAB 脚本(gen_vectors.m)用于生成测试向量。

建议:修改数据格式或重排逻辑后,务必:

  1. 更新 MATLAB 脚本生成新的黄金参考
  2. 重新运行 C-simulation 验证
  3. 检查 Co-simulation(如果启用)

与其他模块的关系

上游依赖

模块 关系 说明
prime_factor_fft_hls_kernels 兄弟模块 共享相似的 DMA 和重排模式,但用于素因子 FFT
channelizer_ifft_and_tdm_fir_graphs 下游消费者 本模块的流输出连接到 TDM FIR 和 2D IFFT

下游依赖

模块 关系 说明
channelizer_vitis_system_kernels 系统集成 Vitis 系统配置引用本模块生成的 XO 文件
polyphase_channelizer_system_integration 系统实例化 最终系统集成使用本模块的 kernel 实例

横向关联


构建与仿真

每个子目录包含独立的 Makefile 和 HLS 配置文件(hls.cfg),支持:

# C-simulation
cd hls/channelizer_dma_src && make csim

# C-synthesis
cd hls/channelizer_dma_src && make csynth

# Co-simulation(需 Vivado Simulator)
cd hls/channelizer_dma_src && make cosim

# 导出 XO 文件
cd hls/channelizer_dma_src && make export

关键配置参数(以 channelizer_dma_src 为例):

part=xcve2802-vsvh1760-2MP-e-S  # VE2802 器件
flow_target=vitis               # Vitis 流程
clock=3.2ns                     # 312.5 MHz
syn.top=channelizer_dma_src_wrapper  # 顶层函数
package.output.format=xo        # 生成 XO 文件
vivado.flow=impl                # 独立实现(OOC)

总结

channelizer_hls_stream_and_dma_kernels 模块是 Versal 信道化器设计的数据基础设施层。它的核心价值在于:

  1. 桥接异构计算:无缝连接 DDR、PL 和 AIE 三种不同的计算/存储域
  2. 优化数据布局:在正确的时机进行多相分解和重排序,最大化 AIE 处理效率
  3. 灵活的可重构性:通过 HLS 参数化设计,支持不同信道数和采样率的配置

对于新加入团队的工程师,理解这个模块的关键是把握数据如何在时间和空间两个维度上流动——时间上通过乒乓缓冲实现流水并行,空间上通过流的分拆与合并实现并行度适配。


子模块文档

On this page