拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CMSIS-DSP源码审计:从矩阵、FFT到滤波器,掌握嵌入式信号处理性能优化

这段时间我在梳理工业固件的信号处理链路时把 ARM 官方的 CMSIS-DSP 源码从头到尾过了一遍。这个库几乎每个嵌入式工程师都接触过但真正常规项目里都是当黑盒用调用 API、翻翻示例、跑通就完事。直到我遇到一个需要压榨 MCU 性能的电机控制项目才意识到如果不懂库内部的实现逻辑连最基本的选哪个 API、开哪个宏、裁剪哪块代码都无从下手。这篇文章等于是一份源码审计笔记。我不会按目录逐行念注释而是把 CMSIS-DSP 的架构拆成几个核心模块讲清楚它内部的数据结构、编译期宏开关、以及矩阵、FFT、滤波器三大核心算子的实现思路。最后再回到工业固件落地分享一下集成、裁剪和实测过程中的经验和坑。适合已经用过 CMSIS-DSP 但想深入源码的开发者也适合正在做资源受限设备选型的固件工程师。1. 为什么工业固件开发者应该较真CMSIS-DSP 源码1.1 CMSIS-DSP 的定位ARM 官方信号处理基础库很多开发者会把 CMSIS-DSP 和一个普通的算法集合划等号这个理解有偏差。CMSIS-DSP 是 ARM CMSIS 软件框架里专门面向 Cortex-M 系列处理器的数字信号处理库它解决的问题是在没有 DSP 芯片的情况下用通用 MCU 完成滤波、变换、矩阵运算、统计计算等任务。和 CMSIS-Core处理器寄存器定义和启动代码、CMSIS-RTOS实时操作系统抽象并列属于 ARM 生态的基础设施层。这个库的价值不在于提供了多少个函数而在于它针对 Cortex-M 的指令集特性做了大量底层优化。同样是 FIR 滤波器自己写一个三重循环和调用 arm_fir_f32在开了 DSP 指令加速的 M4 内核上性能差距可以是数倍。如果只看 API 不看实现就永远无法理解这个差距是怎么来的。1.2 从调用 API到读懂源码的差距工业固件和一般嵌入式原型最大的区别在于三个词确定性、资源边界、可维护性。确定性指的是执行时间必须可预测不能因为数据分布不同而出现明显的执行时间抖动。CMSIS-DSP 的实现大量采用查表和固定循环展开这种以空间换时间的策略就是为确定性服务的。资源边界则更直接工业上用的 MCU 往往内存只有几十 KBFlash 不过几百 KB。CMSIS-DSP 有几十个模块链接进固件之前你就得清楚每个函数会拉进来多少表比如 FFT 的旋转因子表、位反转表占多少 RAM状态结构体大小这些数字不是靠猜的得看头文件里的结构体定义和源码里的静态数组。可维护性更微妙。你会发现网上有很多基于 CMSIS-DSP 二次修改的版本有的改了定标方式有的改了数据布局还有的为了多实例支持做了内存池。如果你不理解原版的设计意图拿到这些改动版根本不敢动。1.3 源码审计的三个核心视角我在审计 CMSIS-DSP 时给自己定了三个问题也推荐你带着同样的问题去读源码数据是怎么组织的——结构体字段的含义、内存布局、对齐要求是什么计算是怎么加速的——哪些是编译器优化的功劳哪些是手写汇编/SIMD 指令的功劳边界是怎么处理的——输入长度非 4 的倍数怎么办矩阵维度不匹配怎么办溢出怎么办带着这三个问题去读源码和漫无目的地一行行看效率完全不同。接下来的内容就是围绕这三个视角展开的。2. CMSIS-DSP 架构全景从目录结构到编译期宏开关2.1 源码目录与模块地图CMSIS-DSP 的源码结构并不复杂核心就是一个 Include 目录加一个 Source 目录。Include 目录下的 arm_math.h 是唯一的对外接口头文件所有模块的函数声明、数据结构定义、编译期宏开关都集中在这里。Source 目录下按功能模块分子文件夹我在审计时整理了一张模块地图目录功能模块典型函数工业场景对应需求BasicMathFunctions基础算术arm_add_f32, arm_mult_q15传感器数据预处理ComplexMathFunctions复数运算arm_cmplx_mag_f32交流采样、相位计算FastMathFunctions快速数学arm_sin_f32, arm_sqrt_f32坐标变换、标定算法FilteringFunctions滤波arm_fir_f32, arm_biquad_cascade_df1_f32信号调理、闭环控制MatrixFunctions矩阵运算arm_mat_inverse_f32, arm_mat_mult_f32状态估计、系统辨识StatisticsFunctions统计特征arm_mean_f32, arm_std_f32在线监测、故障诊断TransformFunctions傅里叶变换arm_rfft_fast_f32, arm_cfft_f32频谱分析、振动监测SupportFunctions数据转换arm_q15_to_float, arm_copy_f32定点/浮点数据交换ControllerFunctions控制算法arm_pid_init_f32闭环调节InterpolationFunctions插值arm_linear_interp_f32查表校准QuaternionMathFunctions四元数arm_quaternion_norm_f32姿态解算BayesianFunctions贝叶斯计算arm_gaussian_naive_bayes_predict_f32故障分类新版本注意几个容易忽略的点。一是 CommonTables 兄弟目录里面放着 FFT 旋转因子表、位反转表等只读常量这些表是链接进固件的体积不能忽略二是新版本里新增的 BayesianFunctions、DistanceFunctions、SVMFunctions这些是面向边缘 AI 分类的如果你的项目只有传统信号处理需求可以直接从编译选项层面关掉。2.2 核心数据结构的设计逻辑CMSIS-DSP 大量使用实例结构体 初始化函数的设计模式。以矩阵为例typedef struct { uint16_t numRows; /* 行数 */ uint16_t numCols; /* 列数 */ float32_t *pData; /* 数据指针按行主序存储 */ } arm_matrix_instance_f32;这个结构体看起来简单但设计意图很明确。把行列信息和数据指针打包成一个结构体好处是 API 调用时只需要传一个指针不需要把维度参数逐个传进去。更重要的是这种设计天然支持多实例同一个函数可以同时处理多个矩阵只要实例结构体不同内部的运算状态就不会互相干扰。这对可重入性和 RTOS 环境非常重要。你会发现所有有状态的计算FIR 滤波器、PID、FFT都有一个以_init_前缀的初始化函数和一个保存状态的结构体。以 FIR 为例typedef struct { float32_t *pState; /* 状态指针保存历史输入 */ const float32_t *pCoeffs; /* 系数指针 */ uint16_t numTaps; /* 抽头数 */ } arm_fir_instance_f32;这个结构体的核心是pState状态指针。FIR 滤波器在计算每个输出样本时需要用到前 N-1 个历史输入这些历史值就存在pState指向的缓冲区里。初始化时你必须手动分配这个缓冲区大小是numTaps blockSize - 1个 float初始化函数只是记住指针不会帮你分配内存。这个设计明确了调用者和库之间的内存责任边界也是初学者最容易踩坑的地方——忘了分配 pState 缓冲区直接跑 filter 函数必挂。2.3 编译期精细控制宏开关与算子替换arm_math.h 里有一整套编译期宏开关这是 CMSIS-DSP 最需要重视的部分。初学者通常直接看默认配置结果在性能上吃了亏也不知道为什么。最重要的宏是ARM_MATH_DSP它告诉库当前编译目标支持 Cortex-M3/M4/M7 的 DSP 指令扩展。定义这个宏之后库内很多通用 C 代码会被替换为利用SMUAD、SMLALD、USAT等指令的手写优化版性能提升非常可观。如果你的芯片是 M4 内核但没定义这个宏库会退回到纯 C 实现白白浪费硬件能力。类似的关键宏还有宏名称作用适用场景ARM_MATH_CM0/CM0PLUS禁用 DSP 指令最保守的纯 C 实现Cortex-M0/M0ARM_MATH_CM4/CM7启用 M4/M7 的 DSP 指令M4/M7 内核ARM_MATH_MATRIX_CHECK启用矩阵维度检查调试阶段生产环境建议关闭ARM_MATH_NEON启用 NEON 向量加速Cortex-A 系列处理器ARM_MATH_HELIUM启用 MVEHelium向量扩展Cortex-M55/M85这里有个经验ARM_MATH_MATRIX_CHECK在生产固件里一定要关掉。矩阵维度检查的代码是运行时占开销的尤其是矩阵乘法这种嵌套循环每层循环都会检查索引是否越界。编译阶段这个宏的默认状态是关闭但一些 IDE 的 SDK 模板会顺手把它打开导致最终固件里多了很多无意义的判断。清零另一个重要的宏是ARM_MATH_BIG_ENDIAN它控制数据字节序。大部分 ARM MCU 默认小端但某些特定外设或通信协议可能产生大端数据流。如果你在调试两个板子间数据不一致的问题先检查两边这个宏的定义是否一致。2.4 编译器选择的影响CMSIS-DSP 的源码是 C 写的但它的优化严重依赖编译器的指令调度能力。我用同一个 .c 文件分别在 ARMCCarmclang和 GCC 下编译做过对比在开启-O2armclang和-O3GCC的前提下armclang 编译出来的 FIR 滤波循环体通常能多挤出 10% 左右的 cycle 收益。原因是 ARMCC 对 Cortex-M 指令调度做了更深度的优化特别是对__SSAT这类饱和内建函数的处理更贴合底层硬件。所以工业固件项目里如果条件允许我建议优先用 ARM Compiler 6 作为 CMSIS-DSP 的编译工具链。GCC 并不是不能用只是你需要多做一轮循环级别的性能实测确保优化级别和编译选项没有拖后腿。3. 源码审计矩阵、FFT、滤波器三大核心模块的关键实现3.1 矩阵乘法行主序的存储布局与循环优化策略矩阵运算在工业代码里最常见的是坐标变换和最小二乘估计。CMSIS-DSP 的矩阵乘法arm_mat_mult_f32是一个标准的朴素三重循环实现但它的循环顺序和指针运算方式很有讲究。核心代码逻辑如下简化版for (i 0; i numRowsA; i) { for (j 0; j numColsB; j) { sum 0.0f; pInA pA i * numColsA; pInB pB j; for (k 0; k numColsA; k) { sum *pInA * *pInB; pInB numColsB; } *pOut sum; } }注意内层循环的访问模式。pInA是顺序访问的也就是取的 A 矩阵的行方向数据这符合行主序存储的连续内存访问pInB则是按列跳着取的每次跳numColsB个元素。这种一顺一跳的访问方式在缓存行有限的小 MCU 上已经是最优解了因为在 Cortex-M 上根本没有大缓存的概念性价比最高的就是保证至少一个操作数是连续访问。进一步深入源码会发现CMSIS-DSP 还单独提供了arm_mat_mult_fast_f32这个函数做了更激进的优化把内层循环展开每轮迭代计算多个输出元素减少循环控制和分支跳转的开销。在我的实测里矩阵规模在 4x4 到 10x10 之间时fast 版本的 cycle 数比普通版本少 15% 到 30%。但有个前提fast 版本要求输入矩阵的行列数必须是 2 的幂或 4 的倍数否则会走到回退分支。虽然它可以处理非对齐场景但性能优势就不再明显。如果你的矩阵维度不满足对齐条件与其硬套 fast 版本不如在系统设计阶段把矩阵维度补零到 4 的倍数。3.2 FFT位反转表、混合基变换与蝶形运算FFT 是 CMSIS-DSP 内部最复杂的算法模块之一。实数 FFT 的入口是arm_rfft_fast_f32它内部会调用复数 FFTarm_cfft_f32并利用实数序列频谱的共轭对称性把 N 点实数 FFT 转换为 N/2 点复数 FFT 来降低计算量。这背后是经典的两步技巧先构造一个复序列实部放偶数采样点、虚部放奇数采样点然后从复数频谱中分离出实数频谱的偶次和奇次分量。整个计算分三个关键环节第一预处理。真实源码里会对输入数据做 reorder将序列按位反转顺序重排。ARM 预先算好了位反转索引表armBitRevIndexTable不用每次运行时重新计算这是它比朴素实现快的重要原因。查表代替计算的策略在数学库中到处都是。第二蝶形运算。CMSIS-DSP 的复数 FFT 默认用基 4 蝶形Radix-4每个蝶形一次处理 4 个点旋转因子从arm_cfft_twiddleCoef表格中查取。基 4 相比基 2 的乘法次数更少但代码复杂度更高。源码头部的注释里有明确说明当长度为 2 的幂时走基 4 划分否则退化为基 2。第三缩放策略。arm_rfft_fast_f32的输出不是原始的 DFT 结果而是经过缩放后的幅值。这是很多使用者疑惑的地方直接用函数返回值做谱分析结果总觉得幅值偏小。标准做法是输出值乘上2/N才能还原真实的单边频谱幅值。仔细看源码注释就会发现库内部对中间结果做了固定缩放来避免中间过程溢出。第三个细节是固定函数名里的fast。arm_rfft_fast_f32支持的最大点数是 4096并且点数必须是 2 的幂。如果你的分析需要 1200 点的 FFT就得用arm_rfft_f32非 fast 版本它支持任意点数但速度慢、代码体积大。规划固件功能时FFT 点数这个约束应该在算法选型阶段就确认好。3.3 定点数处理Q15/Q31 格式与饱和运算工业 MCU 里大量存在不带 FPU 的 Cortex-M0/M3 芯片这类芯片做信号处理全靠定点运算。CMSIS-DSP 对定点支持得非常完善arm_fir_q15、arm_biquad_cascade_df1_q31等函数全部是定点点实现。定点和浮点的核心差异在于小数点的位置。CMSIS-DSP 大量采用 Q151 位符号 15 位小数和 Q311 位符号 31 位小数格式。两个 Q15 数相乘结果是 Q30 格式需要左移一位才能回到 Q15。如果手动写这个处理很容易因为移位方向搞错得出错误结果。CMSIS-DSP 内部用宏封装了这些操作比如__SSAT做饱和截断__QADD做饱和加法保证运算过程不溢出。看一段典型的定点 FIR 核心逻辑简化版for (j 0; j blockSize; j) { sum 0; pState pStateCurnt; for (k 0; k numTaps; k) { sum (q31_t)*pState * (q31_t)*pCoeffs; } *pOut (q15_t)(__SSAT((sum 15), 16)); pStateCurnt; }这里每个乘法累加后都会把结果右移 15 位再经过__SSAT饱和到 16 位。饱和运算的意义是即便中间结果溢出了 32 位定点范围例如多个乘积累加后超过了 Q31 的最大值硬件也会将结果钳位到最大值或最小值而不是回绕从而避免信号严重失真。在实际工业项目中定点数最隐蔽的坑是定标选择。你必须在系数设计阶段就确定每个变量的 Q 格式并且保证所有中间累加的结果不会超过寄存器的表示范围。CMSIS-DSP 的滤波器初始化函数里没有系数定标参数这意味着系数设计是库之外的工作。我一般用 Python 里的scipy.signal设计好滤波器系数后再统一按 Q15 或 Q31 格式归一化和量化量化后的误差再仿真一遍才能进固件。4. 性能优化的底层逻辑SIMD、循环展开与查表策略4.1 编译优化与手写优化的边界很多人在 MCU 上写循环时都有个误区开-O3就万事大吉了。实际上编译器能做的优化是有限度的。以 FIR 滤波器为例手写for循环累加-O3编译出来的结果仍然会有循环计数器加减和跳转指令而 CMSIS-DSP 的 C 源码会显式做循环展开ARM_MATH_DSP宏开启后还会进一步映射到 DSP 指令。这些优化不写在源码里编译器无中生有地变出来。我在 M4 内核主频 168MHz上做过一个测试对 32 阶 FIR 滤波器处理 1024 点数据CMSIS-DSP 的arm_fir_f32比等效的自写循环快 4~6 倍。这些差距主要来自三方面数据访问模式库实现里对 state buffer 的读写是双缓冲策略避免了数据竞争指令级优化用LDRSMLA组合一次完成乘加操作循环展开每次迭代处理多个输出减少分支预测失败的惩罚。4.2 硬件加速路径DSP 指令与 Helium 向量扩展Cortex-M4/M7 的 DSP 扩展指令是 CMSIS-DSP 性能的核心。以SMUAD为例它能在一条指令内完成两个 16 位乘法并把两次乘积相加得到 32 位结果。这种指令对应算法里的复数乘法、矩阵内积等高频操作比通用MULADD的组合快得多。库内部通过宏来判断是否启用这些指令#ifdef ARM_MATH_DSP /* 使用 SMUAD 等 DSP 指令的优化路径 */ #else /* 退化为标准 C 乘法加 */ #endif如果你用的是 Cortex-M55/M85支持 Arm Helium MVE 向量扩展新版 CMSIS-DSP 里还有专门针对 MVE 的实现路径用vld2q_f32这类向量加载指令批量取数一个 cycle 内可以对多个浮点数据做乘加操作。对这些芯片CMSIS-DSP 的性能还能再上一个台阶。我在调试一个振动监测项目时发现不同编译器版本对宏识别的行为有差异。ARMCC 5armcc 编译器对ARM_MATH_CM7这类宏的识别比较直接但 ARMCC 6armclang是基于 clang 的优化选项和宏展开策略都变了。如果你从 ARMCC 5 迁移到 ARMCC 6务必重新做一轮性能和正确性回归不要直接沿用旧编译参数。4.3 工程实践中的性能验证方法源码读了半天总要落到实测。最直接的方法是借助DWT-CYCCNTCortex-M 内核的周期计数器来测量函数执行时长。下面是我常用的测量代码volatile uint32_t start, stop; DWT-CTRL | 1; /* 使能 CYCCNT */ DWT-CYCCNT 0; start DWT-CYCCNT; arm_fir_f32(firState, input, output, BLOCK_SIZE); stop DWT-CYCCNT; printf(cycles: %lu\n, stop - start);注意几个细节测量前要关闭中断或保证测量段不被高优先级中断打断否则计时会偏大重复多次取最小值才是稳定执行时间因为首次执行可能遇到缓存/取指未命中带来的额外延迟。更严谨的做法是把被测函数循环跑 10 次取平均观察执行时间抖动是否在可接受范围内。实测数据能帮我确认两件事一是算法执行时间是否满足控制周期要求二是热点函数在整体 CPU 占用里占据的百分比判断是否值得花力气优化。比如一个电机控制循环里 FFT 占了 60% 的 CPU 时间优化 FFT 就是最高优先级如果只占 5%那不如去优化通信协议栈。5. 工业固件落地从源码到量产固件的完整链路5.1 集成方式选型Pack 接入 vs 源码拷贝 vs 静态库CMSIS-DSP 的集成方式有好几种我实际用过三条路线各有优劣。第一种是 IDE 内通过 CMSIS Pack 管理器添加Keil 的 RTE 界面、IAR 的 SDK 都支持。这是最快的路径勾选模块后自动加入源码和 Include 路径适合快速原型验证。第二种是把 Source 目录的源码直接拷贝到自己的工程里。这是工业项目最常见的做法也是我比较推荐的。原因是源码在你的版本控制体系里修改比如加性能日志、裁剪模块可以追踪编译选项完全可控。第三种是预先编译成静态库.a 或 .lib链接。好处是编译时间短、软件包交付方便尤其当你给下游团队提供 SDK 时坏处是对库里的调试和裁剪不方便。如果采用这种方式建议保留一份与静态库版本完全对齐的源码包作为基线否则后续定位问题时会非常痛苦。我用一个表格帮你对比这三条路线集成方式优点缺点适用场景Pack 自动添加上手快无配置负担受 IDE 版本/网络影响定制性差原型验证、学习测试源码拷贝进工程完全可控可裁剪可加日志初始配置费时工业量产固件预编译静态库编译快交付干净调试不便修改困难SDK 交付、闭源分发5.2 代码裁剪与固件体积控制CMSIS-DSP 全量编译进来的体积不小在 Flash 紧张的 MCU 上必须做裁剪。裁剪主要有三个层面。第一是编译宏裁剪。从 arm_math.h 里把不需要的模块注释掉比如不用矩阵运算就注释掉 MatrixFunctions 相关声明这样链接时就不会拉入对应的 .o 文件。但要注意注释 arm_math.h 的声明不等于一定省 Flash因为有些模块之间存在依赖关系比如 rfft 依赖 cfftcfft 依赖 CommonTables 里的旋转因子表联动裁剪时要保持一致性。第二是链接器裁剪。MCU 的链接脚本里开--gc-sections或等效的 function-sections >__ALIGNED(4) float32_t inputBuffer[BLOCK_SIZE]; __ALIGNED(4) float32_t outputBuffer[BLOCK_SIZE];第二个坑是状态结构体未清零。FIR 滤波器的pState缓冲区、FFT 的实例结构体初始化后如果不清零第一次运行就带有随机历史值输出信号头部会出现几拍异常。正确姿势是在初始化函数之后立即 memset。memset(firState, 0, sizeof(arm_fir_instance_f32)); memset(firState.pState, 0, sizeof(float32_t) * (numTaps BLOCK_SIZE - 1)); arm_fir_init_f32(firState, numTaps, coeffs, firState.pState, BLOCK_SIZE);第三个坑是栈空间不足。CMSIS-DSP 的函数里大量使用局部数组作为临时缓冲区比如arm_rfft_fast_f32内部会创建一个 N/2 点的复数中间缓冲区。如果把任务栈设得和裸机 main 一样小比如 512 字节跑 FFT 时栈溢出几乎是必然的。经验值跑 1024 点 FFT 至少给任务栈留 2KB 余量跑全流程信号处理链建议 4KB 起步。RTOS 环境里一定要针对调用 CMSIS-DSP 的任务单独评估栈大小这个能省去很多随机性 HardFault 的排查时间。第四个坑是浮点运算的不确定性。在一些没有 FPU 的 M0 芯片上CMSIS-DSP 的浮点函数会调用软件浮点库性能差且占 Flash 大。如果你的设备实在用的是 M0建议直接用 Q15/Q31 版本不要硬跑 float 版本。相反如果芯片有 FPU却把ARM_MATH_DSP宏漏定义库会退回纯 C 实现白白浪费硬件加速性能差距非常明显。第五个坑是版本差异。CMSIS-DSP 目前有 4.x 和 5.x 两个大版本系列函数签名、宏名称、错误处理方式都有不少差异。比如 5.x 把一些全局表改为数组结构API 名称有调整。你在参考网上的代码段时先确认对方用的是哪个版本否则编译报错后排查半天才发现是版本不匹配会很浪费时间。5.4 一个落地实例振动监测固件的信号链最后分享一个实际的落地案例。某振动监测设备用 STM32F446Cortex-M4F168MHz需要同时采集 3 路加速度计信号每路 1024 点 FFT 做频谱分析控制周期 10ms。信号链设计如下3 路 ADC 采样DMA 搬运到缓冲区触发中断后调用arm_rfft_fast_f32做 1024 点 FFT取模后用arm_max_f32找峰值频率用于振动幅值判断同时用arm_biquad_cascade_df1_f32做 50Hz 工频陷波避免工频干扰影响分析结果最后通过arm_mean_f32计算时域信号的均值辅助判断基线漂移。实测一组数据三路 1024 点 FFT 加峰值搜索总耗时约 1.1ms滤波处理 3 * 256 点数据约 0.4ms总计信号处理耗时 1.5ms 左右占 10ms 控制周期的 15%。CPU 余量充足还能同时跑 Modbus 从站通信和 LCD 显示刷新。这个项目里最值得注意的经验是FFT 的输入缓冲区和 ADC 的 DMA 目标缓冲区是同一个数组但 DMA 写满后直接切换缓冲区的做法虽然省了一次 memcpy却可能导致 FFT 计算过程中数据被 DMA 覆盖。最终方案是采用双缓冲 DMA在 DMA 半传输和全传输中断里分别标记缓冲区可用性FFT 只处理已完成采样的那一半数据。这个改动看似增加了代码量和内存两份缓冲区但彻底消除了数据竞争问题也让 FFT 的输入数据保持一致性长期跑下来没有出现偶发的频谱跳变。另外这套信号链里所有缓冲区都按 8 字节对齐分配__ALIGNED(8)因为后面计划升级到 M7 内核需要 8 字节对齐才能完整发挥双发射 FPU 的性能。在选型阶段就把对齐要求放宽到下一代内核能省一次全系统重构的工。尾声几个在手项目后的个人建议如果你已经忍到这一节那我猜你不只是想看看热闹而是真的打算去动 CMSIS-DSP 的源码。几条体会供参考。不要在项目交付前一天才开始学源码。CMSIS-DSP 这种库读源码的价值不在当下那个 Bug而在你下一次选型、下一次调优、下一次裁剪时都已经心里有数。我习惯在 SDK 升级时顺手 diff 一下 CMSIS-DSP 版本更新日志看看它对编译器新版本做了哪些适配对指令集优化做了哪些调整这些都是没有写在一般教程里的隐性知识。也不要试图重写所有函数。CMSIS-DSP 里确实有一些为了通用性牺牲了特定场景性能的代码但改动一个库函数意味着你要自己维护差异后续升级库版本时会非常痛苦。我的底线是只有库的行为与需求产生了直接的不可调和的冲突时才考虑 fork 源码做定制并且所有改动都要加清晰的注释和版本标记。比如之前遇到过一个项目需要用 FFT 做密集的短窗分析官方的arm_rfft_fast_f32每次调用都要重置状态我在 fork 版里改成了可复用状态的版本性能提升了约一倍但也为此维护了一条独立的代码分支。值不值取决于你的团队有没有这个维护能力。最后想强调一点CMSIS-DSP 再优化也有物理天花板。如果你发现算法已经吃到 90% 的 CPU 占用与其继续抠汇编不如回头看看算法本身的复杂度能不能降比如用arm_cfft_f32的快速用例大点 FFT 吗能分帧处理吗或者评估一下是否该换带 Helium 的 M55。器件成本固然重要但工业设备一旦量产留 40% 以上的 CPU 余量才敢做后续的软件升级和功能追加。这个道理我是在翻车几次之后才彻底想明白的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门