利用DSP进行快速傅里叶变换

利用DSP进行快速傅里叶变换

流程图

主函数

img

ADC中断

img

外部中断10:15线

img

大致思路

  • 利用ADC采样,并将数据利用DMA搬运至内存
  • 将数据转化为电压,并进行截止频率为100KHz低通滤波
  • 将直流偏置去除后进行快速傅里叶变换,并将变换结果传入串口

初始化

时钟

  • 使能RCC,启用外部晶振
    img
  • 将时钟树安如下配置
    img

中断

  • 中断优先级组设置为0位

按钮

  • 将按钮设置为外部中断触发模式,
    img
  • 将子优先级设置为0
    img

TIM2

  • TIM2分频设置为84 - 1
  • 设重装载值为10 - 1
  • 触发外部事件
    img

ADC

  • 开启采样通道1
  • 使用TIM2作为外部触发源
    img
  • 启用DMA,方向为外设至内存
    img
  • 使能ADC中断,子优先级为0
    img

串口2

img

  • 使能串口2,采样异步通讯,波特率9600,位长8,无校验位,1停止位

准备步骤

  • 启用DSP
    img
  • 在编译设置中定义以下内容
    img
USE_HAL_DRIVER,STM32F401xE,ARM_MATH_CM4,ARM_MATH_MATRIX,ARM_MATH_ROUNDING

若不写入以下定义,DSP库内容可能无法调用,具体宏定义需根据Cortex内核型号填写

代码部分

引入以下头文件

  • main.c
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <math.h>
#include "arm_math.h"
#include "arm_const_structs.h"
/* USER CODE END Includes */
  • main.h
/* USER CODE BEGIN Includes */
#include <stdbool.h>//使用标识符
/* USER CODE END Includes */

全局变量

  • main.c内定义以下全局变量
/* USER CODE BEGIN PV */
float data[NPT];//转化为电压后的采样数据
uint16_t measured_data[NPT];//ADC采样的原始数据
volatile bool measured_flag = false;//测量标识符,真表示测量完成,假表示未完成
uint8_t measure_times = 0;//采样次数

float data_in[NPT*2]={0};//FFT函数需要的复数数组

float data_out[NPT/2];//存储傅里叶变换结果的数组
float filtered_data[NPT];//存储滤波后电压的数组
int i = 0; 
int j = 0;
#define IIR_NUM_STAGES 2

float iir_state[4 * IIR_NUM_STAGES];

/* 4阶 Butterworth LPF, Fs=100k, Fc=45k */
const float iir_coeffs[5 * IIR_NUM_STAGES] = {
    /* Section 1 */
    0.292893f, 0.585786f, 0.292893f,
    0.171573f, -0.207107f,

    /* Section 2 */
    0.292893f, 0.585786f, 0.292893f,
    0.171573f, -0.207107f
};

arm_biquad_casd_df1_inst_f32 iir_filter;//数字滤波器数据结构体
/* USER CODE END PV */
  • 在完成硬件初始化后,将数字滤波器进行初始化
	arm_biquad_cascade_df1_init_f32(
    &iir_filter,
    IIR_NUM_STAGES,
    (float32_t *)iir_coeffs,
    iir_state
);

重定向

  • 将串口进行重定向
int fputc(int c,FILE*stream)
{
HAL_UART_Transmit(&huart2,(unsigned char *)&c,1,1025);
	return 1;
}

主逻辑

  • 在主循环中写入以下内容:
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		 if(measured_flag)//判断是否测量完成
    {
        HAL_TIM_Base_Stop(&htim2);//停止及时去计时
        measured_flag = false;//翻转标识符

        /* 1. ADC 原始数据转电压 */
        for(i = 0; i < NPT; i++)
        {
            data[i] = (float)(measured_data[i]) * 3.3f / 4095.0f;
        }
				float mean = 0.0f;//偏置电压
				for(i = 0; i < NPT; i++)
				{
						mean += data[i];
				}
				mean /= NPT; // 计算平均值(也就是直流分量)

				for(i = 0; i < NPT; i++)
				{
						data[i] -= mean; // 每个数据点减去直流分量
				}
        /* 2.  IIR 低通滤波 */
        arm_biquad_cascade_df1_f32(
            &iir_filter,
            data,
            filtered_data,
            NPT
        );

        /* 3. 构造复数输入(实部=滤波后数据,虚部=0) */
        for(j = 0; j < NPT; j++)
        {
            data_in[j*2]   = filtered_data[j];  // 用滤波后的数据
            data_in[j*2+1] = 0;
        }

        /* 4. FFT */
        arm_cfft_f32(&arm_cfft_sR_f32_len1024, data_in, 0, 1);
        arm_cmplx_mag_f32(data_in, data_out, NPT/2);

        /* 5. 串口输出频谱 */
        for(i = 0; i < 256; i++)
        {
            printf("%f\r\n", data_out[i]);
        }
    }

    HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */


中断

ADC中断

  • 中断服务程序不写入内容,主要内容位于回调函数中
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc){
		if(hadc == &hadc1){//判断中断是否来自ADC1
			measured_flag = true;//将测量标识符设位真
			HAL_ADC_Stop_DMA(&hadc1);///停止ADC采样
		}
}

外部中断10:15线

  • 中断服务程序不写入内容,主要内容位于回调函数中
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
	if(GPIO_Pin == B1_Pin){//判断中断是否来自按钮
		measure_times++;
		HAL_TIM_Base_Start(&htim2);//开始TIM2定时
		HAL_ADC_Start_DMA(&hadc1, (uint32_t*)measured_data, 1024);//开启ADC采样
	}
}

实际演示

  • 输入10k方波
    img
  • 串口显示如下
    img
  • 前256分量如下:
1.400801
2.471793
2.473416
2.465457
2.372946
2.425844
2.521457
2.447601
2.550486
2.523706
2.533057
2.499286
2.565099
2.493804
2.587473
2.616401
2.469507
2.633758
2.644655
2.709357
2.601642
2.516621
2.727076
2.627077
2.565368
2.627743
2.647778
2.836276
2.781068
2.838553
2.836743
2.843075
2.856375
2.923610
2.925535
2.982012
3.000818
3.039506
3.123276
3.126656
3.134918
3.262079
3.245975
3.296554
3.327053
3.366144
3.381118
3.535725
3.569757
3.628863
3.681803
3.697105
3.790247
3.870285
3.980594
4.019612
4.006988
4.235687
4.289810
4.446252
4.513869
4.597230
4.639082
4.837473
4.945637
4.992164
5.230241
5.360620
5.464055
5.689424
5.772608
5.997605
6.239776
6.407490
6.674295
6.978508
7.124632
7.488853
7.746690
8.186080
8.565697
8.858301
9.369551
9.839228
10.456542
10.970904
11.703323
12.510405
13.323750
14.369272
15.531316
16.981569
18.633646
20.606293
23.031265
26.173704
30.428005
35.890980
44.317707
57.513546
81.592537
140.077621
491.141998
327.795105
123.156837
75.927925
54.984604
43.022762
35.445351
30.137138
26.193779
23.227341
20.834982
18.973305
17.331562
16.021149
14.796187
13.829935
13.044458
12.191614
11.574925
10.997891
10.440570
9.951868
9.475358
9.166995
8.729193
8.436425
8.008877
7.709104
7.544075
7.172204
7.053110
6.798564
6.596962
6.385178
6.245797
6.106210
5.940033
5.709278
5.584699
5.455489
5.312902
5.140223
5.106984
5.044437
4.912179
4.699249
4.739985
4.515659
4.368349
4.390322
4.384507
4.220192
4.133438
4.116040
4.011080
4.003201
3.923776
3.765441
3.784959
3.676424
3.661439
3.628921
3.461794
3.437808
3.374477
3.329373
3.281436
3.208076
3.199665
3.124314
3.095588
3.092242
2.991285
3.014721
2.954543
2.862082
2.849180
2.787791
2.792364
2.623590
2.679157
2.631389
2.531603
2.599375
2.491756
2.551666
2.437954
2.314180
2.484884
2.390381
2.308582
2.228652
2.224155
2.317746
2.145042
2.091269
2.091478
2.220466
1.949609
1.969108
2.008158
1.984970
1.970665
2.085511
1.912068
1.838251
1.826392
1.856770
1.834694
1.738589
1.674930
1.741444
1.704563
1.657602
1.599033
1.656065
1.594393
1.620420
1.585091
1.481975
1.474007
1.473816
1.470633
1.435890
1.332363
1.398342
1.333474
1.317497
1.283868
1.277422
1.350957
1.213982
1.233896
1.228456
1.211402
1.177395
1.150801
1.129884
1.130578
1.096754
1.063881
1.069028
1.061341
1.037308
1.026699
1.013479
0.979152
0.986592
0.958967
0.934623
0.966617
0.932155
0.923834
0.909563
  • 10kHz三角波测量结果如下
    img
  • 前256分量数据如下:
0.649583
2.810229
2.643228
2.779343
2.693738
2.711329
2.327062
2.656405
2.678935
2.780862
2.682228
2.701635
2.546489
2.747350
2.737254
2.957501
2.711521
2.790905
2.566398
2.768748
2.873613
2.610902
2.846540
2.876175
2.532493
2.824585
2.948465
2.684249
2.900538
2.984178
2.769548
2.974412
2.982563
2.945202
3.036296
3.066926
2.947274
3.081473
3.026205
3.126396
3.143690
3.184665
3.194311
3.269849
3.293525
3.428915
3.508496
2.704299
3.185874
3.349323
3.565966
3.460532
3.477871
3.520355
3.575672
3.603291
3.746020
3.750503
3.820596
3.900687
3.895358
4.012604
4.059363
4.129432
4.211404
4.270861
4.277719
4.457563
4.575299
4.738853
4.797975
4.960478
5.036260
5.070309
5.350529
5.414076
5.599176
5.824595
6.019831
6.259846
6.488724
6.593686
6.942106
7.313912
7.622691
7.980001
8.398884
8.830192
9.421103
10.119489
10.817778
11.651695
12.711787
13.880409
15.412087
17.355131
19.888010
23.528494
28.624300
36.671040
51.585842
87.770775
306.916901
199.577423
74.586693
45.552212
32.693172
25.251760
20.618612
17.436249
14.991681
13.121219
11.653898
10.441715
9.484595
8.620993
7.882359
7.388324
6.781808
6.283851
5.870582
5.506167
5.214142
4.903636
4.692554
4.357887
4.167548
3.973033
3.712531
3.523134
3.393115
3.216954
3.071324
3.017587
2.851455
2.802532
2.691712
2.478885
2.475109
2.411556
2.198272
2.161745
2.048994
1.930466
1.890762
2.011736
1.843655
1.845186
1.784784
2.170442
1.523980
1.403400
1.413303
1.425483
1.365258
1.348836
1.257966
1.223270
1.129981
1.207060
1.072484
1.055158
1.033518
0.959158
0.939599
0.999773
0.973750
0.932410
0.865382
0.852723
0.768247
0.787283
0.686884
0.752220
0.687259
0.724539
0.691126
0.629130
0.643545
0.607100
0.669545
0.578099
0.561591
0.557304
0.553623
0.392163
0.531467
0.560833
0.336939
0.564185
0.274057
0.275864
0.377559
0.476830
0.296609
0.433766
0.255416
0.245241
0.233973
0.427223
0.119602
0.498301
0.228162
0.295201
0.285919
0.223669
0.188869
0.227943
0.275371
0.220076
0.140616
0.232951
0.251092
0.116627
0.152164
0.271862
0.068363
0.201923
0.241168
0.120365
0.135410
0.123039
0.069993
0.189091
0.171184
0.111168
0.032235
0.192101
0.073844
0.095873
0.230668
0.051739
0.030808
0.190862
0.085055
0.096436
0.147596
0.080737
0.086297
0.133980
0.118378
0.181987
0.107924
0.149397
0.091286
0.098787
0.133606
0.112195
0.106016
0.137703
0.132942
0.127131
0.133899
0.146091
0.162243
0.129078

代码讲解

数字滤波

数组部分

/* 4阶 Butterworth LPF, Fs=100k, Fc=45k */
const float iir_coeffs[5 * IIR_NUM_STAGES] = {
    /* Section 1 */
    0.292893f, 0.585786f, 0.292893f,
    0.171573f, -0.207107f,

    /* Section 2 */
    0.292893f, 0.585786f, 0.292893f,
    0.171573f, -0.207107f
};

这组系数描述的是 Direct Form I 结构的差分方程:

\( y[n] = b_0 \cdot x[n] + b_1 \cdot x[n-1] + b_2 \cdot x[n-2] - a_1 \cdot y[n-1] - a_2 \cdot y[n-2] \)
在这个公式中:

  • \(x[n]\) 是当前输入(你的 ADC 数据)
  • \(y[n]\) 是当前输出(滤波后的数据)
  • \(b_0, b_1, b_2\)前向系数(Feed-Forward / FIR 部分)
  • \(a_1, a_2\)反馈系数(Feedback / IIR 部分)
    二、数组的物理含义(逐行拆解)

数组是这样定义的:

const float iir_coeffs[5 * IIR_NUM_STAGES] = {...};

这里 5 * IIR_NUM_STAGES 是关键。每一个二阶节(Biquad Stage)固定占用 5 个 float 空间

  • 内存布局规则(必须死记)
    CMSIS-DSP 规定,这 5 个数的顺序是:
数组下标 变量名 数学含义
[0] b0 当前输入的增益
[1] b1 前一次输入的增益
[2] b2 前两次输入的增益
[3] a1 的前一次输出的增益
[4] a2 的前两次输出的增益

⚠️ 极度重要的坑点(针对 a1, a2):
在数学公式里是减号(\(- a_1 \cdot y[n-1]\))。
但在 CMSIS-DSP 里,你存入 a1 的值必须是 \(-a_1\)
也就是说,如果你算出来的理论 \(a_1 = -0.171573\),那你存进数组的应该是 0.171573
三、对照代码详解

我们把代码拆开来看:

  • Section 1(第一级滤波)
0.292893f,  // b0: 当前输入权重
0.585786f,  // b1: 上一个输入权重
0.292893f,  // b2: 上上个输入权重
0.171573f,  // a1: -(-0.171573) -> 反馈项1
-0.207107f  // a2: -(0.207107)  -> 反馈项2
  • 观察b0b2 相等,b1 是它们的两倍左右。这是 Butterworth 滤波器的典型特征(对称性)。
  • 观察a2 是负数。这符合 DSP 库的要求(因为 \(a_2\) 理论值通常为正,取负存入)。
  • Section 2(第二级滤波)
0.292893f,  // b0
0.585786f,  // b1
0.292893f,  // b2
0.171573f,  // a1
-0.207107f  // a2
  • 观察:两级参数一模一样。这是因为这是一个 4 阶 (Order 4) Butterworth 滤波器
  • 高阶滤波器必须拆分成多个 2 阶节(因为高于 2 阶的滤波器无法保证稳定)。对于偶数阶 Butterworth,拆分后的每个 Biquad 参数通常是一样的。

四、这些数字是怎么来的?(设计溯源)

你用的是 Fs=100kHz, Fc=45kHz, 4阶 Butterworth

如果用 Python (SciPy) 或 MATLAB 设计,逻辑是这样的:

  1. 归一化频率
    数字频率 \(\omega_c = 2 \pi \frac{F_c}{F_s} = 2 \pi \frac{45k}{100k} = 0.9\pi\)
  2. 极点计算
    Butterworth 滤波器的极点在 S 平面均匀分布。映射到 Z 平面后,得到两个共轭复极点对。
  3. 系数推导
    由极点推导出传递函数 \(H(z)\),最终整理成 \(\frac{b_0 + b_1 z^{-1} + b_2 z^{-2}}{1 + a_1 z^{-1} + a_2 z^{-2}}\) 的形式。

0.292893 实际上是 \(\frac{1}{2 + \sqrt{2}}\) 的近似值,这是这类滤波器在特定截止频率下的固有常数。

六、总结

项目 解释
数组本质 两个串联的二阶滤波器(Biquad)的系数集合
滤波器类型 4 阶 Butterworth 低通
关键顺序 {b0, b1, b2, a1, a2}
a1/a2 符号 存入的是理论值的负数(你的代码是对的)
为何两级相同 4 阶 Butterworth 的标准拆分方式

一句话总结:
这串数字是告诉 STM32 的 DSP 单元:“请用这两个特定的‘漏斗’(\(b\) 系数)去混合新旧输入,再用两个特定的‘回音壁’(\(a\) 系数)去混合新旧输出,从而只允许 45kHz 以下的信号通过。”

函数部分

/**
  @brief         浮点双二阶串联滤波器的处理函数
  @param[in]     S         浮点双二阶级联结构体指针
  @param[in]     pSrc      指向输入数据的地址
  @param[out]    pDst      指向输出数据的地址
  @param[in]     blockSize  数据大小
  @return        none
 */
 void arm_biquad_cascade_df1_f32(
  const arm_biquad_casd_df1_inst_f32 * S,
  const float32_t * pSrc,
        float32_t * pDst,
        uint32_t blockSize);

FFT

        /* 4. FFT */
        arm_cfft_f32(&arm_cfft_sR_f32_len1024, data_in, 0, 1);
        arm_cmplx_mag_f32(data_in, data_out, NPT/2);

一、arm_cfft_f32 —— 核心 FFT 运算

这行代码是真正执行傅里叶变换的地方。

/**
  @brief         浮点复数快速傅里叶变换(FFT)的处理函数
  @param[in]     S              指向FFT结构体的指针
  @param[in,out] p1            指向大小为<code>2*fftLen</code>的复杂数据缓冲区。处理过程在数据执行
  @param[in]     ifftFlag       选择变换方向的标识符
                   - value = 0: 正向变换
                   - value = 1: 逆向变换
  @param[in]     bitReverseFlag 启用/禁用输出位反转的标志
                   - value = 0: 禁用输出位翻转
                   - value = 1: 启用输出位翻转
  @return        none
 */

void arm_cfft_f32(
  const arm_cfft_instance_f32 * S,
        float32_t * p1,
        uint8_t ifftFlag,
        uint8_t bitReverseFlag);
1. 函数功能

它计算复数快速傅里叶变换 (Complex FFT)
它会读取 data_in 中的复数数据,经过一系列蝴蝶运算,将时域信号转换为频域信号
⚠️ 关键点:它是原地操作 (In-place)。也就是说,计算完成后,结果会覆盖掉原来的 data_in 数组。

参数详解
  • 参数 1:&arm_cfft_sR_f32_len1024
  • 类型const arm_cfft_instance_f32*
  • 作用FFT 的配置结构体(控制块)
  • 详解
    • len1024:告诉 DSP 库这次要做 1024 点 FFT。
    • sR:代表使用 位反转 (Bit-Reversal) 查找表。这是为了提高效率。
    • 这个结构体里包含了旋转因子(Twiddle Factors)表和位反转表。你不需要手动修改它,只需根据你的点数选择正确的宏(_len256, _len512, _len1024 等)。
  • 参数 2:data_in
  • 类型float32_t*
  • 作用输入/输出缓冲区
  • 内存布局(极其重要)
    它必须是一个长度为 2 * NPT 的数组。因为 FFT 处理的是复数,需要交替存放实部和虚部:
    data_in[0] = 实部(sample0)  // Re[0]
    data_in[1] = 虚部(sample0)  // Im[0]
    data_in[2] = 实部(sample1)  // Re[1]
    data_in[3] = 虚部(sample1)  // Im[1]
    ...
    
    在你的代码中,你做了这一步:
    data_in[j*2]   = filtered_data[j]; // 实部 = 采样值
    data_in[j*2+1] = 0;                // 虚部 = 0 (因为ADC是实数信号)
    
  • 参数 3:ifftFlag
  • 类型uint8_t
  • 作用指定正向 FFT 还是反向 FFT (IFFT)
  • 取值
    • 0正向 FFT(时域 -> 频域)。你这里用的是这个。
    • 1反向 FFT(频域 -> 时域)。
  • 参数 4:bitReverseFlag
  • 类型uint8_t
  • 作用是否执行位反转
  • 取值
    • 1执行位反转。这是最常用的设置。FFT 算法为了效率,计算出来的结果顺序是乱的(X(0), X(8), X(4)...),位反转会把这些数据重新排列成正常的频率顺序(X(0), X(1), X(2)...)。
    • 0:不执行。除非你对接的底层协议有特殊要求,否则永远填 1

二、arm_cmplx_mag_f32 —— 求模(幅值)

FFT 算完之后,data_in 里存的是复数(实部+虚部)。人眼无法直接看出“某个频率的能量有多大”,除非我们计算它的模(Magnitude)。这个函数就是干这个的。

/**
  @brief         浮点复数模值
  @param[in]     pSrc        指向输入向量的指针
  @param[out]    pDst        指向输出向量的指针
  @param[in]     numSamples  向量的元素数量
  @return        none
 */
void arm_cmplx_mag_f32(
  const float32_t * pSrc,
        float32_t * pDst,
        uint32_t numSamples);
1. 函数功能

计算复数数组中每个元素的模(幅值)
计算公式:\(Mag = \sqrt{Re^2 + Im^2}\)

2. 参数详解
  • 参数 1:data_in
  • 类型float32_t*
  • 作用输入源
  • 注意:它读取的是 刚刚被 arm_cfft_f32 覆盖过的 data_in。此时 data_in 里已经不再是时域波形,而是频域的复数谱。
  • 参数 2:data_out
  • 类型float32_t*
  • 作用输出缓冲区
  • 内容:存放计算出的幅值。
  • 大小:你只需要 NPT/2 个点。为什么?见下文。
  • 参数 3:numSamples
  • 类型uint32_t
  • 作用要处理的复数对数量
  • 为什么是 NPT/2
    这是一个非常经典的 DSP 知识点:
    1. 你输入了 NPT 个实数采样点(虚部补0)。
    2. 经过 FFT 后,产生了 NPT 个复数输出。
    3. 但是,对于实数输入信号(你的 ADC 采样就是实数),其频谱是关于中心点 共轭对称 的。
    4. 因此,后半部分的数据是前半部分的镜像,不包含新信息。
    5. 我们只关心 0Hz 到 Fs/2 (Nyquist频率) 这段频谱。
    6. 这段频谱恰好包含 NPT/2 个有效点(对应 DC 到 50kHz)。
    7. 所以你只需要计算并输出前 NPT/2 个点的幅值。

三、数据流全景图(帮助你建立直觉)

让我们把你这两行代码串起来,看看数据是怎么变的:

  1. 准备阶段
    filtered_data = [1.0, -1.0, 1.0, -1.0, ...] (1024个实数)

  2. 构造复数
    data_in = [1.0, 0, -1.0, 0, 1.0, 0, -1.0, 0, ...] (2048个数)

  3. 执行 arm_cfft_f32
    data_in (原地覆盖) = [Mag_DC, Phase_DC, Mag_F1, Phase_F1, ..., Mag_Nyq, Phase_Nyq]
    (此时数组里是复数,人类很难直接读)

  4. 执行 arm_cmplx_mag_f32
    data_out = [Mag_DC, Mag_F1, Mag_F2, ..., Mag_Nyq]
    (此时数组里是纯幅值,就是你 printf 打印出来的那些数据)

四、总结表格

函数 角色 输入 输出 关键参数
arm_cfft_f32 引擎 复数时域信号 复数频域信号 0(正向), 1(位反转)
arm_cmplx_mag_f32 翻译官 复数频域信号 实数幅值谱 NPT/2(只看一半)
posted @ 2026-07-06 19:47  奶龙大王  阅读(81)  评论(0)    收藏  举报