news 2026/10/14 20:54:44

STM32F103 FFT例程详解:ADC采样+DMA+频谱显示完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 FFT例程详解:ADC采样+DMA+频谱显示完整链路

简介:这是一份针对 STM32F103(Cortex-M3 内核)开发的 FFT 快速傅里叶变换例程,基于 ARM 官方库与 STM32 HAL 库实现,面向嵌入式开发者、单片机初学者以及需要进行实时信号处理的工程师。压缩包共 179 个文件,约 7.34MB,包含 45 个 h 头文件、18 个 c 源文件、32 个 o/d 目标文件与工程调试文件、31 个 crf 交叉引用文件,还有 hex/axf 可执行文件、ioc CubeMX 配置和 uvprojx 工程文件,覆盖了从源码到烧录的全流程。已有 2122 人学习,适合参考借鉴。通过该例程,可以理解 FFT 在 STM32 上的实现原理,掌握 HAL 库外设初始化、DMA 数据传输、固定点运算及性能优化方法,并能基于完整的工程模板快速移植到自己的项目中,用于频谱分析、噪声抑制、音频处理等场景。

1. stm32f103 fft例程:为什么 72MHz 的 M3 也能把频谱做出来

先给一个反直觉的结论:stm32f103 fft例程这行搜索,真正卡住人的往往不是 FFT 变换本身,而是“喂数据”这一步。F103 的主频只有 72MHz,Cortex-M3 内核没有硬件浮点单元,RAM 在低密度型号上只有 20KB,可这并不妨碍它完成 256 点甚至 512 点的实时频谱分析。关键不在于你能跑多快的 FFT,而在于你愿不愿意把定点运算、等间隔采样和内存开销这几件事想清楚。

这篇内容要解决的是:从 ADC 采到一段波形,经过 FFT 得到幅度谱和频率值,最后通过串口或屏幕显示出来的完整链路。适合手上正好有一块 F103 开发板、想把振动信号、电机电流或者音频信号拆出频率分量的工程师,也适合刚学完定时器和 DMA、想从“点灯”跨到“信号处理”的学生。我不会绕开困惑:浮点还是定点、点数和采样率怎么配、为什么你的频谱总是多出一堆莫名其妙的谱线,这些都会在后面的章节里直接拆开。先把一个思想装进脑子里:stm32f103 上做 FFT 不是算力问题,是数据管理和参数匹配问题。

2. F103 上跑 FFT:先弄清楚这套例程的三个边界

很多人拿到例程第一件事就是找arm_cfft_f32抄进去,然后在 F103 上跑出慢得离谱的结果,于是断定“F103 不配做 FFT”。这个判断太早了。FFT 例程选型要先回答三个问题:用浮点还是定点、用实数还是复数、用多少点。这三个答案直接决定你能做到多少刷新率,以及会不会在调优时翻车。

2.1 Cortex-M3 没有 FPU:浮点例程、定点例程与工程配置如何互配

Cortex-M3 没有硬件 FPU,这是 stm32f103 fft例程里最容易被人忽略的底牌。arm_cfft_f32这类浮点 FFT 在 F103 上不是不能跑,而是每一次乘加都由软件浮点库模拟,同样 256 点变换,耗时可能比定点多出数倍。我在实际板子上跑过,浮点版本做单次 256 点 FFT 还能接受,一旦把点数抬到 1024,主循环里就别想再做别的了。

常见做法是优先用定点例程。CMSIS-DSP 提供arm_cfft_q15和arm_cfft_q31,输入数据用 16 位或 32 位整数表示,变换过程中不存在软件浮点开销,速度和内存都友好得多。但定点引入新问题:乘法之后容易溢出,需要留出移位余量。所以工程里通常不是二选一,而是“调试时用浮点验证算法,发布时换定点控制开销”。

先从工程配置说起。使用 STM32CubeMX 生成基础工程后,要确保编译时能链接到 CMSIS-DSP。旧版固件包需要自己把 DSP 源码加入工程,新版固件包和较新的 CubeMX 可以在中间件或组件选择里直接勾选 DSP。头文件的包含顺序很关键。在代码里你至少要看到这样两行:

#define ARM_MATH_CM3 #include "arm_math.h"

ARM_MATH_CM3是告诉 CMSIS-DSP 当前运行在 Cortex-M3 上,这会影响内部汇编优化路径的选择。如果没有定义或定义成 M4,可能出现函数链接到错误的内核优化版本,或者根本找不到适配实现。调试时第一个要确认的,就是编译器预处理宏里有没有ARM_MATH_CM3。如果使用arm_cfft_f32且平台没有 FPU,还应当留意编译选项是否强制使用软浮点 ABI;用 Keil 时在 C/C++ 选项里通常能看到--cpu Cortex-M3和对应的浮点选项为关闭状态,IDE 默认值一般不用改。如果换成 GCC,则不要加-mfloat-abi=hard。

2.2 内存与缓冲区分配:256 点是黄金起点,1024 点必须换定点

F103 的 SRAM 从 20KB 到 64KB 不等,这个限制比算力更先到来。以 256 点复数 FFT 为例:

  • 输入数组要同时放实部和虚部,长度为2 × N。
  • 用float32_t时是256 × 2 × 4 = 2KB。
  • 幅值数组需要N/2 = 128个float32_t,又是 512B。
  • 如果加窗,窗函数表再加256 × 4 = 1KB。
  • ADC 的 DMA 缓冲如果是双缓冲或乒乓缓冲,还要额外预留2 × 256 × 2 = 1KB。

这样算下来基础开销不到 5KB,20KB SRAM 的最低配型号也能放心用。可是一旦换成 1024 点浮点 FFT,输入数组直接涨到 8KB,CMSIS-DSP 内部还要临时数组,几项加起来逼近或超过 20KB。这也是为什么 F103 上的 1024 点例程基本都是q15或q31版本。定点版本里 1024 点输入数组只有 4KB 或 8KB,余量明显宽裕。

我给这个选型过程的建议是:第一次上手永远从 256 点开始。先用小的数据集把整个链路走通,包括 ADC 采集、DMA 搬运、窗函数、FFT、取模、串口输出,然后再根据频率分辨率需求去加大点数。真正要踩内存性能边界的时候,把浮点数组改成q15,代码结构不用大改,CMSIS-DSP 提供同名接口,只是输入数据和幅度计算都变了。

下面对常用点数和数据格式做一个直观对比。

点数浮点输入占用定点 q15 输入占用建议
2562KB1KB入门首选,任何 F103 型号都够用
5124KB2KB大部分 F103 可行,注意 DMA 缓冲区
10248KB4KB建议定点,低密度型号要精打细算

这里还要说一个很实际的问题:FFT 例程不是只包含变换那一段。真正完整的例程必须包含“采样同步”和“数据搬运”的代码,否则你在主循环里读到的数组永远是正在被 DMA 更新的数据,得到的频谱就像一张没拍完就按快门的照片,模糊是必然的。避免这种“数据断层”的方法是让 FFT 计算只锁定某一半稳定的缓冲区,所以后面要提到 DMA 乒乓。

3. 用 ADC 喂数据:从采样配置到 FFT 输出的最小通路

如果只把 FFT 函数抠出来跑一遍,你得到的只是“一组随机数进、一组随机数出”,没有任何工程价值。真正让 stm32f103 fft例程立得住的是:ADC 以固定的采样率把连续信号变成离散序列,FFT 再把这个序列转成频谱。本节按“采样 → 变换 → 验证”三步走,直接给出最小可运行思路。

3.1 定时器触发 + DMA 循环采集:保证等间隔采样的关键

很多初学者喜欢用HAL_ADC_Start加上延时读寄存器,这本质上是不等间隔采样。FFT 公式假设采样间隔严格相等,任何抖动都会在频谱上表现为底噪抬高和主峰变宽。所以这里必须用硬件定时器触发 ADC 转换,再用 DMA 自动把结果搬进内存。

我用 F103 时最顺手的组合是:TIM2 产生触发事件,ADC1 外部触发启动转换,DMA1 把结果循环写入一个uint16_t数组。假设系统时钟 72MHz,想得到 10kHz 采样率,TIM2 预分频取 71,这样定时器计数频率是 1MHz,自动重装载值设 99,计数器从 0 数到 99 后溢出并触发 ADC,得到 1MHz / 100 = 10kHz。代码骨架如下:

#define ADC_BUF_LEN 256 uint16_t adc_raw[ADC_BUF_LEN]; volatile uint8_t fft_ready = 0; static void MX_TIM2_Init(void) { TIM_HandleTypeDef htim2 = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 71; // 72MHz / 72 = 1MHz 计数频率 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 99; // 1MHz / 100 = 10kHz 触发率 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim2); HAL_TIM_Base_Start(&htim2); }

注意Prescaler = 71而不是 72,因为预分频器是从 0 开始计数的,实际分频系数是寄存器值加 1。Period = 99也是同理,一次计数周期包含 100 个计数脉冲。许多人在这里多算或少算一位,最后采样率偏了还不自知。如果怀疑采样率不对,不要用示波器量定时器引脚,最直接的办法是用一个已知频率的信号,比如 1kHz,再观察 FFT 峰值落在第几条谱线上,反推采样率误差。

ADC 部分把 DMA 和外部触发绑在一起。DMA 配置成循环模式,传输方向为外设到内存,数据宽度保持半字,因为 ADC 寄存器是 12 位有效数据:

static void MX_ADC1_Init(void) { ADC_HandleTypeDef hadc1 = {0}; DMA_HandleTypeDef hdma_adc1 = {0}; __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE(); hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_raw, ADC_BUF_LEN); }

这段代码有两点值得展开。一是ContinuousConvMode = DISABLE,连续转换模式和定时器触发是冲突的,如果开了连续转换,ADC 就会自己不停转,定时器反而失去意义。二是DMA_CIRCULAR循环模式让 DMA 填满 256 个点后又回到数组开头继续写,所以只要主循环速度够快,系统可以一直保持运行。HAL_ADC_Start_DMA启动之后不要在主循环里反复调用,否则会重复启动 DMA,干扰已有搬运序列。

3.2 FFT 转换与幅值计算:最小可运行代码

当fft_ready标志被置位后,把 ADC 原始码值换算成电压,放入 FFT 输入数组的实部,虚部清零。CMSIS-DSP 的复数 FFT 输入必须按“实部、虚部、实部、虚部”交错存放,不能前面存实部数组、后面存虚部数组。这一步看起来简单,却是最容易被新手的数组下标绊倒的地方。

#include "arm_math.h" #define FFT_LEN 256 #define FS 10000.0f #define VREF 3.3f float32_t fft_in[FFT_LEN * 2]; float32_t fft_mag[FFT_LEN / 2]; void FFT_Process(void) { for (int i = 0; i < FFT_LEN; i++) { fft_in[2 * i] = adc_raw[i] * VREF / 4096.0f; fft_in[2 * i + 1] = 0.0f; } arm_cfft_f32(&arm_cfft_sR_f32_len256, fft_in, 0, 1); arm_cmplx_mag_f32(fft_in, fft_mag, FFT_LEN / 2); for (int i = 1; i < FFT_LEN / 2; i++) { float amp = fft_mag[i] * 2.0f / FFT_LEN; float freq = (float)i * FS / FFT_LEN; if (amp > 0.02f) { printf("k=%d f=%.2fHz amp=%.3fV\r\n", i, freq, amp); } } fft_ready = 0; }

arm_cfft_f32的第三个参数是正变换标志,传 0 表示时间域到频率域;第四个参数是位反转标志,大部分情况下传 1 由库内部完成重排。fft_mag数组只用到前一半,因为实信号频谱关于奈奎斯特频率对称,后一半是镜像。float amp = fft_mag[i] * 2.0f / FFT_LEN是把 FFT 结果中的能量换算回正弦信号峰值幅度,这里先假定没有加窗,加了窗后面要再补系数。打印判断条件amp > 0.02f是为了过滤数值噪声,如果阈值设得过大可能把真实小信号滤掉,过小则输出一堆无意义的低幅值谱线,可以按实际电路噪声底调整。

3.3 先喂正弦波:串口打印出的三个正确特征

代码写完先别急着接传感器。用信号发生器输出 1kHz、1V 峰值的正弦波,或者用另一块开发板的 DAC 生成正弦信号,接到 ADC 输入引脚。然后观察串口输出:

  • 第一条正确特征:峰值频率在 1kHz 附近,而不是 0Hz 附近。如果峰值一直顶在 0Hz,多半是输入信号占空比不变化或者 ADC 接地了。
  • 第二条正确特征:主峰周围没有大范围“鼓包”。即使信号很干净,因为 10kHz 采样率除以 256 点后频率分辨率约 39Hz,1kHz 落在两个谱线之间,能量会分摊到第 25 和第 26 根谱线上,这是正常现象,不是故障。
  • 第三条正确特征:幅度接近真实值。没有加窗时峰值幅度和真实 1V 误差应该在几十分之一以内。如果出来是 0.5V 左右,说明你用的窗函数把能量摊平了,后面的章节会讲校正。

这一步是整个 stm32f103 fft例程里最值得投入时间的地方,因为信号源给出的输入是确定的,你能一针见血看出整条链路有没有问题。直接在真实电机上用 FFT 去调参,等于把一个未知输入加进已经出错的系统,分不清问题在采样还是算法。

4. 把 FFT 调到可信:采样率、点数、窗函数与幅值校正

能跑出频谱和能跑出“可信”的频谱是两回事。前者的判断标准是软件没死机,后者的判断标准是打印出的频率和幅度与真实输入一致。这一章说的四个参数,正是从“能跑”跨到“可信”的关键。

4.1 fs 与 N 决定分辨率:先算清楚再写代码

频率分辨率由采样率 fs 和点数 N 共同决定,公式是Δf = fs / N。采样率 10kHz、点数 256 时,分辨率约 39.06Hz。这意味着两根频率相差小于 39Hz 的谱线,理论上很难被区分。想分辨 49Hz 和 51Hz 的两个分量,你需要分辨率小于 2Hz,办法要么降低采样率,要么提高点数。

另一个容易被忽略的是频域下标 k 和物理频率 f 的关系:f = k × fs / N。FFT 输出第 k 条谱线对应频率 k 倍分辨率。如果 N 是 256,fs 是 10kHz,第 0 条是直流,第 1 条是 39.06Hz,第 25 条是 976.5Hz,第 26 条是 1015.6Hz。一个 1kHz 信号恰好卡在两条谱线中间,这就是前文说“能量分摊”的由来。

很多例程为了省事直接把N和fs写死,这在项目原型阶段没问题,但如果你想让某个特定频率精确落在整数谱线上,可以反推采样率。比如想让 50Hz 信号刚好落在第 10 条谱线,可以用fs = 50 × 256 / 10 = 1280Hz。这类计算在调试时非常有用,我在换传感器前一定会先算一遍:

fs = 10000.0 N = 256 f0 = 1000.0 df = fs / N k = f0 / df print("分辨率:", df, "Hz") print("目标频率对应k值:", round(k, 2))

如果算出来 k 是整数,表明目标频率和采样频率是整周期关系,不加窗也能得到很尖锐的谱线。如果 k 是小数,例如 25.6,就需要靠窗函数压制泄漏,或者干脆调整 fs 让它接近整数。分辨率、点数、采样率三者互相牵制,修改任何一个都要回头检查另外两个。

4.2 窗函数的取舍:矩形窗、汉宁窗和布莱克曼窗一张表看完

对一段有限长度信号做 FFT,等于在时域上把无限信号截断。截断本身就是乘以矩形窗,所以矩形窗的频谱旁瓣很高,频率偏离谱线时能量泄漏明显。解决手段是在时域乘一个两头衰减的窗,让截断处不再发生突变。常见三类窗各有脾气。

窗函数主瓣宽度旁瓣衰减相干增益典型场景
矩形窗2Δf约 -13dB1.0信号频率恰好落在谱线上,或需要观察瞬时突变
汉宁窗4Δf约 -31dB0.5通用工况,兼顾频率精度和幅度精度
布莱克曼窗6Δf约 -58dB0.42强旁瓣抑制,适合频率成分稀疏的场合

汉宁窗是多数 stm32f103 fft例程的默认选择,因为它在主瓣宽度和旁瓣衰减之间最均衡。加窗后主瓣变宽,两个频率很近的分量虽然分辨不了,但远处泄漏明显减少。如果你的应用里信号频率成分相对稀疏,并且幅度动态范围大,比如需要测量谐波幅度,可以考虑布莱克曼。旁瓣衰减越大,越能压制“虚假谐波”。

在 F103 上最好提前把窗函数算好存成表格,不要每帧数据都重新计算三角函数。初始化函数如下:

#define PI_F 3.14159265f static float32_t win_hann[FFT_LEN]; void InitHannWindow(void) { for (int i = 0; i < FFT_LEN; i++) { win_hann[i] = 0.5f - 0.5f * arm_cos_f32(2.0f * PI_F * i / (FFT_LEN - 1)); } }

使用窗函数时把输入序列逐点相乘。幅度计算时要注意,窗函数会把总能量压低,所以后面的幅值补偿系数必须和窗函数匹配。这里要强调一次:窗函数表长度必须是 N,而不是 N/2,因为时域数据长度本来就是 N。有人图省事只乘了前一半,结果频谱出现严重不对称。

4.3 幅值校正:为什么 1V 正弦看起来只有 0.5V

FFT 输出的第 k 条谱线幅度不仅与输入信号有关,还和窗函数的能量衰减有关。矩形窗没有压低信号,所以输入 1V 峰值正弦时,不加窗的谱线幅度经过幅值 = mag × 2 / N换算后接近 1V。汉宁窗的相干增益约 0.5,加窗后峰值谱线幅度大约变成 0.5V,如果这时候直接沿用原来的换算系数,读数就会乖了一半。

正确的校正方式是把窗的相干增益补回去。相干增益等于所有窗系数相加再除以 N,矩形窗是 1,汉宁窗约 0.5,布莱克曼窗约 0.42。幅度校正公式为:

float corr = 1.0f / 0.5f; // 汉宁窗的相干增益 0.5,补偿系数 2.0 float amp = fft_mag[i] * 2.0f / FFT_LEN * corr;

一定要先算fft_mag[i] × 2 / N,再乘补偿系数,顺序不能反过来,否则补偿量会被一起放大。如果你只做相对比较,比如只关心哪个频率分量大、哪个小,校正不是必须的。但如果你要做绝对电压测量,比如算出谐波总畸变率,这个系数不乘,结果会系统性偏低。调试时搞不懂原始数据对不对,可以先退回矩形窗,也就是不做时域乘法,用已知幅度正弦校准mag与amp的关系,再切回汉宁窗看补偿系数。

5. stm32f103 fft例程避坑指南:跑不出正确频谱先检查这 5 条

这一章没有玄学,全是照着检查和改就能恢复正常的操作。很多人拿到例程后第一反应是怀疑 FFT 库有问题,其实无用的频谱十有八九从采集源就开始错了。

5.1 拿到“错谱”先别改参数:从三个位置按顺序定位

第一条排查线在时域:把adc_raw数组用串口原样打印出来,确认波形不是一条直线、不是随机噪声、没有削顶。波形正常才谈得上频谱正常。

第二条排查线在输入数组:打印进入arm_cfft_f32之前的fft_in,确认实部是“电压 × 窗函数”之后的值,虚部全部为 0。我见过有人把虚部遗忘不写,结果上一帧残留数据被当成当前数据的虚部,频谱里出现两套峰值。

第三条排查线在 bin 到频率的换算:检查freq = i × fs / N,别把 fs 写成定时器触发频率之外的值。定时器预分频算错一位,采样率变成一半,打印出的频率也会系统性地偏差一半。按这三条顺序检查,能筛掉大部分低级问题。如果有人拿着频谱图来问我,我第一句话一定是:先确认时域波形,而不是先调窗函数。

5.2 血泪经验:5 个典型翻车现场与对应解法

第一条,DMA 数据没写完就开始 FFT。现象是相邻两次 FFT 打印出的峰值幅度忽高忽低,每次运行结果都不稳定。原因是主循环没有等待 DMA 半传输或传输完成事件,导致一帧数据里只有一部分是本次采集的,另一部分还是旧的。解决方法是采集完成后先按某种方式拉平标志位,例如在 DMA 传输完成中断里置一次fft_ready,主循环只在该标志置位后才复制数据并执行 FFT。

第二条,ADC 用连续转换加HAL_Delay控制采样间隔。现象是频谱图上主峰旁边多出一圈“裙边”,看起来像频率模糊。原因是软件延时受中断影响,采样间隔严重抖动。解决方法是回到定时器触发的硬件方案,确保ContinuousConvMode为 DISABLE。这一条在实际现场采样时几乎必现,尤其是中断已经很多的情况下。

第三条,定点 FFT 溢出导致频谱“雪花噪声”。现象是使用arm_cfft_q15后,即使输入是干净正弦,输出也在整个频段充满毛刺。原因是输入的 Q15 数据幅度已经接近满量程,FFT 蝶形运算中的中间和会超出 16 位表示范围。解决方法是进入 FFT 前先整体右移几位,比如把 ADC 值先减直流中心,再右移 2 到 3 位,留出足够余量,计算完再按移位倍数补回幅度。这可能让有效位数下降,但换来的是稳定不出错。

第四条,调试器断点一停下来,再运行结果就不对。现象很诡异:单步执行时数组看起来正常,连续跑到 FFT 后输出完全乱掉。原因是 DMA 在后台持续搬运,断点暂停时间过长或者单步执行期间数据已经被覆盖;另一层原因是断点会影响中断响应时间,原本需要实时处理的窗函数变成陈旧数据。解决方法是别依赖仿真器看 FFT 结果,把关键变量通过串口打印,或者把结果写入另一个缓冲区,复位后再分析。我习惯把调试方式总结成“能打印就不打断点”。

第五条,固定频率正弦出现两根并行高谱线,误以为信号有两个频率。现象是 50Hz 信号在频谱上出现在 39Hz 和 78Hz 两处,让人误判。原因是非整周期截断导致频谱泄漏,N 和 fs 的配合没有让 50Hz 落在整数的 k 上。解决方法是先计算k = f0 × N / fs,看它是不是整数,不是就把采样率调成能使 k 为整数的值,或者加窗并用峰值附近功率求和。若已经加了汉宁窗仍有两个峰,通常是分辨率本身不够,要增加 N。

6. 进阶技巧:用双缓冲做连续频谱刷新,并在线切换窗函数

调试通过后,下一步通常是把 FFT 从“跑一次看看”改成“持续刷新测量”。这里最有用的技巧是 DMA 双缓冲加半传输中断。DMA 循环模式的数组被天然分成前半段和后半段,当 DMA 搬完前 128 个点时触发半传输中断,说明前半段已经稳定;此时可以拿前半段做 FFT,DMA 还在继续写后半段。等后半段搬完再触发传输完成中断,交换角色。

volatile uint8_t fft_half = 0; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { fft_half = 1; } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { fft_half = 2; }

主循环里通过fft_half判断要处理哪一半数据,处理时只复制对应区间的数据到fft_in,不要让 DMA 直接写入 FFT 输入数组。原因很简单:FFT 计算需要几十毫秒,而 DMA 每几十微秒就在更新数据,共用同一块内存必然互相覆盖。双缓冲的核心就是“采集的数据先落进待处理区,计算的数据从备份区读”,这两条路径永远不会交叉。

在线切换窗函数可以让调试效率高不少。定义几个窗类型对应不同的系数表,切窗的同时修正幅度补偿系数。窗外表可以在系统启动时一次性生成,运行时只做查表乘法。我在一次电力波形测量中切换汉宁窗和布莱克曼窗时,因为幅度补偿系数没同步更新,打印出的谐波值系统性偏低,花了一晚上才意识到是corr忘改了。

如果你要把这套例程延伸到更复杂的场景,还差一步验证手段:用 FFT 测量一个精度已知的参考信号,把读到的幅度、频率与信号源设置值做对比。这个习惯帮我避开了很多“看着对、实际差一点”的坑。stm32f103 fft例程真正的价值不是那几行变换函数,而是你愿意把采样、搬运、标定每一个环节都校准到位。做真实产品时,我最信奉的一句话是:频谱不可能比你给它的数据更诚实。希望一如既往,以上较真能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 20:52:20

AI 论文降重改写,几款常用降AIGC软件怎么选才安心

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 20:50:52

LangChain、LangGraph、Deep Agents智能体开发实战指南

1. 从零搭建智能体系统&#xff1a;为什么选择LangChain生态1.1 三个框架到底解决什么问题很多刚接触智能体开发的朋友会有一个困惑&#xff1a;LangChain、LangGraph、Deep Agents这三个东西名字都带“Lang”&#xff0c;到底该用哪个&#xff1f;我刚开始也绕了不少弯路&…

作者头像 李华
网站建设 2026/10/14 20:50:10

智慧校园供应商考察报告怎么写?关键要素与评估标准全解析

去年帮一所高校做智慧校园数据中台的选型&#xff0c;前后面谈了六家供应商&#xff0c;每家都拍着胸脯说自家平台架构先进、接口开放、实施经验丰富。可等到考察报告写完、提交评审会的时候&#xff0c;我心里清楚&#xff0c;六家里面至少有两家在“先进架构”四个字上掺了水…

作者头像 李华
网站建设 2026/10/14 20:49:59

3天从85%降到20%!这3个降AIGC平台让我导师直夸原创

还记得上周三凌晨两点&#xff0c;当我第三次收到知网AIGC检测报告时&#xff0c;手心都在冒汗——85%的AI相似度&#xff0c;40%的查重率&#xff0c;这意味着我的毕业论文根本达不到盲审要求。导师直接在我的初稿上批注“学术合规性存疑&#xff0c;建议重写”&#xff0c;那…

作者头像 李华
网站建设 2026/10/14 20:49:19

Fortran实现卡尔曼滤波:从矩阵公式到工程落地避坑指南

简介&#xff1a;这是一份用Fortran实现卡尔曼滤波算法的完整程序包&#xff0c;面向控制系统设计与导航定位、信号处理等领域的开发者、研究生和本科生&#xff0c;可帮助理解噪声环境中状态估计与预测更新机制。压缩包共38个文件&#xff0c;rar格式&#xff0c;体积约959KB&…

作者头像 李华
网站建设 2026/10/14 20:48:42

Superpowers开发者增强工具链:Cursor+Claude+Antigravity+Codex实战指南

1. 项目概述&#xff1a;Superpowers 不是超能力&#xff0c;而是开发者效率的“质变杠杆”你搜“superpowers”时&#xff0c;看到的不是漫威电影里的雷神之锤&#xff0c;也不是DC宇宙里的氪星血脉——而是一群人在深夜调试接口时突然拍桌惊呼&#xff1a;“这功能怎么敢这么…

作者头像 李华