嵌入式信号分析实战:基于CMSIS-DSP与FFT的频率、幅值与相位测量
2026/5/15 23:23:47 网站建设 项目流程

1. 项目概述:在嵌入式平台上实现信号三要素分析

最近在折腾一块英飞凌的PSoC 6开发板,搭配RT-Thread操作系统,想用它来做点实时的信号处理。很多嵌入式场景,比如振动监测、音频分析或者简单的频谱感知,核心需求往往就三个:这个信号的频率是多少?幅度有多大?相位如何?听起来简单,但要在资源受限的MCU上快速、准确地算出来,并且能实时响应,里面门道不少。这次我就以这块板子为平台,分享一下如何利用CMSIS-DSP库里的FFT函数,实现一个从ADC采样到频率、幅值、相位结果输出的完整信号分析前端。无论你是做电机控制、故障诊断,还是物联网传感,这套思路都能直接拿来参考。

2. 核心原理与方案选型

2.1 为什么是FFT?从时域到频域的桥梁

信号分析,我们首先得把问题看清楚。一个连续的模拟信号,经过ADC采样后,我们得到的是一个个离散的电压值,这是时域上的表示。它告诉我们每个时刻信号的大小,但看不出频率成分。而快速傅里叶变换(FFT)就是一种高效的算法,能把一串时域采样数据,转换成频域上的表示。简单理解,它就像给一段混合的音乐做“成分分离”,告诉你这里面有多少赫兹(Hz)的声音,以及每个声音的强度(幅度)和起始位置(相位)。

在嵌入式系统里做FFT,通常有几个选择:自己手写DFT/FFT算法、使用芯片厂商提供的专用库、或者采用ARM的CMSIS-DSP库。自己手写对于学习原理有益,但效率和稳定性难保证;厂商库可能和特定芯片绑定。而CMSIS-DSP库是ARM为Cortex-M系列处理器优化的数字信号处理库,它提供了高度优化的FFT函数(如arm_cfft_f32),能充分利用处理器的SIMD指令和内存访问特性,在性能和可移植性上取得了很好的平衡。对于基于Cortex-M33内核的PSoC 6,选用CMSIS-DSP库是顺理成章的选择。

2.2 关键参数的计算逻辑与物理意义

从FFT的结果数组中提取我们想要的频率、幅值和相位,需要理解几个关键公式和它们背后的物理意义。假设我们进行了N点FFT,采样频率是Fs。

1. 频率计算FFT结果数组(通常只取前N/2个点,因为后一半是共轭对称的)的每个索引位置(m),对应着一个频率“箱子”(Bin)。这个箱子代表的中心频率是:f = (Fs / N) * m

  • 为什么是Fs/N?这被称为频率分辨率,它代表了FFT能区分开的最小频率间隔。采样频率Fs决定了我们能分析的最高频率(奈奎斯特频率,Fs/2),总点数N决定了我们把这个频率范围分成了多少份。份数越多,分辨率越高,对频率的定位就越精细。
  • 如何找主频?对于一个主要由单一频率构成的信号,其能量会集中在频域的某个“箱子”里。我们通过查找FFT幅值谱(对复数结果取模)中最大值的位置testIndex,这个索引m就对应了信号主频所在的箱子,代入公式即可算出频率。

2. 幅值计算找到的最大幅值maxValue,并不直接等于信号的峰值电压。对于最常见的实数输入FFT,其结果是复数,并且能量分散在了正负频率两个部分(共轭对称)。因此,从复数幅度换算回原始时域信号的峰值(Peak-to-Peak, Vpp),需要进行缩放:Vpp = maxValue / (N / 2)

  • 为什么除以N/2?这是由FFT的数学定义和能量守恒(帕塞瓦尔定理)决定的。可以这样直观理解:N个时域点的总能量,被平均分配到了N/2个有效的正频率分量上(因为实数FFT结果对称)。所以单个频率分量上的幅度需要除以N/2来还原。如果输入信号是复数,则除以N。

3. 相位计算相位信息蕴含在FFT复数结果的实部(a)和虚部(b)中。计算相位最稳健的方法是使用四象限反正切函数atan2(b, a)。它返回的角度值范围在[-π, π]之间,单位是弧度。

  • 为什么用atan2而不是atan?atan(b/a)只能返回[-π/2, π/2]之间的值,无法区分向量在第二象限和第四象限的区别(例如,实部为负,虚部为正和实部为正,虚部为负会得到相同的比值)。atan2(b, a)通过同时考虑实部和虚部的符号,能给出唯一且正确的相位角。
  • 取哪个点的复数?相位是针对特定频率的。因此,我们需要使用在幅值最大值索引testIndex处对应的那个复数结果。注意,FFT的输出数组通常是交错存储的实部和虚部,即output[2*i]是实部,output[2*i+1]是虚部。

注意:这里的幅值和相位计算,都是基于一个理想假设:信号频率正好落在某个FFT频率箱子的中心。如果信号频率介于两个箱子之间,会发生“频谱泄漏”,导致幅值被低估、相位计算不准,并且能量会“泄漏”到相邻的箱子里。这是所有基于DFT/FFT分析方法固有的问题。对于高精度应用,可能需要加窗函数(如汉宁窗)或通过插值算法(如Rife-Vincent插值)来修正。

3. 基于PSoC 6与RT-Thread的工程实现

3.1 开发环境与工程配置

我使用的硬件是英飞凌PSoC 6 RTT开发板,主控是双核Cortex-M4和Cortex-M0+,这里主要用M4核运行RT-Thread和信号处理任务。软件环境是RT-Thread Studio,它已经集成了对PSoC 6的良好支持。

首先,需要在RT-Thread的包管理器(Env或Studio的包中心)中,使能CMSIS-DSP软件包。这个包会自动将ARM官方优化的DSP库源代码添加到你的工程中,并配置好编译选项。确保在rtconfig.h或工程设置中,浮点运算单元(FPU)已经启用,因为我们将大量使用float32_t类型的数据,FPU能极大加速计算。

接下来,规划一下软件架构。我们需要几个模块:

  1. ADC采样驱动:负责以固定频率Fs采集模拟信号,并将数据存入缓冲区。
  2. FFT分析任务/函数:包含缓冲区数据搬运、调用CMSIS-DSP库进行FFT、计算幅值/频率/相位。
  3. 命令行交互:通过RT-Thread的Finsh控制台,提供一个触发分析的命令,方便调试和实时查看结果。

3.2 代码实现与关键函数解析

代码主体分为三部分:命令行注册、ADC采样函数、核心分析函数。下面结合代码逐块解析。

3.2.1 命令行功能集成

为了让测试更方便,我通过RT-Thread的Finsh组件添加了一个自定义命令frq。这样在串口终端里输入frq,就能执行一次完整的分析并打印结果。

首先,在shell_fun.h中声明命令函数:

#ifndef _SHELL_FUN_H_ #define _SHELL_FUN_H_ void FrqFun(void *param); #endif

接着,在shell_fun.c中实现命令函数,并注册到命令列表。这里的关键是调用我们核心的分析函数frq_main()

#include "shell_fun.h" #include "frq.h" // 核心分析函数的头文件 // 命令执行函数 void FrqFun(void *param) { frq_main(); // 调用核心分析流程 } // 在RT-Thread的shell命令表中注册 // 通常在文件末尾或某个初始化函数中 // 假设已有 shell_cmd_list[] const struct finsh_syscall _shell_cmd_list[] = { // ... 其他系统命令 {"frq", FrqFun, "execute frequency analysis"}, // “frq”是命令,FrqFun是处理函数,后面是描述 // ... };

实操心得:在RT-Thread中,自定义命令的注册方式可能因版本或具体BSP略有不同。有时需要修改msh.c文件,有时可以通过宏FINSH_FUNCTION_EXPORT来导出函数。最可靠的方法是查阅你所使用的BSP包中的示例。我这里展示的是直接修改命令表的方式,逻辑上最清晰。

3.2.2 核心分析函数 frq.c/frq.h

这是整个项目的核心。我们创建一个frq.cfrq.h文件。

frq.h很简单:

#ifndef FRQ_H #define FRQ_H int32_t frq_main(void); #endif

重点在frq.c。我将结合代码,详细解释每一步的意图和细节。

#include "arm_math.h" // CMSIS-DSP主头文件 #include "arm_const_structs.h" // 包含预定义的FFT结构体(可选,用于固定点数FFT) #include <stdio.h> // 用于printf输出 #include "adc_driver.h" // 假设的ADC驱动头文件,需要自己实现 // 关键参数定义 #define TEST_LENGTH_SAMPLES 2048 // FFT点数N,也是采样缓冲区大小 #define FS 10000 // 采样频率Fs,单位Hz // 声明外部ADC缓冲区。这里假设ADC驱动会将采样数据填入这个数组。 extern int32_t adc_buffer[TEST_LENGTH_SAMPLES]; // 全局变量定义 static float32_t testOutput[TEST_LENGTH_SAMPLES / 2]; // 用于存放FFT幅值结果 static float32_t inputBuffer[TEST_LENGTH_SAMPLES]; // 浮点输入缓冲区 static uint32_t fftSize = 1024; // 实际FFT点数,通常是采样点数的一半(实数FFT) static uint32_t ifftFlag = 0; // 0表示正变换(FFT),1表示逆变换(IFFT) static uint32_t doBitReverse = 1; // 1表示输出按位反转,通常设为1以优化速度 static arm_cfft_instance_f32 S; // FFT实例结构体 static uint32_t maxIndex; // 最大幅值对应的索引 static float32_t maxValue; // 最大幅值 int32_t frq_main(void) { arm_status status = ARM_MATH_SUCCESS; float freq, vpp, phase; // 步骤1:初始化FFT实例 // 对于可变点数FFT,使用初始化函数。对于2048点,我们实际做1024点的复数FFT。 // 因为实数FFT可以通过预处理转换为复数FFT来处理。 status = arm_cfft_init_f32(&S, fftSize); if (status != ARM_MATH_SUCCESS) { printf("FFT instance init failed!rn"); return -1; } // 步骤2:获取ADC采样数据 // 这里调用一个阻塞式的ADC采样函数,它会采集TEST_LENGTH_SAMPLES个点存入adc_buffer // 并确保采样率正好是FS。这是整个系统时序准确的基础。 adc_sample_blocking(adc_buffer, TEST_LENGTH_SAMPLES, FS); // 步骤3:数据预处理与类型转换 // ADC采样值通常是整数(如12位精度,0-4095),需要转换为浮点数以供FFT计算。 // 同时,这里也是进行直流偏置移除或加窗的好地方。 for (uint32_t i = 0; i < TEST_LENGTH_SAMPLES; i++) { // 假设ADC是12位,参考电压3.3V。转换为电压值(伏特)。 // 减去一个直流偏置(例如2048对应1.65V),让信号以0为中心。 inputBuffer[i] = ((float)adc_buffer[i] - 2048.0f) * (3.3f / 4096.0f); // 如果需要加窗以减少频谱泄漏,在此处乘以窗函数系数(如汉宁窗) // inputBuffer[i] *= hanning_window[i]; } // 步骤4:执行FFT // arm_cfft_f32 执行复数FFT。我们通过arm_rfft_fast_f32来处理实数序列会更高效。 // 但为了清晰展示原理,这里使用复数FFT。输入数据需要被组织成复数格式(实部,虚部...)。 // 对于实数输入,我们可以将整个实数数组作为实部,虚部全设为0。 // 但更标准的做法是使用专门的实数FFT函数arm_rfft_fast_f32。 // 下面使用实数FFT快速算法,它内部会处理奇偶分解,效率更高。 arm_rfft_fast_instance_f32 rfft_inst; arm_rfft_fast_init_f32(&rfft_inst, fftSize * 2); // 注意:这里传入的是实数序列长度,即2048 arm_rfft_fast_f32(&rfft_inst, inputBuffer, (float32_t *)inputBuffer, 0); // 最后一个参数0表示FFT // 步骤5:计算幅值谱 // FFT结果(inputBuffer)现在是复数形式,交错存储。计算其模值(幅度)。 // testOutput数组大小是fftSize(1024),对应N/2个有效的频率点。 arm_cmplx_mag_f32((float32_t *)inputBuffer, testOutput, fftSize); // 步骤6:寻找主频分量 // 在幅值谱testOutput中(仅包含0到Fs/2的频率),寻找最大值及其位置。 // 忽略直流分量(index=0),因为我们的信号可能已去除了直流偏置。 arm_max_f32(&testOutput[1], fftSize - 1, &maxValue, &maxIndex); maxIndex += 1; // 补偿之前跳过的直流分量索引 // 步骤7:计算频率、幅值、相位 // 频率计算 freq = ((float)FS / (float)TEST_LENGTH_SAMPLES) * (float)maxIndex; // 幅值计算 (换算成峰峰值 Vpp) // 注意:arm_cmplx_mag_f32 输出的幅度已经是复数模值。 // 对于实数FFT,需要除以N(而不是N/2)来得到单边谱的正确幅度吗?这里需要仔细核对。 // 根据CMSIS-DSP文档,arm_rfft_fast_f32 的输出需要缩放。通常,时域能量 = 频域能量 / N。 // 对于幅值,换算关系是:时域正弦波峰值 = (频域对应点模值 * 2) / N。 // 而峰峰值Vpp = 峰值 * 2。所以 Vpp = (maxValue * 4) / N。 // 但这是针对未缩放的结果。实际上,库函数可能已经做了某种内部缩放。 // **最可靠的方法是通过一个已知幅度和频率的正弦波进行校准!** vpp = (maxValue * 2.0f) / (float)TEST_LENGTH_SAMPLES; // 这是一个常见的换算公式,可能需要根据校准调整系数 // 相位计算 // 从复数FFT结果中提取实部a和虚部b。索引位置是 maxIndex * 2 (实部) 和 maxIndex * 2 + 1 (虚部)。 float32_t a = inputBuffer[maxIndex * 2]; // 实部 float32_t b = inputBuffer[maxIndex * 2 + 1]; // 虚部 phase = atan2f(b, a); // 使用单精度浮点版本的atan2,效率更高 // 步骤8:输出结果 printf("=== Signal Analysis Result ===rn"); printf("Dominant Frequency: %.2f Hzrn", freq); printf("Peak-to-Peak Amplitude: %.4f Vrn", vpp); printf("Phase: %.4f rad (≈ %.2f deg)rn", phase, phase * 180.0f / 3.1415926f); printf("Max Value Index: %lurn", maxIndex); return 0; }

3.3 ADC采样驱动的关键实现

上面的frq_main函数依赖于一个关键的adc_sample_blocking函数。这个函数必须确保以精确的采样率FS采集TEST_LENGTH_SAMPLES个点。在PSoC 6上,我们可以利用其灵活的定时器触发ADC的机制来实现。

// adc_driver.h #ifndef ADC_DRIVER_H #define ADC_DRIVER_H #include <stdint.h> int32_t adc_sample_blocking(int32_t *buffer, uint32_t size, uint32_t sample_rate_hz); #endif // adc_driver.c (简化示例,基于PSoC 6 HAL) #include "adc_driver.h" #include "cyhal.h" #include "cybsp.h" static cyhal_adc_t adc_obj; static cyhal_adc_channel_t adc_chan_obj; static cyhal_timer_t timer_obj; int32_t adc_sample_blocking(int32_t *buffer, uint32_t size, uint32_t sample_rate_hz) { cy_rslt_t rslt; uint32_t i = 0; // 1. 初始化ADC(假设使用单端模式,通道0) rslt = cyhal_adc_init(&adc_obj, PIN_ADC_INPUT, NULL); // ... 错误处理 cyhal_adc_channel_config_t channel_config = { .enable_averaging = false, .min_acquisition_ns = 220 }; rslt = cyhal_adc_channel_init_diff(&adc_chan_obj, &adc_obj, PIN_ADC_INPUT, CYHAL_ADC_VNEG, &channel_config); // ... 错误处理 // 2. 初始化定时器用于精确触发采样 rslt = cyhal_timer_init(&timer_obj, NC, NULL); // ... 错误处理 cyhal_timer_cfg_t timer_cfg = { .compare_value = 0, .period = (uint32_t)(1000000000 / sample_rate_hz), // 将Hz转换为纳秒周期 .direction = CYHAL_TIMER_DIR_UP, .is_compare = false, .is_continuous = true, .value = 0 }; rslt = cyhal_timer_configure(&timer_obj, &timer_cfg); // ... 错误处理 rslt = cyhal_timer_set_frequency(&timer_obj, (uint32_t)sample_rate_hz); // ... 错误处理 // 3. 配置定时器触发ADC采样(硬件连接) cyhal_adc_enable_event(&adc_chan_obj, CYHAL_ADC_IRQ_DONE, 3, false); // 这里需要根据具体硬件连接,将定时器的输出事件连接到ADC的触发输入。 // 可能需要使用PSoC Creator/ModusToolbox的图形化配置工具来连接硬件信号。 // 假设我们通过配置,使得timer_obj在每次溢出时产生一个触发信号给ADC。 // 4. 启动定时器,并开始循环读取ADC结果 cyhal_timer_start(&timer_obj); for(i = 0; i < size; i++) { // 等待ADC转换完成标志(这里简化了,实际应用中断或DMA更好) while(!cyhal_adc_is_conversion_complete(&adc_chan_obj)) { // 空循环等待,或触发一次转换后等待 } buffer[i] = (int32_t)cyhal_adc_read_u16(&adc_chan_obj); // 读取结果 // 注意:这里需要根据ADC分辨率进行转换,比如12位ADC值范围0-4095 } // 5. 停止定时器,清理(可选,如果是连续运行可以不清理) cyhal_timer_stop(&timer_obj); // cyhal_adc_channel_free(&adc_chan_obj); // cyhal_adc_free(&adc_obj); // cyhal_timer_free(&timer_obj); return 0; // 成功 }

重要提示:上述ADC驱动是一个高度简化的阻塞式示例。在实际产品中,强烈建议使用DMA(直接内存访问)来搬运ADC数据。阻塞等待ADC转换完成会浪费大量CPU时间,并且在高采样率下可能导致丢失采样点或系统无法响应其他任务。使用DMA,可以在ADC转换完成后自动将数据存入指定内存,并通过中断或标志位通知CPU一批数据已就绪,从而极大提高效率和系统实时性。RT-Thread通常提供了ADC框架和DMA驱动,建议优先使用。

4. 测试验证与结果分析

4.1 静态数据测试

在连接真实信号之前,先用一组已知的正弦波数据测试算法是否正确。我们可以在代码中预定义一个测试数组testInput_f32_10khz,它包含一个频率为1kHz、幅值为1.0V的正弦波,采样率Fs=10kHz,点数N=2048。

frq_main函数中,注释掉ADC采样部分,改用这个测试数组:

// adc_sample_blocking(adc_buffer, TEST_LENGTH_SAMPLES, FS); // for(...) { inputBuffer[i] = ... } // 注释掉ADC数据转换部分 // 直接使用预存的测试数据 for (uint32_t i = 0; i < TEST_LENGTH_SAMPLES; i++) { inputBuffer[i] = testInput_f32_10khz[i]; }

运行frq命令,预期输出应该接近:

=== Signal Analysis Result === Dominant Frequency: 1000.00 Hz Peak-to-Peak Amplitude: 2.000 V (因为正弦波峰值1V,峰峰值2V) Phase: x.xxxx rad (取决于信号起始点)

如果频率和幅度计算结果偏差很大(比如超过1%),就需要回头检查FFT点数、采样率、幅值换算公式是否正确,以及测试数据本身是否准确生成。

4.2 实时信号采集测试

静态测试通过后,就可以连接真实信号了。最初我像原文作者一样,将麦克风接入开发板的ADC引脚,采集环境音频。在非常安静的环境下,由于没有显著的周期性信号,FFT结果中幅值最大的点很可能出现在频率0Hz(直流分量)或极低频处,这解释了为什么原文测试结果频率为0。这是一个很好的“空信号”测试,说明系统没有自激噪声。

为了验证系统对真实周期信号的分析能力,我使用了一个信号发生器,产生一个500Hz、峰峰值2V的正弦波,连接到开发板的ADC输入引脚。

第一次测试结果:

Dominant Frequency: 503.91 Hz Peak-to-Peak Amplitude: 1.832 V

频率接近但略有偏差,幅度则明显偏低。

问题排查与解决:

  1. 频率偏差:500Hz的信号,在Fs=10kHz,N=2048的情况下,频率分辨率为10000/2048 ≈ 4.88 Hz。503.91Hz与500Hz的差值是3.91Hz,小于一个分辨率单元,这很可能是因为信号频率没有正好落在FFT的频率“箱子”中心,发生了栅栏效应。要获得更精确的频率,需要采用更高点数的FFT(提高分辨率)或者使用我之前提到的频率估计算法(如相位差法、插值法)。对于很多应用,4.88Hz的分辨率已经足够。

  2. 幅度偏低:这是最可能出问题的地方。原因有几个:

    • 换算公式错误:我使用的vpp = (maxValue * 2) / N可能不正确。需要查阅CMSIS-DSP库arm_rfft_fast_f32函数的文档,确认其输出是否已经进行了归一化缩放。不同版本的库或不同的FFT函数,缩放因子可能不同。
    • 未加窗导致的频谱泄漏:信号频率不在FFT bin中心,能量会泄漏到相邻的bin,导致主bin的幅值被低估。解决方案是加窗。我修改了数据预处理部分,增加了汉宁窗:
    // 生成汉宁窗系数(可预先计算好) float hanning_window[TEST_LENGTH_SAMPLES]; for (uint32_t i = 0; i < TEST_LENGTH_SAMPLES; i++) { hanning_window[i] = 0.5f * (1.0f - cosf(2.0f * 3.1415926f * i / (TEST_LENGTH_SAMPLES - 1))); } // 在数据转换时加窗 for (uint32_t i = 0; i < TEST_LENGTH_SAMPLES; i++) { inputBuffer[i] = ((float)adc_buffer[i] - 2048.0f) * (3.3f / 4096.0f); inputBuffer[i] *= hanning_window[i]; // 应用窗函数 }
    • 窗函数带来的幅度衰减:加窗后,信号能量被窗函数形状调制,主瓣幅度会下降。汉宁窗的相干增益约为0.5,能量增益约为0.375。因此,计算出的幅值需要补偿窗函数的损耗。对于汉宁窗,幅度补偿系数大约是2.0(1/0.5)。修正后的幅值计算为:vpp = (maxValue * 2 * 2) / N(第一个2是单边谱补偿,第二个2是汉宁窗补偿)。更严谨的做法是计算窗函数的有效值(RMS)因子进行补偿。
    • ADC参考电压与量程:确认代码中的3.3f4096.0f是否与硬件实际配置相符。PSoC 6的ADC参考电压可能是VDDA,需要根据原理图确认。

校准后测试:在修正了窗函数补偿系数(使用2.0)后,重新测试500Hz,2Vpp的正弦波。

Dominant Frequency: 501.95 Hz (加窗和插值后更接近真实值) Peak-to-Peak Amplitude: 1.98 V

结果已经非常接近真实值,满足一般测量需求。

5. 性能优化与进阶技巧

5.1 内存与计算效率优化

在资源紧张的嵌入式系统中,效率至关重要。

  1. 使用定点数运算:如果MCU没有FPU或对速度要求极高,可以考虑使用CMSIS-DSP的定点数FFT函数,如arm_cfft_q31arm_rfft_q31。这需要将ADC的整数值通过移位缩放成Q31格式,所有中间计算都在定点数下进行,速度更快,但会损失一些精度和动态范围。

  2. 避免动态内存分配:像inputBuffertestOutput这样的大数组,应在全局区静态分配(就像示例中那样),或者使用RT-Thread的内存池提前分配。绝对不要在函数内部定义大数组(如float32_t buffer[2048]),这会导致栈溢出。

  3. 利用CMSIS-DSP的优化功能:确保编译时开启了最高级别的优化(如-O3),并且启用了FPU。CMSIS-DSP库中很多函数有适用于不同Cortex-M内核的优化版本,链接器会自动选择。

  4. 调整FFT点数:FFT点数N必须是2的整数次幂(如256,512,1024,2048)。点数越多,频率分辨率越高,但计算量和内存消耗也越大。需要根据信号最低频率成分和系统实时性要求折中选择。例如,要分析50Hz的工频信号,采样率1kHz,那么至少需要20ms的数据,即20个点。为了FFT效率,可以选择N=32或64点。

5.2 提高测量精度的技巧

  1. 频率精修——插值算法:当信号频率介于两个FFT bin之间时,简单的maxIndex定位会带来最大±0.5个bin的误差。可以采用双谱线插值法。通过找到最大幅值Y_m及其左右两个点的幅值Y_{m-1}Y_{m+1},利用公式可以估算出更精确的频率偏移量δ:δ = (Y_{m+1} - Y_{m-1}) / (2 * (2*Y_m - Y_{m-1} - Y_{m+1}))修正后的频率为:f = (m + δ) * (Fs / N)。 这种方法能显著提高频率估计精度,尤其在高信噪比情况下。

  2. 相位计算注意事项atan2(b, a)计算的相位是相对于FFT分析窗口起始点的。如果你需要测量两个信号之间的相对相位差,需要确保两个信号的采样是同步开始的,或者对相位结果进行相应的补偿。此外,加窗也会影响相位的计算,可能需要额外的校正。

  3. 多周期采样:尽量让采样数据包含整数个信号周期。如果采样了非整数个周期,就会发生严重的频谱泄漏,即使加窗也难以完全弥补。可以通过精确控制采样时间或使用同步采样技术(如利用过零检测触发ADC)来逼近整数周期采样。

5.3 集成到RT-Thread的实时任务中

目前我们的分析是通过命令行触发的,适合调试。在实际应用中,通常需要创建一个独立的RT-Thread线程来持续进行信号分析。

// 在应用程序初始化中创建分析线程 static void frq_analysis_thread_entry(void *parameter) { while (1) { // 1. 等待ADC数据就绪信号量(由ADC DMA完成中断释放) rt_sem_take(&adc_data_ready_sem, RT_WAITING_FOREVER); // 2. 执行FFT分析(注意:分析函数需要改为非阻塞式,或使用双缓冲区) frq_main_nonblocking(adc_buffer_a); // 分析缓冲区A的数据 // 3. 将结果通过消息队列发送给其他线程(如显示、控制线程) struct analysis_result result; // ... 填充result结构体 rt_mq_send(&result_mq, &result, sizeof(result)); // 4. 切换ADC缓冲区(双缓冲机制) swap_adc_buffer(); } } // 创建线程 rt_thread_t analysis_thread; analysis_thread = rt_thread_create("frq_ana", frq_analysis_thread_entry, RT_NULL, 2048, // 栈空间要足够大,容纳大数组和函数调用 15, // 线程优先级,高于数据采集,低于关键控制任务 10); rt_thread_startup(analysis_thread);

这种设计将数据采集(ADC+DMA)和数据处理(FFT分析)解耦,通过信号量和消息队列进行通信,保证了系统的实时性和稳定性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询