空间谱搜索与峰值检测模块 (Spatial Spectrum Search and Peak Finding)
概述:这个模块解决什么问题?
想象你身处一个嘈杂的体育场,周围有多个喇叭在不同方向播放音乐。你的任务是:找出这些喇叭分别位于哪个方向。这就是 MUltiple SIgnal Classification (MUSIC) 算法的核心问题——波达方向估计 (Direction of Arrival, DOA)。
在 MUSIC 算法中,经过子空间分解后,我们得到一个"空间谱"——它就像一张地图,每个角度对应一个数值,表示该方向上是否存在信号源。数值越低,越可能是信号源的方向(这是 MUSIC 算法的特性,与传统频谱相反)。
这个模块 (spatial_spectrum_search_and_peak_finding) 负责完成 MUSIC 算法的最后两个关键步骤:
- 扫描 (Scanning):遍历整个空间谱,标记出所有可能包含信号源的候选区域
- 精确定位 (Finding):在这些候选区域内进行精细搜索,精确计算信号源的到达角度
可以把这想象成寻宝游戏:首先用金属探测器快速扫描大片区域,标记出"可能有宝藏"的位置(Scanner);然后在这些标记点周围仔细挖掘,确定宝藏的精确坐标(Finder)。
架构概览
噪声子空间 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 阶段的数据流
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):
- 数据读取顺序:第一个 kernel 读取并丢弃
noiseSS(仅当START == 0时),这是为了保持数据格式的一致性 - 流水线优化:使用了
chess_prepare_for_pipelining和chess_flatten_loop编译指示,指导 AIE 编译器进行循环展开和流水线优化 - 向量化比较:利用 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 阶段的数据流
关键实现细节 (finder.cpp):
- 边界处理:显式跳过最左和最右的区域(
i == 0 || i == NUM_REGIONS - 1),因为这些区域无法计算完整的邻域信息 - 局部窗口:对于每个候选点,构建一个大小为 10 的测试窗口(
testbed[10]),包含前一个区域的最后一个点、当前区域的 8 个点、下一个区域的第一个点 - 峰值条件: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_KERNELS和NUM_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_KERNELS 或 NUM_FINDER_KERNELS 时,必须确保有足够的列可用(不与其他 kernel 冲突)。
6. 编译指示的重要性
chess_prepare_for_pipelining 和 chess_flatten_loop 不是可选的装饰——它们直接影响生成的 AIE 指令序列:
chess_prepare_for_pipelining:启用软件流水线,重叠不同迭代的数据加载和计算chess_flatten_loop:将嵌套循环展平为单循环,减少循环控制开销
删除这些指示可能导致性能下降 2-5 倍。
与其他模块的关系
- 上游:从
subspace_decomposition_kernels接收噪声子空间和空间谱 - 下游:将精确的 DOA 角度传递给
doa_estimation_output_stage - 系统集成:通过 music_algorithm_mm2s_s2mm_system_integration 与 PL 侧的 DMA 交互