ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CMSIS-DSP源码审计指南:从宏定义到工业级调优

CMSIS-DSP源码审计指南:从宏定义到工业级调优 写这份评测之前我刚用新版 CMSIS-DSP 在一颗无浮点内核的 Cortex-M0 上调试完音频预处理一个 128 点实数 FFT 跑出来的耗时比预期慢了将近 40%最后发现不是库本身的问题而是工程里缺少ARM_MATH_CM0定义整个库走了很不合适的通用路径。类似这样的坑在工业固件评审时经常出现而且它们大多藏在源码视角的盲区里。ARM 的 CMSIS-DSP 作为嵌入式信号处理领域使用最广的开源库大家平时都把它当黑盒来调用但真要落在产品上宏定义、缓冲区对齐、状态变量、定点精度、编译优化每一样都会直接决定性能与可靠性。这篇内容我会从源码审计的角度把 CMSIS-DSP 的架构拆开来看再结合工业固件实际落地场景讲清楚哪些细节必须提前盯死。这次不只写手册里的接口说明。适合阅读的读者包括正在做电机控制、音频处理、振动检测、传感器融合和工业仪表算法的固件工程师尤其是准备把算法从 PC 仿真移植到 Cortex-M 平台的团队。读完你应该能回答几个问题CMSIS-DSP 源码目录是怎么组织的不同芯片指令集适配宏怎么影响执行路径FFT、FIR、矩阵运算这些高频函数内部究竟怎么工作以及到了产线固件里怎样验证和调优才不会被库的默认行为坑到。1. 评测背景与源码范围1.1 为什么这次选择做源码级审计很多工程师习惯直接把 CMSIS-DSP 当作标准库来引用编译过了就跑但在工业现场遇到性能不符合预期或偶发 HardFault 时这样的使用方式很难定位问题。我自己就经历过一次 q15 定点 FIR 滤波器在输入稍大时持续削波查了半天不知道是算法设计问题还是库函数实现问题。后来花一下午读源码发现问题其实出在中间级没有做缩放而库的饱和操作只是兜底削掉的动态范围根本不会告诉你。做源码审计核心是要看清三件事。第一库函数在目标芯片上到底走了哪条实现路径是纯 C 还是编译器内建指令还是 DSP 汇编优化块第二每个函数需要多少额外内存和状态空间尤其在使用定点格式时误差如何累积第三是否存在可以裁剪或定制的地方比如 FFT 旋转因子表过大、某个模块不需要能不能从构建里去掉。这些信息在头文件注释里有一部分但真正准确的答案需要钻进实现里验证。CMSIS-DSP 本身是开源项目License 宽松允许在产品里使用和修改。正因如此把它当成自己代码的一部分来审计是可行的也是一名嵌入式算法工程师应该有的基本习惯。源码就放在那里与其依赖论坛上的二手经验不如直接看分支判断。1.2 评测环境、版本与测试方法这次评测我以当时拉到的 CMSIS-DSP 1.15.x 主线版本为主也对照了 5.9 之后几个版本的差异。测试平台选择了两块有代表性的工业硬件一块是 Cortex-M4F主频 168MHz开启硬件浮点单元另一块是 Cortex-M0主频 64MHz无 FPU、无 DSP 扩展指令。这样做的原因是 CMSIS-DSP 在不同核心上的实现差异非常大只有把“有硬件加速”和“纯软件实现”放到一起对比才能验证宏定义对函数耗时的影响。编译器我分别用了 Arm Compiler 6 和 arm-none-eabi-gcc 12.3工程统一在-O2和-Ofast两个优化档下测试。测量方式使用 DWT 周期计数器在函数调用前后读取DWT-CYCCNT多次采样取最小值以避免中断和缓存干扰。参考数据用 Python NumPy 生成固件端计算结果通过串口发回 PC再做最大误差和 SNR 对比。整条测试链路很直接却可以保证每个结论都能复现。2. CMSIS-DSP 架构全景拆解2.1 源码目录与模块划分第一次打开 CMSIS-DSP 源码的人往往会被一堆文件夹吓到实际上结构相当清晰。核心源码都在Source目录下每个子目录对应一类数学运算比如BasicMathFunctions放加减乘除TransformFunctions放 FFT 和 DCTFilteringFunctions放 FIR/IIRMatrixFunctions放矩阵计算CommonTables放旋转因子表和各类查表数据。CMSIS-DSP/Source/ ├── BasicMathFunctions/ ├── CommonTables/ ├── ComplexMathFunctions/ ├── ControllerFunctions/ ├── FastMathFunctions/ ├── FilteringFunctions/ ├── InterpolationFunctions/ ├── MatrixFunctions/ ├── StatisticsFunctions/ ├── SupportFunctions/ ├── TransformFunctions/ ├── QuaternionMathFunctions/ ├── WindowFunctions/ ├── DistanceFunctions/ └── ...每个功能目录内部通常按数据类型拆成独立文件。比如BasicMathFunctions里会有arm_add_f32.c、arm_add_q15.c、arm_add_q31.c。头文件统一通过arm_math.h引入使用时只需要包含这一个头文件不需要逐个模块去 include。这种设计对应用层十分友好但副作用是整个头文件很大条件编译也多审计时要有耐心。CommonTables是一个容易被忽视但其实很关键的目录。FFT 用到的旋转因子、某些快速函数的查表数据都放在这里而且多数是const数组占用 Flash。工业固件如果 Flash 紧张需要重点关注这些表的大小并考虑是否使用新版本提供的裁剪宏来减少冗余。2.2 接口分层与数据类型的背后逻辑CMSIS-DSP 的接口设计有一个明显特征几乎所有函数都遵循“指针加长度”的裸接口风格而不是面向对象的句柄式封装。这样做的好处是调用开销小、适合底层移植坏处是需要使用者自己管理好缓冲区生命周期稍不留神就会越界。数据类型方面库统一使用float32_t、q31_t、q15_t、q7_t。q系列是定点数其中q15_t通常表示[-1, 1)范围的 16 位有符号数q31_t是 32 位定点数。源码里大量函数以这些类型为后缀形成三套或四套实现。为什么不能只保留浮点版本因为不是所有 Cortex-M 芯片都有 FPU在无 FPU 的 M0/M0 上浮点运算要靠编译器模拟速度非常慢工业上很多场景仍然必须使用定点。命名规则同样很有规律前缀是arm_中间是功能名称后缀是数据类型。例如arm_add_f32、arm_mat_mult_q31、arm_biquad_cascade_df1_f32。看到函数名基本就能猜到它的入参和适用场景。实际做源码审计时依照命名规律快速定位函数再在头文件里搜索#if defined就能判断当前编译条件下的实现路径。2.3 指令集适配宏一个宏决定性能命运CMSIS-DSP 的性能优劣很多情况下并不是由库代码本身决定的而是由工程里是否定义了正确的指令集适配宏决定。在源码里到处都能看到这类条件编译#if defined(ARM_MATH_CM4) || defined(ARM_MATH_CM7) /* 更快的内核优化路径 */ #else /* 通用 C 实现 */ #endif不同宏对应不同芯片指令集使用时应与实际核心严格匹配。下面这张表整理了我常用的几个宏和目标芯片关系宏定义目标核心主要影响ARM_MATH_CM0Cortex-M0/M0禁用 DSP 和 FPU走基础 C 实现ARM_MATH_CM3Cortex-M3部分使用乘加优化ARM_MATH_CM4Cortex-M4/M7/M33启用 FPU 与饱和算术、部分 SIMDARM_MATH_CM7Cortex-M7使用更多 M7 双发射相关优化ARM_MATH_MVEICortex-M55/M85 整型启用 Helium 整型向量指令ARM_MATH_MVEFCortex-M55/M85 浮点启用 Helium 浮点向量指令ARM_MATH_NEONCortex-A 系列启用 NEON 向量指令这里特别想提醒一点如果只定义了ARM_MATH_CM4但编译器没有开启硬件浮点编译选项很多浮点函数依然可能退化为软件浮点路径。所以宏定义、编译选项、芯片型号三者必须保持一致。源码审计时可以打开预处理输出查看系统头文件里的__FPU_USED是否被正确置位这是最直接的验证方式。3. 关键模块源码审计记录3.1 BasicMathFunctions精度分级与饱和陷阱基础数学函数看起来没什么好写的加减法和乘法而已。但源码审计下来发现定点版本的每一个操作都藏着精度和溢出问题。以arm_add_q15为例内部实现不是简单地把两个数相加而是通过__SSAT内建指令把结果饱和到 16 位范围*pDst __SSAT((q15_t)((q15_t) *pSrcA (q15_t) *pSrcB), 16);这样做的好处是运算结果永远不会像普通 C 语言加法那样回绕反转。信号处理时最怕发生“大数吃小数”或溢出后变成反向大数饱和操作至少能把异常限制在边界。然而饱和也意味着失真一旦持续削波波形中会出现平坦的截断段谐波数量剧增。arm_mult_q15的实现更典型它把两个 16 位定点数相乘先提升为 32 位右移 15 位后再做饱和*pDst (q15_t) __SSAT(((q31_t) *pSrcA * *pSrcB) 15, 16);从数学上看两个 Q15 数相乘结果仍是 Q15但中间计算会超过 16 位因此必须保留中间位宽。源码审计建议要关注的是这类函数不适合直接级联。假设一个 IIR 滤波器有多个 biquad 级每一级调用arm_biquad_cascade_df1_q15都会做饱和连续几级之后噪声会显著增加。工业做法是提前做增益规划在关键节点用arm_scale_q15或右移操作把数据拉回安全范围不依赖函数末尾的饱和兜底。3.2 TransformFunctionsFFT 的查表与蝶形运算FFT 模块是很多人使用 CMSIS-DSP 的最初动力。源码里arm_cfft_f32是比较新的混合基实现内部会根据fftLen和ifftFlag、doBitReverse参数来决定是否执行位反转以及选择哪种蝶形分解方式。值得注意的一点是旋转因子不会在函数运行时现场计算而是从CommonTables的twiddleCoef_f32等常量表里直接读取。这些常量表是预先算好并定义为const float32_t的静态数组放在 Flash 里而不是每次计算。这样做的好处是每次蝶形运算只需要查一次表省去了大量三角函数调用反正旋转因子在程序运行过程中不会变化。坏处是表的数据量比较大且把所有支持的 FFT 长度都覆盖了。新版本提供了裁剪宏可以只保留实际用到的长度表工业固件 Flash 紧张时建议仔细研究一下。位反转逻辑也值得一看。arm_bitreversal_f32会根据doBitReverse决定是否执行如果频谱数据已经在频域重新排序可以跳过这一步节省时间。但常规时域到频域变换不能跳过位反转否则结果顺序完全不对。源码审计时我曾试着把doBitReverse强制设为 0 看输出结果频域峰值的位置全部乱掉这让我印象极其深刻。对于实数信号处理官方推荐使用arm_rfft_fast_f32它本质上利用实数频谱的共轭对称性用一半长度的复数 FFT 近似完成实数 FFT。实测同点数下比直接调用arm_cfft_f32快不少这在长期运行的工业采集系统里非常值得采用。不过要注意arm_rfft_fast_f32的输入输出缓冲区和 FFT 实例结构体的初始化方式跟普通 CFFT 不太一样使用前需要仔细看头文件注释。3.3 MatrixFunctions行主序、对齐与定点细节矩阵运算在电机控制、惯性导航和卡尔曼滤波里经常出现。CMSIS-DSP 的矩阵默认是行主序存储arm_matrix_instance_f32结构体里有numRows、numCols和指向数据区的pData。这里的pData必须是连续排布的一维数组传参时常见错误是拿二维数组名直接强转结果因编译器内存布局差异导致行列错乱。源码审计arm_mat_mult_f32时我发现内层循环不是教科书式最简单的 i-j-k 三层循环而是对累加顺序做了处理以利于编译器流水线和缓存复用。在支持 MVE 或 NEON 的内核上矩阵乘法还会走向量化路径此时缓冲区如果只做到 4 字节对齐可能无法满足 SIMD 指令的 16 字节对齐要求。工业固件建议统一用__ALIGNED(16)声明矩阵缓冲区这是最省心的方式。定点矩阵乘法比浮点版本复杂得多。Q31 矩阵乘法中每个元素相乘后需要左移或者右移来保持 Q 格式常见做法是乘完右移 31 位再累计但这样很容易丢掉低位的精度。源码中的实现已经做了中间 64 位累加实际使用时还要注意矩阵元素绝对值和最终结果是否会溢出。建议在算法设计阶段就用 Python 或 MATLAB 按定点规则做一次仿真把每一步的最大理论值算出来再决定缩放因子。3.4 FilteringFunctions状态变量不能动的秘密数字滤波器是 CMSIS-DSP 里调用频率最高的模块之一。arm_fir_f32的使用方式看起来很简单初始化实例传入系数和状态缓冲然后不断调用处理块。可是很多人没意识到状态缓冲区pState的大小必须等于numTaps blockSize - 1并且每次调用后内容都会更新。源码审计时我特别留意了arm_fir_f32对状态缓冲区的读写逻辑。它从pState读取历史输入和滤波器系数做累加然后把新 block 中的最后numTaps - 1个样本写回状态数组。如果两次调用之间把状态数组重新清零或者用别的数据覆盖了就会出现类似“滤波器被重置”的现象输出会在分块边界处跳变。arm_biquad_cascade_df1_f32的状态变量是每级 4 个浮点数这些状态变量必须跨调用保持。源码里每个 stage 的状态依次是x1, x2, y1, y2在级联循环中不断更新。如果状态数组分配在局部变量里函数返回后状态就丢了输出就会异常。工业固件里这类问题尤其危险因为采样中断和主循环任务可能在同时调用同一个滤波器实例。PID 控制器函数也有类似陷阱。arm_pid_init_f32会根据 Kp/Ki/Kd 做预计算把结果存在结构体里的 A0/A1/A2 字段。运行时如果直接改结构体里的 Kp而不重新调用初始化函数控制器内部参数根本不会更新。源码审计这件事之后我在所有代码评审中都要求 PID 参数更新必须走统一接口避免直接操作结构体成员。4. 工业固件落地从宏定义到内存布局4.1 编译工具链与 CMSIS-DSP 宏定义配置工业固件在芯片选型和工具链上往往偏保守很多老项目还在用 Arm Compiler 5 或者移植到 Arm Compiler 6。CMSIS-DSP 对这套流程支持得还不错但有几个地方必须提前处理。首先是正确传递宏定义。以 Keil MDK 为例需要把类似ARM_MATH_CM4、ARM_MATH_DSP加到 C/C Preprocessor Symbols 中GCC 工程则在 Makefile 或 CMake 里添加-DARM_MATH_CM4 -DARM_MATH_DSP。如果使用 Arm Compiler 5 的老工程请注意新版 CMSIS-DSP 可能已经放弃对 AC5 的默认支持。我踩过的一个坑是库源码本身能编译但内部部分 intrinsic 在 AC5 下无法识别最后要么换编译器要么退回老版本库。建议新项目直接用 Arm Compiler 6开启-O3和 Link-Time OptimizationCMSIS-DSP 内部函数有机会跨文件内联性能提升明显。另一个容易忽略的是arm_math.h会依据编译器预定义或工程宏来决定是否启用__FPU_USED。如果你用的芯片有 FPU 但在工程设置里没有开-mfloat-abihard即使定义了ARM_MATH_CM4部分浮点代码仍然可能走软件浮点路径。CMake 工程里建议显式加上target_compile_options(dsp PRIVATE -mcpucortex-m4 -mthumb -mfloat-abihard -mfpufpv4-sp-d16 -DARM_MATH_CM4 -DARM_MATH_DSP )编译完成后可以通过预处理输出里的__FPU_USED宏判断硬件浮点是否真的生效。与其运行到一半才发现性能不对不如把这一步做成工程检查的一部分。4.2 内存布局、缓冲区对齐与 MPU 保护工业固件的内存规划比 demo 工程严谨得多。CMSIS-DSP 函数很多直接对缓冲区做读写不做越界检查缓冲区任何对齐问题都可能导致 HardFault 或者运行结果不确定。对于float32_t数组至少要 4 字节对齐对于q31_t数组也要求 4 字节如果使用了 MVE 或 NEON 相关的优化路径建议统一 16 字节对齐。声明数组时我习惯这样写__ALIGNED(16) static float32_t fftBuf[4096]; __ALIGNED(16) static float32_t firState[256];把缓冲区对齐提到类型声明层面可以避免局部变量在栈上出现未对齐地址的隐患。要注意在 AC6 和 GCC 下__ALIGNED(n)宏都支持但如果换成 IAR可能要用#pragma data_alignment16移植时需要注意。工业现场还应该用 MPU 保护关键内存区域。将存放 DSP 缓冲区的 SRAM 区域设置为非可执行、限制权限可以防止栈溢出或者指针错误跳到数据区执行。很多安全认证的固件审计也会要求做这一步。另外Cortex-M7 这类带 TCM 的内核把频率最高的状态数组放到 DTCM 可以避免 AHB 总线冲突把 FFT 旋转因子这类只读表放在 Flash 或 ITCM也能减少取指停顿。4.3 RTOS 任务与中断上下文的集成方式嵌入式实时系统的常见问题是中断里直接做繁重 DSP 计算。以 256 点 FFT 为例虽然耗时不算极长但如果在高优先级定时器中断里调用会把整个系统的中断延迟拉得很高导致其他实时任务抖动。工业上我习惯把数据采集和算法处理彻底分开ADC 或 DMA 中断只负责搬数据和置标志位真正调用 CMSIS-DSP 的代码放在一个专门的高优先级任务里。对于连续采集场景推荐 Ping-Pong 双缓冲。DMA 正在往 A 缓冲区写数据时CPU 处理 B 缓冲区下一次交换。这样可以避免共享缓冲区的锁开销也让 DSP 任务和采集中断互不阻塞。CMSIS-DSP 的各函数都是同步调用只要保证当前缓冲区的数据不再被 DMA 写就不需要复杂的临界区保护。RTOS 集成还有一个容易被忽略的内存问题。如果任务局部变量里定义了大数组任务栈会吃不消。比如arm_mat_mult_f32的中间数组动辄几百字节放在栈上必须确保任务栈空间足够。我通常给 DSP 任务栈分配 2KB 以上并且把较大的临时缓冲区定义为静态全局数组这样既便于对齐也避免栈溢出导致 HardFault 时难以排查。5. 固件集成验证与性能调优5.1 用 Python/MATLAB 生成 Golden Model 数据把 CMSIS-DSP 函数跑通很容易但结果是否正确需要一套独立的参考实现来验证。我在项目里最常用的方式是 Python 加 NumPy 生成输入序列用浮点计算参考输出再和固件端输出对比。以 FIR 滤波器为例Python 端可以用scipy.signal.lfilter得到参考输出固件端把同一段输入用arm_fir_f32滤波再通过串口把结果打印出来。对比标准不宜只看“看起来一样”。对于浮点版本我一般计算最大绝对误差和信噪比对于定点版本要先把 Q 格式对应关系换算成实际物理量再比较。比如 Q15 数据转浮点就是val_f (float)q15_val / 32768.0f。如果最大误差保持在 1e-3 量级基本可以认为链路正确如果误差达到 1e-1大概率是格式化不对或者缩放因子错位。这套流程最好自动化。我写的测试脚本会先让固件输出一个魔数字节对齐串口帧然后读取 CMSIS-DSP 处理结果自动做误差计算并生成报告。每次修改算法或更新库版本后跑一遍回归测试能省下大量现场调试时间。5.2 DWT 周期计数器做函数级耗时测评评估 DSP 函数性能我很少直接用示波器翻转 GPIO因为 GPIO 操作本身会占用时间而且读到的只是大概。更推荐使用内核自带的 DWT 周期计数器。初始化代码只有几行CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;测量时先读一次调用函数后再读一次uint32_t t0 DWT-CYCCNT; arm_cfft_f32(cfftInst, data, 0, 1); uint32_t cost DWT-CYCCNT - t0;为了减少中断和缓存带来的误差我把被测函数连续执行多次取最小值作为接近理论性能的数值。工业调试时编译优化开关必须设置为和发布版本一致否则测出来的数据和用户实际体验完全不是一回事。另外要注意DWT 周期计数器在低功耗模式下可能被关闭测量时需要保持正常运行状态。5.3 汇编级检查与几条加速建议性能调优到一定程度光看 C 代码不够我会直接反汇编固件确认 CPU 到底执行了哪些指令。比如用arm-none-eabi-objdump -d app.elf查看 FFT 或 FIR 函数附近的指令如果看到vldr、vadd.f32这类浮点指令说明硬件 FPU 路径确实生效如果全是bl __aeabi_fadd这类软件浮点调用说明硬件浮点没有真正开启。汇编级检查还能帮助定位某些奇怪的性能问题。我之前遇到过滤波器耗时忽高忽低反汇编后发现系数表被放在外部 Flash每访问一次都要等待 Flash 读取总线停顿严重。把系数表放到 DTCM 或者内部 SRAM 后耗时立刻稳定下来。几条我实测有效的加速建议第一实数信号处理优先用arm_rfft_fast_f32不要拿普通复数 FFT 硬算第二分块处理时把blockSize适当调大比如 32 或 64有利于循环展开和减少函数调用开销第三多个 FIR 级联时如果可以合并成一个更高阶的 FIR 或者一个 biquad 级联实例能减少状态缓存访问第四开启 Link-Time Optimization库函数有机会在调用处直接优化这对-O3工程尤其明显。6. 常见坑点与排查技巧速查6.1 编译阶段典型错误编译期问题通常是最好解决的但也容易被编译器提示误导。我这里整理了几个高频错误现象常见原因处理方式arm_math.h: No such file or directory头文件路径未添加把 CMSIS-DSP/Include 加入 Include 路径undefined reference to arm_cfft_f32源文件没有参与编译检查库源文件是否加入构建#error Dont use...新旧 CMSIS 版本冲突统一包含路径避免同时引用多个包编译报__SSAT未定义内核架构指令不匹配检查-mcpu和-mthumb设置是否正确老项目从 Arm Compiler 5 升级到 Arm Compiler 6 时还会出现一些隐式转换和双精度字面量警告。AC6 对类型检查更严格比如float32_t f 0.1;会提示 double 转为 float 的精度损失虽然不影响功能但建议全部改成0.1f洗一遍代码总没坏处。6.2 运行期 HardFault 与数据异常运行期 HardFault 是工业现场最不愿意看到的。在 DSP 函数附近发生 HardFault大概率可以往三个方向查缓冲区未对齐、数组越界、状态结构体未正确初始化。调试时先看SCB-CFSR中的数据访问违例位再用调试器查看 PC 指针是否落在 CMSIS-DSP 的某个vldr或str指令附近。数据异常不一定崩溃。比如定点输出整段变成 0x7FFF 或 0x8000这叫饱和削波问题不在库函数而是输入增益过大频域结果出现镜像分量可能是采样率设置低于信号最高频率的 2 倍跟 FFT 算法本身无关。遇到这类问题我建议先把数据导出来用 Python 画成波形通过波形形状而不是肉眼观察判断规律。6.3 性能不符合预期的定位路径性能不符合预期时不要急着怀疑库写的烂。首先要确认编译优化开关不要在 Debug 模式或-O0下评价性能其次检查预处理结果看源码中对应的#if分支是否进入了硬件加速路径最后用反汇编确认实际生成的指令。如果确认库路径没问题再回到数据缓存和内存访问模式上找原因。一个实用的对照方法是做 A/B 测试。只改变一个变量比如从ARM_MATH_CM4改为不定义对比耗时差异能很快判断宏带来的性能收益。再比如把缓冲区从外部 PSRAM 挪到内部 SRAM对比耗时差异就能评估总线等待的影响。这种方法虽然土但定位效果非常直接。6.4 我的几点排障习惯最后分享几个个人积累的排障习惯。第一在工程里放一个强制检查宏的编译期断言比如#ifndef ARM_MATH_CM4 #error ARM_MATH_CM4 must be defined in this project! #endif这样可以避免团队成员忘记配置宏定义。第二CMSIS-DSP 的任何修改我都在 Git 分支里维护拉取上游新版本时用 tag 固定不跟着主分支乱动升级后必须跑一遍 Golden Model 回归测试。第三碰到奇怪问题先做一个最小复现工程去掉 RTOS、去掉驱动只留一个串口打印和 DSP 函数很多时候问题会自己现出原形。做嵌入式信号处理越久我越觉得 CMSIS-DSP 这种开源库反而更需要深入源码。一次源码审计花掉一个下午换来的是对每个关键函数内存访问方式和优化路径的清晰认识。以后再遇到性能问题或偶发异常至少能判断是库的问题还是自己的问题。这篇内容后续我还会把定点 FFT 在不同内核上的精度实测数据整理出来方便做算法评估的同行查阅。
返回列表