简介:本资源是一套基于STM32平台的嵌入式频谱分析与波形识别完整工程,面向嵌入式初学者、电子类课程设计学生及STM32进阶开发者,解决低频段(50Hz–200Hz)信号实时分析与常见周期波形自动判别难题。压缩包含395个文件,以85个C源文件和81个头文件构成核心算法与驱动逻辑,辅以51个编译中间文件(.o/.d)、47个调试信息文件(.crf)及TFTLCD显示、触摸屏交互、定时器采样控制等模块代码;整体9.78MB,结构清晰,适配正点原子精英板并支持直接烧录运行。已有33305人学习下载,体现其在教学实践与毕业设计中的高复用价值。用户可获得双版本工程:基础FFT频谱分析版(输出基频及3/5/7次谐波峰值),以及支持触屏动态调节采样频率的优化版(提升频率分辨率与识别精度),同时集成正弦/方波/锯齿波/三角波四类波形识别功能,配套工程已通过Keil MDK完整构建验证。
1. 这不是示波器,但能实时“听懂”正弦波、方波、三角波:一个跑在STM32F407上的轻量级频谱分析+波形分类系统
你手头有一块带ADC的STM32F407开发板,想快速验证一段模拟信号里有没有50Hz工频干扰?能不能区分电机启动时的冲击电流波形和稳态运行时的正弦电流?又或者,想在嵌入式端不依赖PC,直接对传感器采集的振动信号做频域特征提取,为后续阈值告警或简单模式识别打基础?这个项目就是为你写的——它把完整的FFT频谱计算(1024点实数FFT)、幅度谱归一化、主频提取、以及基于时域+频域双特征的三类波形(正弦/方波/三角波)在线识别逻辑,全部塞进了STM32F407ZGT6的片上RAM(仅占用约28KB)和Flash(约96KB),主频72MHz下单次分析耗时<12ms。它不追求高精度仪器级指标,但足够稳定跑在裸机环境,代码结构清晰、模块解耦、注释密集,特别适合嵌入式初学者理解信号处理落地链路,也方便进阶者替换自己的特征工程或分类模型。如果你正在做电机状态监测、电源质量分析、或低成本振动诊断类毕业设计/原型开发,这份资源能让你跳过FFT库移植踩坑、ADC采样同步玄学、以及浮点运算性能焦虑,直接拿到可运行、可调试、可改写的核心骨架。
2. 工程结构与技术栈:从CubeMX配置到核心算法分层
2.1 整体架构:五层分离,每层职责明确
整个工程采用典型的嵌入式信号处理流水线设计,分为硬件抽象层(HAL)、数据采集层(ADC+DMA)、预处理层(滤波+窗函数)、频谱计算层(FFT+幅值计算)、以及识别决策层(特征提取+规则分类)。这种分层不是为了炫技,而是为了让你能独立调试每一环:比如先屏蔽FFT,用ADC采集的原始数据点灯验证采样是否正常;再打开窗函数,用串口打印加窗前后首尾数据对比,确认汉宁窗生效;最后才接入FFT,避免问题交织成黑匣子。所有层之间通过结构体指针传递数据,无全局变量污染,符合MISRA-C基础规范。
2.2 CubeMX关键配置:DMA双缓冲+定时器触发,拒绝采样抖动
ADC必须工作在连续扫描模式,且绝对禁止使用HAL_ADC_Start()轮询启动——这会导致采样间隔严重不均。本项目采用TIM2定时器(10kHz频率)触发ADC规则组转换,配合DMA循环模式(Circular Mode)+双缓冲(Double Buffer)实现无缝数据流。关键配置如下:
- ADC1:Resolution=12bit,Data Alignment=Right,Scan Direction=Forward
- DMA1_Stream0:Memory Data Size=Half Word(16bit),Peripheral Data Size=Half Word,Circular Mode=Enabled,Double Buffer=Enabled
- TIM2:Prescaler=71,Counter Period=99 → 72MHz/(72×100)=10kHz触发频率
- NVIC:使能DMA Stream0中断(用于双缓冲切换通知),禁用ADC中断(避免干扰DMA流程)
提示:双缓冲地址需手动在
MX_DMA_Init()中绑定,CubeMX GUI不自动生成。初始化后,adc_buffer_a和adc_buffer_b分别指向两块1024元素的uint16_t数组,DMA自动在二者间翻转填充,你在中断里只需交换指针并标记新缓冲区就绪。
2.3 FFT实现:CMSIS-DSP库的正确打开方式
项目未手写FFT,而是调用ST官方CMSIS-DSP库的arm_rfft_fast_f32()函数,但做了三处关键适配:
- 输入数据类型转换:ADC原始数据是
uint16_t,需映射到float32_t范围[-1.0, +1.0],公式为fval = (float32_t)(raw - 2048) / 2048.0f(2048为12bit中点); - 实数FFT长度对齐:CMSIS要求实数FFT点数为2^n,但1024点实数FFT输出513个复数(含DC+奈奎斯特),需额外申请513×2=1026个
float32_t空间存实部/虚部; - 内存对齐强制:
arm_rfft_fast_instance_f32结构体及输入/输出数组必须按32字节对齐,否则FFT结果全乱。在.ioc文件中启用__ALIGN_BEGIN宏,或在定义时显式添加__attribute__((aligned(32)))。
// 关键对齐声明(放在全局变量区) __ALIGN_BEGIN float32_t adc_f32[1024] __ALIGN_END; __ALIGN_BEGIN float32_t fft_output[1026] __ALIGN_END; // 实部虚部交错存储 __ALIGN_BEGIN arm_rfft_fast_instance_f32 S __ALIGN_END; // 初始化FFT实例(在main()中调用一次) arm_rfft_fast_init_f32(&S, 1024);2.4 波形识别逻辑:时域过零率+频域主频能量比双特征判决
识别不依赖机器学习,而是用两个轻量级特征:
- 时域特征:过零率(Zero-Crossing Rate, ZCR)
统计相邻采样点符号变化次数,正弦波ZCR≈2,方波接近2但波动小,三角波因斜率缓和ZCR略低(约1.7~1.9)。计算时先中心化(减均值),再遍历比较sign(adc[i]) != sign(adc[i-1])。 - 频域特征:主频能量占比(Dominant Frequency Ratio, DFR)
对FFT幅度谱(sqrt(real²+imag²))求最大值索引idx_max,再计算该点能量占前10个峰值能量总和的比例。正弦波DFR>0.85,方波因谐波丰富DFR≈0.4~0.6,三角波介于两者之间(0.6~0.75)。
最终判决表硬编码在waveform_classifier.c中,支持动态调整阈值:
| 波形类型 | ZCR范围 | DFR范围 | 置信度权重 |
|---|---|---|---|
| 正弦波 | 1.95~2.05 | >0.85 | 0.7 |
| 方波 | 1.90~2.00 | 0.40~0.60 | 0.9 |
| 三角波 | 1.70~1.85 | 0.60~0.75 | 0.8 |
3. 编译与烧录:Keil MDK-ARM v5.37下的零配置启动
3.1 工程导入与编译选项设置
下载解压后,打开Core/Project.uvprojx(Keil v5工程)。无需修改任何路径,所有CMSIS库、HAL驱动、用户源码均已按相对路径组织。重点检查以下三项:
Options for Target → Target → XRAM:勾选Use Memory Layout from Target Dialog,XRAM Size设为0(本项目不外扩RAM);Options for Target → C/C++ → Define:确保已定义ARM_MATH_CM4,__FPU_PRESENT=1,USE_HAL_DRIVER;Options for Target → Linker → Use Memory Layout from Target Dialog:勾选,Flash起始地址0x08000000,大小512KB(F407ZGT6规格)。
编译成功后,Build Output窗口应显示:
Program Size: Code=95240 RO-data=4232 RW-data=2840 ZI-data=12480 Total=114824其中ZI-data(零初始化数据)12.2KB即为运行时RAM占用,远低于F407的192KB RAM上限。
3.2 串口调试:用printf重定向查看实时分析结果
项目已配置ITM(Instrumentation Trace Macrocell)和SWO(Serial Wire Output)实现零开销printf调试,无需占用UART外设。操作步骤:
- 将ST-Link V2的SWO引脚(PA3)接到逻辑分析仪或支持SWO的调试器;
- Keil中
Debug → Settings → Trace → Core Clock填入72000000,勾选Trace Enable; - 在
main.c中调用ITM_SendChar('A')测试,若KeilView → Serial Windows → Debug (printf) Viewer弹出字符,即配置成功。
注意:若用普通USB转TTL模块,需改用
usart_printf.c(已提供),将printf重定向至USART1(PA9/PA10),波特率115200,需在main()中调用usart_printf_init()。
3.3 硬件接线与信号注入
- ADC输入:使用PA0(ADC1_IN0),接信号源时务必串联1kΩ限流电阻+100nF旁路电容到GND,防止过压损坏;
- 参考电压:VREF+接3.3V,VREF-接GND,确保ADC量程0~3.3V;
- 信号源建议:函数发生器输出峰峰值2Vpp正弦波(频率50Hz~5kHz),或用STM32另一路DAC输出仿真波形。首次测试推荐50Hz/1Vpp正弦波,便于肉眼核对FFT主频是否准确落在第1个bin(50Hz × 1024 / 10000 ≈ bin 5.12 → 实际取整为bin 5)。
3.4 首次运行验证:三步确认法
- ADC验证:注释掉FFT相关代码,在DMA中断里用
ITM_SendChar(adc_buffer_a[0]>>8)发送高位字节,用逻辑分析仪捕获波形,确认采样率严格10kHz(周期100μs); - FFT验证:恢复FFT,打印
fft_output[0](DC分量)和fft_output[10](对应50Hz bin的实部),输入50Hz正弦波时,DC应≈0,50Hz bin实部应显著大于邻近bin; - 识别验证:串口打印
waveform_result_t结构体,包含type(0=正弦,1=方波,2=三角波)、zcr、dfr、confidence,观察不同波形下数值是否落入理论区间。
4. 常见问题排查:那些让我熬夜到凌晨三点的血泪经验
4.1 现象:FFT输出全为0或随机大数,幅度谱无规律
原因:
- ADC数据未正确转换为float32_t,仍以uint16_t格式传入FFT函数(CMSIS会将其解释为极大浮点数);
adc_f32数组未按32字节对齐,导致FFT引擎读取内存越界;- DMA双缓冲切换时,未等待当前缓冲区填充完成就强行读取(竞态条件)。
解决: - 在
HAL_ADC_ConvCpltCallback()中添加断点,检查adc_f32[0]值是否在[-1.0, +1.0]内; - 用
printf("align=%p\n", &adc_f32);验证地址末两位是否为0x00(32字节对齐); - 在DMA中断中增加标志位
buffer_ready = 1,主循环中while(!buffer_ready);再处理,而非直接访问。
4.2 现象:波形识别结果频繁抖动,同一信号反复在正弦/方波间跳变
原因:
- 未对ADC数据做直流偏移校准,导致ZCR计算受基线漂移影响;
- 窗函数(汉宁窗)系数未归一化,造成幅度谱整体缩放,DFR阈值失效;
- 主频搜索范围过窄(仅查前200个bin),高频谐波被误判为主频。
解决: - 在预处理层增加
dc_offset_calibrate()函数:采集100ms静默期数据求均值,后续每帧减去该值; - 汉宁窗系数表
hanning_win[1024]需满足sum(hanning_win) == 1024,否则FFT后需除以sum(win)补偿; - 主频搜索扩展至
bin 1 ~ bin 512(对应50Hz~5kHz),并增加二次插值(parabolic_interpolation)提升频率分辨率。
4.3 现象:串口打印乱码,或ITM窗口无输出
原因:
- SWO引脚未正确连接(PA3悬空或接触不良);
- Keil Trace设置中Core Clock填错(如填了8MHz晶振频率而非72MHz系统时钟);
ITM->LAR = 0xC5ACCE55;未在SystemInit()后执行(调试端口未解锁)。
解决:- 用万用表通断档测PA3到ST-Link SWO引脚是否导通;
- 在
main()开头添加CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;强制使能跟踪; - 若仍无效,临时改用
usart_printf方案,确认是硬件还是软件问题。
4.4 现象:编译报错undefined reference to 'arm_rfft_fast_init_f32'
原因:
- CMSIS-DSP库未添加到工程:
CMSIS/DSP/Source/TransformFunctions/arm_rfft_fast_init_f32.c未加入Source Group; - 函数名拼写错误(如
arm_rfft_fast_init_f32误写为arm_rfft_init_f32); - 浮点ABI不匹配:Keil中
Options for Target → C/C++ → Float ABI未设为Hard(F4系列需硬件浮点)。
解决: - 在Keil Project窗口右键
Source Group 1 → Add Existing Files to Group,添加上述.c文件; - 全局搜索
arm_rfft_fast_init_f32,确认调用处与声明处完全一致; Float ABI必须为Hard,Use FPU必须为FPv4。
5. 核心模块代码逐行解析:从ADC采样到波形判决
5.1 DMA双缓冲切换中断:安全交接数据的临界区
// dma.c 中 HAL_DMA_IRQHandler 的精简版(仅保留关键逻辑) void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma) { if (__HAL_DMA_GET_FLAG(hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma))) { // 传输完成 __HAL_DMA_CLEAR_FLAG(hdma, __HAL_DMA_GET_TC_FLAG_INDEX(hdma)); // 关键:原子操作切换缓冲区指针 if (hdma->Instance == DMA1_Stream0) { if (current_buffer == BUFFER_A) { current_buffer = BUFFER_B; buffer_ready = 1; // 标记B就绪 // 启动FFT处理B缓冲区(在主循环中) } else { current_buffer = BUFFER_A; buffer_ready = 2; // 标记A就绪 } } } }逻辑说明:current_buffer是全局volatile变量,buffer_ready为1或2表示对应缓冲区已满。主循环中if(buffer_ready == 1) { process_buffer_b(); buffer_ready = 0; },避免中断与主循环同时访问同一缓冲区。此处未用RTOS信号量,因裸机下简单标志位足矣,且无上下文切换开销。
5.2 FFT幅度谱计算:避免sqrt精度损失的定点技巧
// fft_processor.c 中 magnitude_spectrum_calc() void magnitude_spectrum_calc(float32_t *fft_out, float32_t *mag_spec, uint16_t len) { for(uint16_t i = 0; i < len; i += 2) { // 实部i, 虚部i+1 float32_t real = fft_out[i]; float32_t imag = fft_out[i+1]; // 优化:用平方和代替sqrt,仅当需要真实幅度时才开方 mag_spec[i/2] = real*real + imag*imag; // 存储平方值,节省CPU周期 } }参数说明:len=1026(513个复数点),mag_spec长度为513。存储平方值而非开方后幅度,是因为后续DFR计算只需比较相对大小,且平方运算比sqrtf()快3倍以上(Cortex-M4硬件不支持单周期sqrt)。若需真实dB值,再统一sqrtf(mag_spec[i])。
5.3 双特征融合判决:带置信度加权的硬规则
// waveform_classifier.c 中 classify_waveform() waveform_result_t classify_waveform(float32_t *mag_spec, float32_t zcr) { waveform_result_t res = {0}; // 步骤1:找主频bin(跳过DC bin0,搜索bin1~bin512) uint16_t dominant_bin = 1; float32_t max_mag = mag_spec[1]; for(uint16_t i = 2; i <= 512; i++) { if(mag_spec[i] > max_mag) { max_mag = mag_spec[i]; dominant_bin = i; } } // 步骤2:计算DFR(前10个峰值能量和) float32_t top10_sum = 0.0f; // (此处省略峰值检测代码,实际用滑动窗口找局部最大值) float32_t dfr = max_mag / top10_sum; // 步骤3:双特征加权判决 if(zcr >= 1.95f && zcr <= 2.05f && dfr > 0.85f) { res.type = WAVE_SINE; res.confidence = 0.7f * 0.95f + 0.3f * (dfr - 0.85f)/0.15f; // 时域权重0.7,频域0.3 } else if(zcr >= 1.90f && zcr <= 2.00f && dfr >= 0.40f && dfr <= 0.60f) { res.type = WAVE_SQUARE; res.confidence = 0.9f * 0.8f + 0.1f * (0.6f - dfr)/0.2f; } else if(zcr >= 1.70f && zcr <= 1.85f && dfr >= 0.60f && dfr <= 0.75f) { res.type = WAVE_TRIANGLE; res.confidence = 0.8f * 0.85f + 0.2f * (dfr - 0.6f)/0.15f; } else { res.type = WAVE_UNKNOWN; res.confidence = 0.0f; } res.zcr = zcr; res.dfr = dfr; return res; }逻辑说明:confidence非简单布尔值,而是融合两个特征的加权得分(0~1.0),便于上层做阈值过滤。例如,若ZCR=1.98(正弦波典型值)但DFR=0.78(偏低),则置信度=0.7×0.95+0.3×(0.78−0.85)/0.15≈0.665,低于0.7阈值可判定为“疑似正弦需复核”。
6. 二次开发指南:如何接入你的传感器、替换算法、或升级为AI识别
6.1 接入新传感器:只需改三处,不碰FFT内核
假设你要接入MPU6050加速度计,目标是分析振动频谱:
- 硬件层:将MPU6050的INT引脚接至STM32任意GPIO(如PB0),配置为下降沿中断;
- 采集层:在
mpu6050_read_acc_xyz()函数中,每次中断读取3轴数据,按时间顺序存入adc_buffer_a(覆盖原ADC数据),注意单位转换(MPU6050原始值需×0.000061g/LSB); - 预处理层:在
preprocess_data()中,将加速度数据减去静态重力分量(如Z轴均值≈16384),再进行高通滤波(一阶数字滤波器y[n] = 0.95*y[n-1] + 0.05*(x[n]-x[n-1]))去除低频漂移。
血泪经验:MPU6050默认ODR=1kHz,但本项目FFT要求10kHz采样率,需在初始化时配置
setRate(100)(若支持)或外接更高频传感器。若无法提速,可将FFT点数降至256,保持1kHz采样率,此时频谱分辨率=1000/256≈3.9Hz,仍够用。
6.2 替换FFT为更优算法:用CMSIS的arm_cfft_radix4_f32提速
arm_rfft_fast_f32虽方便,但Radix-4 CFFT在1024点时快15%。替换步骤:
- 将实数输入
adc_f32[1024]补零成复数数组complex_in[1024](虚部全0); - 调用
arm_cfft_radix4_init_f32(&S, 1024, 0, 1)(0=forward, 1=bit-reversal); - 执行
arm_cfft_radix4_f32(&S, complex_in); - 幅度谱计算同前,但复数输出为
complex_in[i].real/complex_in[i].imag。
注意:Radix-4要求点数为4^n(1024=4^5),且需手动做位逆序排列,CMSIS已封装在init函数中。
6.3 升级为轻量级AI识别:用TensorFlow Lite Micro部署TinyML模型
若想用CNN识别更多波形(如锯齿波、脉冲波、噪声),可集成TFLite Micro:
- 模型训练:用Python生成1024点波形数据集(含噪声),训练TinyML模型(输入1024×1,输出4类),量化为int8;
- 模型部署:将
.tflite模型转为C数组(xxd -i model.tflite > model_data.cc),放入Core/Model/; - 推理集成:在
classify_waveform()末尾,若规则判决为WAVE_UNKNOWN,则调用tflite::MicroInterpreter推理:
// 伪代码 TfLiteStatus status = interpreter->Invoke(); if(status == kTfLiteOk) { TfLiteTensor* output = interpreter->output(0); int8_t* pred = output->data.int8; res.type = argmax(pred, 4); // 取最高概率类别 res.confidence = softmax_score(pred[res.type]); // 计算softmax置信度 }资源消耗:TFLite Micro在F407上运行1024点CNN约需45KB Flash+12KB RAM,与原规则引擎共存无压力。
6.4 性能调优终极技巧:关闭浮点单元异常,释放最后10%算力
Cortex-M4默认开启浮点异常(如除零、溢出),每次异常触发中断,耗时数百周期。对于FFT这类确定性计算,可安全关闭:
// 在main()开头添加 SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2)); // 启用CP10/CP11 __set_FPSCR(__get_FPSCR() & ~(0x03UL << 24)); // 清除AZ(Flush-to-zero)和DN(Default-NaN)位 // 关键:禁用所有浮点异常 FPU->FPCCR &= ~FPU_FPCCR_ASPEN_Msk; // 禁用自动状态保存 FPU->FPCCR &= ~FPU_FPCCR_LSPEN_Msk; // 禁用懒惰状态保存实测关闭后,1024点FFT耗时从11.8ms降至10.6ms,提升10.2%。这招在电机控制等硬实时场景是刚需,但从那以后我每次做浮点密集型嵌入式项目,都强制走一遍FPU异常配置检查——它不会报错,但会悄悄拖慢你30%性能,直到你用逻辑分析仪抓到那几微秒的异常中断。希望帮到你。
本文还有配套的精品资源,点击获取