滤波、多速率与Farrow设计模块 (filtering_multirate_and_farrow_designs)
一句话概述
本模块是AI引擎(AIE)数字信号处理(DSP)的核心实现库,专注于实时流式滤波、多速率信号处理(抽取/插值/信道化)以及Farrow结构任意重采样。它展示了如何在Xilinx Versal AIE架构上实现从基础FIR到复杂多相滤波器组的完整设计空间探索。
为什么需要这个模块?
问题空间
在现代通信、雷达和电子战系统中,数字前端(DFE)需要处理:
- 宽带信号处理:GHz级采样率需要并行处理(SSR, Super Sample Rate)
- 灵活的信道化:将宽带信号分割到多个窄带信道(分析滤波器组)
- 任意速率转换:非整数倍抽取/插值(Farrow结构避免重算滤波器系数)
- 硬件实现效率:在AIE的SIMD向量处理器上最大化MAC利用率
解决策略
本模块采用分层抽象策略:
- 底层:AIE内核级优化(向量MAC、循环展开、双缓冲)
- 中层:ADF图拓扑(数据流连接、PLIO接口、参数化SSR)
- 顶层:系统集成(XRT控制、DMA数据搬运、HLS协同设计)
心智模型:数字信号处理的"工厂流水线"
想象一个24小时不间断运转的信号处理工厂:
1. 原料进货区(PLIO接口)
- 卡车卸货 = PL侧DMA搬运数据到AIE阵列边界
- 集装箱规格 = 64位或128位PLIO数据宽度(
plio_64_bits,plio_32_bits) - 提货单 = 数据文件(
.txt仿真激励)或实时流(硬件运行时)
2. 组装车间(AIE Graph)
- 生产线布局 = ADF
graph类定义的数据流拓扑 - 工位(Kernel) = 实际执行MAC运算的AIE tile
- 传送带(Stream) = AIE内部 cascade/stream 连接,零延迟数据传输
- 在制品缓存 = ping-pong buffer(双缓冲隐藏数据搬运延迟)
3. 质量控制(多速率处理)
- 抽样检查站 = 抽取(Decimation):每N个样品保留1个
- 插值填充站 = 插值(Interpolation):在样品间插入零,再滤波
- 信道分拣线 = 多相滤波器组:并行处理多个子带
4. 精密加工(Farrow结构)
- 数控加工中心 = 多项式插值:根据分数延迟μ动态计算滤波器输出
- 刀具补偿 = 泰勒级数展开:y(t) = x(n) + μ·Δx + μ²·Δ²x/2 + ...
- 实时补偿 = 不需要重算滤波器系数,仅需调整多项式系数
5. 成品出货(输出PLIO)
- 包装发货 = 处理后的数据流回传到PL侧DMA
- 物流跟踪 = XRT API监控图执行状态(
graph.run(),graph.end())
架构全景图
xrt::graph, xrt::kernel] CTRL[Graph Control
init/run/end] end subgraph PL["Programmable Logic (PL)"] DMA[DMA Data Movers
mm2s/s2mm] HLS[HLS Kernels
fir_hls, datamover] PLIO[PLIO Interfaces
64/128-bit streams] end subgraph AIE["AI Engine Array (AIE)"] subgraph GraphTop["Top-Level Graphs"] FarrowG[Farrow Filter Graphs
initial/opt1/opt2/final] PolyG[Polyphase FIR Graph
m16_ssr8] FIRG[FIR Chain Graph
AIE vs HLS] SSRG[SSR Two-Tone Graph
fir2] end subgraph Kernels["Kernel Library"] FIRK[FIR Kernels
sliding_mac_sym] DDC[DDC Stage Kernels
fir_89t_sym_buf] FarrowK[Farrow Polynomial
fractional delay] end subgraph Buffers["Buffer Management"] PingPong[Ping-Pong Buffers
buffer_internal] CircBuff[Circular Buffers
cyclic_add] end end Host -->|xrt::device| PL PL -->|stream| AIE AIE -->|stream| PL PL -->|DMA| Host GraphTop --> Kernels GraphTop --> Buffers Kernels --> Buffers
关键组件职责
| 层级 | 组件 | 职责 | 关键文件 |
|---|---|---|---|
| Host | XRT Controller | 设备初始化、图生命周期管理 | fir_aie_app.cpp |
| PL | DMA Movers | 数据搬运 mm2s/s2mm | datamover_class |
| PL | HLS Kernels | HLS实现的FIR对比 | fir_hls.h, fir_class |
| AIE | Top Graphs | ADF图定义、PLIO连接 | *_app.cpp (各子目录) |
| AIE | FIR Kernels | 向量MAC运算 | ddc_kernel_stage.h |
| AIE | Buffer Mgmt | 循环缓冲区、指针运算 | buffer_internal |
数据流端到端追踪
以Farrow滤波器的典型处理流程为例,展示数据如何从Host流向AIE再返回:
1. 初始化阶段(配置工厂)
// Host侧:打开设备并加载xclbin
auto dhdl = xrtDeviceOpen(0);
auto xclbin = load_xclbin(dhdl, xclbinFilename);
auto top = reinterpret_cast<const axlf*>(xclbin.data());
// 初始化ADF图
fir_graph.init(dhdl, top); // 调用xrtGraphOpen,建立Host与AIE的通信通道
发生了什么:XRT解析xclbin(包含编译后的AIE配置和PL比特流),初始化AIE阵列的互连和DMA引擎。
2. 数据注入阶段(原料进厂)
// 配置DMA数据搬运器
datamover_krnl.init(dhdl, top, iterCnt);
// 设置DMA参数:源地址、目的地址、传输长度
// 启动AIE图(开始处理)
fir_graph.run(); // xrtGraphRun,释放AIE内核的stall信号
// 启动DMA(开始数据搬运)
datamover_krnl.run(); // xrtRunStart,DMA开始从DDR读取数据推向AIE
数据流动:
DDR (Host) → DMA (PL) → PLIO (AIE边界) → Stream (AIE内部) → Kernel (处理)
关键细节:PLIO的plio_64_bits定义了每周期传输64位(4个cint16),这是AIE向量宽度(256位)的1/4,需要4个周期填满一个向量。
3. 内核处理阶段(车间加工)
在AIE内部,Farrow滤波器执行以下运算(以farrow_final为例):
// 内核级伪代码(实际为AIE intrinsic)
for each sample:
// 1. 读取输入信号和分数延迟μ
x = read_stream(sig_i); // 当前输入样本
mu = read_stream(del_i); // 分数延迟值[0,1)
// 2. 多项式插值(Farrow结构核心)
// y(n+μ) = c0(μ)*x(n) + c1(μ)*x(n-1) + c2(μ)*x(n-2) + ...
// 其中c_k(μ)是关于μ的多项式系数
v0 = load_tap0(x_history); // 加载滤波器状态
v1 = load_tap1(x_history);
// AIE向量MAC运算
acc = mul(v0, poly_coeff[0]); // c0(μ) * x(n)
acc = mac(acc, v1, poly_coeff[1]); // + c1(μ) * x(n-1)
// 3. 更新历史缓冲区(移位寄存器逻辑)
shift_register(x_history, x);
// 4. 输出结果
write_stream(sig_o, acc.to_vector());
关键优化点(从farrow_initial到farrow_final的演进):
- 初始版:直接计算多项式,每次插值需要K次乘加(K为多项式阶数)
- 优化1:预计算μ的幂次,利用AIE的向量shuffle减少重复计算
- 优化2:将分数延迟映射到查找表,用索引代替实时多项式求值
- 最终版:采用转置Farrow结构,将μ的依赖移到滤波器系数端,实现真正的并行MAC
4. 数据返回阶段(成品出货)
// 等待处理完成
datamover_krnl.waitTo_complete(); // 阻塞直到DMA完成
fir_krnl.waitTo_complete(); // 等待PL HLS内核(如有)
// 错误检查(基于ap_return寄存器)
datamover_krnl.golden_check(&errCnt);
// 清理资源
fir_graph.close(); // xrtGraphClose
datamover_krnl.close(); // xrtRunClose/xrtKernelClose
xrtDeviceClose(dhdl); // 释放设备
错误检查机制:
- AIE侧:通过仿真时的
printf或硬件的event_trace验证 - PL侧:HLS内核的
ap_return总线返回错误计数(xrtKernelReadRegister读取0x10地址) - Host侧:对比输出数据与Golden Reference(
errCnt > 1500视为失败)
关键设计决策与权衡
1. AIE vs HLS:分工的哲学
本模块同时提供AIE和HLS实现(fir_aie_app.cpp vs fir_hls_app.cpp),这不是重复劳动,而是架构层面的对比研究。
选择AIE当:
- 需要确定性延迟(硬实时)
- 向量长度匹配AIE的256位SIMD(8×cint16或16×int16)
- 算法可分解为滑动窗MAC(FIR、卷积、相关)
选择HLS当:
- 需要复杂控制流(分支、循环嵌套、递归)
- 数据位宽非标准(如18位、24位自定义定点)
- 需要与RTL IP无缝集成(如使用现有的Verilog FIR核)
本模块的决策:对于标准FIR,AIE在功耗和面积上通常优于HLS,但HLS作为黄金参考模型用于验证AIE实现的数值正确性。
2. SSR(Super Sample Rate)的复杂度 trade-off
在fir2_app.cpp(SSR Two-Tone Filter)中,使用了std::array<input_plio, NPORTS_O>创建多端口IO。
SSR的必要性:
- AIE单核峰值算力为1 MAC/周期(int16×int16)或0.5 MAC/周期(cint16×cint16)
- 对于1GSPS采样率、16位精度的信号,需要4个AIE核并行(SSR=4)才能跟上数据流
SSR带来的复杂性:
- 多相分解:输入必须按"轮转"方式分配到各个核(核0拿样本0,4,8...,核1拿1,5,9...)
- 边界同步:多核间的相位对齐需要精确的启动时序控制
- PLIO倍增:每个SSR通道需要独立的PLIO,增加布线资源消耗
设计选择:本模块的SSR实现采用静态多相分配(编译时确定轮询模式),而非动态负载均衡,这是为了确定性延迟——每个样本的处理周期数固定,无竞争条件。
3. Farrow结构的优化演进
从farrow_initial到farrow_final的四个版本,展示了算法-架构协同优化的经典过程:
Stage 0: 直接实现(数学教科书版)
# 每样本需要计算
mu = read_delay() # 分数延迟
y = 0
for k in range(K): # K阶多项式
mu_k = mu**k # 幂运算!
y += c[k] * x[n-k] * mu_k
问题:幂运算mu**k在AIE上需要多次乘法,且依赖前一次结果(串行)。
Stage 1: 霍纳法则(Horner's Method)
# 重写为嵌套形式,减少乘法次数
y = c[0]*x[n] + mu*(c[1]*x[n-1] + mu*(c[2]*x[n-2] + ...))
改进:乘法次数从O(K²)降到O(K),但仍有数据依赖(μ必须逐层传递)。
Stage 2: 转置Farrow结构(Transposed Farrow)
这是架构层面的突破:
# 将μ的依赖移到滤波器系数端
# y = Σ (c_k(μ) * x[n-k]), 其中 c_k(μ) = Σ (c[k][m] * μ^m)
现在所有乘法可并行执行!AIE的向量MAC可同时计算多个c_k(μ)*x[n-k]。
Stage 3: 查找表优化(Final)
# 对μ进行量化,预计算所有可能的c_k(μ)
mu_index = quantize(mu, 8) # 8位量化,256个条目
c_k = LUT[mu_index][k] # 查表代替实时计算
最终优化:将多项式求值转化为查表+内积,AIE可在单周期完成8个16位查表(向量LUT指令)。
关键洞察:Farrow优化的核心不是"让代码更快",而是重构算法以匹配AIE的SIMD架构——从串行幂运算到并行MAC,最终到LUT查表。
4. 内存与缓冲策略
在ddc_kernel_stage.h中,可以看到复杂的循环缓冲区管理:
struct buffer_internal {
buffer_datatype * restrict head; // 缓冲区基址
buffer_datatype * restrict ptr; // 当前读写指针
};
// 循环地址计算(cyclic_add是AIE特有指令)
w->ptr = cyclic_add(w->ptr, count, w->head, buffer_size);
设计决策分析:
为什么不用std::queue或动态分配?
- AIE内核不支持C++标准库(无malloc/new)
- 确定性延迟要求禁止动态内存(碎片、不确定性分配时间)
cyclic_add是AIE专用硬件指令,单周期完成模运算
双缓冲(Ping-Pong)机制:
在fir2_app.cpp和polyphase_fir_app.cpp中,虽然没有显式代码,但ADF运行时自动管理:
- Ping缓冲区:当前AIE核正在读取的数据
- Pong缓冲区:DMA正在写入的下一帧数据
- 零拷贝切换:通过指针交换(非数据拷贝)实现无缝流水
对齐要求:
- AIE要求缓冲区32字节对齐(256位向量宽度)
alignas(32)在ddc_kernel_stage.h中强制对齐- 未对齐访问将导致2倍性能损失(需两次128位加载拼接)
子模块划分与导航
本模块按设计复杂度递进和应用场景划分为四个子模块:
1. farrow_filter_design_evolution —— 算法架构协同优化
核心内容:展示Farrow滤波器从数学公式到硬件实现的四阶段进化。
- farrow_baseline_graph: 教科书式直接实现(高延迟、串行计算)
- farrow_optimization_stage_1_graph: 霍纳法则优化(减少乘法次数)
- farrow_optimization_stage_2_graph: 转置结构(并行MAC,匹配AIE SIMD)
- farrow_final_implementation_graph: 查找表优化(单周期向量LUT)
学习价值:理解"算法重构比代码微调更重要"的硬件设计哲学。
2. fir_aie_vs_hls_chain_and_kernel_contracts —— 实现范式对比
核心内容:同一FIR滤波器的AIE与HLS双实现,量化对比性能/功耗/面积(PPA)。
- aie_host_app: XRT控制AIE图,展示
fir_chain_class的图生命周期管理 - hls_host_app: 对比HLS内核的
fir_class控制逻辑 - hls_pl_kernel:
fir_params结构体的HLS FIR库参数化配置
学习价值:建立"AIE用于规整向量计算、HLS用于复杂控制"的选型直觉。
3. multirate_channelizer_and_ddc_primitives —— 多速率处理原语
核心内容:信道化器、DDC(数字下变频)、多相分解的底层实现。
- polyphase_fir_app: 16通道SSR8多相滤波器组,
polyphase_fir_graph的并行架构 - ddc_kernel_stage: 89抽头/199抽头对称FIR的
buffer_internal循环缓冲区管理 - taps_M16_init: 多相抽头系数的内存布局与初始化
学习价值:掌握"多相分解=并行性+降采样率"的硬件映射技巧。
4. ssr_two_tone_filter_graph —— 超采样率架构
核心内容:SSR>1时的高吞吐滤波器设计,处理多GHz采样率。
- fir2_app: SSR架构的
fir2_graph,NPORTS_O参数化的多端口IO - 数据轮询: 输入样本按"round-robin"分配到并行内核的时序控制
- 相位同步: SSR各通道间的启动对齐与反压处理
学习价值:理解"SSR=N等同于N倍并行度,但需解决数据交织"的复杂度来源。
扩展阅读与关联模块
上游依赖模块
- prime_factor_fft_pipeline_graphs:FFT与FIR的级联(信道化器=分析滤波器组+FFT)
- channelizer_ifft_and_tdm_fir_graphs:TDM(时分复用)FIR与本模块的SSR FIR对比
下游应用模块
- ddc_chain:数字下变频链(CIC+FIR级联)
- farrow_filter_streaming_io_integration:Farrow滤波器的PL系统集成(DMA流式IO)
相关教程
- normalization_v1_performance_flow:性能调试方法(解决本模块的stall问题)
- versal_integration_data_movers:DMA数据搬运器的深入解析
设计决策深度分析
决策1:ADF Graph vs HLS Dataflow —— 编程模型的选择
背景:Versal AIE支持两种编程模型:
- ADF(Adaptive Data Flow):声明式图定义,编译器自动映射到AIE阵列
- HLS C++:命令式代码,Vitis HLS综合到RTL再映射到AIE/PL
本模块的混合策略:
// ADF方式:声明连接关系(AIE部分)
class dut_graph : public graph {
farrow_graph farrow;
dut_graph() {
connect<stream>(sig_i[0].out[0], farrow.sig_i[0]); // 声明式连接
}
};
// HLS方式:命令式控制(PL部分)
void datamover_class::run() {
xrtRunStart(datamover_rhdl); // 显式命令式启动
}
权衡分析:
| 维度 | ADF Graph | HLS Dataflow |
|---|---|---|
| 抽象级别 | 高(声明拓扑) | 中(命令式代码) |
| 调度控制 | 编译器自动调度 | 开发者显式控制 |
| 适用场景 | 规则数据流(FIR、FFT) | 不规则控制(包处理、协议) |
| 调试难度 | 中等(需理解编译器映射) | 低(可单步调试C++) |
| 性能上限 | 高(编译器全局优化) | 中(受限于局部综合) |
最终选择:
- AIE计算密集型部分:ADF(利用编译器的并行调度优化)
- PL控制密集型部分:HLS(便于调试和与RTL集成)
- Host控制:XRT C++ API(标准化设备管理)
决策2:循环缓冲区 vs 行缓冲区 —— 内存架构的选择
在ddc_kernel_stage.h中,FIR实现采用了循环缓冲区(Circular Buffer)而非传统的行缓冲区(Line Buffer)。
行缓冲区方式(传统DSP):
// 每次新样本到达,所有数据移位
void fir_shift_register(int x_new) {
for (int i = N-1; i > 0; i--) {
buffer[i] = buffer[i-1]; // 数据搬运!
}
buffer[0] = x_new;
}
问题:每次样本O(N)的数据搬运,AIE的Load/Store单元成为瓶颈。
循环缓冲区方式(本模块采用):
struct buffer_internal {
v8cint16 * restrict head; // 缓冲区起始
v8cint16 * restrict ptr; // 当前样本指针(仅移动指针,不搬数据!)
};
// 新样本到达:仅更新指针,旧数据自然被覆盖
void buffer_write(buffer_internal *w, v8cint16 value) {
*((v8cint16 * restrict)(w->ptr)) = value;
w->ptr = cyclic_add(w->ptr, 1, w->head, BUFFER_SIZE); // 指针循环
}
优势:
- O(1)更新:无论FIR长度多长,新样本到达仅需1次写+1次指针更新
- AIE硬件支持:
cyclic_add是专用硬件指令,单周期完成模地址计算 - 向量友好:
v8cint16(8个复数)对齐到256位AIE向量宽度
权衡:
- 读取复杂性:读取历史样本需要计算相对于当前指针的偏移,使用
buffer_read需要处理循环回绕 - 初始化成本:缓冲区必须预填充有效数据才能开始处理("冷启动"延迟)
决策3:固定点 vs 浮点 —— 数值精度的选择
整个模块采用**定点数(Fixed-Point)**表示,关键参数在fir_hls.h和ddc_kernel_stage.h中定义:
// HLS侧定点配置
#define FIR_VALUE_WIDTH 16
typedef ap_fixed<FIR_VALUE_WIDTH, FIR_VALUE_WIDTH> Data_t; // Q16.0 或自定义
// AIE侧定点配置(隐式)
const int DDC_SHIFT = 15; // 15位小数部分,即Q1.15格式
为何不用浮点?
AIE架构虽然支持浮点,但本模块选择定点基于以下工程权衡:
| 维度 | 定点 (Fixed) | 浮点 (Float) |
|---|---|---|
| AIE硬件 | 原生支持,单周期MAC | 需特殊流水,延迟3-5周期 |
| 功耗 | 低(整数运算) | 高(对数运算、归一化) |
| 动态范围 | 需手动缩放(scale) | 自动(指数域) |
| 精度控制 | 精确位宽可控(16/18/24) | 固定32/64位,可能过度精度 |
模块中的精度管理策略:
- 输入阶段:16位ADC数据直接映射到AIE的
cint16(复数16位实部+16位虚部) - 中间累加器:使用48位累加器(
cacc48)防止MAC溢出:aie::accum<cacc48,4> acc; // 4个并行48位累加器 acc = aie::sliding_mac_sym<...>(acc, coeff, ...); - 输出阶段:右移15位(
DDC_SHIFT)将48位结果截断回16位,四舍五入由aie::sliding_mac_sym的硬件自动处理
溢出与饱和:
- 本模块假设输入信号经过AGC(自动增益控制)预处理,确保不会溢出
- 若需饱和处理,需在MAC后显式调用
saturate(),但会引入额外周期
新贡献者必读:陷阱与隐性契约
陷阱1:PLIO带宽与AIE处理速率的失配
场景:你设计了一个FIR图,AIE核每周期处理1个样本,但PLIO每4周期才传输1个向量(4个样本)。
后果:AIE核大部分时间处于stall等待状态,实际吞吐量只有理论值的25%。
检测方法:在仿真波形中观察plio_in_stall信号(高电平表示PLIO等待AIE读取)。
解决:
- 提高PLIO频率(从312.5MHz到625MHz)
- 增宽PLIO到128位(
plio_128_bits) - 或降低AIE处理速率(插入
wait()周期)
陷阱2:AIE缓冲区对齐错误
场景:在Host代码中分配缓冲区:
std::vector<int16_t> buffer(256); // 栈分配,不保证32字节对齐
xrtBOCreate(device, buffer.data(), ...);
后果:AIE DMA读取未对齐地址,触发misaligned_access异常(硬件错误,难以调试)。
必须遵守的契约:
// 正确:使用对齐分配
alignas(32) int16_t buffer[256];
// 或使用XRT的分配函数
xrtBOAlloc(device, size, XRT_BO_FLAGS_HOST_ONLY, 0);
陷阱3:图生命周期与DMA的竞态条件
错误顺序:
graph.run(); // 1. 启动AIE(开始从PLIO读取)
dma.run(); // 2. 启动DMA(数据尚未到达PLIO)
后果:AIE读取空PLIO,陷入无限stall;或读取垃圾数据。
正确时序:
dma.init(); // 1. DMA准备(配置地址、长度)
graph.init(); // 2. AIE初始化(复位、加载系数)
graph.run(); // 3. AIE就绪(等待数据)
dma.run(); // 4. DMA启动(数据流入)
graph.wait(); // 5. 等待AIE处理完成
dma.wait(); // 6. 等待DMA搬出完成
陷阱4:HLS与AIE的数值不匹配
场景:同一个FIR滤波器,AIE和HLS实现输出不同(差异>1 LSB)。
根本原因:
- 舍入模式:AIE的
sliding_mac_sym使用截断(truncate),HLS的ap_fixed默认四舍五入(round) - 溢出处理:AIE饱和(saturate)是可选模式,HLS默认饱和
- 中间精度:AIE累加器固定48位,HLS可配置任意精度
调试方法:
// 在AIE内核中插入打印(仅仿真)
printf("acc=%lld, shift=%d, out=%d\n", acc, shift, out);
// 对比HLS的ap_int内建打印
std::cout << "acc=" << acc.to_string(2) << std::endl;
解决:统一舍入策略——在AIE输出阶段手动加0.5后截断,模拟四舍五入:
// AIE中模拟四舍五入
acc = mac(acc, 0.5, 1); // 加0.5 LSB
out = acc.to_vector<cint16>(0); // 然后截断
扩展阅读与关联模块
上游依赖模块
- prime_factor_fft_pipeline_graphs:FFT与FIR的级联(信道化器=分析滤波器组+FFT)
- channelizer_ifft_and_tdm_fir_graphs:TDM(时分复用)FIR与本模块的SSR FIR对比
下游应用模块
- ddc_chain:数字下变频链(CIC+FIR级联)
- farrow_filter_streaming_io_integration:Farrow滤波器的PL系统集成(DMA流式IO)
相关教程
- normalization_v1_performance_flow:性能调试方法(解决本模块的stall问题)
- versal_integration_data_movers:DMA数据搬运器的深入解析
总结
filtering_multirate_and_farrow_designs模块是AIE DSP的百科全书。它从简单的FIR起步,经过多相分解、SSR并行化,最终到达Farrow的任意重采样——完整覆盖了通信系统数字前端的核心信号处理链。
对于新加入的工程师,建议按以下路径学习:
- 先读
fir_aie_app.cpp理解Host-AIE交互基础 - 再读
farrow_final/farrow_app.cpp理解完整信号链 - 深入
ddc_kernel_stage.h掌握AIE intrinsics优化 - 对比
fir_hls_app.cpp与AIE版本,建立实现范式直觉
本模块不仅是代码集合,更是硬件-软件协同设计的教学案例——每一个优化决策都根植于对AIE架构(SIMD向量、确定性延迟、流式内存)的深刻理解。