news 2026/10/26 2:17:30

STM32 ADC+FFT测量幅值频率相位差实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 ADC+FFT测量幅值频率相位差实战

简介:面向嵌入式开发者与电赛、课设学习者的 STM32 FFT 实践工程,聚焦正弦波幅值、频率与相位差的实时精准测量这一常见难点。工程以 STM32F407 的 Cortex-M4 内核和硬件浮点单元为依托,把 ADC 采样、窗函数处理、快速傅里叶变换、频谱分析与结果输出串成一条完整链路,可作为电机控制、音频分析、无线通信等场景的频域调试参考。压缩包共 283 个文件、约 9.98MB,以 60 个 h 头文件、46 个 c 源文件和 50 个 d 依赖文件构成标准库外设驱动与算法主干,配套 .o、.crf、.axf、.hex、.map 等编译中间与烧录文件,另有 uvprojx、uvoptx 工程配置及少量 txt 说明,用 Keil 打开即可复现;预览中的 stm32f4xx_tim.c、stm32f4xx_rcc.c 等即标准库底层驱动,便于按外设分层阅读。已有 1668 人学习下载,可参照其目录组织方式,从采样配置到频谱换算逐段对照,快速搭建自己的测量与验证流程。

1. 从 ADC 到频域只差一次 FFT:1024 个采样点里能读出什么

两路正弦信号,一路参考、一路过被测网络,要知道幅值、频率和相位差。拿示波器卡光标,读数随波形抖,重复性差;换成片上 ADC 采一段数据,做一次快速傅里叶变换(FFT),时域波形搬到频域,这三个量就能在同一根谱线上算出来:幅值看谱峰高度,频率看谱峰位置,相位差看两条谱线的相角之差。这话说得轻巧,落到标准库工程里,"能跑出频谱"和"能把三个量测准"之间横着采样率、窗函数、归一化系数、相位缠绕好几道坎。这篇按可复现的步骤把 STM32 上这套链路拆开:定时器触发采样怎么配、DSP 库怎么塞进标准库工程、幅值频率相位分别怎么算、哪些坑会让读数偏一倍甚至反相。适合手里已经有 Cortex-M 板子、想把测量从"看波形"升级成"出数值"的人。

2. 采样链路先立住:定时器触发、DMA 与三个参数的相互牵制

FFT 的结果准不准,七成取决于送进算法的那段数据干不干净。软件轮询 ADC 再存数组,采样间隔被中断响应和主循环拖得忽长忽短,频谱底噪立刻抬起来,谱峰变胖,幅值和相位都跟着飘。业内比较稳的做法是定时器 TRGO 触发 ADC,DMA 搬运,CPU 全程不进中断,采完一整块再一次性处理。

2.1 用 TIM 触发 ADC + DMA 组成不占 CPU 的采样链

以带 FPU 的 M4 内核为例,TIM2 跑在 84MHz,预分频到 1MHz 计数,ARR 直接对应采样间隔。TRGO 选更新事件,ADC 的外部触发源配成这个定时器,规则组转换完成后由 DMA 请求把结果搬到内存。整条链路上没有任何一处依赖软件时序。

/* 采样定时器:溢出频率即为采样率 fs */ void SampleTimer_Init(uint32_t fs_hz) { TIM_TimeBaseInitTypeDef tb; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); tb.TIM_Prescaler = 84 - 1; /* 84MHz -> 1MHz 计数时钟 */ tb.TIM_Period = (1000000U / fs_hz) - 1; /* 1MHz / fs = 每个采样点的计数 */ tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tb); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_Update); /* 更新事件作为 ADC 触发源 */ TIM_Cmd(TIM2, ENABLE); } /* ADC1 双通道规则扫描 + DMA 循环搬运 */ void ADC_DualScan_Init(uint16_t *buf, uint32_t halfword_cnt) { ADC_InitTypeDef adc; ADC_CommonInitTypeDef adc_common; DMA_InitTypeDef dma; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); /* ADCCLK = PCLK2 / 4 = 21MHz,不能超过手册上限 */ ADC_CommonStructInit(&adc_common); adc_common.ADC_Prescaler = ADC_Prescaler_Div4; adc_common.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; adc_common.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(&adc_common); ADC_StructInit(&adc); adc.ADC_Resolution = ADC_Resolution_12b; adc.ADC_ScanConvMode = ENABLE; /* 一次触发扫两个通道 */ adc.ADC_ContinuousConvMode = DISABLE; /* 靠外部触发,不自由跑 */ adc.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfConversion = 2; ADC_Init(ADC1, &adc); /* 采样时间 15 周期 + 12 位转换 12 周期 = 27 个 ADCCLK ≈ 1.29us */ ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_15Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_15Cycles); DMA_StructInit(&dma); dma.DMA_Channel = DMA_Channel_0; dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma.DMA_Memory0BaseAddr = (uint32_t)buf; dma.DMA_DIR = DMA_DIR_PeripheralToMemory; dma.DMA_BufferSize = halfword_cnt; 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.DMA_Priority = DMA_Priority_High; DMA_Init(DMA2_Stream0, &dma); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConv(ADC1); /* 先起一次,之后每来一个 TRGO 走一组 */ }

定时器那段的关键参数只有两个:TIM_Prescaler把计数时钟压到 1MHz,TIM_Period决定每多少个计数触发一次,二者相乘的倒数就是采样率。这里把预分频写死成 84,换来的是 ARR 可以直接写1000000/fs - 1,改采样率只动一个数,不会牵连别的地方。DMA 用循环模式配双缓冲,采完前一半后一半接着采,处理端在标志位上做乒乓,不会出现"读到一半被 DMA 覆盖"的撕裂。

ADC_SampleTime_15Cycles不是随便填的。采样时间太短,前端驱动能力弱的话采样电容充不满,幅值会系统性偏低,而且信号频率越高偏得越多——这条在第 5 章还会展开。

2.2 采样率、点数、频率分辨率:三个数只能定两个

FFT 的分辨率由采样率和点数共同决定:Δf = Fs / N。这个式子看着平淡,实际是整条链路的第一约束。

采样率 Fs点数 N频率分辨率 Δf采集时长 T可测上限
10240 Hz102410 Hz100 ms5120 Hz
10240 Hz40962.5 Hz400 ms5120 Hz
51200 Hz102450 Hz20 ms25600 Hz

选型时几个常见判断:

  • 被测算量的最低频率不能低于 2~3 个 Δf,否则谱峰挤在 bin1/bin2 上,直流泄漏和负频率镜像都会干扰,幅值和相位都没法看。
  • 采样率至少是被测信号最高频率的 2.5 倍,前端有抗混叠滤波时按滤波器截止频率定,没有滤波器就老老实实留 4 倍以上余量。
  • N 必须是 2 的整数次幂,CMSIS-DSP 的 FFT 只支持这种长度。N 翻四倍,分辨率提高四倍,但内存占用和运算时间同步翻四倍,1024 点 float32 的输入输出各占 4KB,小容量型号要掂量一下。
  • 采集时长 T = N / Fs 决定了频率的"绝对精度下限"。10Hz 的分辨率意味着 1kHz 信号的理论读数误差可达 ±5Hz,想做到 ±1Hz 就得靠第 6 章的插值。

2.3 两路信号不同步,相位差从一开始就是错的

这一节比前面都容易被忽略。用单 ADC 规则组扫描两个通道,两路之间天然隔着一个转换时间:采样 15 周期加转换 12 周期,27 个 ADCCLK,在 21MHz 下约 1.29us。这个固定时延对相位差的影响是可以量化的:

  • 被测 50Hz:360° × 50 × 1.29e-6 ≈ 0.023°,忽略不计。
  • 被测 1kHz:约 0.46°,一般场合也能接受。
  • 被测 10kHz:约 4.6°,已经超过多数测量任务允许的误差。
  • 被测 50kHz:约 23°,结果基本没法用。

解法有两条。硬件上选双 ADC 同步规则模式,两个 ADC 同一时刻对各自通道采样保持,采样时刻严格对齐,代价是占用两个 ADC 和更多引脚。软件上保留顺序采样,在相位差里补一个2πf·Δt的补偿项,Δt 由转换时序算出来,但这种做法的前提是两路信号频率一致且已知——测未知信号时这条路走不通。

常见做法是先在双 ADC 同步和单 ADC 顺序采样之间做选择:要测相位差就优先同步;只测幅值和频率,顺序采样完全够用。选错了硬件方案,后面软件怎么补都是补丁。

3. 在标准库工程里接入 CMSIS-DSP:编译开关、头文件与内存布局

标准外设库那套工程结构和 DSP 库不是一套东西。前者是寄存器级的外设驱动,后者是算法实现,中间靠编译器选项和几个宏连起来。把这几个开关配错,现象往往是五花八门的:链接时报一堆 undefined,或者编译过了但一执行浮点就进 HardFault,或者能跑但结果精度莫名地差。

3.1 标准库工程接入 DSP 库:三个编译开关不对就白干

先把库文件加进工程:DSP 库的源码或预编译库、arm_math.h及其包含的头文件目录。然后配宏和编译选项。

宏 / 选项取值作用配错的后果
ARM_MATH_CM41选中 M4 内核的优化实现链接报大量 undefined symbol
__FPU_PRESENT1告知库该内核带 FPU退化成软件浮点,慢一个量级
__FPU_USED1配合内核头文件打开 CPACR执行浮点指令直接 HardFault
-mfloat-abihard函数参数走 FPU 寄存器软硬浮点混用,参数传递错乱
-mfpufpv4-sp-d16指定单精度 FPU同上,行为不可预期

用 GCC 工具链的话,命令行长这样:

# 关键编译与链接选项,M4F 单精度 CFLAGS += -mcpu=cortex-m4 -mthumb CFLAGS += -mfpu=fpv4-sp-d16 -mfloat-abi=hard CFLAGS += -DARM_MATH_CM4 -D__FPU_PRESENT=1 -D__FPU_USED=1 CFLAGS += -O2 -ffunction-sections -fdata-sections

换成图形化 IDE 时,对应的是 Target 页里的 Floating Point Hardware 选项,选单精度,然后手工把三个宏加到预处理符号表里。

两个坑值得单独说。一个是__FPU_PRESENT重定义:标准库的设备头文件里通常已经定义过这个宏,再从命令行-D一遍,编译会刷一屏警告,处理办法是先看设备头文件里的取值,一致就不重复定义。另一个是精简版启动文件里缺了 CPACR 使能那段,SystemInit只做了时钟,没开协处理器,第一次执行 VMOV 就进 HardFault。遇到"明明编译通过、一跑就挂"的情况,先去看SystemInit里有没有SCB->CPACR |= 0x00F00000。

提示:Cortex-M4 的中断入口硬件会自动压栈 S0-S15 和 FPSCR,所以中断服务程序里调用浮点 FFT 本身是安全的,不需要额外保存上下文。前提是 FPU 已经使能,且没有关闭 lazy stacking。

3.2 arm_cfft_f32 和 arm_rfft_fast_f32 怎么选

库里有两条路:复数 FFT 和实数 FFT。

arm_cfft_f32吃的是 N 点复数序列,长度 2N 个 float,实部虚部交织存放。把 N 个实数补零虚部当 N 点复数喂进去,数学上是对的,前 N/2 个 bin 就是正频率分量,结果直接可用。代价是算了整整一倍冗余——实数序列的负频率部分是正频率的共轭,算了也用不上。

arm_rfft_fast_f32专为实数输入设计,只算前半谱,运算量约为复数版本的二分之一,内存也省一半。1024 点在 168MHz 的 M4 上跑一次大约是百微秒量级,对秒级刷新的测量任务完全够。

它的输出是打包格式,这一处必须记准:

  • out[0]是直流分量的实部,虚部恒为 0。
  • out[1]是奈奎斯特频率分量的实部,虚部同样没有。
  • 从 bin1 开始,out[2k]和out[2k+1]分别是第 k 根谱线的实部与虚部,k 从 1 到 N/2-1。

按常规"一个 bin 两个 float"去遍历数组,从 bin1 起就开始错位,算出来的幅值能对,相位全错。这个错位是血泪经验,排查时优先怀疑它。

还有一处 API 行为要注意:arm_rfft_fast_f32会改写输入缓冲区,位倒序是就地做的。两路信号共用一块输入缓冲,第二路算完第一路的相位就没了,必须各备一块。

3.3 用合成正弦自测:先验证 FFT 再谈测量

不要一上来就接真实信号。先用已知参数的合成序列跑通,把整条数值链路钉死。

#include "arm_math.h" #define FFT_N 1024 static float32_t wave[FFT_N]; static float32_t spec[FFT_N]; static arm_rfft_fast_instance_f32 rfft; /* 生成 1kHz、峰值 1.0、初相 0 的正弦,采样率 10240 */ void make_test_wave(float32_t amp, float32_t freq, float32_t ph) { uint32_t n; for (n = 0; n < FFT_N; n++) { wave[n] = amp * arm_sin_f32(2.0f * PI * freq * n / 10240.0f + ph); } } void selftest(void) { uint32_t k, kpeak = 0; float32_t mag, maxmag = 0.0f; arm_rfft_fast_init_f32(&rfft, FFT_N); make_test_wave(1.0f, 1000.0f, 0.0f); arm_rfft_fast_f32(&rfft, wave, spec, 0); for (k = 1; k < FFT_N / 2; k++) { float32_t re = spec[2 * k]; float32_t im = spec[2 * k + 1]; mag = sqrtf(re * re + im * im); if (mag > maxmag) { maxmag = mag; kpeak = k; } } /* 期望:amp≈1.0,freq≈1000Hz,相位≈0°(cos 基准) */ printf("amp=%.4f freq=%.1f\r\n", 2.0f * maxmag / FFT_N, kpeak * 10240.0f / FFT_N); }

合成 1000Hz 时正好落在 k=100 上(10240/1024=10Hz,1000/10=100),谱峰在格点上,没有栅栏损失。看三件事:幅值是否等于 1.0,误差应在一个百分点内;频率是否等于 1000;相位是否等于 0(注意正弦的相位基准比余弦晚 90°,代码里换成arm_cos_f32更直观)。三项都对上,再把输入换成 ADC 数据。

通过这一步之后如果实测偏,问题一定在模拟前端和采样时序,不在 FFT 本身。这个判断顺序能省掉大量在算法里瞎找的时间。

4. 幅值、频率、相位差的提取:归一化系数、谱峰定位与相位解缠

拿到频谱数组只是中间结果,三个量各自有一套换算规则,每一处都有一个容易写错的系数或范围。

4.1 幅值:除以 N 还是 N/2,差一个因子就差一倍

对一个峰值 A、频率落在整数格点上的单频正弦,arm_rfft_fast_f32在谱峰处的模等于 A·N/2。反推回去:

/* 单边谱还原正弦峰值 */ float32_t amp = 2.0f * maxmag / (float32_t)FFT_N;

这里省略了另一套常见写法。很多人直接两份相加再除 N,或者忘了那个 2,然后对着示波器调了半天"校准系数"。校准系数确实能对上一条谱线,但换一个频率、换一个窗函数立刻偏掉——因为它本质是把归一化错误用拟合掩盖了。

加窗之后系数要跟着改。Hann 窗的相干增益是 0.5(窗序列均值),谱峰模变成 A·N/4,补偿方式是再除以 0.5:

/* 加 Hann 窗后的幅值还原,WIN_CG 为窗的相干增益 */ #define WIN_CG 0.5f float32_t amp = 2.0f * maxmag / ((float32_t)FFT_N * WIN_CG);

如果谱峰不落在整数格点上,能量会散到相邻两三根谱线,单看峰值必然低估。这种场合要把峰附近几条谱线的功率加起来开方,再用窗的等效噪声带宽修正,或者直接用 4.2 的插值法修正峰值。

还有一条容易被忽略:ADC 码值到电压的换算系数。满量程 4095 对应参考电压,参考电压不同板子不同,这个系数写死在代码里,换板子幅值就成比例地偏。建议做成宏,标定时实测一次。

4.2 频率:谱线序号乘分辨率,栅栏效应怎么修

最直接的算法是找模最大的谱线序号 k,频率取 k·Fs/N。

/* 在 1 ~ N/2-1 内找峰,跳过直流和奈奎斯特 */ float32_t freq = (float32_t)kpeak * SAMPLE_FS / (float32_t)FFT_N;

问题是 k 只能是整数,真实频率落在两条谱线之间时,读数就在格点上跳。这就是栅栏效应:分辨率 10Hz 时,1013Hz 和 1027Hz 的信号可能都读成 1020Hz。频率误差最大可达 ±Δf/2。

改善的办法按成本从低到高排:

  • 提高 N。1024 换 4096,分辨率从 10Hz 到 2.5Hz,代价是采集时长和内存翻四倍。
  • 补零到 4N 再做 FFT。谱线变密,但分辨率并没有真正提高(信息量由原始采样时长决定),只是把谱峰形状画得更细,对找峰有帮助,对精度帮助有限。
  • 插值修正。用峰值和相邻谱线的幅度关系算出小数偏移量,不增加任何采样成本,能做到 Δf 的十分之一量级。这是第 6 章的主题。

选哪种看需求。测工频附近的量、只做定性判断,第一种就够;要出小数点后一位的读数,第三种跑不掉。

4.3 相位差:atan2 的 ±180° 和时间延迟补偿

两路信号各自做 FFT,在同一个 kpeak 上取相角:

float32_t re1 = spec1[2 * kpeak], im1 = spec1[2 * kpeak + 1]; float32_t re2 = spec2[2 * kpeak], im2 = spec2[2 * kpeak + 1]; float32_t ph1 = atan2f(im1, re1); float32_t ph2 = atan2f(im2, re2); float32_t dph = ph1 - ph2; /* 缠绕到 (-pi, pi] */ while (dph > PI) dph -= 2.0f * PI; while (dph < -PI) dph += 2.0f * PI; /* 顺序采样带来的固定时延补偿:t_delay 由 ADC 时序算出 */ float32_t freq = (float32_t)kpeak * SAMPLE_FS / (float32_t)FFT_N; dph -= 2.0f * PI * freq * ADC_SEQ_DELAY_S; float32_t phase_deg = dph * 180.0f / PI;

几个必须想清楚的点:

相位基准依赖信号形式。FFT 的相位是相对余弦的,被测信号写成 sin 时算出来的相位天然差 90°,两路都用同一种形式时差值不受影响,只有单路绝对相位才会踩这个坑。

缠绕范围是 ±180°。相位差真实值超过这个范围就出现周期模糊,比如 190° 会被读成 -170°。判断依据是信号频率和两路时延的乘积:如果预期相位差可能越过 ±180°,得靠扫频或已知时延来解缠,不能只看单次结果。

顺序采样的补偿方向不要搞反。第二路比第一路晚采 Δt,相当于信号被延迟了,测得相位差里含了一个负的2πf·Δt,补偿时是减还是加,拿一个已知相位的信号标一次就知道了,比推导靠谱。

两路增益不一致也会污染相位。幅度差本身不直接影响相角,但如果两路存在串扰或者 ADC 通道间增益差很大,谱峰附近的泄漏叠加会让小信号那一路的相位抖动明显。前端加缓冲、两路走同型号运放,比在算法里修更省事。

4.4 加窗之后三个量怎么跟着改

矩形窗频率分辨率最高,但泄漏严重,非整周期采样时谱峰两侧拖出一串旁瓣,幅值低估、相位偏移。Hann 窗把旁瓣压下去 30dB 以上,代价是主瓣变宽,两个挨得近的频率分量会糊在一起。

工程上的取舍我一般这么定:

场景窗函数幅值系数说明
单频、整周期采样矩形窗2/N精度最高,但要求 fs/f 是整数比
单频、任意频率Hann 窗2/(N·0.5)幅值稳定,频率靠插值补
多频、强弱并存Hann 或 Hamming同上抑制强信号旁瓣掩盖弱信号
需要测相位差Hann 窗同上泄漏对相位的影响比幅值更敏感

加窗是在时域乘,FFT 之前做。窗序列可以预先算好存成常量数组,也可以实时算——1024 点的 Hann 窗实时算一遍余弦,在 M4 上是微秒级,不值得为它占 4KB flash。

窗函数一变,幅值系数、插值公式的适用性都要跟着复核。矩形窗用重心插值偏差明显,Hann 窗用才对得上,这一点在第 6 章会给出具体公式。

5. 避坑与排查:五个把测量值带偏的现场问题

下面五条都是实际调试里反复遇到的,按"现象→原因→解决"整理,遇到读数不对可以顺着往下比。

5.1 现象:幅值随信号频率升高而腰斩,低频正常高频偏低

原因:ADC 采样保持时间不足是主因。采样电容在前端阻抗较高时充电不充分,信号频率越高、采样点之间电压变化越大,误差越明显。其次是前端没有抗混叠滤波,高频分量混叠进来把谱峰抬高或压低,干扰了峰值判断。

解决:把ADC_SampleTime从 15 周期提到 480 周期试一次,如果幅值随采样时间明显变化,就是驱动能力问题,前端加电压跟随器;如果换成已知幅值的信号源仍然偏低,检查从码值到电压的换算系数是否用了错误的参考电压。逐项排除,别一起改。

5.2 现象:频率读数永远停在 10Hz 的整数倍上,缓慢调信号源时读数一格一格跳

原因:栅栏效应,谱峰位置被量化到整数谱线。分辨率 10Hz 时,任何落在两条谱线之间的频率都会被归到最近的那条。

解决:短期用第 6 章的插值法,把小数偏移量算出来;中期把 N 提到 4096,分辨率到 2.5Hz;长期看需求,如果要求 0.1Hz 精度,靠加长采样时间比堆算法更可靠。

5.3 现象:相位差在 0° 和 180° 附近来回跳,同一路信号重复测也不稳定

原因:三种可能混在一起。一是两路信号幅度相差太大,小信号那一路的谱峰被噪声和泄漏主导,相角随机游走;二是被测频率接近奈奎斯特频率,正负频率的镜像分量靠得太近,互相污染;三是没有做顺序采样补偿,在较高频率下误差累积到几十度,缠绕后看起来像跳变。

解决:先看两路谱峰模值,相差超过 20dB 就先修前端增益;再看被测频率是否超过 Fs/4,超过就把采样率提上去;最后用一个同源分路的信号(相位差理论为 0)标定系统固有相移,把补偿系数定下来。

5.4 现象:DMA 中断里直接算 FFT,偶尔整段数据错乱,重跑一次又好了

原因:在循环模式的 DMA 传输完成中断里就地运算,此时 DMA 已经回头往缓冲区头部写新数据,运算读到的是新旧混合的样本。这种错误是偶发的,很容易被误判成"玄学"。

解决:改成乒乓缓冲。DMA 配成半满和全满双中断,半满时处理前半段、全满时处理后半段,处理区和采集区永远不重叠。缓冲区长度按 2N 分配,指针切换用状态机而不是标志位。

/* 半满:前半段已就绪;全满:后半段已就绪 */ void DMA2_Stream0_IRQHandler(void) { if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HTIF0)) { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HTIF0); ready_buf = 0; /* 处理 buf[0..N-1] */ process_flag = 1; } if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0); ready_buf = 1; /* 处理 buf[N..2N-1] */ process_flag = 1; } }

5.5 现象:同一份固件换一块板子,直流偏置和幅值都对不上

原因:前端偏置电阻的容差、参考电压的离散性、运放失调电压,都会让零点和满量程发生偏移。代码里如果写死了一个理论系数,换板必偏。

解决:上电做一次自校准。短接输入端采一段,算出零点偏置存起来;再接入已知幅度的参考信号,反算出增益系数。两个系数做成结构体,跟着板子走。另外直流分量一定要在 FFT 之前减掉,不能指望 bin0 单独处理——直流泄漏会抬高整个低频段。

6. 亚谱线精度:比值法把频率修到 0.1Hz、相位修到 1° 以内

谱线插值这件事,说破了就是在谱峰附近做一次加权,把落在两条谱线之间的真实位置估出来。我常用的是重心插值,公式简单、对 Hann 窗效果稳定,不需要额外存储:

/* 需要 kpeak-1、kpeak、kpeak+1 三根谱线的模 */ float32_t m0 = mag_at(kpeak - 1); float32_t m1 = mag_at(kpeak); float32_t m2 = mag_at(kpeak + 1); /* 小数偏移量 delta,范围 (-0.5, 0.5) */ float32_t delta = (m2 - m0) / (m0 + 2.0f * m1 + m2); /* 修正后的频率 */ float32_t freq = ((float32_t)kpeak + delta) * SAMPLE_FS / (float32_t)FFT_N;

delta的物理含义是真实频率偏离整数谱线的比例。分母是三点模值加权和,用模值而不是功率谱,是因为幅度谱的峰形更对称,插值线性更好。这个公式在 Hann 窗下能稳定到 Δf 的 5% 以内,也就是 10Hz 分辨率下做到 0.5Hz 的精度,如果采样时长拉到 400ms,亚赫兹精度是可以期待的。

相位差同步修正的思路一致。相位对谱峰位置的偏移很敏感,频率偏 0.1 个谱线间距,相位就可能偏十几度。修正方式有两种:一是用插值得到的delta反推相位补偿量,二是把两路信号都搬移到零中频再取相位比。前一种实现简单,但要针对自己的窗函数标定系数;后一种更稳,运算量也更大。我一般先用第一种,精度不够再换。

工程化落地时,我会加一份自检清单,每次改完代码都走一遍:

  1. 合成 1kHz、幅值 1.0V 的正弦,幅值读数误差是否在 1% 内。
  2. 换成 1007Hz,在 10Hz 分辨率下delta是否落在 0.7 附近。
  3. 两路同源信号,相位差是否稳定在 0°±1°。
  4. 断开一路输入,另一路读数是否正常,验证通道间无串扰。
  5. 常温下连续跑 10 分钟,读数漂移是否在允许范围内。

这套流程不是用来做验收的,是给自己留后悔药。有一次为了赶进度跳过了第 2 项,结果频率修正公式里把m2 - m0写成了m0 - m2,读数在 1007Hz 和 993Hz 之间来回翻,现场怎么调信号源都对不上,回头查了两个小时才看到符号写反了——那一刻才明白,插值公式的符号不是推导问题,是习惯问题。从那以后我每次动频谱代码,都先把合成信号的自检跑一遍再上真实信号,符号、系数、索引一次看清,比事后排查省太多时间。

希望帮到你。

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

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

Tomcat 7.0.108 Windows x64 部署配置与排错指南

简介&#xff1a;这份压缩包提供Apache Tomcat 7.0.108的Windows 64位发行版&#xff0c;面向需在本地部署和测试Java Web应用的开发者与学习者&#xff0c;可用于Servlet 3.0、JSP 2.2项目的运行验证。包内共640个文件、约10.1MB&#xff0c;以html说明、class与java源码、jsp…

作者头像 李华
网站建设 2026/10/26 2:16:33

LEFT JOIN性能问题排查:字符集不一致的陷阱

1. 问题现象&#xff1a;LEFT JOIN引发的性能灾难那天下午&#xff0c;我正在优化一个报表查询&#xff0c;原始SQL是个简单的子查询&#xff1a;SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE vip_level > 3)执行时间7ms&#xff0c;完全符合预期。…

作者头像 李华
网站建设 2026/10/26 2:16:06

STM32F103串口空闲中断+DMA实现精准帧接收

简介&#xff1a;本资源是一份面向嵌入式初学者与STM32进阶开发者的UART通信实战工程&#xff0c;聚焦STM32F103平台下利用空闲中断实现不定长串口数据收发的核心技术。针对无固定帧尾、数据长度动态变化的工业通信场景&#xff0c;提供完整可运行的HAL库实现方案&#xff0c;涵…

作者头像 李华
网站建设 2026/10/26 2:15:55

OpenAI DevDay深度解析:dots与Spaces如何重塑AI开发工作流

1. 发布会整体量级与最值得关注的变化先说结论&#xff1a;这届 OpenAI DevDay 从信息密度上看&#xff0c;是历届最高的一次。20 多项发布里&#xff0c;真正算得上“改变开发方式”的并不多&#xff0c;但每一个都踩在关键位置上。如果你只看热搜标题&#xff0c;可能会以为重…

作者头像 李华
网站建设 2026/10/26 2:15:49

电力数据API对接实战:dp123电天下平台接入指南

1. 项目概述最近在做一个电力数据相关的项目&#xff0c;需要接入dp123电天下平台的API接口来获取电天下列表搜索数据。这个平台在电力行业算是比较知名的数据服务提供商&#xff0c;他们的API接口文档相对完善&#xff0c;但实际对接过程中还是遇到了不少值得记录的问题和技巧…

作者头像 李华
网站建设 2026/10/26 2:15:37

深入解析JVM运行时数据区与内存管理优化

1. 为什么需要理解JVM运行时数据区&#xff1f;第一次遇到Java程序内存溢出时&#xff0c;我盯着控制台那行"java.lang.OutOfMemoryError"足足发呆了五分钟。当时正在处理一个电商促销活动的高并发场景&#xff0c;系统突然崩溃的瞬间&#xff0c;我才真正意识到——…

作者头像 李华