基于STM32进行FFT滤波并计算插值DA输出
文章目录
- 一、前言背景
- 二、项目构思
- 1. 确定FFT点数、采样率、采样点数
- 2. 双缓存设计
- 三、代码实现
- 1. STM32CubeMX配置和HAL库初始化
- 2. 核心代码
- 四、效果展示和后话
- 五、项目联想与扩展
- 1. 倍频
- 2. 降频
- 3. 插值
- 3.1 线性插值
- 3.2 样条插值
一、前言背景
STM32 对 AD 采样信号进行快速傅里叶变换(FFT),以获取其频谱信息。
信号源为信号发生器(正点原子DM40A)产生的 100Hz 三角波:
AD 采样由定时器触发,触发频率为 1280Hz。AD 以该频率采样 100Hz 的三角波,并对采样数据进行 FFT。FFT 计算出的结果是复数形式(实部、虚部交替排列的数组,数组大小为 2 N 2N 2N, N N N 为FFT的点数)的双边频谱:
即在 [ − f s 2 , f s 2 ] [- \frac{f_{s}}{2} , \frac{f_{s}}{2}] [−2fs,2fs] 频率范围内对称分布( f s f_{s} fs为采样率)。对应的计算结果存储在一个复数数组中,该数组的索引范围为 0 0 0 到 2 N − 1 2N-1 2N−1。我们把 0 0 0 到 N N N 之间的部分,定义为正频率分量;相应的, N N N 到 2 N − 1 2N -1 2N−1 之间的部分则为负频率分量。
通常,我们在频率分析时,会计算幅频相应:
这个应用很常见,马上就是要寻找峰值,然后就可以确定信号的主要频率成分及其幅度;又或者计算相位谱,以分析各频率分量的相位关系等等。
但很少有见过对频率谱进行操作(如滤波、调制等等)后再通过逆傅里叶变换(IFFT)还原信号的应用。
于是笔者突发奇想,进行了一次小小的测试,目标是要从100Hz的三角波中滤波得到它的基波100Hz的正弦波并通过DA将波形输出至示波器展示,平台是STM32H743VIT6。
二、项目构思
1. 确定FFT点数、采样率、采样点数
AD/DA 的触发源均由定时器提供,定时器源自 PLL 倍频/分频前的 25MHz 晶振。而 100Hz 的三角波由独立的信号发生器产生,因此 AD 采样时钟与信号发生器的输出之间可能存在一定的相位漂移或频率偏差。如果希望波形满足实时性要求,AD 和 DA 需要由同一触发信号驱动,以保证同步(当然,也可以让 DA 的触发频率高于 AD 的触发频率,因为使用片上AD/DA,本质都共享同一时钟源,理论上不会产生频率偏差或相位漂移)。
其次是FFT点数、采样率、采样点数的安排。对于 100Hz 的三角波信号,为了能够较好地表征其频谱信息,选择12.8倍于100Hz的采样率进行采样,即 f s = 100 × 12.8 = 1280 f_{s} = 100 \times 12.8 = 1280 fs=100×12.8=1280Hz。这一采样率远高于奈奎斯特采样定理规定的最低要求( 2 × 100 = 200 2 \times 100 = 200 2×100=200Hz),可以有效减少失真,特别是对于高次谐波较多的三角波信号。
在时域上,这意味着每个周期大约对应 12-13 个采样点。采用 128 点 FFT 进行变换,则 FFT 频率分辨率为:
f r e s = f s N = 1280 128 = 10 H z f_{res} = \frac{f_{s}}{N} = \frac{1280}{128} = 10Hz fres=Nfs=1281280=10Hz
即每个 FFT 频谱格宽度为10Hz。而100Hz是10Hz的整数倍,因此可以较好地反映100Hz三角波的各次谐波分量。
当然,在理论上,FFT 频率分辨率越高越好(即采样率越低或 FFT 点数越高),可以更精细地刻画信号的频谱结构。然而,我们的系统具有实时性要求,即需要在 AD 采样完成后,立即执行 FFT 变换、滤波、IFFT 处理,并将结果通过 DA 输出。因此,处理时间必须控制在 T T T 的时间窗口内:
T = A D 采样点数 × 1 采样率 T = AD采样点数 \times \frac{1}{采样率} T=AD采样点数×采样率1
基于此,我选择 12.8 倍于原始信号频率(100Hz)的采样率,即 1280Hz,并进行 128 点 FFT。
处理完成后,DA 端以相同采样率重建信号,每个周期包含 12-13 个采样点,这会让波形看上去很数字。故需要通过模拟低通滤波器(LPF)滤除采样频率 1280Hz 及其倍频分量,使得输出信号平滑,得到 100Hz 的正弦波。
2. 双缓存设计
在 AD 采集 128 个点后,我们需要执行 FFT 变换、滤波、IFFT 处理,并将结果通过 DA 输出。在此过程中,如果仅使用单一缓存,会导致数据处理期间无法继续采样,造成数据丢失。为了解决这一问题,我们采用 双缓存(Ping-Pong Buffering) 机制,使得数据采集与处理能够并行进行,提高实时性。
具体设计如下:
- 建立两个缓冲区 BufferA 和 BufferB。
- 当 DMA 将 ADC 采样数据搬运至 BufferA 时,我们对 BufferB 进行 FFT 变换、滤波、IFFT 处理,并准备其数据供 DAC 输出。
- 当 BufferA 搬运完成时,DMA 产生中断,设置一个标志位,并切换缓冲区:
- ADC 的 DMA 目标地址切换到 BufferB,开始采集新数据。
- BufferA 处理完成的数据被传输至 DAC,作为 DAC的DMA 的数据源。
- 在下一个 DMA 传输周期,两个缓冲区的角色再次交换,如此交替进行,确保数据采集与处理不间断。
三、代码实现
1. STM32CubeMX配置和HAL库初始化
ADC配置
DAC配置(利用OPAMP跟随输出)
并完成HAL库的初始化代码:
_Noreturn void Main(void)
{RetargetInit(&huart1);HAL_ADCEx_Calibration_Start(&hadc1, ADC_CALIB_OFFSET_LINEARITY, ADC_SINGLE_ENDED);bsp_Adc_Start(&hadc1);bsp_SetTimerFreq(&htim1, SAMPLERATE);HAL_TIM_Base_Start_IT(&htim1);HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t *)dac_dma_buf, DAC_DMA_BUF_NUM, DAC_ALIGN_12B_R);HAL_OPAMP_Start(&hopamp1);while (true){process_fft_ifft();}
}
2. 核心代码
定义采样率、ADC采样点数、DAC的DMA缓存点数、FFT点数:
#define SAMPLERATE ((100)*(12.8))#define ADC_DAC_NUM 128#ifndef DAC_DMA_BUF_NUM
#define DAC_DMA_BUF_NUM ADC_DAC_NUM
#endifextern uint16_t dac_dma_buf[DAC_DMA_BUF_NUM];#define ADC_DMA_BUF_NUM ADC_DAC_NUM#define ADCx 1
#if ADCx==1
extern uint16_t adc_dma_buffer[ADC_DMA_BUF_NUM];
#elif ADCx==2
extern uint32_t adc_dma_buffer[ADC_DMA_BUF_NUM];
#endif#define FFT_NUM ADC_DAC_NUM
最核心的代码在ADC的DMA的完成中断中执行(这里这样写是因为H7配置了MPU,需要考虑Cache未击中问题):
使用两个指针完成对双缓存的调度
void signalTask(void)
{/* 半传输完成中断 */if((ADC_DMAx->LISR & ADC_DMA_Streamx_HC) != RESET){float32_t *target_buffer = (active_buffer) ? bufferA : bufferB;float32_t *processing_buffer = (active_buffer) ? bufferB : bufferA;SCB_InvalidateDCache_by_Addr((uint32_t *)(&adc_dma_buffer[0]), ADC_DMA_BUF_NUM);for (uint16_t i = 0; i < FFT_NUM / 2; ++i) {target_buffer[i] = adc_dma_buffer[i];dac_dma_buf[i] = (uint16_t)processing_buffer[i];}adc_state = 1;ADC_DMAx->LIFCR = ADC_DMA_Streamx_HC;}/* 传输完成中断 */if((ADC_DMAx->LISR & ADC_DMA_Streamx_TC) != RESET){float32_t *target_buffer = (active_buffer) ? bufferA : bufferB;float32_t *processing_buffer = (active_buffer) ? bufferB : bufferA;SCB_InvalidateDCache_by_Addr((uint32_t *)(&adc_dma_buffer[ADC_DMA_BUF_NUM / 2]), ADC_DMA_BUF_NUM);for (uint16_t i = 0; i < FFT_NUM / 2; ++i) {target_buffer[i + FFT_NUM / 2] = adc_dma_buffer[i + FFT_NUM / 2];dac_dma_buf[i + FFT_NUM / 2] = (uint16_t)processing_buffer[i + FFT_NUM / 2];}adc_state = 2;ADC_DMAx->LIFCR = ADC_DMA_Streamx_TC;}
}
根据adc_state
的状态判断ADC的DMA是否完成,来执行FFT、滤波、IFFT处理 process_fft_ifft
。
完成处理置位标志位active_buffer
。
float32_t fft_input[FFT_NUM*2];void process_fft_ifft(void)
{if (adc_state == 2){float32_t *processing_buffer = (active_buffer) ? bufferA : bufferB;for (uint16_t i = 0; i < FFT_NUM; ++i){fft_input[i * 2] = processing_buffer[i];fft_input[i * 2 + 1] = 0;}arm_cfft_f32(&arm_cfft_sR_f32_len128, fft_input, 0, 1);uint16_t cutoff_bin = (uint16_t)((120.0 / SAMPLERATE) * (FFT_NUM * 2));for (uint16_t i = cutoff_bin; i < FFT_NUM/2; i++) {fft_input[2 * i] = 0.0f;fft_input[2 * i + 1] = 0.0f;fft_input[2 * (FFT_NUM - i)] = 0.0f;fft_input[2 * (FFT_NUM - i) + 1] = 0.0f;}arm_cfft_f32(&arm_cfft_sR_f32_len128, fft_input, 1, 1);for (uint16_t i = 0; i < FFT_NUM; i++) {processing_buffer[i] = fft_input[i * 2];}active_buffer = !active_buffer;adc_state = 0;}
}
这里的滤波方法是暴力滤波(优化方法:可以采用平滑过渡,例如用 Hanning 窗或 Hamming 窗在频域做过渡,而不是直接硬性截断)。
在滤波的时候,请不要忘记处理负频率:
for (uint16_t i = cutoff_bin; i < FFT_NUM/2; i++) {
// 正频率fft_input[2 * i] = 0.0f;fft_input[2 * i + 1] = 0.0f;
// 负频率 fft_input[2 * (FFT_NUM - i)] = 0.0f;fft_input[2 * (FFT_NUM - i) + 1] = 0.0f;
}
四、效果展示和后话
最终效果展示
这里我没有加上LPF(懒)
后话
FFT滤波时,我直接截断高频分量相当于乘以一个矩形窗,在时域上会导致高斯振铃(Gibbs 现象)(类似于时域上的方波傅里叶变换导致的振铃效应),后续优化可以采用平滑过渡,例如用 Hanning 窗或 Hamming 窗在频域做过渡,而不是直接硬性截断。
例如,使用 Hanning 窗 对截止频率附近的分量进行平滑处理:
for (uint16_t i = cutoff_bin; i < FFT_NUM/2; i++) {float window_coeff = 0.22 * (1 + cosf(M_PI * (i - cutoff_bin) / (FFT_NUM/2 - cutoff_bin))); // Hanning 窗fft_input[2 * i] *= window_coeff;fft_input[2 * i + 1] *= window_coeff;fft_input[2 * (FFT_NUM - i)] *= window_coeff;fft_input[2 * (FFT_NUM - i) + 1] *= window_coeff;
}
这样可以让高频分量逐渐衰减,而不是直接消失,减少 Gibbs 现象。
此外,FFT 频域滤波更适合用于选取特定频率分量的滤波器。在实际应用中,我们通常希望滤波器具有一定的过渡带,而不是突变的截止频率。这是因为理想的 “砖墙” 低通滤波器(即瞬间截止所有高频分量)在物理上不可实现,实际滤波器通常需要一定的过渡区域,以减少时域上的振铃效应并改善信号的平滑性。
项目地址: https://github.com/mico845/STM32FFTFilter
五、项目联想与扩展
1. 倍频
在前文中,我们提到 DA的触发频率高于 AD 的触发频率。由于我们使用的是 片上 AD/DA,它们本质上共享同一时钟源,因此 理论上不会产生频率偏差或相位漂移。
在本项目中,我们采用双缓冲技术,理论上可以在 DA 输出缓存区中存储 100 Hz 的正弦波(通过滤波器得到的),然后以更高的触发频率进行输出,从而合成更高频率的正弦波信号。
说干就干,我决定进行实验测试,尝试让系统在输入100 Hz三角波的情况下,输出300 Hz正弦波,以实现更灵活的需求。
代码实现
这里修改DAC,让定时器2的Trigger Out event事件触发
配置定时器的触发频率为DAC_FREQ
:
bsp_SetTimerFreq(&htim2, DAC_FREQ);
HAL_TIM_Base_Start_IT(&htim2);
#define SAMPLERATE ((100)*(12.8))
#define DAC_FREQ (SAMPLERATE * 3)
效果展示
2. 降频
基于倍频的原理,我们同样可以设计降频系统。其方法是保持采样频率不变,但增加采样点数,从而降低输出信号的频率。例如,若希望输出信号的频率降低至原来的一半,则需要将采样点数增加一倍。或者进行插值,增多DA输出点数。
说干就干,我决定进行实验测试,尝试让系统在输入100 Hz三角波的情况下,输出50Hz正弦波,以实现更灵活的需求。
采样率和DAC频率如下:
#define SAMPLERATE ((100)*(12.8) * 2)
#define DAC_FREQ ((100)*(12.8))
这里玩一下宏,利用宏拼接的方式,方便调整FFT点数:
#define FFT_NUM ADC_DAC_NUM#define __s_arm_cfft_f32(x) arm_cfft_sR_f32_len##x
#define _s_arm_cfft_f32(x) __s_arm_cfft_f32(x)
#define s_arm_cfft_f32 _s_arm_cfft_f32(FFT_NUM)
FFT和IFFT就变成了
arm_cfft_f32(&s_arm_cfft_f32, fft_input, 0, 1);
arm_cfft_f32(&s_arm_cfft_f32, fft_input, 1, 1);
效果展示
但是,由于DA的触发频率低于AD的触发频率,导致采样数据在DA输出时无法准确对齐,进而引发相位偏移。为了解决这一问题,我决定采用插值的方法。
3. 插值
3.1 线性插值
利用插值算法,在保证 ADC 采样率不变的前提下,使 DAC 在采样点之间生成更多的插值点,从而平滑输出,提高信号的连续性。
我修改了DMA 完成中断的一些细节,以确保数据存储、处理和输出的正确:
void signalTask(void)
{/* 半传输完成中断 */if((ADC_DMAx->LISR & ADC_DMA_Streamx_HC) != RESET){float32_t *target_buffer = (active_buffer) ? bufferA : bufferB;float32_t *processing_buffer = (active_buffer) ? bufferB : bufferA;SCB_InvalidateDCache_by_Addr((uint32_t *)(&adc_dma_buffer[0]), ADC_DMA_BUF_NUM);for (uint16_t i = 0; i < ADC_DMA_BUF_NUM / 2; ++i) {target_buffer[i] = adc_dma_buffer[i];}for (uint16_t i = 0; i < DAC_DMA_BUF_NUM / 2; ++i) {dac_dma_buf[i] = (uint16_t)processing_buffer[i];}adc_state = 1;ADC_DMAx->LIFCR = ADC_DMA_Streamx_HC;}/* 传输完成中断 */if((ADC_DMAx->LISR & ADC_DMA_Streamx_TC) != RESET){float32_t *target_buffer = (active_buffer) ? bufferA : bufferB;float32_t *processing_buffer = (active_buffer) ? bufferB : bufferA;SCB_InvalidateDCache_by_Addr((uint32_t *)(&adc_dma_buffer[ADC_DMA_BUF_NUM / 2]), ADC_DMA_BUF_NUM);for (uint16_t i = 0; i < ADC_DMA_BUF_NUM / 2; ++i) {target_buffer[i + ADC_DMA_BUF_NUM / 2] = adc_dma_buffer[i + ADC_DMA_BUF_NUM / 2];}for (uint16_t i = 0; i < DAC_DMA_BUF_NUM / 2; ++i) {dac_dma_buf[i + DAC_DMA_BUF_NUM / 2] = (uint16_t)processing_buffer[i + DAC_DMA_BUF_NUM / 2];}adc_state = 2;ADC_DMAx->LIFCR = ADC_DMA_Streamx_TC;}
}
接下来,我打算使用线性插值做第一次尝试:
线性插值(Linear Interpolation,简称 Lerp)是一种用于在已知数据点之间估算新数据点的数学方法。它的基本思想是:如果两个已知点之间的数据是平滑变化的,我们可以用一条直线来近似它们之间的过渡,并计算中间值。
先简单了解一下线性插值算法:
假设有两个已知点:
- A ( x 1 , y 1 ) A(x_{1}, y_{1}) A(x1,y1)
- B ( x 2 , y 2 ) B(x_{2}, y_{2}) B(x2,y2)
你想知道在这两个点之间,某个位置 x x x处的 y y y是多少。
在数学上,线性插值使用下面的公式:
y = y 1 + ( x − x 1 ) ( x 2 − x 1 ) × ( y 2 − y 1 ) y = y_{1} + \frac{(x - x_{1})}{(x_{2} - x_{1})} \times (y_{2} - y_{1}) y=y1+(x2−x1)(x−x1)×(y2−y1)
解释一下这个公式,也就是计算 x x x在 x 1 x_{1} x1和 x 2 x_{2} x2之间的相对位置:
f r a c = ( x − x 1 ) ( x 2 − x 1 ) frac = \frac{(x - x_{1})}{(x_{2} - x_{1})} frac=(x2−x1)(x−x1)
这个值介于 0 0 0和 1 1 1之间,表示 x x x在 x 1 x_{1} x1和 x 2 x_{2} x2之间的比例。
然后依照这个比例,计算出 y y y方向的偏移量:
Δ y = ( y 2 − y 1 ) × f r a c \Delta y = (y_{2} - y_{1}) \times frac Δy=(y2−y1)×frac
最终得到插值的结果:
y = y 1 + Δ y y = y1 + \Delta y y=y1+Δy
在我们的这个系统中,也就是改成数组的方式来解释,可以直接写成:
uint16_t w = output_size / input_size;// 省略 ... index = (float)i / w;idx = (uint16_t)index;frac = index - idx;
我来解释一下,假设 input_size = 4
,output_size = 8
,那么 w = output_size / input_size = 8 / 4 = 2
。
output
数组的索引 i
从 0
到 7
,然后 index = i / 2
变为:
i = 0 → index = 0 / 2 = 0.0i = 1 → index = 1 / 2 = 0.5i = 2 → index = 2 / 2 = 1.0i = 3 → index = 3 / 2 = 1.5i = 4 → index = 4 / 2 = 2.0i = 5 → index = 5 / 2 = 2.5i = 6 → index = 6 / 2 = 3.0i = 7 → index = 7 / 2 = 3.5
相应的idx
和frac
也就是:
i = 0 → index = 0.0, idx = 0, frac = 0.0i = 1 → index = 0.5, idx = 0, frac = 0.5i = 2 → index = 1.0, idx = 1, frac = 0.0i = 3 → index = 1.5, idx = 1, frac = 0.5i = 4 → index = 2.0, idx = 2, frac = 0.0i = 5 → index = 2.5, idx = 2, frac = 0.5i = 6 → index = 3.0, idx = 3, frac = 0.0i = 7 → index = 3.5, idx = 3, frac = 0.5
frac
等于0
时,该点为采样点;反之,该点为需要插入的点。
我们手动计算i=1
时的frac
,根据公式可以得到frac = (1 - 0)/(2 - 0) = 0.5
,和我们的计算结果也是吻合的。
从公式上理解,也就是
y = y 1 + ( x − x 1 ) ( x 2 − x 1 ) × ( y 2 − y 1 ) = y 1 + ( y 2 − y 1 ) × f r a c = y 1 × ( 1 − f r a c ) + f r a c × y 2 y = y_{1} + \frac{(x - x_{1})}{(x_{2} - x_{1})} \times (y_{2} - y_{1}) = y_{1} + (y_{2} - y_{1}) \times frac = y_{1} \times (1 - frac) + frac \times y_{2} y=y1+(x2−x1)(x−x1)×(y2−y1)=y1+(y2−y1)×frac=y1×(1−frac)+frac×y2
代码上也就是:
output[i] = (1 - frac) * input[idx] + frac * input[idx + 1];
故,我们完成线性拟合函数:
void lerp(uint16_t *input, uint16_t *output, uint16_t input_size, uint16_t output_size)
{uint16_t w = output_size / input_size;float index;uint16_t idx;float frac;for (uint16_t i = 0; i < output_size; i++){index = (float)i / w;idx = (uint16_t)index;frac = index - idx;if (idx < input_size - 1){output[i] = (uint16_t)((1 - frac) * input[idx] + frac * input[idx + 1]);}else{output[i] = input[input_size - 1];}}
}
在process_fft_ifft
调用函数lerp
:
// 省略 ...arm_cfft_f32(&s_arm_cfft_f32, fft_input, 1, 1);uint16_t interpolated_output[DAC_DMA_BUF_NUM];uint16_t interpolated_input[FFT_NUM];for (uint16_t i = 0; i < FFT_NUM; i++) {interpolated_input[i] = fft_input[2 * i];}lerp(interpolated_input, interpolated_output, FFT_NUM, DAC_DMA_BUF_NUM);for (uint16_t i = 0; i < DAC_DMA_BUF_NUM; i++) {processing_buffer[i] = interpolated_output[i];}active_buffer = !active_buffer;adc_state = 0;// 省略 ...
这是对系统的采样率,DA触发频率,AD/DA的DMA缓存区大小配置:
#define INTERP_FACTOR 2#define SAMPLERATE ((100)*(12.8))
#define DAC_FREQ (SAMPLERATE * INTERP_FACTOR)#define ADC_DAC_NUM 128#ifndef DAC_DMA_BUF_NUM
#define DAC_DMA_BUF_NUM (ADC_DAC_NUM * INTERP_FACTOR)#define FFT_NUM ADC_DAC_NUM
效果展示
3.2 样条插值
ARM_DSP库支持了样条插补,双线性插补和线性插补。
在这里我打算追求更高的精度、平滑性,尝试使用样条插值。样条插值常见的有:
- 二次样条插值(Quadratic Spline)
- 三次样条插值(Cubic Spline)
样条插值的核心思想是:
- 用多个低阶多项式拼接,形成平滑曲线
- 确保曲线在数据点处连续且光滑(导数连续)
例如,三次样条插值公式就是:
每个区间 [ x i , x i + 1 ] [x_{i},x_{i+1}] [xi,xi+1]定义一个三次多项式:
S i = a i + b i ( x − x i ) + c i ( x − x i ) 2 + d i ( x − x i ) 3 S_{i} = a_{i} + b_{i}(x - x_{i}) + c_{i}(x - x_{i})^{2} + d_{i}(x - x_{i})^{3} Si=ai+bi(x−xi)+ci(x−xi)2+di(x−xi)3
需要解的参数是 a i a_{i} ai、 b i b_{i} bi、 c i c_{i} ci、 d i d_{i} di,它们满足:
- 函数值连续(相邻多项式在连接点处相等)
- 一阶导数连续(保证曲线的光滑性)
- 二阶导数连续(减少振荡)
这些条件形成一个线性方程组,求解后就能得到插值函数。
而ARM_DSP的样条插补主要通过这两个函数实现:arm_spline_init_f32
和 arm_spline_f32
。
这里面需要理解的配置参数有:
ARM_SPLINE_NATURAL
自然样条插补ARM_SPLINE_PARABOLIC_RUNOUT
抛物线样条插补
自然样条插补对应三次样条插值,精度更高,更光滑,但计算量更大;抛物线样条插补对应二次样条插值,比三次样条计算量小,性能略逊自然样条插补。
这里我打算使用抛物线样条插补。
先定义结构体和变量:
arm_spline_instance_f32 S;
#define SpineTab DAC_DMA_BUF_NUM / FFT_NUM
float32_t xn[FFT_NUM];
float32_t xnpos[DAC_DMA_BUF_NUM];
float32_t coeffs[3*(FFT_NUM - 1)]; /* 插补系数缓冲 */
float32_t tempBuffer[2 * FFT_NUM - 1]; /* 插补临时缓冲 */
对x
轴的下标进行初始化:
void spline_Init(void)
{for(uint16_t i = 0; i < FFT_NUM; i++){xn[i] = i*SpineTab;}for(uint16_t i = 0; i < DAC_DMA_BUF_NUM; i++){xnpos[i] = i;}
}
添加到Main函数中
_Noreturn void Main(void)
{
// 省略 ...spline_Init();
// 省略 ...
}
编写计算函数spline
:
void spline(float32_t *input, float32_t *output)
{arm_spline_init_f32(&S,ARM_SPLINE_PARABOLIC_RUNOUT ,xn,input,FFT_NUM,coeffs,tempBuffer);
/* 样条计算 */arm_spline_f32 (&S,xnpos,output,DAC_DMA_BUF_NUM);
}
在process_fft_ifft
调用函数spline
:
// 省略 ...arm_cfft_f32(&s_arm_cfft_f32, fft_input, 1, 1);float32_t interpolated_output[DAC_DMA_BUF_NUM];float32_t interpolated_input[FFT_NUM];for (uint16_t i = 0; i < FFT_NUM; i++) {interpolated_input[i] = fft_input[2 * i];}spline(interpolated_input, interpolated_output);for (uint16_t i = 0; i < DAC_DMA_BUF_NUM; i++) {processing_buffer[i] = interpolated_output[i];}active_buffer = !active_buffer;adc_state = 0;// 省略 ...
效果展示
这个结果比线性插值的光滑得多了,不再出现时不时一下的不连续问题。
测试时,将插值点数提高,最终点数来到12800
点时:
#define INTERP_FACTOR 100#define SAMPLERATE ((100)*(12.8))
#define DAC_FREQ (SAMPLERATE * INTERP_FACTOR)#define ADC_DAC_NUM 128#ifndef DAC_DMA_BUF_NUM
#define DAC_DMA_BUF_NUM (ADC_DAC_NUM * INTERP_FACTOR)
#endif
也就相当光滑了。这样同时也提高了DA的采样频率,后续想要制作LPF已经相当容易了。
相关文章:
基于STM32进行FFT滤波并计算插值DA输出
文章目录 一、前言背景二、项目构思1. 确定FFT点数、采样率、采样点数2. 双缓存设计 三、代码实现1. STM32CubeMX配置和HAL库初始化2. 核心代码 四、效果展示和后话五、项目联想与扩展1. 倍频2. 降频3. 插值3.1 线性插值3.2 样条插值 一、前言背景 STM32 对 AD 采样信号进行快…...
【用 Trace读源码】PlanAgent 执行流程
前提条件 在 Trae 中打开 OpenManus 工程,使用 build 模式,模型选择 claude-sonnet-3.7 提示词 分析 agent/planning.py 中 main 方法及相关类的执行流程,以流程图的方式展示PlanningAgent 执行流程图 以下流程图展示了 PlanningAgent 类…...
AI代码编辑器:Cursor和Trae
Cursor 定义:Cursor 是一款基于AI的代码编辑器,它继承了VS Code的核心功能,并在此基础上增加了深度AI支持。它支持代码生成、优化、重构以及调试等功能,提供直观的Diff视图和自动补全功能,是一款功能强大的编程工具。…...
LSM-Tree(Log-Structured Merge-Tree)详解
1. 什么是 LSM-Tree? LSM-Tree(Log-Structured Merge-Tree)是一种 针对写优化的存储结构,广泛用于 NoSQL 数据库(如 LevelDB、RocksDB、HBase、Cassandra)等系统。 它的核心思想是: 写入时只追加写(Append-Only),将数据先写入内存缓冲区(MemTable)。内存数据满后…...
介绍一个测试boostrap表格插件的好网站!
最近在开发一个物业管理系统。用到bootstrap的表格插件bootstrap table,官方地址: https://bootstrap-table.com/ 因为是英文界面,对国人不是很友好。后来发现了IT小书童网站 IT小书童 - 为程序员提供优质教程和文档 网站: IT…...
虚拟路由与单页应用(SPA):详解
在单页应用(SPA,Single Page Application)中,虚拟路由(也称为前端路由)是一种关键的技术,用于管理页面导航和状态变化,而无需重新加载整个页面。为了帮助你更好地理解这一概念&#…...
基于树莓派3B+的人脸识别实践:Python与C联合开发
基于树莓派3B的人脸识别实践:Python与C联合开发 引言 树莓派因其小巧的体积和丰富的扩展性,成为嵌入式开发的理想平台。本文将分享如何通过Python与C语言联合开发,在树莓派3B上实现从硬件控制、摄像头拍照到百度API人脸比对的完整流程。项目…...
尝试使用Tauri2+Django+React项目(2)
前言 尝试使用tauri2DjangoReact的项目-CSDN博客https://blog.csdn.net/qq_63401240/article/details/146403103在前面笔者不知道怎么做,搞了半天 笔者看到官网,原来可以使用二进制文件,好好好 嵌入外部二进制文件 | Taurihttps://v2.taur…...
Qt桌面客户端跨平台开发实例
在Windows平台上,桌面客户端软件通常使用C/C语言和Qt跨平台开发框架进行开发。因此,大部分代码可以运行于不同平台环境,但是程序运行依赖的三方库以及代码中一些平台相关的头文件和接口需要进行平台兼容。本文以windows桌面端应用迁移到Linux…...
c++进阶之------红黑树
一、概念 红黑树(Red-Black Tree)是一种自平衡二叉查找树,它在计算机科学的许多领域中都有广泛应用,比如Java中的TreeMap和C中的set/map等数据结构的底层实现。红黑树通过在每个节点上增加一个颜色属性(红色或黑色&am…...
政安晨【超级AI工作流】—— 使用Dify通过工作流对接ComfyUI实现多工作流协同
政安晨的个人主页:政安晨 欢迎 👍点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益,如有不足之处,欢迎在评论区提出指正! 目录 一、准备工作 Dify跑起来 ollama局域网化配置 Dify配置并验证 启动ComfyUI 二、…...
javaweb开发以及部署
先说一个阿里云学生无门槛免费领一年2核4g服务器的方法: 阿里云服务器学生无门槛免费领一年2核4g_阿里云学生认证免费服务器-CSDN博客 Java Web开发是使用Java编程语言开发Web应用程序的过程,通常涵盖了使用Java EE(Java Enterprise Edition…...
树莓派5介绍与系统安装
简介 Raspberry Pi 5采用运行频率为2.4GHz的64位四核Arm Cortex-A76处理器,与Raspberry Pi 4相比, CPU性能提高了2至3倍。此外,它还配备了一个800MHz的VideoCore VII GPU,可以提供大幅度的图形 性能提升,通过HDMI实现…...
菜鸟之路Day25一一前端工程化(二)
菜鸟之路Day25一一前端工程化(二) 作者:blue 时间:2025.3.19 文章目录 菜鸟之路Day25一一前端工程化(二)1.概述2.Element快速入门3.综合案例一.布局二.组件三.Axios异步加载数据1. 生命周期钩子概述2. mo…...
vue如何获取 sessionStorage的值,获取token
// 使用Axios发送请求并处理下载 import axios from axios;const handleDownload () > {const params {warehouseId: selectedWarehouseId.value};const apiUrl /api/materials/wmMatCheck/export-wmMatCheckDetail;axios.get(apiUrl, {params,responseType: blob, // 接…...
图解AUTOSAR_CP_DiagnosticLogAndTrace
AUTOSAR 诊断日志和跟踪(DLT)模块详解 AUTOSAR 经典平台中的诊断和调试关键组件 目录 1. 概述2. DLT模块架构 2.1 模块位置2.2 内部组件2.3 接口定义 3. DLT操作流程 3.1 初始化流程3.2 日志和跟踪消息处理3.3 控制命令处理 4. 数据结构与配置模型 4.1 配置类4.2 消息格式4.3 …...
微调实战 - 使用 Unsloth 微调 QwQ 32B 4bit (单卡4090)
本文参考视频教程:赋范课堂 – 只需20G显存,QwQ-32B高效微调实战!4大微调工具精讲!知识灌注问答风格微调,DeepSeek R1类推理模型微调Cot数据集创建实战打造定制大模型! https://www.bilibili.com/video/BV1…...
金仓KESV8R6任务调度
基本概念 • 程序(program) 程序对象描述调度器要运行的内容。 • 调度计划(schedule) 调度计划对象指定作业何时运行以及运行多少次。调度计划可以被多个作业共享。 • 作业(job) 作业就是用户定义的…...
Maven常见问题汇总
Maven刷新,本地仓库无法更新 现象 This failure was cached in the local repository and resolution is not reattempted until the update interval of aliyunmaven has elapsed or updates are forced原因 因为上一次尝试下载,发现对应的仓库没有这个maven配置…...
颠覆者的困局:解构周鸿祎商业哲学中的“永恒战争”
引言:被误解的破坏者 在北京海淀区知春路银谷大厦的某间会议室里,周鸿祎用马克笔在白板上画出一个巨大的爆炸图案——这是2010年360与腾讯开战前夜的战术推演场景。这个充满硝烟味的瞬间,恰是《颠覆者》精神内核的完美隐喻:在中国…...
基于ChatGPT、GIS与Python机器学习的地质灾害风险评估、易发性分析、信息化建库及灾后重建高级实践
第一章、ChatGPT、DeepSeek大语言模型提示词与地质灾害基础及平台介绍【基础实践篇】 1、什么是大模型? 大模型(Large Language Model, LLM)是一种基于深度学习技术的大规模自然语言处理模型。 代表性大模型:GPT-4、BERT、T5、Ch…...
如何实现单点登录?
单点登录(Single Sign-On, SSO)是一种身份验证机制,允许用户在多个应用系统中只登录一次,就能够访问所有受保护的系统或服务,而无需重复登录。SSO通过集中式认证来简化用户的登录体验,提高安全性,并减少管理复杂性。 一、原理 SSO的核心原理是通过一个认证中心(Ident…...
01 Overview
版本pytorch 0.4,应用期的技术 学习的前提 线性代数和概率分布,高数 内容 穷举、贪心、分治算法、动态规划 花书是经典中的经典 机器学习历史 1 基于规则的 2 经典的机器学习方法 3 深度学习 深度学习竞赛识别率超过了人类 神经网络是数学和工…...
第二天 开始Unity Shader的学习之旅之熟悉顶点着色器和片元着色器
Shader初学者的学习笔记 第二天 开始Unity Shader的学习之旅之熟悉顶点着色器和片元着色器 文章目录 Shader初学者的学习笔记前言一、顶点/片元着色器的基本结构① Shader "Unity Shaders Book/Chapter 5/ Simple Shader"② SubShader③ CGPROGRAM和ENDCG④ 指明顶点…...
moveit2基础教程上手-使用xarm6演示
0、前置信息 开发环境:wsl。 ros版本:jazzy,ubuntu版本:24.04 xarm-ros2地址 1、启动Rviz,加载 Motion Planning Plugin,实现演示功能 Getting Started — MoveIt Documentation: Rolling documentation…...
头部姿态估计(Head Pose Estimation)领域,有许多开源工具和库可供选择,一些常用的工具及其特点
在头部姿态估计(Head Pose Estimation)领域,有许多开源工具和库可供选择。以下是一些常用的工具及其特点比较: 1. OpenCV 特点: OpenCV 是一个广泛使用的计算机视觉库,提供了丰富的图像处理和计算机视觉算法。虽然 O…...
Qt调用Miniconda的python方法
1、 Win 64环境下载及安装 Miniconda 首先下载Windows 版Miniconda,https://docs.conda.io/en/latest/miniconda.html或 https://repo.anaconda.com/miniconda/ 安装界面及选择如下图所示: 安装完python3.12版报错如下。 说明:python3.11版…...
【Linux 下的 bash 无法正常解析, Windows 的 CRLF 换行符问题导致的】
文章目录 报错原因:解决办法:方法一:用 dos2unix 修复方法二:手动转换换行符方法三:VSCode 或其他编辑器手动改 总结 这个错误很常见,原因是你的 wait_for_gpu.sh 脚本 文件格式不对,具体来说…...
DSP数字信号处理
数字信号处理(Digital Signal Processing,简称DSP)是一门研究如何通过数字技术对信号进行分析、修改和合成的学科。DSP在现代电子系统中无处不在,广泛应用于音频处理、视频处理、通信、雷达、医学成像等领域。 什么是数字信号处理…...
vue3 获取当前路由信息失败问题
刷新浏览器时获取当前路由信息失败:undefined import { ref, reactive, onMounted } from vue; import { useRoute } from vue-router; const route useRoute();onMounted(()>{// 打印当前路由信息console.log(当前route, route ); // 这里的打印有值console.…...
数据驱动进化:AI Agent如何重构手机交互范式?
如果说AIGC拉开了内容生成的序幕,那么AI Agent则标志着AI从“工具”向“助手”的跨越式进化。它不再是简单的问答机器,而是一个能够感知环境、规划任务并自主执行的智能体,更像是虚拟世界中的“全能员工”。 正如行业所热议的:“大…...
汽车芯片成本控制:挑战、策略与未来趋势
一、引言 随着汽车行业的快速发展,汽车芯片在车辆中的应用越来越广泛。从简单的发动机控制单元到复杂的自动驾驶系统,芯片已成为汽车智能化、电动化的核心部件。然而,汽车芯片的高成本一直是制约汽车行业发展的重要因素之一。本文将深入探讨…...
RIP实验
RIP实验 一、实验背景 RIP协议: RIP协议(Routing Information Protocol,路由信息协议)是一种基于距离矢量的内部网关协议,即根据跳数来度量路由开销,进行路由选择。相比于其它路由协议(如OSPF、…...
NAT 实验:多私网环境下 NAPT、Easy IP 配置及 FTP 服务公网映射
NAT基本概念 定义:网络地址转换(Network Address Translation,NAT)是一种将私有(保留)地址转化为合法公网 IP 地址的转换技术,它被广泛应用于各种类型 Internet 接入方式和各种类型的网络中。作…...
电力和冷却管理:如何让数据中心“高效降温”同时节能增效
电力和冷却管理:如何让数据中心“高效降温”同时节能增效 数据中心作为现代信息技术基础设施的核心,承担着处理、存储和传输海量数据的重任。然而,这些庞大的服务器和存储设备在高速运转时,不仅需要大量电力供应,还产生了大量热量。如何平衡电力消耗与有效冷却,成为了数…...
LangChain Chat Model学习笔记
Prompt templates: Few shot、Example selector 一、Few shot(少量示例) 创建少量示例的格式化程序 创建一个简单的提示模板,用于在生成时向模型提供示例输入和输出。向LLM提供少量这样的示例被称为少量示例,这是一种简单但强大的指导生成的方式&…...
嵌入式硬件篇---Keil51中的关键字
文章目录 前言1. 存储类型关键字1.1code作用地址范围用途示例 1.2data作用地址范围用途示例 1.3idata作用地址范围用途示例 1.4xdata作用地址范围用途示例 1.5pdata作用地址范围用途示例 1.6volatile作用用途示例 2. 其他常用关键字2.1bit作用示例 2.2sbit作用示例 2.3sfr / sf…...
《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步
《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步 《TCP/IP网络编程》学习笔记 | Chapter 20:Windows 中的线程同步用户模式和内核模式用户模式同步内核模式同步 基于 CRITICAL_SECTION 的同步内核模式的同步方法基于互斥量对象的同步基于…...
MyBatis 中 #{} 和 ${} 的区别详解
目录 1. #{} 和 ${} 的基本概念 1.1 #{} 1.2 ${} 2. #{} 和 ${} 的工作原理 2.1 #{} 的工作原理 2.2 ${} 的工作原理 3.共同点:动态 SQL 查询 4. 区别:处理方式和适用场景 4.1 处理方式 4.2 适用场景 (1)#{} 的适用场景…...
C++学习之网盘项目单例模式
目录 1.知识点概述 2.单例介绍 3.单例饿汉模式 4.饿汉模式四个版本 5.单例类的使用 6.关于token的作用和存储 7.样式表使用方法 8.qss文件中选择器介绍 9.qss文件样式讲解和测试 10.qss美化登录界面补充 11.QHTTPMULTIPART类的使用 12.文件上传协议 13.文件上传协议…...
Lineageos 22.1(Android 15)制定应用强制横屏
一、前言 有时候需要系统的某个应用强制衡平显示,不管他是如何配置的。我们只需要简单的拿到top的Task下面的ActivityRecord,并判断包名来强制实现。 二、调整wms com.android.server.wm.DisplayRotation /*** Given an orientation constant, return…...
基于deepseek的智能语音客服【第四讲】封装milvus数据库连接池封装
通过工厂模式创建链接 static {// 创建连接池工厂BasePooledObjectFactory<MilvusServiceClient> factory new BasePooledObjectFactory<MilvusServiceClient>() {Overridepublic MilvusServiceClient create() throws Exception {return new MilvusServiceClient…...
【GeeRPC】项目总结:使用 Golang 实现 RPC 框架
文章目录 项目总结:使用 Golang 实现 RPC 框架谈谈 RPC 框架什么是 RPC 框架实现一个 RPC 框架需要什么?项目总结文章结构安排 Part1:消息编码编解码器的实现通信过程 Part2:服务端Accept:阻塞地等待连接请求并开启 go…...
人工智能在医疗影像诊断中的应用与挑战
引言 近年来,人工智能(AI)技术在医疗领域的应用逐渐成为研究热点,尤其是在医疗影像诊断方面。AI技术的引入为医疗影像诊断带来了更高的效率和准确性,有望缓解医疗资源紧张的问题,同时为患者提供更优质的医疗…...
烧结银技术赋能新能源汽车超级快充与高效驱动
烧结银技术赋能新能源汽车超级快充与高效驱动 在新能源汽车领域,高压快充技术的突破与高功率密度驱动系统的创新正成为行业竞争的焦点。比亚迪于 2025 年发布的超级 e 平台,通过整合全域千伏高压架构、兆瓦级闪充技术及碳化硅(SiC࿰…...
大模型幻觉产生的【九大原因】
知识问答推理幻觉产生的原因 1.知识库结构切割不合理 大段落切割向量化 切分太小可以实现更精准化的回复内,向量匹配相似度越高。检索内容碎片化严重、可能包含不符合内容的文本数据。切分太大内容资料更完整,但是会影响相似度,同时更消耗资…...
4小时速通shell外加100例
🔥 Shell 基础——从入门到精通 🚀 🌱 第一章:Shell,简单说! 👶 什么是Shell?它到底能做什么?这章让你快速了解Shell的强大之处! 👶 什么是Shell…...
AD(Altium Designer)更换PCB文件的器件封装
一、确定是否拥有想换的器件PCB封装 1.1 打开现有的原理图 1.2 确定是否拥有想换的器件PCB文件 1.2.1 如果有 按照1.3进行切换器件PCB封装 1.2.2 如果没有 按照如下链接进行添加 AD(Altium Designer)已有封装库的基础上添加器件封装-CSDN博客https://blog.csdn.net/XU15…...
Postgresql 删除数据库报错
1、删除数据库时,报错存在其他会话连接 ## 错误现象,存在其他的会话连接正在使用数据库 ERROR: database "cs" is being accessed by other users DETAIL: There is 1 other session using the database.2、解决方法 ## 终止被删除数据库下…...
人工智能时代——深度探索如何构建开放可控的专利生态体系
# 人工智能时代——深度探索如何构建开放可控的专利生态体系 引言:AI专利革命的战略抉择第一章 战略认知与基本原则1.1 人工智能专利革命的范式重构1.1.1 技术维度变革1.1.2 法律维度挑战1.1.3 文明安全的不可控风险 1.2 战略定位体系构建1.2.1 双循环治理框架的立体…...