做嵌入式这行,尤其是跟Arm Cortex-M系列打交道的朋友,对CMSIS-DSP应该都不陌生。但说实话,大部分项目里它就是个“黑盒”——调用arm_mat_mult_f32、arm_fir_f32的时候顺手得很,真到性能不达标、数据算错或者HardFault的时候,很少有人愿意翻开Source目录逐行去看官方到底写了什么。这篇东西我想换个角度,不聊API怎么用,而是把Arm-CMSIS-DSP这个库当成一份开源代码来“审”一遍:从架构分层、核心算法实现,到工业固件落地时那些文档里不会写的细节,一次性串起来讲清楚。适合正在做电机控制、电源、音频、振动分析或者传感器融合的朋友,尤其是要上量产的固件工程师。
先说结论:CMSIS-DSP不是一个“用就行了”的代码集,它背后是Arm对Cortex-M微架构的深度理解,也是工业代码在面积、确定性、精度三者之间反复博弈之后的样本。把这份源码审计完,你对嵌入式信号处理库的理解,绝对会超过市面上90%的人。
1. 先摸清这摊代码的家底:CMSIS-DSP架构与目录语义
1.1 从arm_math.h开始:一个头文件如何串联整个库
进到CMSIS-DSP的目录,Include文件夹下的arm_math.h是整个库的门面。很多人不知道,这个头文件的组织方式很有意思:它不是把所有函数声明堆在一起,而是分了两层——最上面是架构和编译器相关的条件编译判断,下面才按数据类型、函数族分组声明。
以Arm Compiler 5/6加GCC三个工具链为例,arm_math.h开头一大段全是#if defined ( __CC_ARM )、#if defined ( __GNUC__ )这类判断,目的只有一个:统一不同编译器在__STATIC_INLINE、__ALIGNED、__ASM这些关键词上的差异。这层封装直接决定了你的代码“换编译器不换算法”,在工业项目里价值很大。紧接着是#include "core_cm4.h"之类的内核头文件,这步很关键,CMSIS-DSP会用到__SSAT(饱和指令)、__SMLAD(双16位乘加)这类DSP扩展指令,这些指令的C封装就来自内核头文件。
整个库的函数声明基本按这个规律:arm_前缀 + 功能名 + 数据类型后缀。比如arm_add_f32表示32位浮点加法,arm_add_q15表示Q15定点加法。这个命名规则看着简单,实则每条都有约定:f32、f64是浮点,q7、q15、q31是定点。我建议新接手CMSIS-DSP项目的工程师,先花半小时把arm_math.h里的函数列表整体过一遍,远比急着调FFT有用——因为很多冷门函数(比如arm_cmplx_dot_prod复数点积、arm_conv_partial部分卷积)能省掉你大量手写数学公式的时间。
1.2 从BasicMath到Transform:官方目录的模块划分逻辑
CMSIS-DSP从v1.x发展到现在的v1.16(以及CMSIS 5.9.x),Source目录下的模块划分逻辑基本稳定。下面这张表是我整理的当前版本核心模块:
| 目录名 | 核心功能 | 典型函数 |
|---|---|---|
| BasicMathFunctions | 向量加减乘除、点积、绝对值、偏移、缩放 | arm_add_q15、arm_dot_prod_f32、arm_scale_f32、arm_offset_q31 |
| FastMathFunctions | 快速三角函数、平方根 | arm_sin_f32、arm_cos_q31、arm_sqrt_q15 |
| FilteringFunctions | FIR、IIR、LMS、卷积、相关 | arm_fir_f32、arm_biquad_cascade_df2T_f32、arm_conv_f32 |
| MatrixFunctions | 矩阵加减乘、转置、逆矩阵 | arm_mat_mult_f32、arm_mat_inverse_f32、arm_mat_trans_q15 |
| TransformFunctions | FFT、DCT、复数FFT、实数FFT | arm_cfft_f32、arm_rfft_fast_f32、arm_dct4_f32 |
| StatisticsFunctions | 均值、方差、RMS、峰峰值 | arm_mean_q15、arm_rms_f32、arm_max_q31 |
| SupportFunctions | 数据类型转换、拷贝、填充 | arm_q15_to_float、arm_float_to_q31 |
| ComplexMathFunctions | 复数运算 | arm_cmplx_mult_cmplx_f32、arm_cmplx_mag_squared_q15 |
| InterpolationFunctions | 线性/二次插值 | arm_linear_interp_f32、arm_bilinear_interp_q15 |
从工业固件的角度看,这个划分有两条隐含线索。第一,所有函数都是“纯函数式”设计,输入输出靠指针传递,不保留全局状态。这意味着在RTOS多任务环境下可以放心并发调用(只要各自的缓冲区独立),这一点比很多第三方算法库做得干净。第二,定点函数(q7/q15/q31)和浮点函数(f32/f64)完全平行,同一算法两套实现。这绝不是代码冗余,而是给没有FPU的Cortex-M0/M0+/M3以及需要强确定性的场景留了后路。
1.3 架构分层的幕后:核心层、调度层与应用层的隐性约定
严格来说CMSIS-DSP源码里没有“调度层”这个文件夹,但函数实现上有三层角色:底层内核函数、带_fast或_f32后缀的优化层、以及面向用户的高层封装。
以实数FFT为例,arm_rfft_fast_f32只是做数据重排(把实数序列拆成两路复数),真正算FFT的是内部的arm_cfft_f32;而arm_cfft_f32又会根据ifftFlag和bitReverseFlag选择不同蝶形路径;具体到点数的奇偶性,又分arm_cfft_radix4_f32和arm_cfft_radix2_f32两层。这种多层分包在性能上其实损耗极小(只是多调一层函数,核心循环没变),但换来的维护性和代码复用性很强——你要支持1024点和2048点,不需要复制整个FFT逻辑,共用一套蝶形内核就行。
这里有个实际启发:当你需要魔改官方算法时,不要动用户层函数,最好从最底层的内核函数下手。比如你想把FFT输出改成非线性的对数幅度谱,在arm_cmplx_mag_f32那层抓数据做映射,而不要碰arm_rfft_fast_f32,否则升级CMSIS-DSP版本时你的patch会非常痛苦。
2. 源码审计:三个值得逐行看的核心模块
2.1 定点算术基座:Q7/Q15/Q31的饱和运算到底怎么写的
嵌入式信号处理绕不开定点数,CMSIS-DSP对定点算术的封装可以作为一份教科书。很多人只知道q15乘q15直接结果是q31,但源码里是怎么处理的?关键在两处:一是乘法宏,二是饱和指令。
看BasicMathFunctions里的arm_mult_q15.c,核心循环会对两个输入先做__SSAT饱和保护,然后乘累加。__SSAT这个内建函数实际映射到ARM指令集里的SSAT指令,功能很简单:如果结果超过指定bit宽度,就把值压到边界上,不产生异常。这一步很关键,因为DSP运算最容易出问题的不是算不准,而是算溢出——一旦溢出,后面的反馈滤波器可能会发散到不可控。
再说Q15乘法的写法,源码里常见这种模式:
q31_t inA1 = (q31_t) *pSrcA++; q31_t inB1 = (q31_t) *pSrcB++; q31_t mul1 = __SSAT((q31_t) (((q63_t) inA1 * inB1) >> 15), 16);注意它先把q15临时扩展成q31做乘法,然后右移15位,再做饱和到16位。为什么要右移15而不是右移16?因为Q15格式的定标是2^-15,两个Q15数相乘结果是Q30格式,要转回Q15需要右移15(保留一位额外精度),然后再饱和。这个小数点的处理经验,是定点信号处理和浮点最大的差别:浮点靠指数自动调标度,定点全靠手工移动小数点。
我的建议是,不管你用不用CMSIS-DSP,都要理解__SSAT的语义。在Cortex-M4/M7/M33/M55这些带DSP扩展的内核上,它是单周期指令,而在M0/M0+上,CMSIS-DSP会退化为用C函数模拟饱和,性能差距可以达到几十倍。这也是为什么选型时有人说“同样的CMSIS-DSP,M0+上跑一圈能慢到怀疑人生”。
2.2 矩阵运算:为什么CMSIS坚持行主序的in-place设计
矩阵运算在姿态解算、卡尔曼滤波里天天用。CMSIS-DSP的矩阵库设计上有两个特点值得留意:一是统一行主序(Row-major)存储,二是大量操作支持in-place计算。
行主序的意思是,一个M行N列的矩阵在内存里按“第一行完整放完,再放第二行”的顺序排布。这个选择在嵌入式上非常合理,因为DMA搬运、SIMD加载(比如LDRD一次读两个float)都是线性访问,行主序可以让同一行元素在内存中连续,访问cache/紧耦合内存时命中率高。对比某些PC端库用列主序(比如早期MATLAB),CMSIS-DSP的选择是明显向硬件靠拢的。
而in-place操作(允许输出指针和输入指针指向同一个缓冲区)可以在源码里看到大量pDst = pSrcA这样的用法。为什么敢这样设计?关键在于循环里数据的读取总是先于写入。以arm_mat_trans_f32为例,虽然转置理论上in-place会覆盖数据,但官方实现并没有全面支持转置的in-place,而是用临时变量交替拷贝。这里提醒大家一个审计结论:CMSIS-DSP的in-place支持是“函数级”的,不是“全家通用”的——arm_mat_mult_f32可以in-place(因为乘法是读A和B、写C,C与A/B同一块内存时只要保证A/B不被优先覆盖即可),但arm_mat_inverse_f32就别in-place了,里面用了高斯消元,中途覆盖原始矩阵会算出完全错误的结果。
我审源码时还发现一个有意思的点:arm_mat_inverse_f32的实现方式。它不是像课本那样算伴随矩阵,而是先做LU分解,然后再解线性方程组。这样计算量更小,数值稳定性也更好。如果你想在M4上做3x3矩阵求逆,实测用官方库大概几百个周期量级,对姿态解算每毫秒算一次都完全没压力。
2.3 FFT:混合基蝶形、旋转因子,以及那个被忽略的缩放参数
FFT是CMSIS-DSP里最核心、也是源码审计价值最高的模块。TransformFunctions目录下的arm_cfft_f32.c和arm_cfft_radix4_f32.c、arm_cfft_radix2_f32.c值得逐行读。
从算法上讲,CMSIS-DSP主要用Radix-4和Radix-2混合基。为什么不用更流行的Radix-8?因为Cortex-M的寄存器资源有限,radix-4每个蝶形需要处理4个输入输出,平衡了指令级并行和寄存器压力;而radix-8需要的中间变量太多,压栈开销反而大。另外,对于不能整除4的点数(比如512 = 2^9,能整除2但最后一次是2^1),代码会自适应切到radix-2蝶形收尾。所以你会发现官方例子里FFT点数限制是4的倍数或者2的幂,这在源码里都有注释说明。
旋转因子的处理也很有意思。官方没有在每次调用时重新算cos/sin,而是把所有旋转因子预计算好放在TransformFunctions/Table/下的常量表里,比如arm_cfft_radix4_f32_twiddle_coeffs。这是一张大表,如果MCU的Flash不够用,很多人会在这栽跟头——一个1024点复数FFT的旋转因子表在F32下大约要占十几KB的Flash。我踩过这个坑,项目选了64KB Flash的MCU,放完协议栈后放不下旋转因子表了。解决方案有两个:一是改用Q15定点FFT,旋转因子表用q15存,面积直接减半;二是分段把旋转因子放到外部Flash或XIP区域,代价是访问变慢。没有免费的午餐。
还有缩放参数。arm_cfft_f32的API里没有缩放,意味着官方浮点版本不做幅度缩放;但arm_cfft_q15和arm_cfft_q31在每级蝶形后都有右移操作(一般是1位或2位),这是为了防止中间结果溢出。这个缩放细节特别容易让新人困惑:同一个正弦波,FFT后用浮点算出来的幅值是500,用Q15算出来却只有62——其实62就是定标后的结果,乘上2^(级数-1)就回来了。工业固件里建议把所有定点FFT的缩放统一做成宏定义,调试时集中修正,别在十几个模块里各写各的。
2.4 滤波器:block式API的缓冲区设计
CMSIS-DSP的滤波器全套都是基于块的“block processing”,这和很多PC端DSP库按样本处理不同。以arm_fir_f32为例,它一次处理blockSize个样本。对应地,状态缓冲区大小必须是numTaps + blockSize - 1。这个"减1"的细节背后是FIR延迟线的本质:每次输入一个样本,旧的样本要往下移,最老的样本被丢弃,而块处理时需要一个长度为numTaps + blockSize - 1的窗口才能完整计算块内所有输出。
这个缓冲区设计是源码审计时最容易被忽略、又最菜鸟杀手的地方。很多人自己定义float32_t firState[40],结果numTaps=32, blockSize=16时越界写入,轻则数据被改写,重则直接HardFault。正确做法是从arm_convolution_example_f32.c里学:状态缓冲区要么static声明到.bss段,要么用__ALIGNED(4)修饰,确保4字节对齐(如果用了SIMD,还需要16字节对齐)。
IIR这块,源码里比较推荐的是arm_biquad_cascade_df2T_f32,为什么是DF2T(转置直接II型)而不是DF1?因为DF2T只需要两个延迟单元(针对二阶),而DF1需要四个,状态变量减半,而且DF2T每个二阶节的数值特性更不容易被量化误差放大。在定点arm_biquad_cascade_df2T_q15里,官方还做了中间64位累加来降低量化噪声,这个细节对音频降噪、电源环路补偿这种对噪声敏感的应用影响很大。
3. 工业固件落地:从源码评审到量产固件的最后一公里
3.1 先回答“用哪个版本、用哪套精度”:FPU和MCU型号说了算
工业项目最忌“一刀切全用浮点”或者“全用定点”。我给的选型思路很直接:
- Cortex-M0/M0+/M3(无FPU、无DSP扩展):核心算法用Q15/Q31,或者干脆用官方Q7做轻量分类。避免频繁
float运算,因为软件浮点库(比如__aeabi_fmul)一次乘法要几十个周期,实时性完全不够。 - Cortex-M4(带FPU,比如STM32F4系列):控制环路、FFT、滤波优先F32。M4F的FPU是单精度,F32乘加基本一个周期,性能优势巨大。
- Cortex-M7(双发射+FPU,比如i.MX RT、STM32H7):可以用F32做大部分工作,主频高、还有L1 cache,但要注意缓存一致性问题,DMA搬运的数据要小心。
- Cortex-M33/M55(带MVE可选):CMSIS-DSP 1.10以上有针对MVE(Helium)的优化,Q15性能比M4快好几倍,但前提是开启
__ARM_FEATURE_MVE宏。
版本上,建议直接跟CMSIS 5.9.x的CMSIS-DSP版本对齐,不要用太老的v1.4那样散落在ST标准外设库里的老代码,那些老版本对新架构优化很少,而且API不兼容。另外,CMSIS-DSP从5.x开始把FFT实例结构体从arm_cfft_instance_f32调整过,如果你是从很老的版本升级,需要重新对照arm_math.h改类型名。
3.2 内存对齐和静态分配的工业化约束
工业固件对“确定性”要求极高,所以CMSIS-DSP的官方例子里很少用malloc,基本都是静态或栈上分配。这一点和它的内存对齐要求有关。
在arm_math.h里,结构体定义中频繁出现__ALIGNED(4),比如:
typedef struct { uint16_t numTaps; uint16_t stateIndex; float32_t *pState; const float32_t *pCoeffs; float32_t b0, b1, b2, a1, a2; } arm_biquad_casd_df1_inst_f32;如果你定义实例时用了普通全局变量,编译器一般会自然对齐到4字节。但如果你要动态分配,malloc返回的地址不一定对齐到16字节(在有的RTOS heap实现里只保证8字节),这时用__ALIGNED(16)或者aligned_alloc更稳妥。我见过一个项目,用pvPortMalloc给arm_fir_instance_f32分配状态缓冲,结果状态数组地址8字节对齐但不是16字节对齐,在M4上开-O2后跑到一半就HardFault。后来改成静态ALIGN_32BYTES数组,问题立刻消失。
另外,一个容易被忽略的点是:CMSIS-DSP的pState缓冲区是一次性归零的。你需要在初始化函数里显式调用arm_fir_init_f32并传NULL或清零状态,或者自己memset(pState, 0, size)。很多“滤波器上电输出异常”的bug,其实就是状态缓冲区里残留了RAM的随机值。
3.3 定标和缩放:ADC原始值到Q15的完整链路
做传感器采集的都要面对“原始ADC值”到“DSP定点数”的转换。这段链路处理不好,后面滤波、FFT全是错的。
以12位ADC为例,输出范围是0~4095。如果你要转成Q15格式(范围-32768~32767),常见做法:
uint16_t adcRaw; q15_t q15Value; adcRaw = adc_read(ch); q15Value = (q15_t)(((int32_t) adcRaw - 2048) << 3);为什么减2048?因为ADC通常以中间值为“零”点(双极性信号居中)。为什么要左移3位?因为12位满量程是4096,而Q15满量程是32768,比例是8倍,转换成Q15后峰峰值为±1。这样做的最大好处是:后面所有系数(比如FIR的Q15系数)都在[-1,1)内,滤波器增益的定标不出错。
但这里有个精度陷阱:左移时如果原始信号本来就接近满幅,减法后的差值范围是-2048~2047,再左移3位,范围变成-16384~16376,其实没有占满Q15的动态范围。这个动态范围损失是ADC本身决定的,不该强行左移4位——那会溢出。正确的做法是先按传感器量程做标度变换,再转Q15。比如说,你更关心加速度的零偏和噪声,那就该以灵敏度为单位换算,而不是一味追求满幅。
实际落地时,我习惯在工程里统一封装三个函数:sensor_to_q15()、q15_to_float()、float_to_q15(),所有模块只跟Q15打交道,隔离硬件差异。这样调试的时候只需要看一个地方的定标代码,省心很多。
3.4 编译选项与链接优化:从-O0到-Ofast差距有多大
同样的CMSIS-DSP源码,不同编译选项下性能可能差出一个量级。在Arm Compiler 5(AC5)下,我常用-O3 -Otime;在Arm Compiler 6(AC6)下用-O2或-O3;在GCC下用-O2甚至-Ofast。
但-Ofast有风险,它会把-ffast-math也开起来,忽略IEEE浮点的严格语义。CMSIS-DSP源码里不少地方对NaN和Inf的处理是依赖标准浮点语义的,比如arm_rms_f32里如果输入有NaN,-Ofast下可能会因为假设“无NaN”而算出错误结果。更稳妥的是开-O3 -fno-math-errno,或者单独针对某个DSP C文件用#pragma GCC optimize ("O3"),这样能拿到大部分优化收益,还不至于破坏数值行为。
还有一个技巧是检查编译器是否启用了DSP扩展指令。一个常见错误是,在Cortex-M4上写代码,但编译时没指定-mcpu=cortex-m4,编译器可能默认按M3生成指令,结果就是__SSAT、__SMLAL这些优化全部失效,性能大打折扣。AC6下写:
#pragma GCC push_options #pragma GCC target ("arch=armv7e-m+simd") ... #pragma GCC pop_options或者直接在工程选项里指定正确的CPU类型。这个排查往往比优化算法本身更见效。
4. 我踩过的坑:常见问题与排查实战
4.1 HardFault的三种典型“死法”
CMSIS-DSP引发HardFault,我总结下来基本三种:未对齐访问、越界写、用未初始化实例指针。
未对齐访问最隐蔽。Cortex-M0/M0+对未对齐访问会直接fault;M3/M4虽然支持部分非对齐访问,但如果你开了MPU,超出的非对齐访问照样fault。CMSIS-DSP里要求4字节对齐的函数很多,尤其是带q31、f32的向量处理和矩阵运算。排查办法很简单:断点停在fault handler里,看PC寄存器和BFAR(只对某些fault有效),也就是“谁在什么地址访问了什么”,然后reverse check你的缓冲区地址是否%4==0。
越界写也常见。比如FIR状态缓冲区numTaps + blockSize - 1,很多人算错,导致pState写到别的数组上。这类型bug最恶心的地方在于,溢出不一定会立刻挂,而是过了几十万个周期后,某个滤波器系数的值被悄悄改了,输出突然不对。我处理过的一个案例:一条电机控制板的振动数据,前10分钟完全正常,之后出现尖峰,最后发现是另一个模块的arm_conv_opt_q15状态数组越界,把FIR系数表覆盖了。我的排查建议:在调试阶段每个状态缓冲区后面加一两个“哨兵变量”,固定写成0xDEADBEEF,周期性检查哨兵是否被改,能快速定位谁越界了。
未初始化实例指针就更低级了。CMSIS-DSP很多函数的第一个参数是指向实例结构体的指针,如果你忘了调用对应的arm_xxx_init_xxx,结构体里的pCoeffs、pState全是随机值,运行直接HardFault。养成习惯:新建模块时,先把init函数写了再写算法调用。
4.2 FFT结果“看起来不对”的定位流程
“FFT出来的频谱不对”是社区里提问率最高的问题。按照我的经验,80%出在输入定标上,15%出在缓冲区和点数不匹配,只有5%是库本身有问题。
定位流程:
- 先把输入信号改成纯正弦波,频率和采样率已知,幅值调到满幅的90%。
- 打印FFT输出,找到峰值幅度对应的bin序号(频率分辨率 = 采样率 / FFT点数)。
- 看该bin幅度和理论值是否匹配。如果偏小很多,大概率是缩放出错;如果峰值出现在不该出现的位置,大概率是输入buffer的点数或者采样率算错。
- 检查
ifftFlag和bitReverseFlag是否设对了。做FFT时ifftFlag=0、bitReverseFlag=1;做IFFT时反过来。搞反了,输出可能是时间反转或者共轭的结果,频谱形状乱成一团。 - 定点库的话,再叠加一个缩放因子的检查。
还有一个非常实用的技巧:用MATLAB或Python(numpy.fft)跑同一个输入信号,把输出和单步调试抓到的CMSIS-DSP输出做对比,前十几个样本应该完全一样(浮点库),如果不一样就说明数据定标有问题。这个对比方法我第一次用时直接帮我省了两天时间。
4.3 性能不达标:先看优化选项再看算法复杂度
不少人在M4上做1024点实数FFT,发现耗时几毫秒,离设计指标差一个数量级。遇到这种情况,先别急着优化算法,按下面这张表逐项排查:
| 检查项 | 期望值 | 如果没达到 |
|---|---|---|
是否启用了-O3/-O2 | 比-O0快5~10倍甚至更多 | 优先先开优化 |
| 是否启用FPU | M4F浮点运算单周期 | 检查-mfpu=fpv4-sp-d16和SCB->CPACR |
| 是否启用DSP扩展指令 | 饱和、SIMD类指令可用 | 确认编译CPU类型为cortex-m4等 |
| FFT点数是否合适 | 大点数耗时指数上升 | 考虑分段FFT或降点数 |
| DMA/中断是否频繁打断 | 核心运算不被频繁抢占 | 算FFT时短暂关中断或用专用缓冲 |
这里我要特别强调CPACR。Cortex-M4F/M7的FPU默认可能是关闭的,需要设置SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2))来使能。如果没使能,代码一执行浮点指令就进UsageFault,很多人误以为是硬件有问题。另外,RTOS里做FPU上下文切换时,要确保任务栈空间足够——FPU的寄存器现场很大(M4F是32个S寄存器加FPSCR),栈开小了会栈溢出,直接HardFault。我建议每个RTOS任务的栈至少加大128字节(没使能FPU时另算)。
4.4 工具链切换:从ARM Compiler 5到ARM Compiler 6的真实体验
这两年不少项目从AC5切到AC6,CMSIS-DSP的代码也要跟着验证。AC6的优化能力明显更强,但兼容性有几个坑:
第一,AC6默认按C11/C++11标准,老代码里一些隐式转换警告会变Error,尤其是arm_math.h里大量把浮点常量赋值给Q15/Q31的写法,经常报“warning: implicit conversion”。这是好事,提醒你注意类型安全,但改起来工作量大,别指望直接过编译。
第二,AC6对__STATIC_INLINE的处理和AC5略有差异。CMSIS头文件里很多static inline函数在AC5下如果未使用,编译器不生成代码;AC6也基本如此,但在某些-O0模式下会生成未使用的静态函数,导致代码体积变大。如果你发现固件Flash占用突然飙升,先查编译选项,把-Oz(优化尺寸)打开。
第三,链接脚本的--keep、--fission这类选项两边不通用,从一个工具链迁到另一个时,最好重新生成链接脚本,不要拷贝旧的。一个老项目迁移到AC6后,启动文件里中断向量表被链接器乱序,结果所有中断都指向了默认handler,就是这个问题。
切工具链这事,我的建议是:在项目早期就定好工具链,不要在量产前频繁换。如果非要换,至少预留两周回归测试时间,单独盯着DSP相关模块跑满24小时。
5. 这个库还能怎么用:从源码审计里挖出来的进阶思路
最后分享两个从源码审计里得到的启发,不算总结,算是给以后做DSP固件时的储备。
第一,CMSIS-DSP的源码是极好的“Cortex-M性能调优教材”。你盯着arm_mat_mult_f32的循环看,能看到它如何安排循环展开、如何用__SIMD32一次读两个数据、为什么要用*pSrcA++这种指针自增而不是数组下标。这些技巧几乎通用于所有嵌入式算法实现。我后来手写一个矩阵乘,参考官方写法,性能从刚过验收变成直接富余三倍,很值。
第二,官方库的“安全性”是相对的。它默认相信调用者传的参数是对的,不做边界检查。在对抗性强的工业环境里,关键算法入口加一层参数校验(点数、对齐、空指针)是必要的,但注意别把校验放在性能敏感的内层循环里。我一般是在init阶段校验,运行阶段不再校验,既保证了安全又不损失实时性。
如果只带走一条经验,我会说:CMSIS-DSP不是黑盒,它以源码形式开放,就是等你逐行去读的。每读懂一个函数的实现,你对Arm Cortex-M体系架构的理解就加深一层。下次遇到性能、精度、稳定性问题,先翻源码,再查硬件,最后才是上网提问。