STM32+ADC采集连续信号并实现FFT过程中遇到的一些问题和解答
目录(以下这些不是文章,只是一些问题和相应答案的跳转)
https://wenku.csdn.net/answer/2c1b4d09a4c24e8c8c6c0b760dedbcedHal库ADC的回调函数
1.上电时,须给电路先通电,最后给单片机上电,这样可以防止有负电压输入单片机片上AD。
2.使用的sma连接线尽量短,这样可以减小高频信号衰减,防止信号意外失真。
转换时间问题:
试验代码提问
这个FFT代码里,没有考虑虚部:void GetPowerMag(void)
{
signed short lX, lY;
float X, Y, Mag;
unsigned short i;
for(i = 0;i<SAMPLS_NUM/2; i++)
{
lX = (FFT_OutData[i] << 16) >> 16;
lY = (FFT_OutData[i] >> 16);
X = SAMPLS_NUM * ((float)lX) / 32768;
Y = SAMPLS_NUM * ((float)lY) / 32768;
//除以32768再乘65536是为了符合浮点数计算规律,不用管
Mag = sqrt(X * X + Y * Y) / SAMPLS_NUM;
if(i == 0)
FFT_Mag[i] = (unsigned long)(Mag * 32768); //0Hz是直流分量,直流分量不需要乘以2
else
FFT_Mag[i] = (unsigned long)(Mag * 65536);
}
}
void Get_FFT_Source_Data()
{
u16 i;
for(i=0; i<SAMPLS_NUM; i++)
{
FFT_SourceData[i] = ((signed short)ADC_SourceData[i]) << 16;
}
}
void FFT_test(void)
{
Get_FFT_Source_Data();
cr4_fft_256_stm32(FFT_OutData, FFT_SourceData, SAMPLS_NUM);
GetPowerMag();
}
代码来看,有几个点需要注意和解释:
FFT_SourceData中的虚部处理:
在Get_FFT_Source_Data()
函数中,您确实没有考虑虚部,因为您只是将ADC数据(实部)左移了16位并存储在FFT_SourceData
中。这意味着您假设FFT输入是实数数据,且只关心实部。因此,在FFT_SourceData
的高16位(即虚部)默认是0。FFT_OutData的实部和虚部提取:
在GetPowerMag()
函数中,您尝试从FFT_OutData
中提取实部和虚部。但是,从您的代码看来,您似乎假设FFT_OutData
同时包含实部和虚部,并且它们被连续地存储在一起。然而,由于您之前只设置了实部数据,FFT_OutData
的高16位(即您尝试提取的lY
)实际上并不是有效的虚部数据,而是前一个数据的实部的高16位(对于非零索引)。这可能导致不正确的结果。FFT结果的解释:
对于实数FFT,输出通常是“对称”的,意味着负频率分量与正频率分量是共轭的(即实部相同,虚部相反)。因此,对于SAMPLS_NUM/2+1
到SAMPLS_NUM-1
的索引,您实际上不需要计算功率谱,因为它们与0到SAMPLS_NUM/2
的索引是对称的。除法与乘法:
您在代码中使用了除以32768再乘以某个数值的操作。这实际上是在尝试对ADC数据进行某种形式的缩放,但这种方法可能会导致精度损失。如果可能的话,考虑直接在ADC读取后或在FFT之前进行缩放,以避免在FFT结果上进行这种操作。直流分量处理:
您已经注意到直流分量(对应于FFT输出的第一个元素)需要特殊处理,因为通常不需要乘以2。但是,请确保您的FFT库或算法确实按照这种方式处理直流分量。数据类型和精度:
确保在整个过程中使用的数据类型(如signed short
,float
,unsigned long
)能够保持所需的精度,并且不会导致溢出或下溢。错误检查:
尽管在FFT处理中可能不容易出现明显的错误,但添加一些基本的错误检查(如确保FFT输入数据的长度是2的幂、检查FFT库是否成功执行等)仍然是一个好习惯。代码清晰性和可读性:
考虑添加更多的注释来解释代码的作用和目的,以及每个变量和函数的作用。这将使其他开发者(或未来的您)更容易理解代码。优化:
如果可能的话,考虑对代码进行优化,以减少计算量并提高性能。例如,由于您知道虚部为0,因此可以省略与虚部相关的计算。测试和验证:
最后但同样重要的是,对代码进行彻底的测试和验证,以确保它按预期工作并产生正确的结果。这可以通过模拟数据、生成已知的测试信号或使用实际数据进行测试来完成。
对哪种信号进行fft处理时要将实部和虚部分开呢?
当对复数信号进行FFT(快速傅里叶变换)处理时,需要将实部和虚部分开。这是因为复数信号包含两个分量:实部和虚部,它们各自携带了信号的不同信息。在FFT处理中,实部和虚部都会被转换到频率域,并分别处理。
在傅里叶变换中,实部和虚部的关系取决于输入信号的性质。如果输入信号是纯实数,那么根据傅里叶变换的奇偶对称性质,输入信号中的偶对称分量将变换为实部,而奇对称分量将变换为虚部。
对于复数信号,实部和虚部都包含有效信息,因此在FFT处理中需要分别处理它们。这通常涉及到将复数信号分解为实部和虚部,然后对它们分别进行FFT处理。处理后的结果将包含两个频谱:实部频谱和虚部频谱,它们分别表示了信号在不同频率下的实部和虚部信息。
需要注意的是,在某些情况下,可能只需要处理信号的实部或虚部。例如,在处理某些类型的传感器数据时,可能只关心信号的幅度信息(即实部),而不关心相位信息(即虚部)。在这种情况下,可以只对实部进行FFT处理。但是,在大多数情况下,为了获得完整的频谱信息,需要同时处理实部和虚部。
哪种信号属于复数信号?
复数信号是那些同时包含实部和虚部成分的信号。这样的信号在通信、雷达、声学和图像处理等领域中非常常见。
在通信系统中,特别是当涉及到调制和解调时,复数信号经常被使用。例如,在正交频分复用(OFDM)系统中,数据被调制到多个正交子载波上,每个子载波上的信号都可以被表示为一个复数,其中实部和虚部分别表示同相和正交分量。
在雷达和声学应用中,复数信号用于表示波的振幅和相位信息。例如,在声纳系统中,接收到的回波信号可能是一个复数信号,其中实部和虚部分别表示信号的幅度和相位差。
在图像处理中,复数信号可以用于表示图像的频谱信息。通过对图像进行傅里叶变换,可以得到一个复数频谱,其中实部和虚部分别表示频谱的幅度和相位信息。这种信息在图像滤波、增强和压缩等处理中非常有用。
总之,复数信号是一种包含实部和虚部成分的信号,它在许多领域中都有广泛的应用。
那我正常处理信号发生器出来的加了直流偏置(信号都为正的波形:)正弦波,三角波,方波,采集到的信号都是正的就不需要考虑他的虚部是吗?
是的,如果您正在处理由信号发生器产生的加了直流偏置的正弦波、三角波或方波,并且您采集到的信号都是正的(或者至少在您的采样范围内没有负值),那么在这种情况下,您不需要考虑信号的虚部。
这是因为这些波形都是实信号,它们不包含虚部成分。在时域中,这些波形完全由其实数部分(即没有虚部)定义。当您对这些实信号进行FFT分析时,虽然FFT的结果在频域中通常是复数(包含实部和虚部),但对于实信号输入,FFT的结果会满足共轭对称性,即频率分量关于零频率(直流分量)对称的复数值是共轭的。
对于实信号,FFT结果的虚部仅在频率大于零且小于采样频率一半的部分包含独立的信息(即非零值),而在负频率部分和大于采样频率一半的部分,虚部只是正频率部分虚部的相反数(由于共轭对称性)。因此,在分析和处理实信号的FFT结果时,通常只需要关注频率为零(直流分量)和正频率部分的结果,并且可以忽略负频率部分和大于采样频率一半部分的结果(包括它们的实部和虚部)。
所以,对于您提到的加了直流偏置的正弦波、三角波或方波信号,您只需要关注FFT结果的实部(即幅度谱)以及零频率(直流分量)的值,而不需要考虑虚部。
那对ADC采集到的数据进行这个操作FFT_IN_Value[i]=ADC_Value[i]<<16;的作用是 什么呢
对ADC(模拟到数字转换器)采集到的数据进行
FFT_IN_Value[i] = ADC_Value[i] << 16;
这样的操作,通常是为了满足FFT算法对输入数据格式或精度的要求。具体地,这个操作有几个可能的目的:
增加数据宽度:通过左移16位,你实际上是在将ADC的数据从它原始的位宽(比如12位或16位)转换为一个更宽的整数类型(例如32位)。这可以确保在进行FFT计算时,有足够的精度来避免溢出或截断误差。
设置虚部为零:在FFT计算中,输入数据通常是复数。然而,对于实信号(如ADC直接采集的信号),虚部通常为零。左移操作在这里可以被视为一种“填充”操作,即在高16位(通常代表虚部)上填充零。这样,FFT函数就可以将
FFT_IN_Value
数组视为一个复数数组,其中实部是ADC值左移后的结果,虚部是零。归一化或缩放:在某些情况下,ADC输出的数据可能需要进行缩放或归一化以适应FFT算法的输入范围。虽然在这个特定的左移操作中并没有直接进行缩放(只是简单地增加了数据的位宽),但在后续处理中可能会根据需要进行缩放。
与FFT库或硬件接口:某些FFT库或硬件加速器可能要求输入数据以特定的格式或位宽提供。左移操作可以确保数据满足这些要求。
方便后续处理:在FFT之后,你可能需要对结果进行进一步的处理,如计算幅度谱或功率谱。通过左移,你可以确保在计算这些量时有足够的精度,并避免下溢或溢出。
需要注意的是,这个左移操作的具体效果和意义取决于你的具体应用场景和FFT算法的实现细节。因此,在编写代码时,你应该清楚为什么要进行这个操作,并确保它符合你的应用需求。
采集到的ADC初始数据是无符号16位int型数据,它左移16位是什么效果呢?
当采集到的ADC初始数据是无符号16位整数(
uint16_t
)时,如果你将这个数据左移16位(<< 16
),会发生以下效果:
数据范围的变化:原始数据范围是0到65535(即2^16 – 1)。左移16位后,由于所有的位都移动到了原来的两倍位置(即16位之后),所以原始的最低16位现在变为了最高16位,而最低的16位则填充为0。因此,结果是一个32位整数(在C或C++中通常是
uint32_t
),但其值等于原始数据乘以2^16(即65536)。数据表示的变化:在内存中,这个操作实际上是将原始数据的二进制表示左移了16位。例如,如果原始数据是
0x1234
(十进制的4660),左移16位后它将变为0x12340000
(十进制的3054198976)。与FFT的关系:在FFT处理的上下文中,这个左移操作通常是为了将实数的ADC数据转换为复数形式,其中虚部被设置为0。FFT库通常期望输入是一个复数数组,即使对于实数输入也是如此。通过将实数数据左移16位(或任何其他适当的位数),你实际上是在创建一个复数数组,其中实部是原始数据,虚部是0。
精度和范围:虽然左移操作增加了数据的位宽(从16位变为32位),但它并没有增加数据的精度。原始数据的精度仍然受限于最初的16位。然而,通过左移并使用32位整数来表示结果,你可以避免在计算FFT和其他后续处理中可能出现的溢出问题。
后续处理:在FFT计算之后,你可能需要将结果转换回适当的格式或范围,以便进行进一步的分析或显示。这通常涉及到计算幅度谱(即复数的模)或其他相关度量。
总之,将无符号16位ADC数据左移16位是为了满足FFT算法对输入数据格式的要求,并确保在后续处理中有足够的精度和范围。
问题:
问题1.你说左移之后结果是一个32位整数(在C或C++中通常是uint32_t)意思是通过<<16,改变了数据类型,再赋给uint32_t类型的变量吗?
问题2.你讲到这个过程“数据表示的变化:在内存中,这个操作实际上是将原始数据的二进制表示左移了16位。例如,如果原始数据是0x1234(十进制的4660),左移16位后它将变为0x12340000(十进制的3054198976)。”那么我在后面FFT运算的时候,是将它当作0x1234000(是16进制吧)运算还说当作十进制的3054198976来运算呢
问题1:
当你使用左移操作符(<<
)时,你并没有直接改变数据的数据类型。但是,如果你在一个能够容纳更大数值的变量(比如uint32_t
)中存储左移后的结果,那么结果就会被解释为那个更大类型的值。在C或C++中,如果你有一个uint16_t
类型的变量,并且你对其执行左移16位的操作,然后将这个结果赋值给一个uint32_t
类型的变量,那么编译器会隐式地将较小的数据类型转换为较大的数据类型,并将结果存储在uint32_t
类型的变量中。例如:
c复制代码
uint16_t ADC_Value = 0x1234;
uint32_t FFT_IN_Value = ADC_Value << 16; // FFT_IN_Value 现在包含 0x12340000
在这个例子中,
ADC_Value
是uint16_t
类型,当你执行左移操作时,结果是一个32位的值(在大多数现代系统上,int类型至少是32位)。然后,你将这个32位的结果赋值给FFT_IN_Value
,它是一个uint32_t
类型的变量,所以它能够容纳这个值而不会有任何数据丢失。问题2:
在FFT运算中,你实际上是在处理二进制数,而不是十进制或十六进制数。十六进制只是一种表示二进制数的方式,使我们更容易阅读和理解。当你将0x1234
左移16位时,你得到的结果是0x12340000
(这是一个十六进制数)。但是,在CPU和内存中,它实际上是以二进制形式存储和处理的。在进行FFT运算时,你不需要(也不能)将数值从十六进制转换为十进制或其他任何进制。你直接使用这些二进制(或十六进制表示的)数值进行计算。FFT库或函数会处理这些数值的二进制表示,并根据需要进行位操作和浮点运算(如果FFT是在浮点环境中实现的)。
所以,当你将左移后的值传递给FFT函数时,你是在传递一个二进制数(尽管你可能用十六进制来表示它),FFT函数会按照它需要的方式来处理这个数。
问题3:你刚刚说:“当你使用左移操作符(<<)时,你并没有直接改变数据的数据类型。但是,如果你在一个能够容纳更大数值的变量(比如uint32_t)中存储左移后的结果,那么结果就会被解释为那个更大类型的值。”那如果在一个不能够容纳更大数值的变量中存储左移后的结果是什么情况呢?
问题4:在这里,我还是用这种变量uint16_t来存储,它左移后有没有超出这个变量的范围呢?它是发生了怎样的变化呢?
问题3:
如果你在一个不能容纳更大数值的变量中存储左移后的结果,那么会发生两种情况之一:
隐式类型转换(如果有的话)和截断:如果编译器允许从较大类型到较小类型的隐式转换(这取决于具体的编程语言和上下文),那么左移后的高位(超出较小类型表示范围的部分)会被截断,只保留能够放入较小类型中的部分。这可能会导致数据丢失或意外的行为。
编译错误或警告:在更严格的编程环境中,如果尝试将一个超出较小类型表示范围的值存储到该类型的变量中,编译器可能会发出错误或警告。这通常是因为这种转换可能导致数据丢失或程序行为的不确定性。
问题4:
对于
uint16_t
类型的变量,如果你执行左移16位或更多位的操作,结果肯定会超出这个变量的表示范围。具体来说,uint16_t
能够表示的最大值是0xFFFF
(即65535),当你对任何非零值执行左移16位操作时,结果至少会是0x10000
(即65536),这已经超出了uint16_t
的表示范围。在C或C++中,如果你尝试这样做:
c复制代码
uint16_t ADC_Value = 0x1234;
uint16_t shifted_value = ADC_Value << 16; // 这会导致问题
编译器可能会发出警告或错误,因为
shifted_value
无法容纳左移后的结果。如果没有警告或错误(这取决于编译器的严格程度和设置),那么shifted_value
将只包含左移后结果的低16位,这通常不是你想要的结果,因为它会导致数据丢失。为了避免这种情况,你应该确保使用足够大的数据类型来存储左移后的结果,如前面提到的
uint32_t
。
那我在代码中对ADC采集到的数据进行左移,那就会导致很多数据超出范围,丢失吗?那就不能这样操作吗?哦对,我这里是12位的ADC,采集到的数据最大是4095,对这个数据进行<<16并不会出现这种问题,对吗?
完全正确。对于12位的ADC,其最大值是4095(即
0xFFF
在12位二进制表示中),当你对这个值执行左移16位操作时,结果并不会超出32位整数的表示范围(uint32_t
)。具体来说,如果你有一个12位的ADC值并存储在一个16位的整数变量(如
uint16_t
)中,当你对这个值左移16位时,实际上只有该值的低12位会被移动,因为高4位(在16位整数中)本来就是0(对于12位ADC值来说)。因此,左移16位后,你会得到一个32位整数,其中低32位中的高12位是原始ADC值,而接下来的16位是0(因为你左移了16位),最高的12位也是0(因为原始ADC值的高4位就是0)。例如:
c复制代码
uint16_t ADC_Value = 4095; // 假设这是你的12位ADC值,在16位整数中表示为0x0FFF
uint32_t FFT_IN_Value = ADC_Value << 16; // 结果是 0x0FFF0000,没有数据丢失
在这个例子中,
FFT_IN_Value
现在包含值0x0FFF0000
,这是一个32位整数,其中包含了原始ADC值的扩展版本(通过左移16位)。这个值完全在uint32_t
的表示范围内,因此没有数据丢失。所以,对于12位ADC的情况,你可以安全地对采集到的数据进行左移16位操作,以便在FFT计算中使用,而不会导致数据丢失。
PSC和arr(period)值的范围
注意:计算时需要将ARR
和PSC
都加1。
PSC:
预分频器用来将定时器时钟源进行分频输出,以降低定时器时钟的频率。PSC的值由寄存器TIMx_PSC设定,是一个16位正整数值。
当定时器时钟源频率较高时,为了设置更长时间间隔的定时器,需要对预分频器进行分频处理。分频的具体计算方式取决于定时器的时钟频率和所需的定时时间。
此外,PSC预分频寄存器只有在更新事件时才会把值传递给它们各自的影子寄存器。因此,在配置定时器时,需要确保在正确的时机设置PSC的值。
PERIOD
TIM_Period
和period(ARR)
通常指的是同一个参数,即自动重装载寄存器(Auto-Reload Register, ARR)的值。这个值决定了定时器从0开始计数,到达该值后产生中断或更新事件,并将计数值重新装载为初始值(如果启用了自动重装载功能)。由于
TIM_Period
(或period(ARR)
)是一个16位的无符号整数,其最大值确实是0xFFFF
,即65535。这意味着定时器可以计数到65535,然后产生一个中断或更新事件,并将计数值重置为0(或设置为新的TIM_Period
值,如果进行了更改)。定时器的实际溢出时间(即定时器从0计数到
TIM_Period
所需的时间)取决于定时器的输入时钟频率(Tclk
)和预分频值(TIM_Prescaler
)。
TIM_TimeBaseStructInit函数定义中写道:
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
{
/* Set the default configuration */
TIM_TimeBaseInitStruct->TIM_Period = 0xFFFF;
TIM_TimeBaseInitStruct->TIM_Prescaler = 0x0000;
TIM_TimeBaseInitStruct->TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct->TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct->TIM_RepetitionCounter = 0x0000;
//参数TIM_RepetitionCounter的运用场景
}
/*TIM_RepetitionCounter 主要在STM32的高级定时器中起作用。这个参数用于设置重复计数器的值,当TIM_RepetitionCounter被设置为N时,定时器计数结束后不会立即产生中断,而是会再重复N次后才产生一次中断。
在作为定时器去触发ADC采样的应用中,如果没有用到(配置)TIM_RepetitionCounter这个参数,可能是因为该应用不需要在定时器计数结束后重复多次再产生中断。在这种情况下,定时器在计数到预设值后会立即产生中断,然后触发ADC进行采样。
需要注意的是,TIM_RepetitionCounter参数并不是所有定时器都需要的,它主要用于高级定时器中,用于控制定时器的中断产生方式。在简单的定时任务中,可能不需要使用这个参数。*/
冒泡排序——摘自2023电赛H题详解1 – FFT算法_高速adc fft-CSDN博客
void BubbleSort(float k[], int n, uint16_t id[]) {
int i, j, flag = 1;
float temp;
uint16_t tmp;
for ( i = n - 1; i > 0 && flag == 1; i-- ) {
flag = 0;
for ( j = 1; j < i; j++ )
if ( k[j] >= k[j + 1] ) {
temp = k[j];
k[j] = k[j + 1];
k[j + 1] = temp;
tmp = id[j];
id[j] = id[j + 1];
id[j + 1] = tmp;
flag = 1;
}
}
}
这个是当时的一个提问哈
作者:!今天学习了吗