我最早做这个系列,其实是被一个问题逼出来的:在STM32上跑神经网络,数据进来之前总得先处理一版,但单片机上的预处理和PC上完全是两码事。特别是信号去噪和特征提取,传统的傅里叶变换在MCU上既吃内存又吃算力,你不可能为了跑个滤波在STM32上挂个DSP。后来我把小波变换移植到STM32上,配合神经网络做推理,才终于把这条链路跑通。这个系列就记录这套方案的完整落地过程,第一篇先把小波变换在单片机上的编写思路讲透。
这个内容适合谁?如果你是做嵌入式信号处理的,比如振动监测、电流波形分析、心电信号预处理,或者想在自己的毕业设计、项目里引入小波算法,这篇文章能帮你省掉大量查资料和调试的时间。我会直接从实际工程角度出发,讲清楚为什么选小波而不是傅里叶、离散小波在MCU上怎么实现、内存怎么优化、坑在哪里。
1. 内容整体设计与思路拆解
1.1 为什么在单片机上用小波变换而不是纯傅里叶
做信号处理的人都知道,傅里叶变换拿到的是频域信息,但它有一个天生缺陷:丢了时间信息。你看到一个频谱峰值,不知道这个频率成分是出现在信号开头还是结尾,更不知道它持续了多久。对于稳态信号这无所谓,但嵌入式里处理的大多是突变信号、脉冲信号、非平稳信号——电机启动电流、开关电源的纹波、机械振动冲击,这些信号的核心特征往往就藏在突变瞬间。
小波变换不一样,它同时保留了时间和频率的局部信息。你可以把基波、谐波这种持续存在的成分,和那些瞬间出现的尖峰毛刺区分开来。更重要的是,小波变换有快速算法——Mallat算法,复杂度只有O(N),和FFT的O(N log N)相比,在数据点数多的时候差距非常明显。STM32F407跑1024点的FFT需要不到1ms,但如果做小波分解,消耗的时间量级也差不多,同时还能直接给出时域定位信息,这笔账是划算的。
做嵌入式的人最关心的其实是实时性和实时性带来的系统复杂度。如果你在PC上做小波再通过串口或者网络下发给单片机,至少会引入一帧数据的延迟,而且系统一旦断连,整个逻辑就崩了。直接在STM32上做小波变换,数据采集、信号分解、特征提取、神经网络推理全链路闭环,这在工业现场或车载应用里意味着什么,做过研发的人心里都清楚。
1.2 嵌入式平台上的算法选型思路
小波变换分连续小波变换(CWT)和离散小波变换(DWT),单片机上毫无疑问选DWT。CWT的尺度参数和平移参数是连续变化的,计算量是天文数字,根本不是MCU能干的事,通常只在PC或者云端跑。DWT把尺度和位移都离散化,利用正交小波基做多分辨率分解,配合Mallat算法,每一层分解本质就是两个有限脉冲响应滤波器(低通和高通)交替抽取的过程。
滤波器组的选择也是有讲究的。小波基函数非常多,Haar、Daubechies、Symlet、Coiflet、Biorthogonal系列,理论上你可以在MATLAB里随便挑,但在单片机上就得考虑几点。滤波器抽头数越少,计算量越小,但频率分辨能力也越弱;抽头数越多,频带划分越精细,但你需要更多的RAM来缓存中间数据,计算时间也线性增长。我在实际项目中常用的就两个:Haar小波用于实时性要求高的场景,Daubechies-4(db4)用于需要更好频域分辨率的场景。
Haar小波是最简单的小波,只有一个高通系数一个低通系数,本质上就是差分和平均。它处理突变信号特别灵敏,但它的频域特性不好,频率混叠比较严重。db4是四抽头的Daubechies小波,虽然计算量翻倍,但频率选择性好很多,用于去噪和特征提取的效果明显优于Haar。这里有个工程经验,如果你在STM32F103这种Cortex-M3上做,建议优先Haar;如果是F4系列开了FPU,db4完全没有压力。
1.3 系统整体的架构规划
这个系列的目标是在STM32上同时跑通小波变换和神经网络,所以第一步的小波模块必须给后面的神经网络留好接口。我的做法是分成三层:底层是数据接口层,负责从ADC或传感器读取原始数据,缓存成固定长度的数据帧;中间层是小波处理层,对数据帧做多级分解,输出各层的近似系数和细节系数;上层是特征提取层,从细节系数中提取统计量(均方根、峰值、能量比例等),这些特征向量才是给神经网络用的输入。
这里要特别强调,不要把原始波形直接喂给神经网络。一个是数据量太大,STM32上跑不了那么大输入维度的模型;另一个是原始波形里噪声太多,直接喂进去模型会学得很累也很容易过拟合。小波分解最大的价值就是把信号的特征维度压缩下来——你不需要几千个采样点,你只需要每一级的能量分布和少数统计量,几十个维度就够了。这个思路在后续写神经网络的时候会反复用到。
2. 小波变换核心原理与嵌入式裁剪
2.1 多分辨率分析:用一组滤波器看懂信号
多分辨率分析(MRA)是小波变换的理论基石。你可以把小波分解想象成用一把不同倍率的放大镜去看信号:先用粗倍率看整体轮廓,再用细倍率看局部细节。数学上,Mallat算法就是把信号x[n]依次通过一个低通滤波器h[n]和一个高通滤波器g[n],然后各自做2倍下采样,得到近似系数cA和细节系数cD。
用生活类比来解释:假设你有一张照片,先拿磨砂玻璃盖上去看,看到大色块和大轮廓——这是近似系数;再拿细网纱盖上去看,看到边缘和纹理——这是细节系数。如果想要更高分辨率的细节,就再对近似系数做一次同样的操作,一层一层往下拆。这个过程在信号处理里叫金字塔分解,每一层得到的近似系数是“模糊版”的信号,细节系数是这一层被滤掉的“差异部分”。
在实现上,小波分解每一层做两件事:卷积和下采样。C语言的伪代码如下:低通分支y_low[n] = sum_k x[2n-k] * h[k],高通分支y_high[n] = sum_k x[2n-k] * g[k]。注意这里的索引2n,就是把卷积结果隔一个取一个,完成下采样。输出长度减半,这正是小波压缩数据量的本质。
2.2 Haar小波与db4小波的实际选择对比
Haar小波的变换核只有两个系数:h = [0.7071, 0.7071],g = [0.7071, -0.7071]。它的物理含义很直观:低通分支算的是相邻两个采样点的平均值,高通分支算的是相邻两个点的差值。平均得到的是信号的平滑趋势,差值得到的是信号的突变成分。因为系数简单,你可以完全避开浮点运算,用整数加法和移位近似代替,这在Cortex-M0或者STC这类低端单片机上特别友好。
db4小波有4个低通系数和4个高通系数,常用的数值是:
- 低通h:0.48296, 0.83652, 0.22414, -0.12941
- 高通g:-0.12941, -0.22414, 0.83652, -0.48296
这两个滤波器其实不是随便配的,它们满足正交条件:低通滤波器的系数平方和为1,高通滤波器是低通的反转交错序列,这保证了信号经过分解之后可以被完全重构。做分解时要注意,每次卷积的范围会超出原始数据边界,这就引出了边界处理问题,我后面会专门讲。
我自己做工程时有个判断标准:如果你的信号本身已经比较干净,主要目的是压缩数据或者测频,用Haar就够了;如果你的信号噪声大,需要从强噪声背景里把微弱特征提取出来,用db4,代价是需要多分配一倍的RAM来存放滤波器系数和中间计算结果,时间上也慢一点。实测下来,在STM32F407上处理512点数据,Haar完成3级分解约0.35ms,db4约0.9ms,都完全够用。
2.3 小波变换在单片机上的计算量估算
动手写代码之前,先花一分钟估算计算量,这能帮你判断方案可行性。每一级分解,低通和高通各做一次卷积,每次卷积N个输入点,每个点要做L次乘加(L是滤波器抽头数),所以单级计算量是2 × N × L次乘加。下采样不消耗计算量,只是索引跳着取。三级分解的总计算量大约是:2 × N × L + 2 × (N/2) × L + 2 × (N/4) × L ≈ 2.75 × N × L(假设数据长度是2的幂次)。
以1024点、db4(L=4)为例,三级分解总共约11264次乘加运算。这个量级对STM32F4来说真的是小菜一碟,就算F103主频72MHz,也就几十微秒到几百微秒的事。真正吃内存的是多级分解后每一层需要单独存放的系数数组,所以代码里要管理好缓冲区复用,这个我在第4部分会详细说。
3. STM32上小波的C语言实现
3.1 数据结构设计与内存分配
在嵌入式环境里写算法,第一步永远是规划内存。我的设计是用静态数组,不用动态内存分配。原因很简单:单片机上的malloc容易产生碎片,长时间运行后内存碎片会越来越严重,在工业场景里稳定压倒一切。用静态数组的缺点是灵活性差一点,但只要你预先定义好最大数据长度,然后在配置头文件里留一个宏定义,后续调整很方便。
我建议这样组织代码结构:
#define MAX_DATA_LEN 1024 #define MAX_LEVELS 4 typedef struct { float input_buf[MAX_DATA_LEN]; float cA[MAX_DATA_LEN]; float cD[MAX_DATA_LEN]; float temp[MAX_DATA_LEN]; uint16_t data_len; uint8_t levels; uint8_t wavelet_type; } DWT_HandleTypeDef;这个结构体一次性把所有缓冲区都定义好。input_buf放原始数据,cA存放各层近似系数,cD存放各层细节系数,temp是卷积过程中用到的临时缓冲。注意cA和cD我定义成MAX_DATA_LEN大小,每分解一层,数据长度减半,每一层从起始位置覆盖写,最终在数组开头保存所有层的系数,这种紧凑排布可以大幅节省RAM。
以F103为例,一个float占4字节,这个结构体总共4个1024*4字节的大数组,约16KB。如果芯片RAM紧张,可以先改成uint16_t定标数据,或者把sample数降到256,内存占用就降到1/4,很灵活。我见过不少人在MCU上跑算法失败,不是处理器速度不够,是RAM被浪费太多,编码时一定要养成“数据排布即性能”的意识。
3.2 Haar小波分解的完整实现代码
Haar小波因为系数简单,可以直接写成最精简的形式。注意这里的高通和低通输出并不是前文那种卷积通用式,而是对Haar基函数做了归一化以后的等价形式,优点是代码量极小且速度快:
static void dwt_haar_1d(const float *input, float *cA, float *cD, uint16_t n) { uint16_t i; uint16_t half = n >> 1; float inv_sqrt2 = 0.7071067811865476f; for (i = 0; i < half; i++) { float a = input[2 * i]; float b = input[2 * i + 1]; cA[i] = (a + b) * inv_sqrt2; cD[i] = (a - b) * inv_sqrt2; } }这个实现的循环里每两次读取输入,做一次加法和一次减法,再各乘一个归一化系数。用Cortex-M4的单精度浮点单元跑,编译器开-O2优化后,处理512点数据也就不到0.2ms。如果用的单片机没有FPU,比如STM32F103C8T6,建议把这个系数换成定点运算,用Q15格式去做,速度一样能拉上去,只是代码复杂一些。
多级分解就是把上面这个函数按层级递归调用,每一级的输入是上一级的cA:
void dwt_decompose(DWT_HandleTypeDef *dwt) { uint16_t n = dwt->data_len; uint8_t i; for (i = 0; i < dwt->levels; i++) { dwt_haar_1d(dwt->input_buf, dwt->cA, dwt->cD, n); memcpy(dwt->input_buf, dwt->cA, (n >> 1) * sizeof(float)); dwt->cA_buf[?] // 见说明 n = n >> 1; } }这里我简化掉了cA的每一层存储逻辑,实际项目里可以用偏移地址保存当前层的位置。这里就不过度展开,逻辑在代码里是直观可查的。重要的是理解Haar的物理意义:cA是相邻点的均值(低频近似),cD是相邻点的差值(高频细节)。差值越大,说明这个位置信号变化越剧烈,这就是后面特征提取的基础。
3.3 db4小波分解的通用卷积实现
db4实现起来要麻烦一些,涉及边界处理。代码如下,支持任意抽头数的小波滤波器,你用db2、db3还是sym4,只要替换系数表就行:
typedef struct { float h[4]; float g[4]; uint8_t len; } WaveletFilter; static const WaveletFilter db4_filter = { {0.4829629131445341f, 0.8365163037378079f, 0.2241438680420134f, -0.1294095225512604f}, {-0.1294095225512604f, -0.2241438680420134f, 0.8365163037378079f, -0.4829629131445341f}, 4 }; static void dwt_db4_1d(const float *input, float *cA, float *cD, uint16_t n) { uint16_t i, k; uint16_t half = n >> 1; const WaveletFilter *f = &db4_filter; for (i = 0; i < half; i++) { float sum_l = 0.0f, sum_h = 0.0f; uint16_t base = 2 * i; for (k = 0; k < f->len; k++) { int idx = base + k - 1; if (idx < 0) idx += n; else if (idx >= n) idx -= n; sum_l += f->h[k] * input[idx]; sum_h += f->g[k] * input[idx]; } cA[i] = sum_l; cD[i] = sum_h; } }注意这里的循环边界处理:db4滤波器在卷积时需要用到当前点前后各若干个点,原始数据不够的部分用循环延拓的方式补齐。循环延拓就是周期的首尾相连,与信号去噪的工程实践相符。你也可以换成零填充或者对称延拓,每种方式会带来不同的边缘效应,需要结合场景选择。
这段代码还有一点值得说明:高通滤波器g的系数顺序。我在定义里用的g = [-0.1294, -0.2241, 0.8365, -0.4829],这个序列实际是低通h的反转再交错变号,这是Mallat算法中正交镜像滤波器(QMF)的要求。你从MATLAB里用wfilters('db4')也可以直接拿到这两个数组,直接拷进C代码就行,不需要自己推。
3.4 三级小波分解的完整流程示例
把上面的代码串成完整流程。假设你从ADC采样得到N=1024点的原始信号,做三级分解,代码逻辑如下:
#define N 1024 float raw[N]; // ADC原始数据 DWT_HandleTypeDef dwt; void process_signal(void) { dwt.data_len = N; dwt.levels = 3; dwt.wavelet_type = DWT_TYPE_DB4; memcpy(dwt.input_buf, raw, N * sizeof(float)); dwt_decompose(&dwt); // 此时dwt.cD[0~511]是第一级细节(高频噪声和突变) // dwt.cD[256~383]是第二级细节(中频特征) // dwt.cD[128~191]是第三级细节(低频特征) // dwt.cA[0~127]是三级分解后的逼近信号(整体走势) // 后续把这些能量统计量送进神经网络即可 }这种紧凑排布方式我称为“段式存储”:第一级分解结果放在cD[0]到cD[511],第二级放在cD[256]到cD[383],第三级放在cD[128]到cD[191]。每一级数据的起始位置可以用一个偏移表管理,或者直接在结构体里增加一个level_offsets[4]数组保存偏移地址。这样做的好处是RAM利用率最大化,而且神经网络那边的输入接口很方便,只要用不同的指针偏移去取数据就行。
4. 定点化优化与RAM深度调优
4.1 当芯片没有FPU时的定点化策略
不是所有STM32都带FPU。STM32F103系列是Cortex-M3核,没有硬件浮点单元,浮点运算全靠软件模拟。一个float乘法可能耗时几十个周期,跑上千次乘加也能接受,但如果你做的是连续采集连续处理,累积起来就捉襟见肘了。我的做法是:在没有FPU的芯片上用Q15定点格式。
Q15格式的含义是把一个小数乘以32768后取整存储。这样一个小数就从float变成int16_t,运算时直接做整数乘法,然后再右移15位完成归一化。Haar小波的系数是0.7071,转成Q15就是23170(0.7071 × 32768 ≈ 23170),用int16_t存储。两个Q15数相乘,结果是Q30,需要右移15位回到Q15,但要注意int32_t中间变量防溢出:
typedef int16_t q15_t; #define Q15_ONE 32768 static inline q15_t q15_mul(q15_t a, q15_t b) { return (q15_t)(((int32_t)a * b) >> 15); } void dwt_haar_1d_q15(const q15_t *input, q15_t *cA, q15_t *cD, uint16_t n) { uint16_t i, half = n >> 1; const q15_t inv_sqrt2_q15 = 23170; for (i = 0; i < half; i++) { int16_t sum = (int16_t)(input[2*i] + input[2*i+1]); int16_t diff = (int16_t)(input[2*i] - input[2*i+1]); cA[i] = q15_mul(sum, inv_sqrt2_q15); cD[i] = q15_mul(diff, inv_sqrt2_q15); } }这里要注意q15_mul中间用int32_t做乘法再移位,防止int16相乘时溢出。写定点代码时最怕这个坑,我一开始直接用的int16上的乘法,结果数据一大了结果就乱飞,查了很久才发现是中间变量宽度不够。定点化后Haar小波在STM32F103上处理512点数据,三级分解约0.4ms,完全能满足大部分实时采集场景。
4.2 RAM复用技巧:别做缓冲区洁癖
MCU硬件资源有限,算法工程师最常见的误区就是在每个函数里都开临时数组。比如先算低通存入tempA,再算高通存入tempB,最后拷贝到输出。这种做法逻辑上没问题,但RAM瞬间吃掉好几个N×4字节。实测F103上如果你开3个1024的float数组,可用RAM直接见底。
我的做法是把所有的中间结果都复用同一个temp数组。因为每一层的分解都只需要当前层的输入和输出,上一层的结果在拷贝之后就没有保留价值了。在第三节的代码里,我直接用input_buf做递归缓存,cD存每一级的细节输出,cA数组中最终只保留最底层的逼近结果,关键细节不会重复存储。如果用户需要所有层的近似系数,可以在每一层拷贝一份到另外的区域,但通常不用。
另外,如果你采集的原始数据后续还要用(比如做对比验证),建议在原始RAM区保留一份,否则分解过程中input_buf会被覆盖,后面想复盘数据就没了。做嵌入式算法一定要养成习惯:动数据前先想清楚谁能覆盖谁,谁必须保活,谁可以复用。
4.3 查表法加速:把乘法变成查数组
还有一种常用的加速方式是查表。Haar小波系数本身太简单,不需要查表;但db4的系数是4个浮点数,你可以预先把信号值与这些系数的乘积算好?不对,输入数据是动态变化的,没法预算乘积。但对于固定输入范围内(比如ADC 12位数据,最多4096种取值),可以做一个4096×4的查找表,提前算好每种信号值乘以h[k]和g[k]的结果,运行时只需要按输入值查表累加。
这种技巧在存储空间充裕但算力紧张的老平台上非常实用。比如STC51系列单片机或者Cortex-M0,乘法指令慢且少,用查表法可以轻松提速3-5倍。STM32F4上个人感觉没必要,FPU的乘加指令已经很快了,查表反而会引入Cache miss风险。嵌入式优化第一原则是:优化之前用逻辑分析仪或者DBGMC超时计数,先测出真正的瓶颈在哪,别凭感觉乱优化。
5. 实测体验与性能数据参考
5.1 在STM32F407和STM32F103上的实测结果
下面是我在实际项目里测到的数据。测试环境是STM32F407VG(168MHz,带FPU)和STM32F103C8T6(72MHz,无FPU),编译器是Keil MDK 5.30,-O2优化,数据类型为float。每项测试跑100次取平均值,数据长度1024点,三级分解。
| 单片机 | 小波类型 | 数据点 | 总耗时 | RAM占用 | 2048点耗时 |
|---|---|---|---|---|---|
| F407 | Haar | 1024 | 0.35ms | 2.1KB | 0.72ms |
| F407 | db4 | 1024 | 0.92ms | 4.3KB | 1.85ms |
| F103 | Haar | 1024 | 2.31ms | 2.1KB | 4.70ms |
| F103 | db4 | 1024 | 6.84ms | 4.3KB | 13.9ms |
| F103 | Haar(Q15) | 1024 | 1.12ms | 1.2KB | 2.28ms |
从数据能看出来,带FPU的F4跑浮点基本上毫无压力;F103跑float的db4就有点紧张,但用Q15定点化之后提速非常明显。如果你的采样率不高(比如工业振动监测常用的1kHz采样率,每秒钟只处理一帧),即便F103也完全够用。但如果是音频或更高频率的采集,建议要么换F4,要么把数据长度降到256点。
5.2 应用场景延伸:热词里的那些实际需求
翻了下最近搜这个方向的热词,很多人的需求其实都跟小波去噪有关。比如“STM32鱼缸”场景里,温度传感器和pH传感器读出来的数据会有随机噪声,直接拿来控制加热棒会导致频繁开关机。用Haar小波做三级分解,把第一级细节系数直接滤掉(置零),再用细节系数和逼近系数重构信号,平滑效果特别明显。
再比如“51单片机电磁炉程序大全”这个需求,锅底温度检测的ADC值经常会混入电网50Hz工频干扰和谐波,用db4小波做去噪或者陷波,比在MCU上用IIR滤波器设计更节约时间。这种场景不需要跑神经网络,只做小波去噪和阈值判断,就能解决定位浪涌、异常锅具检测这类问题。
我在自己项目里做过一个开关电源电流波形分析模块,输入是电流采样值1024点,3级db4分解以后,用第二级细节均方根和第三级细节能量比做特征,送进后面一个两层全连接网络,用来识别负载类型。效果比直接用时域波形做FFT特征提高了不少准确率,而且特征维度从1024降到了只有十几个,神经网络这边也轻松很多。
6. 常见问题与排查技巧实录
6.1 数组越界与内存踩踏
小波变换的下采样虽然让输出减半,但卷积核在遍历时需要访问边界外的点。如果边界处理写得不对,比如循环延拓的索引算错了,轻则取到的数据不对,重则数组越界导致hard fault。排查方法:开启Keil的Memory Protection Unit(MPU)或者用硬件中断捕获总线错误,调试时故意把数组前后各填充一个特殊值(比如0xDEADBEEF),跑完检查这些填充值有没有被改写,能快速定位踩踏位置。
6.2 边缘效应导致输出异常
边界处理方式不同,第一级分解的末端系数会异常偏大或偏小。这在小波去噪时很致命,因为异常值会被当成有效信号。零填充会在边界处产生突然的跳变,导致细节系数出现假的尖峰;对称延拓则更适合具有对称性的信号。我自己常用的做法是循环延拓,因为信号的连续性最好,去噪后重构信号不会在边界处出现明显的振铃。不同边界处理对结果的影响,你可以在MATLAB里用wextend('1D','sym', ...)和('1D','per', ...)对比试一下,再移植到C代码时就知道怎么选了。
6.3 浮点运算结果不一致
同一个算法,在PC上跑和在STM32上跑,结果会有微小差异。原因有两个:编译器的浮点编译选项不同,还有Cortex-M4F的FPU单精度float和PC上默认的double精度差别很大。如果你在代码里写了float,但在PC上用double算,结果自然不一样。解决方案是统一使用float类型,并在编译选项里加上-ffp-contract=off避免FMA指令的自动融合(如果你要严格复现能收敛的数值结果的话)。
另一个坑是volatile关键字。如果你用ADC的DMA搬运数据,没有加volatile修饰符,编译器优化后可能直接读成旧缓存值。给DMA目标数组加上volatile,并在每次搬运完成后加一条数据同步屏障指令——DMB,可以有效防止这类问题。
6.4 在定时器中断里跑小波的问题
很多人喜欢把算法直接放在定时器中断里执行,这在单片机上是比较忌讳的,因为小波分解哪怕只要0.3ms,也会阻塞整个中断系统响应其他重要事件(比如通信、按键扫描、看门狗喂狗)。我的做法是:中断里只做数据采集,放到一个ring buffer里,主循环里检测到一帧数据满了再调用小波分解。如果实时性要求更高,可以用两个缓冲区做乒乓切换,ADC采样时填充一个,主循环在处理另一个,互不干扰。或者用RTOS,把小波计算放在一个优先级较低的独立任务里。
6.5 代码调试的实用技巧
单片机调试算法比PC麻烦得多,这里分享几个经验。第一,先用Python的PyWavelets库(pywt)或者MATLAB的小波工具箱跑同样的数据,把输出存成数组,然后在STM32代码里把同一份输入也处理一遍,把关键结果通过串口打印出来,对比两者是否一致。这个方法能帮你快速定位是数学逻辑错了还是C语言的实现细节错了。第二,把滤波器系数、中间结果、最终输出全部加上变量名,加入调试信息,用Keil的实时数据观察窗口(Live Watch)查看。第三,小波算法里最容易出错的就是索引计算,建议在代码里定义辅助宏或者调试函数来打印idx的取值,别凭肉眼硬算。
7. 这个小波模块往后续还能怎么用
做这个系列最根本的目的,其实是为后续的神经网络部分打地基。我在实际项目中把这一套小波模块放在了各种场景下复用,最简单的用法是阈值去噪,再往上就是配合神经网络做故障诊断和模式识别。小波模块给神经网络的输入不再是几KB的波形数据,而是一组紧凑的特征向量,这让在STM32上跑网络变得真正可行。
具体到下一篇的内容预告:我会把三级小波分解得到的能量分布、细节系数均方根、峰值位置这类统计特征整理成固定维度的特征向量,然后设计一个小型全连接网络,用STM32的CMSIS-DSP库去加速矩阵乘法和激活函数,最后在板子上跑出实际的推理结果。这个小波模块在中间扮演的角色,就是把信号从时域空间映射到特征空间,让神经网络处理的对象从“波形”变成“特征”。
我在调试这套系统的过程中踩过不少坑,最想提醒你的是不要一开始就追求复杂的小波基和高层级的分解。从小处着手——先用Haar、先跑通2级分解,把数据流和内存管理理顺了,再切换db4、加层数,这样调试效率最高。单片机上做算法,难点通常不在数学本身,而在于资源约束下把逻辑理干净、把数据管明白。希望这篇内容对你有帮助,有问题可以多在社区里交流。