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

STM32F103 FFT频谱分析实战:采样链、定点算法与上位机全解析

简介面向STM32F103C8T6的FFT信号分析工程包聚焦单片机ADC采样与快速傅里叶变换实现适合嵌入式开发者、电子竞赛选手及信号处理初学者。工程以C代码为主包含完整外设配置ADC、USART、定时器及FFT运算逻辑通过串口将频域结果上传至上位机便于观察频谱。压缩包共112个文件主要类型为41个h头文件、41个c源文件、19个汇编s文件另有IAR工程文件ewp/ewd、链接脚本、烧录脚本等整体仅524KB结构紧凑可直接导入工程阅读。已有1719人学习。资源提供源代码、配置文件、编译脚本及上位机程序可从中学习Cooley-Tukey等FFT分解方法、ADC采样率与分辨率对频谱的影响以及嵌入式端与PC端的数据交互流程是一份可实操的入门参考。1. FFT能否在Cortex-M3上实时跑关键不在算力在采样链STM32F103C8T6 没有 FPU不少人拿到这个 DSO 工程的第一反应是FFT 会不会把 CPU 拖死。实际拆过之后会发现256 点定点 FFT 在 72MHz 主频下只需要毫秒级计算真正决定频谱质量的不是算力而是 ADC 的采样时序。内部 ADC 若不配合定时器触发和 DMA 搬运采集到的样本间隔会产生微秒级抖动FFT 会把这种抖动直接折算成频谱噪底后续算法再准也白搭。这个工程把 TIM 触发 ADC、DMA 循环搬运、定点 FFT、串口上传和 TFT 绘制串成一条完整链路对入门嵌入式信号处理的人非常合适对做工业采集的人它的采样率分档、窗函数取舍和通信协议设计也有直接参考价值。2. ADC 采样链路TIM 触发、DMA 搬运和 256 点缓冲2.1 为什么选择 TIMADCDMA 而不是 while 轮询FFT 对样本的等间隔性极其敏感。软件轮询 ADC 的做法在主循环里会受中断、分支和任务调度影响采样间隔抖动可达微秒级但 FFT 默认这些样本是均匀时间间隔的抖动就等效于给信号叠了一层相位噪声频谱上表现为本底抬升、小信号被淹没。工程上的常见做法是让定时器的比较事件直接触发 ADC 转换再由 DMA 把结果搬进内存CPU 只在整帧采集完成后做一次 FFT主循环完全不参与采样时序。这套链路在 F103 上的标准接法是TIM2 输出 PWM1 信号映射到 ADC1 的外部触发输入每个上升沿启动一次转换转换结束后数据自动写入 ADC1-DRDMA1 通道1 把 DR 里的 16 位结果搬进用户数组。ADC 的连续转换模式要关掉否则内部会自动启动下一轮外部触发就失去意义了。DMA 开循环模式256 次搬运完成后地址自动回卷可以一帧接一帧地采不需要每帧重新初始化。2.2 定时器触发 ADC 的关键配置2.2.1 TIM2 产生触发脉冲TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; // 72MHz 总线时钟PSC3 得到 18MHz 计数时钟 tim.TIM_Prescaler 3; tim.TIM_Period 899; // 18MHz / (8991) 20kHz tim.TIM_ClockDivision TIM_CKD_DIV1; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tim); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_Pulse 450; // 50% 占空比 oc.TIM_OutputState TIM_OutputState_Enable; TIM_OC1Init(TIM2, oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);这段配置把采样率定在 20kHz。计算关系是采样率 72MHz / ((TIM_Prescaler1) * (TIM_Period1))所以 PSC3、ARR899 得到 20kSps改成 PSC3、ARR359 就是 50kSps。TIM_Pulse 只影响触发边沿的相位不影响触发频率但占空比不要太小否则输出比较边沿抖动会变大间接增加 ADC 触发时间的不确定性。2.2.2 ADC 与 DMA 的衔接ADC_InitTypeDef adc; DMA_InitTypeDef dma; adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode DISABLE; adc.ADC_ContinuousConvMode DISABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_CC1; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 1; ADC_Init(ADC1, adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_MemoryBaseAddr (uint32_t)adc_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize 256; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; DMA_Init(DMA1_Channel1, dma);ADC_SampleTime 选 239.5 周期是刻意的。F103 的 ADC 在 12 位模式下采样时间越长内部采样电容充电越充分输入源阻抗带来的增益误差越小。ADC 的 12 位结果是右对齐存在 DR 低 16 位所以 DMA 宽度必须配成半字。循环模式在这里是关键它让采集和 FFT 计算可以错峰进行DMA 搬运后半帧数据时CPU 正在算前半帧的 FFT互不阻塞。2.3 采样率分档与频率分辨率采样率TIM2 PSCTIM2 ARR奈奎斯特上限256点FFT频率分辨率20 kSps389910 kHz78.125 Hz50 kSps335925 kHz195.3125 Hz频率分辨率等于采样率除以点数也就是相邻两个频点之间的间距。做音频频谱20kSps 已经够用做宽频噪声或脉冲信号观测可以切到 50kSps但频点间隔变粗。想同时提高采样率和分辨率只能增加 FFT 点数比如 1024 点 FFT 在 F103 的 20KB SRAM 里也放得下代价是计算时间按 N·log2(N) 增长屏幕刷新率会明显下降。不要把采样率往上调太狠。F103 的 ADC 时钟上限约 14MHz单通道 12 位转换需要 12.5 个周期加采样时间239.5 周期采样在 12MHz ADC 时钟下大约 20μs所以 50kSps 已经是这个配置下的稳妥上限。超过之后 ADC 结果开始跳动FFT 出现的就不再是真实频谱而是量化噪声。2.4 抗混叠滤波与采样抖动FFT 的结果默认输入信号带宽在奈奎斯特频率以内。如果被测信号里混入了高于采样率一半的频率分量它会折叠回低频段在频谱上形成假的尖峰。工程上的常见做法是在 ADC 输入引脚前加一阶 RC 低通截止频率取采样上限的 60%70%。以 20kSps 为例把截止频率放在 12kHz 附近R120Ω、C100nF 的标称值算出来约 13.3kHz够用也不会明显衰减目标频段的信号。采样抖动这块更容易被忽略的是硬件来源。PCB 走线过长、VREF 纹波过大、触发边沿不够陡都会让 ADC 的采样时刻漂移。抖动对高频分量的影响比对低频明显因为时间误差折算成相位误差时要乘以信号频率。把 VREF 的 100nF 滤波电容尽量贴近芯片引脚减少模拟输入走线长度多数噪底偏高的问题能消掉一大半这比在软件里做任何平滑都管用。3. 定点 FFT位反转、蝶形运算与旋转因子表3.1 在 F103 上做 FFT 的选型定点优于软浮点Cortex-M3 没有硬件浮点单元C 库里的 float 运算会编译成软浮点子程序调用一次复数蝶形涉及多个函数跳转和栈操作256 点 FFT 跑下来往往是十几毫秒。定点实现的思路是把信号归一化到 [-1,1) 区间用 Q15 格式表示复数乘法用 32 位中间变量完成再移位回 16 位速度通常比软浮点快一个量级。STM32 官方提供的 CMSIS-DSP 库里 arm_cfft_q15 确实高效但这个工程里看不到对 DSP 库的依赖旋转因子表和蝶形运算都是自己实现的。对学习来说这反而是好事你能看到每一级蝶形的索引变化而不是把 FFT 当成黑盒。对工程来说手写定点 FFT 的内存占用非常可控实部、虚部各 256 个 int16_t加上旋转因子表 512 字节总共约 1.5KB在 F103C8 的 20KB SRAM 里留出了充足的余量。工程里带 stm32f10x_flash.c比较顺手的做法是把旋转因子表在 PC 端算好以 const 数组烧进 Flash避免上电时调用 sin/cos。三种实现路径的取舍关系如下实现方式相对速度额外资源维护成本软件浮点慢无最低随便改手写 Q15 定点快旋转因子表约 512B中等适合教学CMSIS-DSP最快依赖库内存需对齐低但偏黑盒3.2 基2时间抽取 FFT 的工程化实现3.2.1 位反转排序static void bit_reverse(int16_t *re, int16_t *im, uint16_t n) { for (uint16_t i 1, j 0; i n; i) { uint16_t bit n 1; for (; j bit; bit 1) j ^ bit; j | bit; if (i j) { int16_t t re[i]; re[i] re[j]; re[j] t; t im[i]; im[i] im[j]; im[j] t; } } }这段位反转手法在数字信号处理教材里叫比特逆序核心是让 j 按二进制从高位向低位进位等效于把索引的二进制位序颠倒。n256 时 bit 从 128 开始右移如果 j 的当前位为 1就异或清除并继续向左试探直到找到一个 0 位再置 1。if (i j)保证每一对元素只交换一次避免换过去又换回来。位反转完成后后续蝶形就可以按 2、4、8……的跨度逐级合并不再需要处理乱序索引。3.2.2 蝶形运算主体void fft_q15(int16_t *re, int16_t *im, uint16_t n) { const int16_t *cos_tab; /* Q15 余弦表长度 n/2 */ const int16_t *sin_tab; /* Q15 正弦表长度 n/2 */ uint16_t len, i, k; bit_reverse(re, im, n); for (len 2; len n; len 1) { uint16_t half len 1; uint16_t step n / len; for (i 0; i n; i len) { for (k 0; k half; k) { int16_t w_re, w_im; int32_t t_re, t_im; w_re cos_tab[k * step]; w_im -sin_tab[k * step]; t_re ((int32_t)re[i k half] * w_re - (int32_t)im[i k half] * w_im) 15; t_im ((int32_t)re[i k half] * w_im (int32_t)im[i k half] * w_re) 15; re[i k half] re[i k] - t_re; im[i k half] im[i k] - t_im; re[i k] t_re; im[i k] t_im; } } } }外层 len 是当前蝶形组长度每轮翻倍。旋转因子的索引不是直接用 k而是 k 乘以 step因为越靠后的级旋转因子在整表里取点越稀疏这是基 2 FFT 分治结构的固有规律。Q15 乘法最容易出错的是移位位置两个 16 位定点数相乘得到 32 位中间结果低 15 位是小数部分必须右移 15 位才能回到 Q15 格式。用 int32_t 承接乘积再移位防止中间结果溢出。如果换成 arm_cfft_q15官方库会用块浮点机制自动做中间级缩放防止溢出手写版为了安全输入信号最好预留 6dB 余量或者全部样本先右移一位再进 FFT。DSO 场景里 ADC 很少满幅输入做一次整体衰减是常见做法对显示影响很小。3.3 幅值计算整数开方替代 sqrt()FFT 的结果是复数显示频谱时需要模长 sqrt(re² im²)。F103 的 C 库 sqrt 是软浮点实现128 个频点逐个调用会拖慢刷新率。常见做法是用整数逐位逼近开方或者干脆用近似公式。逐位开方在 Cortex-M3 上编译后只有几十个周期static uint16_t isqrt32(uint32_t x) { uint32_t res 0; uint32_t bit 1u 30; while (bit x) bit 2; while (bit) { if (x res bit) { x - res bit; res (res 1) bit; } else { res 1; } bit 2; } return (uint16_t)res; }这个函数的循环次数固定不依赖浮点库每次调用开销很低。FFT 输出幅度会随 N 缩放显示前统一乘一个固定增益系数就行不必每次计算。还有一个细节直流分量在 FFT 结果里是第一个点幅值通常远大于其他频点绘图前可以单独把它裁剪掉否则纵轴自动缩放后交流分量会贴到底部。3.4 频谱泄漏与窗函数矩形窗看起来最简单但 DSO 采集的信号很难做到整周期截断频谱主瓣旁边会出现明显旁瓣泄漏。工程上的常见做法是在时域乘汉宁窗w(n) 0.5 * (1 - cos(2πn / (N-1)))。用 Q15 实现时先把窗函数表算好采样缓冲乘完窗再进 FFT避免运行时反复算三角函数。加窗后主瓣变宽靠近的两个频率分量更容易混在一起同时幅度会有约 0.5 的加权损失显示时乘 2 补偿回来。想要更准的频率读数可以对峰值附近的 3 个点做质心插值公式是f_est f_k (X[k-1] - X[k1]) / (2*(X[k-1] - 2*X[k] X[k1])) * Δf。在 78.125Hz 分辨率的 20kSps 档下这能把频率读数做到十几赫兹以内的精度代价只有几次整数运算很适合 DSO 里的频率游标功能。4. 串口帧协议一帧 516 字节的带宽边界4.1 帧格式设计频谱数据要送往上位机UART 是最省事的通道。工程用 USART1 跑 115200、8N1帧格式设计得很紧凑字节偏移内容长度说明0~10xAA 0x552同步头2~513幅度谱512256 个 uint16_t小端514~5150x0D 0x0A2帧尾定长帧的好处是接收端逻辑简单找同步头、收满长度、校验帧尾完成一帧。115200bps 下每个字节实际传输 10 bit516 字节需要约 44.8ms。如果目标是 20fps 刷新率也就是 50ms 一帧串口占用接近 90% 带宽已经没有余量给上位机处理和屏幕绘制了。我一般会把刷新率控制在 1015fps留出 20% 以上余量否则数据会出现排队积压旧的频谱还没画完新的就来了。4.2 DMA 双缓冲发送发送端直接阻塞等 TXE 标志会卡死主循环516 字节约 44.8ms 的发送时间里 CPU 什么正事都干不了。常见做法是 DMA 发送外加双帧缓冲。DMA 配置如下DMA_InitTypeDef dma_tx; dma_tx.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; dma_tx.DMA_MemoryBaseAddr (uint32_t)tx_buf; dma_tx.DMA_DIR DMA_DIR_PeripheralDST; dma_tx.DMA_BufferSize 516; dma_tx.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma_tx.DMA_MemoryInc DMA_MemoryInc_Enable; dma_tx.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; dma_tx.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; dma_tx.DMA_Mode DMA_Mode_Normal; DMA_Init(DMA1_Channel4, dma_tx); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);双缓冲的思路是A 缓冲通过 DMA 发送时CPU 往 B 缓冲写下一帧DMA 发送完成中断里交换两个缓冲的指针。这样 ADC 采集、FFT 计算、串口发送三者并行主循环几乎不被阻塞。需要注意 DMA 发送完成中断触发时最后一个字节可能还在移位寄存器里此时立刻改写缓冲会破坏它。稳妥的做法是在发送完成中断里先等一下USART_FLAG_TC再换指针。4.3 接收端容错与 Modbus 式超时判断定长帧在链路质量差的时候会丢同步。协议上可以借鉴 Modbus RTU 的 3.5 字符超时规则接收端如果两个字节间隔超过 3.5 个字符时间就认为上一帧结束状态机复位。对 115200bps 来说3.5 字符约 304μs在 PC 上位机里可以用时间戳判断实现成本很低。帧头 0xAA 0x55 和帧尾 0x0D 0x0A 提供双重校验即使中间丢了几个字节状态机也会在下一帧同步头处自动恢复不需要复位设备或清空缓冲。这里容易被漏掉的是采样率切换的通知。帧里只有幅度谱没有采样率信息上位机如果固定按 20kSps 画横轴切换到 50kSps 档后频率全错。工程做法是在帧头后面加一个字节的采样率档位或者用不同的同步头区分两档上位机解析时把采样率作为参数传入而不是硬编码。5. Python 上位机帧同步、绘制与 MATLAB 交叉验证5.1 上位机选型串口数据用 Python 处理非常直接pyserial 负责收数据matplotlib 负责绘图整个调试环境可以在一台没有专业软件的普通电脑上跑起来。这个链路还有一个额外好处调试期可以把接收到的原始帧导出成 CSV再用 MATLAB 或者 numpy 独立做一次 FFT和板端结果对比很快就能判断是采集问题还是算法问题。如果只在串口助手里看十六进制频谱曲线形态很难目测出来排查效率会低很多。5.2 状态机解码import serial import struct import numpy as np import matplotlib.pyplot as plt ser serial.Serial(COM5, 115200, timeout0.5) state 0 buf bytearray() while True: chunk ser.read(256) for v in chunk: if state 0 and v 0xAA: state 1 elif state 1 and v 0x55: state 2 buf.clear() elif state 2: buf.append(v) if len(buf) 512: mag struct.unpack(256H, bytes(buf)) np.savetxt(fft_mag.csv, mag, delimiter,) # 20kSps 下正频域仅前 128 个点有效 freq np.linspace(0, 10000, 128) plt.clf() plt.plot(freq, mag[:128]) plt.pause(0.05) state 0逐字节状态机的好处是不会被串口缓冲里的粘包带偏。ser.read(256)一次读出的数据可能包含多帧内容但状态机只有完整收到 512 字节数据后才切回同步头检测如果中间出现坏字节下一帧的 0xAA 0x55 会自动让它重新同步。struct.unpack(256H)按小端解析 256 个无符号短整型必须和板端发送字节序一致ARM 默认小端所以这里没有问题。Linux 下把端口名改成/dev/ttyUSB0Windows 保持COM5。timeout0.5让 read 超时返回空字节方便 CtrlC 退出。刷新率由plt.pause(0.05)控制对应 20Hz实际使用建议改成 0.1减轻 CPU 负载避免上位机把整台机器的串口缓冲垫高。5.3 导出 CSV 到 MATLAB 做算法级核对上面代码里的np.savetxt每次循环都会覆盖fft_mag.csv这个文件可以直接被 MATLAB 读取data csvread(fft_mag.csv); plot(abs(fft(data, 256)));把 MATLAB 的结果和板端串口发出来的幅度谱叠图如果峰值位置和相对幅度对得上说明 ADC 采集、定点 FFT、串口协议整条链路没有逻辑错误。如果板端幅度系统性偏小重点检查定点 FFT 里的移位位置最常见的是复数乘法后忘了15导致每级蝶形结果被放缩如果峰值位置偏了一个或两个频点检查位反转和旋转因子表索引是否一致。用 MATLAB 交叉验证这一招不需要额外硬件成本。它把“板端算法错误”和“上位机解析错误”快速分开板端和 MATLAB 结果一致但频谱不对问题在采集或输入信号不一致问题在 FFT 或协议解析。6. TFT 频谱显示优化局部重绘与峰值保持6.1 局部刷新与峰值保持工程里带着 pic.c、picture.c 这类位图资源以及 TFT.cspy.bat 这种带 cspy 命名的批处理脚本后者通常是 IAR EWARM 调试器生成的命令行烧录脚本说明原工程是在 TFT 屏上直接绘制频谱的。全屏刷新的瓶颈不在 FFT而在屏幕写像素的速度SPI 驱动的小屏全屏刷新一帧要几十毫秒。常见做法是只更新频谱曲线经过的像素点上一帧用背景色把旧位置画掉这一帧再画新位置每根谱线只操作一个点256 根谱线就是 256 个点比整屏填充快一个数量级。峰值保持是 DSO 很实用的功能。开一个int16_t peak[128]数组每次算完幅度谱后做一次更新for (i 0; i 128; i) { if (mag[i] peak[i]) peak[i] mag[i]; else peak[i] - 2; /* 缓慢衰减 */ }每帧刷新时把 peak 数组用另一种颜色叠加在主谱线上方。偶发毛刺在普通刷新下可能一闪而过峰值保持可以让它停留在屏幕上直到衰减结束。peak[i] - 2的衰减速度适配 10fps 刷新率改小留得更久改大则接近实时显示。注意 peak 数组要初始化为全 0并且只在采样率切换时清空否则切换档位后旧峰值会跨频率范围残留。6.2 下载与信号验证最后提两个容易卡的硬件点。SWD 下载失败时先看 BOOT0、BOOT1 跳线BOOT0 必须拉低才会执行用户 Flash 里的程序DAP 仿真器接长杜邦线时通信不稳定把 SWD 时钟从 4MHz 降到 1MHz 大多能解决。这类最小系统板还要确认复位电容和 VREF 旁路电容确实存在否则 ADC 采样值会漂移频谱上表现为低频段毛刺增多。验证 FFT 是否正确的办法很直接用一个引脚输出已知频率的 PWM 方波飞线到 ADC 输入。方波在频域里是基波加奇次谐波如果基波位置和设置频率一致说明采样率、FFT 点数、上位机横轴三者是吻合的。把信号源换到 3kHz 正弦波重复上面的频率读数核验两个频点都对得上FFT 的频标就是可信的后面再改显示和协议都只是在已有骨架上加肉。本文还有配套的精品资源点击获取
分享:

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

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