🏠

空间谱搜索与峰值检测模块 (Spatial Spectrum Search and Peak Finding)

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

想象你身处一个嘈杂的体育场,周围有多个喇叭在不同方向播放音乐。你的任务是:找出这些喇叭分别位于哪个方向。这就是 MUltiple SIgnal Classification (MUSIC) 算法的核心问题——波达方向估计 (Direction of Arrival, DOA)。

在 MUSIC 算法中,经过子空间分解后,我们得到一个"空间谱"——它就像一张地图,每个角度对应一个数值,表示该方向上是否存在信号源。数值越低,越可能是信号源的方向(这是 MUSIC 算法的特性,与传统频谱相反)。

这个模块 (spatial_spectrum_search_and_peak_finding) 负责完成 MUSIC 算法的最后两个关键步骤:

  1. 扫描 (Scanning):遍历整个空间谱,标记出所有可能包含信号源的候选区域
  2. 精确定位 (Finding):在这些候选区域内进行精细搜索,精确计算信号源的到达角度

可以把这想象成寻宝游戏:首先用金属探测器快速扫描大片区域,标记出"可能有宝藏"的位置(Scanner);然后在这些标记点周围仔细挖掘,确定宝藏的精确坐标(Finder)。

架构概览

graph LR subgraph InputStream["输入数据流"] A[noiseSS
噪声子空间 N×N] --> B[Pm_null
空间谱 NUM_POINTS] end subgraph ScannerStage["Scanner 阶段"] C[scanner_kernel0
处理前半段区域] --> D[scanner_kernel1
处理后半段区域] B --> C end subgraph Intermediate["中间结果"] D --> E[带标签的空间谱
Pm_null + tag] end subgraph FinderStage["Finder 阶段"] F[finder_kernel0] --> G[finder_kernel1] G --> H[省略] H --> I[finder_kernel15] E --> F end subgraph OutputResult["输出"] I --> J[精确的DOA角度索引] end style C fill:#e1f5fe style D fill:#e1f5fe style F fill:#fff3e0 style G fill:#fff3e0 style I fill:#fff3e0

核心组件

组件 文件位置 职责
dut_graph (scanner) aie/scanner/scanner_app.cpp Scanner 顶层图定义,连接 PLIO 和 kernel
scanner_graph aie/scanner/scanner_graph.h Scanner 块级图,管理 2 个 scanner kernel
Scanner aie/scanner/scanner.h/cpp 模板化的 scanner kernel,执行阈值比较和区域标记
dut_graph (finder) aie/finder/finder_app.cpp Finder 顶层图定义,连接 PLIO 和 kernel
finder_graph aie/finder/finder_graph.h Finder 块级图,管理 16 个 finder kernel
Finder aie/finder/finder.h/cpp 模板化的 finder kernel,执行局部峰值检测和插值

核心抽象与心智模型

1. 空间谱的数据组织

空间谱被组织为一个一维数组 Pm_null[NUM_POINTS],其中 NUM_POINTS = 256。每个元素是一个复数向量(aie::vector<cfloat, N>,N=8),实际上存储了 8 个连续角度点的空间谱值。

角度索引:  0-7      8-15     16-23    ...    248-255
         ┌─────┐  ┌─────┐  ┌─────┐       ┌─────┐
Pm_null: │vec0 │→│vec1 │→│vec2 │→ ... →│vec31│
         └─────┘  └─────┘  └─────┘       └─────┘
           ↑
        每个 vec 包含 8 个 cfloat

2. 区域 (Region) 的概念

为了并行处理,256 个点被划分为 NUM_REGIONS = 32 个区域,每个区域包含 ANGLE_FINDER_REGION_LEN = 8 个点。

区域 0:   角度 0-7    (由 finder_kernel[0] 处理)
区域 1:   角度 8-15   (由 finder_kernel[0] 处理)
...
区域 30:  角度 240-247 (由 finder_kernel[15] 处理)
区域 31:  角度 248-255 (由 finder_kernel[15] 处理)

3. 两阶段流水线设计

第一阶段 - Scanner:像筛子一样过滤

  • 输入:完整的空间谱 Pm_null[256]
  • 处理:将每个点的实部与阈值 DOA_MIN_THRESHOLD (0.25) 比较
  • 输出:二进制标签 tag[32],标记哪些区域可能包含信号源

第二阶段 - Finder:像显微镜一样精细

  • 输入:带标签的空间谱
  • 处理:在标记为"活跃"的区域内,检查局部最小值(因为 MUSIC 谱是倒置的)
  • 输出:精确的峰值位置索引

4. Kernel 的模板参数化

两个 kernel 都使用 C++ 模板来实现编译时配置:

  • Scanner<START, END>:指定处理的区域范围 [START, END)
  • Finder<REGION_ID_0, REGION_ID_1>:指定处理的两个区域 ID

这种设计允许编译器针对特定范围生成最优代码,同时保持代码的可重用性。

数据流详解

Scanner 阶段的数据流

sequenceDiagram participant Input as 输入缓冲区 participant SK0 as scanner_kernel[0] participant SK1 as scanner_kernel[1] participant Output as 输出缓冲区 Note over Input: noiseSS (128 bytes)
Pm_null[0..31] (1024 bytes) Input->>SK0: 读取 noiseSS (占位,未使用) Input->>SK0: 读取 Pm_null[0..15] SK0->>SK0: 比较阈值,生成 tag[0..15] SK0->>SK1: 传递 Pm_null[16..31] + tag[0..15] SK1->>SK1: 比较阈值,生成 tag[16..31] SK1->>Output: Pm_null[0..31] + tag[0..31]

关键实现细节 (scanner.cpp):

  1. 数据读取顺序:第一个 kernel 读取并丢弃 noiseSS(仅当 START == 0 时),这是为了保持数据格式的一致性
  2. 流水线优化:使用了 chess_prepare_for_pipeliningchess_flatten_loop 编译指示,指导 AIE 编译器进行循环展开和流水线优化
  3. 向量化比较:利用 AIE 的 SIMD 能力,一次比较 8 个浮点数:
    const aie::vector<float,N> compare_result = aie::min(Pm_null_float, THRESHOLD_VECTOR);
    tag[i] = !(aie::equal(compare_result, THRESHOLD_VECTOR));
    

Finder 阶段的数据流

sequenceDiagram participant Input as 输入缓冲区 participant FK0 as finder_kernel[0] participant FK1 as finder_kernel[1] participant FK15 as finder_kernel[15] participant Output as 输出缓冲区 Note over Input: Pm_null[256] + tag[32] Input->>FK0: 完整数据 FK0->>FK0: 处理区域 0,1 的峰值检测 FK0->>FK1: 传递数据 FK1->>FK1: 处理区域 2,3 的峰值检测 FK1->>FK15: 链式传递... FK15->>FK15: 处理区域 30,31 的峰值检测 FK15->>Output: 更新后的 tag (包含精确位置)

关键实现细节 (finder.cpp):

  1. 边界处理:显式跳过最左和最右的区域(i == 0 || i == NUM_REGIONS - 1),因为这些区域无法计算完整的邻域信息
  2. 局部窗口:对于每个候选点,构建一个大小为 10 的测试窗口(testbed[10]),包含前一个区域的最后一个点、当前区域的 8 个点、下一个区域的第一个点
  3. 峰值条件:MUSIC 谱的峰值是局部最小值(注意与常规频谱不同):
    if ((testbed[index] < DOA_MIN_THRESHOLD) && 
        (testbed[index-1] > testbed[index]) && 
        (testbed[index] < testbed[index+1]))
    

设计权衡与决策

1. 为什么分成 Scanner 和 Finder 两个阶段?

替代方案:可以在一个 kernel 中完成所有工作

选择的理由

  • 资源效率:Scanner 只需要简单的阈值比较,可以用较少的 tile;Finder 需要更复杂的逻辑但只处理候选区域
  • 并行度:32 个区域可以并行分配给 16 个 finder kernel(每个处理 2 个区域)
  • 流水线友好:两阶段设计允许数据在 kernel 间流动,隐藏延迟

代价

  • 增加了数据传输开销(需要传递 tag 数组)
  • 增加了代码复杂度(需要维护两个独立的图)

2. 为什么选择链式连接而非广播?

Scanner 和 Finder 的 kernel 都采用链式连接(kernel[i].out[0] -> kernel[i+1].in[0])。

替代方案:使用广播或全连接

选择的理由

  • 数据局部性:每个 kernel 只需要看到部分数据流,不需要完整复制
  • 带宽节省:避免了大规模数据广播带来的带宽压力
  • 同步简单:链式连接的同步语义更简单,易于推理

代价

  • 延迟累积:数据必须经过所有前置 kernel 才能到达目标 kernel
  • 负载不均衡:如果某个 kernel 处理速度较慢,会阻塞后续所有 kernel

3. 模板参数化的权衡

替代方案:使用运行时参数传递 START/END

选择的理由

  • 性能:编译时可以针对特定范围进行循环展开和优化
  • 类型安全:错误的范围配置在编译期就能发现
  • 零开销:没有运行时分支判断的开销

代价

  • 代码膨胀:每个不同的模板实例都会生成独立的代码
  • 灵活性降低:无法在运行时动态调整分区策略

4. 内存对齐要求

代码中使用了 alignas(32) 来对齐私有数组:

alignas(32) aie::vector<cfloat, N> Pm_null[NUM_REGIONS];

原因

  • AIE 的向量加载/存储指令对对齐有严格要求
  • 32 字节对齐确保向量操作不会跨越缓存行边界
  • 未对齐访问会导致性能下降或硬件异常

关键配置参数

来自 music_parameters.h

参数 含义
NUM_POINTS 256 空间谱的总点数(角度分辨率)
NUM_REGIONS 32 划分的区域数量
ANGLE_FINDER_REGION_LEN 8 每个区域的角度点数
NUM_SCANNER_KERNELS 2 Scanner 阶段的并行 kernel 数
NUM_FINDER_KERNELS 16 Finder 阶段的并行 kernel 数
DOA_MIN_THRESHOLD 0.25f 判定信号源存在的阈值
NOISE_SUBSPACE_THRESHOLD 0.05f 噪声子空间阈值(5%)

重要约束

  • NUM_POINTS 必须是 ANGLE_FINDER_REGION_LEN 的整数倍
  • NUM_REGIONS 必须是 NUM_SCANNER_KERNELSNUM_FINDER_KERNELS 的整数倍
  • DOA_INTERVAL_LEN (4) 必须是 COL (8) 的因子

新贡献者需要注意的陷阱

1. 空间谱的"反向"特性

陷阱:MUSIC 空间谱与传统频谱相反——数值越小表示越可能是信号源

// 这是正确的峰值检测逻辑(找局部最小值)
if ((testbed[index] < DOA_MIN_THRESHOLD) && 
    (testbed[index-1] > testbed[index]) && 
    (testbed[index] < testbed[index+1]))

如果你习惯性地寻找局部最大值,会得到完全错误的结果。

2. 边界区域的处理

Finder 显式跳过了第一个和最后一个区域:

if (i == 0 || i == NUM_REGIONS - 1) continue;

这是因为峰值检测需要左右邻居的信息。如果你的应用场景需要检测边缘角度的信号源,需要修改这一逻辑。

3. 复数数据的实部提取

空间谱数据以 cfloat(复数)形式存储,但实际比较只使用实部:

for (unsigned j = 0; j < N; ++j) {
  const cfloat tmp = Pm_null_cfloat[i][j];
  Pm_null_float[j] = tmp.real;  // 只取实部!
}

确保上游模块正确填充了实部,虚部可以被忽略。

4. Tag 数组的双重用途

tag 数组在 Scanner 阶段是二进制的(0 或 1),在 Finder 阶段被改写为精确的峰值索引:

// Scanner 输出:0 或 1
tag[i] = !(aie::equal(compare_result, THRESHOLD_VECTOR));

// Finder 输出:精确的峰值位置 n
tag_cfloat[i % N] = cfloat({float(n), 0.0f});

下游模块需要根据上下文正确解释 tag 的值。

5. Kernel 放置的硬编码

Tile 放置是硬编码的:

static constexpr unsigned COL_START = 12;
for (unsigned i = 0, col = COL_START; i < NUM_SCANNER_KERNELS; ++i, ++col) {
  adf::location<adf::kernel>(dut.scanner_kernel[i]) = adf::tile(col, ROW_0);
}

修改 NUM_SCANNER_KERNELSNUM_FINDER_KERNELS 时,必须确保有足够的列可用(不与其他 kernel 冲突)。

6. 编译指示的重要性

chess_prepare_for_pipeliningchess_flatten_loop 不是可选的装饰——它们直接影响生成的 AIE 指令序列:

  • chess_prepare_for_pipelining:启用软件流水线,重叠不同迭代的数据加载和计算
  • chess_flatten_loop:将嵌套循环展平为单循环,减少循环控制开销

删除这些指示可能导致性能下降 2-5 倍。

与其他模块的关系

graph TB subgraph MUSIC_Pipeline["MUSIC Pipeline"] A[qrd, QR分解] --> B[svd, 奇异值分解] B --> C[subspace_decomposition_kernels, 子空间分解] C --> D[spatial_spectrum_search_and_peak_finding, 本模块] D --> E[doa_estimation_output_stage, DOA输出] end subgraph External_Interface["外部接口"] F[PL DMA, mm2s/s2mm] --> A E --> G[PS Apps, Host Control] end
  • 上游:从 subspace_decomposition_kernels 接收噪声子空间和空间谱
  • 下游:将精确的 DOA 角度传递给 doa_estimation_output_stage
  • 系统集成:通过 music_algorithm_mm2s_s2mm_system_integration 与 PL 侧的 DMA 交互

延伸阅读

On this page